attackmap-analyzer-iac 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,235 @@
1
+ """Misconfiguration records (iac#1).
2
+
3
+ Container and cluster misconfigurations go to core's `CloudMisconfig` record
4
+ (AttackMap#334), which core turns into findings with CWE/OWASP ids. They used
5
+ to be `AuthHint`s, which core reads as recon: an `AuthHint` in a Dockerfile
6
+ even counted as an auth control for the `EXPOSE` route next to it.
7
+
8
+ With a core that predates `CloudMisconfig` (feature-detected: the SDK doesn't
9
+ export it, or `ScanResult` has no `cloud_misconfigs`), the legacy `AuthHint`
10
+ (if the check had one) is emitted instead. The `gha_*` / `shell_*` posture
11
+ hints are `FrameworkHint`s on every core, old ones included: they were never
12
+ auth signals and no core version consumes them by name.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from bisect import bisect_right
18
+
19
+ from attackmap.sdk import AuthHint, ScanResult
20
+
21
+ try: # cores before 0.6 don't have the record; then no records are emitted
22
+ from attackmap.sdk import CloudMisconfig
23
+ except ImportError: # pragma: no cover - exercised against older cores
24
+ CloudMisconfig = None # type: ignore[assignment,misc]
25
+
26
+ _EVIDENCE_MAX = 200
27
+ # Feature detection: an older core neither exports the record nor has the field.
28
+ CLOUD_RECORDS = CloudMisconfig is not None and "cloud_misconfigs" in getattr(ScanResult, "model_fields", {})
29
+
30
+ # Dedup keys per result, so appending stays O(1) on hostile input with
31
+ # tens of thousands of signals (a linear `any(...)` scan per append is
32
+ # quadratic). Keyed by id(); the result object is kept to rule out reuse.
33
+ _SEEN: dict[int, tuple[object, set]] = {}
34
+
35
+
36
+ def first_seen(owner: object, key: tuple) -> bool:
37
+ """True the first time ``key`` is offered for ``owner`` (a result, or a
38
+ hint list of one)."""
39
+ entry = _SEEN.get(id(owner))
40
+ if entry is None or entry[0] is not owner:
41
+ entry = _SEEN[id(owner)] = (owner, set())
42
+ if key in entry[1]:
43
+ return False
44
+ entry[1].add(key)
45
+ return True
46
+
47
+
48
+ # Line starts of the file being analyzed: `line_of` / `line_snippet` from the
49
+ # SDK rescan the text on every call, which is quadratic for a file with
50
+ # tens of thousands of matches. Files are analyzed one at a time, so one
51
+ # cached index is enough.
52
+ _LINES: list = [None, [0]]
53
+
54
+
55
+ def _starts(content: str) -> list[int]:
56
+ if _LINES[0] is not content:
57
+ starts = [0]
58
+ index = content.find("\n")
59
+ while index != -1:
60
+ starts.append(index + 1)
61
+ index = content.find("\n", index + 1)
62
+ _LINES[0], _LINES[1] = content, starts
63
+ return _LINES[1]
64
+
65
+
66
+ def line_of(content: str, offset: int) -> int:
67
+ """1-indexed line of ``offset`` (same numbering as `attackmap.sdk.line_of`)."""
68
+ return bisect_right(_starts(content), max(offset, 0))
69
+
70
+
71
+ def line_snippet(content: str, line: int, max_len: int = 200) -> str:
72
+ """Stripped text of 1-indexed ``line`` (as `attackmap.sdk.line_snippet`)."""
73
+ starts = _starts(content)
74
+ if line < 1 or line > len(starts):
75
+ return ""
76
+ end = starts[line] - 1 if line < len(starts) else len(content)
77
+ text = content[starts[line - 1]:end].strip()
78
+ return text if len(text) <= max_len else text[: max_len - 1] + "…"
79
+
80
+
81
+ def release(*owners: object) -> None:
82
+ for owner in owners:
83
+ _SEEN.pop(id(owner), None)
84
+
85
+
86
+ def append_misconfig(
87
+ result: ScanResult,
88
+ kind: str,
89
+ file: str,
90
+ line: int | None,
91
+ evidence: str,
92
+ *,
93
+ severity: str,
94
+ confidence: float = 0.8,
95
+ resource: str | None = None,
96
+ platform: str | None = None,
97
+ escalation: str | None = None,
98
+ context: str | None = None,
99
+ legacy_auth_hint: str | None = None,
100
+ ) -> None:
101
+ """One record per (kind, file, line, resource)."""
102
+ evidence = " ".join(evidence.split())
103
+ if len(evidence) > _EVIDENCE_MAX:
104
+ evidence = evidence[: _EVIDENCE_MAX - 1] + "…"
105
+ if not CLOUD_RECORDS:
106
+ if legacy_auth_hint and first_seen(result, ("auth", legacy_auth_hint, file)):
107
+ result.auth_hints.append(
108
+ AuthHint(hint=legacy_auth_hint, file=file, line=line, evidence_text=evidence, confidence=0.9)
109
+ )
110
+ return
111
+ if not first_seen(result, ("misconfig", kind, file, line, resource, evidence)):
112
+ return
113
+ result.cloud_misconfigs.append(
114
+ CloudMisconfig(
115
+ kind=kind, file=file, line=line, resource=resource, platform=platform, context=context,
116
+ escalation_path=escalation, evidence_text=evidence, severity=severity, confidence=confidence,
117
+ )
118
+ )
119
+
120
+
121
+ # Linux capabilities that let a process break out of its container or take
122
+ # over the node (high) or its network (medium). `CAP_` prefix stripped.
123
+ DANGEROUS_CAPABILITIES = {
124
+ "ALL": "high",
125
+ "SYS_ADMIN": "high",
126
+ "SYS_MODULE": "high",
127
+ "SYS_PTRACE": "high",
128
+ "SYS_RAWIO": "high",
129
+ "DAC_READ_SEARCH": "high",
130
+ "BPF": "high",
131
+ "NET_ADMIN": "medium",
132
+ "SYS_TIME": "medium",
133
+ }
134
+
135
+ # Host paths whose mount hands a container the node: runtime sockets, the
136
+ # root filesystem, kubelet state, kernel interfaces.
137
+ _RUNTIME_SOCKETS = ("/var/run/docker.sock", "/run/docker.sock", "/var/run/containerd", "/run/containerd",
138
+ "/var/run/crio", "/run/crio", "/var/run/cri-dockerd.sock")
139
+ _SENSITIVE_HOST_PATHS = (*_RUNTIME_SOCKETS, "/etc", "/root", "/proc", "/sys", "/dev", "/boot",
140
+ "/var/lib/kubelet", "/var/lib/docker", "/var/lib/containerd")
141
+
142
+
143
+ # Read-only system data under the sensitive trees above: CA bundles, time
144
+ # zone, the syslog socket.
145
+ _BENIGN_HOST_PATHS = ("/etc/ssl", "/etc/pki", "/etc/ca-certificates", "/etc/localtime", "/etc/timezone",
146
+ "/etc/machine-id", "/etc/os-release", "/etc/letsencrypt", "/dev/log", "/dev/dri",
147
+ "/usr/share/ca-certificates")
148
+
149
+
150
+ def _normalize(path: str) -> str:
151
+ path = path.strip().strip("'\"")
152
+ while "//" in path:
153
+ path = path.replace("//", "/")
154
+ return path.rstrip("/") or "/"
155
+
156
+
157
+ def host_path_risk(path: str) -> tuple[str, str]:
158
+ """``(severity, escalation)`` for mounting host ``path``."""
159
+ norm = _normalize(path)
160
+ if any(norm == s or norm.startswith(s + "/") for s in _RUNTIME_SOCKETS):
161
+ return "high", "container runtime socket → start a privileged container → root on the node"
162
+ if norm == "/":
163
+ return "high", "host root filesystem → node root, kubelet credentials and every pod's secrets"
164
+ if any(norm == s or norm.startswith(s + "/") for s in _BENIGN_HOST_PATHS):
165
+ return "low", f"host {norm} is readable from the container"
166
+ if any(norm == s or norm.startswith(s + "/") for s in _SENSITIVE_HOST_PATHS):
167
+ return "high", f"host {norm} → node credentials or kernel interfaces → root on the node"
168
+ return "low", f"host {norm} is readable (and possibly writable) from the container"
169
+
170
+
171
+ def capability_severity(cap: str) -> str | None:
172
+ return DANGEROUS_CAPABILITIES.get(cap.strip().strip("'\"").upper().removeprefix("CAP_"))
173
+
174
+
175
+ # ---------- Kubernetes RBAC and Service exposure (shared by manifests) ----------
176
+
177
+ # API groups whose objects are cluster control. A wildcard on a CRD group an
178
+ # operator owns (`monitoring.coreos.com`) or on `metrics.k8s.io` is normal.
179
+ SENSITIVE_API_GROUPS = frozenset({
180
+ "", "*", "apps", "batch", "extensions", "policy", "networking.k8s.io", "rbac.authorization.k8s.io",
181
+ "admissionregistration.k8s.io", "certificates.k8s.io", "apiextensions.k8s.io",
182
+ })
183
+ # Resources whose write (or read, for secrets) is a step toward cluster control.
184
+ SENSITIVE_RESOURCES = frozenset({
185
+ "*", "secrets", "pods", "pods/exec", "pods/attach", "pods/ephemeralcontainers", "deployments", "daemonsets",
186
+ "statefulsets", "replicasets", "jobs", "cronjobs", "replicationcontrollers", "serviceaccounts",
187
+ "serviceaccounts/token", "roles", "rolebindings", "clusterroles", "clusterrolebindings", "nodes",
188
+ "nodes/proxy", "persistentvolumes", "mutatingwebhookconfigurations", "validatingwebhookconfigurations",
189
+ "customresourcedefinitions", "certificatesigningrequests", "certificatesigningrequests/approval",
190
+ "configmaps", "namespaces", "networkpolicies",
191
+ })
192
+ # Roles whose grant to every authenticated user is a takeover.
193
+ ADMIN_LIKE_ROLES = frozenset({"cluster-admin", "admin", "edit"})
194
+
195
+
196
+ def rbac_wildcard(groups: set[str] | None, verbs: set[str], resources: set[str]) -> bool:
197
+ """A `*` that matters: on a control-plane API group (``groups`` None
198
+ means the rule names none), and either every resource or a sensitive
199
+ one. `verbs: ["*"]` on `events` or `metrics.k8s.io` is not reported."""
200
+ if "*" not in verbs and "*" not in resources:
201
+ return False
202
+ if groups is not None and not groups & SENSITIVE_API_GROUPS:
203
+ return False
204
+ return "*" in resources or bool(resources & SENSITIVE_RESOURCES)
205
+
206
+
207
+ # Annotation key suffix → values that make a LoadBalancer internal.
208
+ _INTERNAL_LB_ANNOTATIONS = {
209
+ "aws-load-balancer-internal": {"true", "0.0.0.0/0"},
210
+ "aws-load-balancer-scheme": {"internal"},
211
+ "azure-load-balancer-internal": {"true"},
212
+ "oci-load-balancer-internal": {"true"},
213
+ "openstack-internal-load-balancer": {"true"},
214
+ "load-balancer-type": {"internal"}, # networking.gke.io/, cloud.google.com/
215
+ "ibm-load-balancer-cloud-provider-ip-type": {"private"},
216
+ "alibaba-cloud-loadbalancer-address-type": {"intranet"},
217
+ }
218
+
219
+
220
+ def internal_lb(annotations: dict[str, str]) -> bool:
221
+ """True when a cloud's internal-load-balancer annotation says so. An
222
+ explicit ``"false"`` / ``internet-facing`` is external; a templated
223
+ value (``__TPL__`` / ``${...}``) is unknown, so treated as internal
224
+ (not reported)."""
225
+ for key, value in annotations.items():
226
+ suffix = key.rsplit("/", 1)[-1].lower()
227
+ accepted = _INTERNAL_LB_ANNOTATIONS.get(suffix)
228
+ if accepted is None:
229
+ continue
230
+ text = value.strip().strip("\"'").lower()
231
+ if "__tpl__" in text or "${" in text or text.startswith("var."):
232
+ return True
233
+ if text in accepted:
234
+ return True
235
+ return False
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: attackmap-analyzer-iac
3
+ Version: 0.2.0
4
+ Summary: Infrastructure-as-Code analyzer plugin for AttackMap (Dockerfile, docker-compose, Kubernetes, Helm, GitHub Actions, .env templates, shell installers)
5
+ Author: AttackMap Contributors
6
+ Author-email: Matthew Davis <mlaify@mlaify.io>
7
+ Maintainer-email: Matthew Davis <mlaify@mlaify.io>
8
+ License-Expression: MIT
9
+ Project-URL: Homepage, https://github.com/mlaify/attackmap-analyzer-iac
10
+ Project-URL: Repository, https://github.com/mlaify/attackmap-analyzer-iac
11
+ Project-URL: Issues, https://github.com/mlaify/attackmap-analyzer-iac/issues
12
+ Project-URL: Changelog, https://github.com/mlaify/attackmap-analyzer-iac/blob/main/CHANGELOG.md
13
+ Project-URL: AttackMap core, https://github.com/mlaify/AttackMap
14
+ Keywords: security,static-analysis,appsec,defensive-security,attackmap,attackmap-plugin,iac,docker,docker-compose,github-actions,kubernetes,helm
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Information Technology
18
+ Classifier: Intended Audience :: System Administrators
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3 :: Only
25
+ Classifier: Topic :: Security
26
+ Classifier: Topic :: Software Development :: Quality Assurance
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.11
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: pydantic>=2.7.0
32
+ Requires-Dist: PyYAML>=6.0
33
+ Provides-Extra: core
34
+ Requires-Dist: attackmap>=0.6.0; extra == "core"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
37
+ Requires-Dist: build>=1.2.0; extra == "dev"
38
+ Requires-Dist: attackmap>=0.6.0; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # attackmap-analyzer-iac
42
+
43
+ > [!IMPORTANT]
44
+ > **Active development, slow pace.** AttackMap is under active development, but
45
+ > progress may be slow until more contributors or co-maintainers join. Help is
46
+ > very welcome with the core engine, an analyzer, the macOS app, or the docs —
47
+ > see [CONTRIBUTING.md](CONTRIBUTING.md) or open an issue on
48
+ > [mlaify/AttackMap](https://github.com/mlaify/AttackMap/issues) to say hello.
49
+ > Security reports are still welcome at [security@mlaify.io](mailto:security@mlaify.io).
50
+
51
+ Infrastructure-as-Code analyzer plugin for [AttackMap](https://github.com/mlaify/AttackMap).
52
+
53
+ Covers the class of files that live outside application source but drive the actual runtime posture:
54
+
55
+ - **Dockerfile** (also `Dockerfile.*`, `Dockerfile-*`, `*.Dockerfile`, `*.dockerfile`, `Containerfile*`): non-root user and healthcheck, both checked on the *final* build stage, including what it inherits from an earlier stage it is built `FROM`; exposed ports; `RUN curl | bash`; `ADD <url>`; base-image pinning, one hint per unpinned image, skipping `scratch` and stage aliases; literal secrets in `ENV`/`ARG`
56
+ - **docker-compose** (`docker-compose*.y[a]ml`, `compose*.y[a]ml`, so overrides like `docker-compose.override.yml` too): service list (feeds the topology graph), `env_file` references, host-mount volumes, privileged containers, Docker socket mounts, `cap_add: SYS_ADMIN`/`ALL`, `pid: host`, `seccomp:unconfined`, published ports that bind all interfaces (explicit `0.0.0.0`, or short syntax with no host IP such as `"5432:5432"`), `network_mode: host`, and literal secrets in `environment:`
57
+ - **GitHub Actions workflows** — `pull_request_target` with checkout, third-party actions pinned by tag vs SHA, secret exposure to fork PRs, step-level `permissions:`
58
+ - **`.env` templates** (`.env.example`, `sample.env`) — secret inventory (the shape, not the value)
59
+ - **Shell installers** — `curl | bash` patterns, `sudo` scope, TLS material handling
60
+ - **Kubernetes manifests** (any `.yaml`/`.yml` with top-level `apiVersion:` and `kind:`) — workloads (`Pod`, `Deployment`, `StatefulSet`, `DaemonSet`, `ReplicaSet`, `Job`, `CronJob`, `ReplicationController`): `privileged`, `allowPrivilegeEscalation: true`, `runAsUser: 0`, missing `runAsNonRoot` / `allowPrivilegeEscalation: false`, `hostPath` volumes (high for the runtime socket, `/`, `/etc`, `/proc`, `/sys`, `/dev`, `/var/lib/kubelet`…), `hostNetwork`/`hostPID`/`hostIPC`, dangerous capabilities, `Unconfined` seccomp/AppArmor, `:latest`/untagged images; RBAC roles with `*` on core/RBAC/apps groups, `escalate`/`bind`/`impersonate`, `pods/exec`, Secret reads; bindings to `cluster-admin` or `system:authenticated`/`unauthenticated`/`anonymous`; `LoadBalancer`/`NodePort` Services without `loadBalancerSourceRanges` (entrypoints too; internal load balancers skipped); `Ingress` host/path rules (routes); literal `Secret` values
61
+ - **Helm charts** — templates under `templates/` next to a `Chart.yaml` are read without rendering: `{{ }}` actions are stripped (line numbers kept) and a templated value is never reported, because the chart's values decide it. A setting inside `{{ if }}`/`{{ with }}` is reported with low confidence (it may be off by default). `values.yaml` defaults (`privileged`, `hostNetwork`/`hostPID`/`hostIPC`, `allowPrivilegeEscalation`, `runAsUser: 0`, capabilities) are reported with confidence 0.5
62
+
63
+ Emits routes for exposed ports, external calls for third-party actions and `curl` fetches, `SecretHint` for env-template inventory, service-topology hints for compose `services:` — feeding the same pipeline as the source-code analyzers.
64
+
65
+ ### Misconfigurations and posture signals
66
+
67
+ Every signal carries `file`, `line` and `evidence_text`. Signals about something *missing* (no `USER`, no `HEALTHCHECK`, no `permissions:`) point at the final `FROM` line or line 1.
68
+
69
+ Container and cluster misconfigurations are `CloudMisconfig` records (`cloud_misconfigs`, iac#1), which AttackMap core turns into findings with CWE/OWASP ids. Each names the `resource` (`Deployment/web container app`, `service agent`), a per-instance `severity` and `confidence`, and an `escalation_path` (e.g. `container runtime socket → start a privileged container → root on the node`):
70
+
71
+ | Kind | Dockerfile / compose | Kubernetes / Helm |
72
+ |---|---|---|
73
+ | `container_runs_as_root` | no `USER` in the final stage (low), `USER root` (medium) | `runAsUser: 0` |
74
+ | `privileged_container` | `privileged: true` | `securityContext.privileged: true` |
75
+ | `host_path_mount` | runtime socket and `/etc`, `/proc`, … bind mounts (`/srv/data` is not reported) | `hostPath` volumes |
76
+ | `host_namespace_shared` | `pid: host`, `ipc: host`, `network_mode: host` | `hostPID`, `hostNetwork`, `hostIPC` |
77
+ | `dangerous_capability` | `cap_add` | `capabilities.add` (`SYS_ADMIN`, `ALL`, `SYS_MODULE`, `SYS_PTRACE`, … high; `NET_ADMIN` medium) |
78
+ | `security_profile_disabled` | `seccomp:unconfined`, `apparmor:unconfined` | `seccompProfile`/`appArmorProfile` `Unconfined` |
79
+ | `unpinned_container_image` | `FROM image:latest` or untagged | `image:` `:latest` or untagged |
80
+ | `dockerfile_remote_add` / `dockerfile_curl_pipe_shell` | `ADD <url>` without `--checksum`, `RUN curl \| sh` | |
81
+ | `privilege_escalation_allowed`, `missing_security_context` (low) | | `allowPrivilegeEscalation: true`; no `runAsNonRoot` / `allowPrivilegeEscalation: false` |
82
+ | `rbac_wildcard`, `rbac_escalation`, `rbac_secret_access`, `cluster_admin_binding`, `exposed_service` | | RBAC and Services |
83
+
84
+ No `AuthHint`s are emitted: core reads those as auth signals, and one in a Dockerfile counted as an auth control for its `EXPOSE` route. With an AttackMap core older than the `CloudMisconfig` record, the old `AuthHint`s (`dockerfile_no_user_directive`, `dockerfile_user_root`, `compose_privileged_container`, `compose_docker_socket_mount`, `compose_cap_add_sys_admin`, `compose_pid_host`, `compose_seccomp_unconfined`) are emitted instead. The other signals are split by meaning (AttackMap#258):
85
+
86
+ | Signal list | Hints |
87
+ |---|---|
88
+ | `entrypoint_hints` (network exposure) | `compose_port_binding_all_interfaces:<binding>`, `compose_network_mode_host`, `k8s_service_loadbalancer:<name>`, `k8s_service_nodeport:<name>`, `k8s_ingress:<host>` |
89
+ | `framework_hints` (build, supply-chain, CI and deployment config) | `dockerfile_no_healthcheck`, `dockerfile_run_curl_pipe`, `dockerfile_add_remote`, `dockerfile_base_image_unpinned:<image>`, `compose_env_file_reference`, `compose_host_mount:<host path>`, `gha_third_party_action_tag_pinned:<owner/repo>`, `gha_pull_request_target_with_checkout`, `gha_no_top_level_permissions`, `shell_curl_pipe_installer`, `shell_sudo_used`, `shell_permissive_chmod` |
90
+ | `service_hints` | `service_name:<compose service>` |
91
+
92
+ The Dockerfile user check looks at the final stage's effective user (the last `USER`, inherited through `FROM <stage>`). A `USER nobody` in a builder stage doesn't hide a root final image, and `USER root` followed by `USER app` isn't flagged. A final base tagged `:nonroot` counts as setting a user.
93
+
94
+ Secret-shaped `ENV`/`ARG` names (`*SECRET*`, `*TOKEN*`, `*PASSWORD*`, `*_KEY`, …) and `environment:` entries with a literal value become `SecretHint(kind="hardcoded")`. `*_FILE` paths, `${VAR}` references, booleans and `ARG` without a default are not flagged.
95
+
96
+ Bluesky FINDINGS §2 documented this as the biggest coverage gap: `bluesky-social/pds` (a deployment repo) was 95% invisible to AttackMap because every non-JS file was outside the analyzer model. This plugin closes that gap.
97
+
98
+ ## Install
99
+
100
+ ```bash
101
+ uv tool install attackmap --with attackmap-analyzer-iac \
102
+ --index mlaify=https://pkg.mlaify.io/pypi/simple/ --index-strategy first-index
103
+ # or from source:
104
+ pip install git+https://github.com/mlaify/attackmap-analyzer-iac.git
105
+ # or as part of the bundle
106
+ pip install "attackmap[all] @ git+https://github.com/mlaify/AttackMap.git"
107
+ ```
108
+
109
+ ## Usage
110
+
111
+ Runs automatically via the `attackmap.analyzers` entry-point group once installed:
112
+
113
+ ```bash
114
+ attackmap analyze /path/to/deployment-repo --output reports
115
+ ```
116
+
117
+ ## Contract
118
+
119
+ Implements `AnalyzerProtocol` from `attackmap.sdk`. See the [external-analyzer guide](https://github.com/mlaify/AttackMap/blob/main/docs/external-analyzers.md).
120
+
121
+ ## Scope
122
+
123
+ **In:** patterns any competent operator recognizes on inspection — the docker-compose service graph, `pull_request_target` combined with untrusted checkout, non-root User directive presence, curl-piped installer scripts.
124
+
125
+ **Out:** deep semantic analysis of container image contents, live registry scanning, executed-runtime introspection. Those belong in other tools; AttackMap's differentiator is narrative attack-path reasoning over the code you can see.
@@ -0,0 +1,10 @@
1
+ attackmap_analyzer_iac/__init__.py,sha256=LfcfmMs_g8mrDejtXJ_ZKd7gFxW-hfoAwnK9h_L_ggU,61
2
+ attackmap_analyzer_iac/analyzer.py,sha256=CaruInKUt_26Ex2oz_p4tvc7Hrqe65YWPIq3dkzElj0,40061
3
+ attackmap_analyzer_iac/k8s.py,sha256=ob1tP_I4MlJQL4V1fxOmBTETqyAetMze_NqHFc2KMlA,30981
4
+ attackmap_analyzer_iac/records.py,sha256=xEi6EhKv8Skt6PNzWDD73bglHCbFGxT1EPgKE7LweeM,9974
5
+ attackmap_analyzer_iac-0.2.0.dist-info/licenses/LICENSE,sha256=vtPQVGUyxq359j0JmD3_eJaXyHmaHyjzaMOtaLwJc88,1097
6
+ attackmap_analyzer_iac-0.2.0.dist-info/METADATA,sha256=qKXMUTd80RB5BlupL7qmGf7Xv8_MJiHO8iBOmYpEL_E,10657
7
+ attackmap_analyzer_iac-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
8
+ attackmap_analyzer_iac-0.2.0.dist-info/entry_points.txt,sha256=8s0ttnoJWChZWfjezqokU7l3wyyF2UQne_Xj0Nlu-fo,63
9
+ attackmap_analyzer_iac-0.2.0.dist-info/top_level.txt,sha256=0RcqPMapdhERWyJIirMDbHofmUmxrj7H-4Vp6dlTmsw,23
10
+ attackmap_analyzer_iac-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [attackmap.analyzers]
2
+ iac = attackmap_analyzer_iac:IacAnalyzer
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matthew Davis and AttackMap Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ attackmap_analyzer_iac