attackmap-analyzer-swift 0.1.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.

Potentially problematic release.


This version of attackmap-analyzer-swift might be problematic. Click here for more details.

@@ -0,0 +1,3 @@
1
+ from .analyzer import SwiftAnalyzer
2
+
3
+ __all__ = ["SwiftAnalyzer"]
@@ -0,0 +1,210 @@
1
+ """Swift ecosystem analyzer for AttackMap.
2
+
3
+ Slice #186 (scaffold): SwiftPM project detection and a dependency SBOM parsed
4
+ from ``Package.resolved`` (formats v1/v2/v3), plus light framework and entrypoint
5
+ recon. Server-side route extraction (Vapor/Hummingbird, #187) and the iOS/macOS
6
+ app attack surface (#188) land in later slices.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ from pathlib import Path
14
+
15
+ from .contracts import (
16
+ AnalyzerMetadata,
17
+ DependencyHint,
18
+ EntrypointHint,
19
+ FrameworkHint,
20
+ ScanResult,
21
+ )
22
+
23
+ SKIP_DIRS = {
24
+ ".git",
25
+ ".build", # SwiftPM build products
26
+ "build",
27
+ "DerivedData", # Xcode build products
28
+ "Pods", # CocoaPods vendored sources
29
+ "Carthage",
30
+ "node_modules",
31
+ "__pycache__",
32
+ }
33
+
34
+ # Known server-side frameworks worth surfacing when seen in Package.swift deps.
35
+ _FRAMEWORK_HINTS: dict[str, str] = {
36
+ "vapor": "Vapor (server-side Swift web framework)",
37
+ "hummingbird": "Hummingbird (server-side Swift web framework)",
38
+ "kitura": "Kitura (server-side Swift web framework)",
39
+ "perfect": "Perfect (server-side Swift web framework)",
40
+ }
41
+
42
+ # `.package(url: "https://github.com/vapor/vapor.git", ...)` and the newer
43
+ # `.package(name: "X", url: "...")` / path-based forms.
44
+ _PACKAGE_URL_RE = re.compile(r"\.package\s*\([^)]*?url\s*:\s*\"([^\"]+)\"", re.DOTALL)
45
+
46
+
47
+ def _identity_from_url(url: str) -> str:
48
+ """SwiftPM identity = the last path component of the repo URL, lowercased,
49
+ without a trailing ``.git`` (matches how Package.resolved v2/v3 records it)."""
50
+ tail = url.rstrip("/").rsplit("/", 1)[-1]
51
+ if tail.endswith(".git"):
52
+ tail = tail[:-4]
53
+ return tail.lower()
54
+
55
+
56
+ def _line_of(content: str, needle: str) -> int | None:
57
+ idx = content.find(needle)
58
+ if idx < 0:
59
+ return None
60
+ return content.count("\n", 0, idx) + 1
61
+
62
+
63
+ class SwiftAnalyzer:
64
+ metadata = AnalyzerMetadata(
65
+ name="swift",
66
+ display_name="Swift Analyzer",
67
+ version="0.1.0",
68
+ description="Swift ecosystem analyzer: SwiftPM project detection and Package.resolved dependency SBOM. Server-side routes and app attack surface land in later slices.",
69
+ scope="Swift source trees and SwiftPM/Xcode projects. Detects the SwiftPM toolchain, server-side web frameworks in the manifest, and resolved package dependencies for CVE/SBOM.",
70
+ targets=["swift", "swiftpm", "vapor", "hummingbird"],
71
+ languages=["swift"],
72
+ priority=20,
73
+ experimental=True, # Early scaffold — coverage is intentionally partial.
74
+ enabled_by_default=True,
75
+ )
76
+
77
+ @property
78
+ def name(self) -> str:
79
+ return self.metadata.name
80
+
81
+ # ---------- Public entry points ----------
82
+
83
+ def detect(self, repo_path: str | Path) -> bool:
84
+ root = Path(repo_path).resolve()
85
+ if not root.exists() or not root.is_dir():
86
+ return False
87
+ for path in root.rglob("*"):
88
+ if any(part in SKIP_DIRS for part in path.parts):
89
+ continue
90
+ name = path.name
91
+ if name in {"Package.swift", "Package.resolved"}:
92
+ return True
93
+ if path.is_dir() and path.suffix in {".xcodeproj", ".xcworkspace"}:
94
+ return True
95
+ if path.is_file() and path.suffix == ".swift":
96
+ return True
97
+ return False
98
+
99
+ def analyze(self, repo_path: str | Path) -> ScanResult:
100
+ root = Path(repo_path).resolve()
101
+ result = ScanResult(root=str(root))
102
+ if not root.exists() or not root.is_dir():
103
+ return result
104
+
105
+ direct = self._analyze_manifest(root, result)
106
+ self._analyze_resolved(root, result, direct)
107
+ self._analyze_entrypoints(root, result)
108
+ return result
109
+
110
+ # ---------- Package.swift (manifest) ----------
111
+
112
+ def _analyze_manifest(self, root: Path, result: ScanResult) -> set[str]:
113
+ """Record SwiftPM + framework hints; return the set of *direct* dependency
114
+ identities declared in the manifest (used to mark resolved pins direct)."""
115
+ direct: set[str] = set()
116
+ for manifest in root.rglob("Package.swift"):
117
+ if any(part in SKIP_DIRS for part in manifest.parts):
118
+ continue
119
+ try:
120
+ text = manifest.read_text(encoding="utf-8", errors="replace")
121
+ except OSError:
122
+ continue
123
+ rel = self._rel(manifest, root)
124
+ result.framework_hints.append(FrameworkHint(
125
+ hint="SwiftPM", file=rel, line=1,
126
+ evidence_text="Package.swift present — Swift Package Manager project."))
127
+ for m in _PACKAGE_URL_RE.finditer(text):
128
+ url = m.group(1)
129
+ identity = _identity_from_url(url)
130
+ direct.add(identity)
131
+ for key, label in _FRAMEWORK_HINTS.items():
132
+ if key in identity:
133
+ result.framework_hints.append(FrameworkHint(
134
+ hint=label, file=rel, line=_line_of(text, url) or 1,
135
+ evidence_text=f"Declared dependency: {url}"))
136
+ return direct
137
+
138
+ # ---------- Package.resolved (SBOM) ----------
139
+
140
+ def _analyze_resolved(self, root: Path, result: ScanResult, direct: set[str]) -> None:
141
+ for resolved in root.rglob("Package.resolved"):
142
+ if any(part in SKIP_DIRS for part in resolved.parts):
143
+ continue
144
+ try:
145
+ data = json.loads(resolved.read_text(encoding="utf-8", errors="replace"))
146
+ except (OSError, json.JSONDecodeError):
147
+ continue # a malformed pin file must not abort the scan
148
+ rel = self._rel(resolved, root)
149
+ for pin in _iter_pins(data):
150
+ identity = (pin.get("identity") or pin.get("package") or "").lower()
151
+ if not identity:
152
+ continue
153
+ state = pin.get("state") or {}
154
+ version = state.get("version") or "" # branch/revision pins have no version
155
+ location = pin.get("location") or pin.get("repositoryURL") or ""
156
+ result.dependencies.append(DependencyHint(
157
+ name=identity,
158
+ version=version,
159
+ ecosystem="swiftpm",
160
+ file=rel,
161
+ resolved=True,
162
+ direct=identity in direct,
163
+ evidence_text=location or None,
164
+ source_analyzer="swift",
165
+ ))
166
+
167
+ # ---------- Entrypoints ----------
168
+
169
+ def _analyze_entrypoints(self, root: Path, result: ScanResult) -> None:
170
+ for path in root.rglob("*.swift"):
171
+ if any(part in SKIP_DIRS for part in path.parts):
172
+ continue
173
+ try:
174
+ text = path.read_text(encoding="utf-8", errors="replace")
175
+ except OSError:
176
+ continue
177
+ rel = self._rel(path, root)
178
+ if path.name == "main.swift":
179
+ result.entrypoint_hints.append(EntrypointHint(
180
+ hint="main.swift (top-level executable entrypoint)",
181
+ file=rel, line=1, evidence_text="Swift top-level main.swift"))
182
+ elif "@main" in text:
183
+ result.entrypoint_hints.append(EntrypointHint(
184
+ hint="@main attribute (executable entrypoint)",
185
+ file=rel, line=_line_of(text, "@main") or 1, evidence_text="@main"))
186
+
187
+ # ---------- helpers ----------
188
+
189
+ @staticmethod
190
+ def _rel(path: Path, root: Path) -> str:
191
+ try:
192
+ return str(path.resolve().relative_to(root)).replace("\\", "/")
193
+ except ValueError:
194
+ return path.name
195
+
196
+
197
+ def _iter_pins(data: object):
198
+ """Yield pin dicts across Package.resolved formats: v1 nests them under
199
+ ``object.pins``; v2/v3 put ``pins`` at the top level."""
200
+ if not isinstance(data, dict):
201
+ return
202
+ pins = data.get("pins")
203
+ if pins is None:
204
+ obj = data.get("object")
205
+ if isinstance(obj, dict):
206
+ pins = obj.get("pins")
207
+ if isinstance(pins, list):
208
+ for pin in pins:
209
+ if isinstance(pin, dict):
210
+ yield pin
@@ -0,0 +1,39 @@
1
+ """Re-exports of the AttackMap analyzer SDK surface this plugin depends on.
2
+
3
+ Everything here comes from the stable ``attackmap.sdk`` contract (requires
4
+ attackmap >= 0.4.29, which exports ``DependencyHint``). Kept in one place so a
5
+ future SDK move is a one-file change.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from attackmap.sdk.contracts import AnalyzerMetadata, AnalyzerProtocol
11
+ from attackmap.sdk.models import (
12
+ AuthHint,
13
+ DatabaseHint,
14
+ DependencyHint,
15
+ EntrypointHint,
16
+ ExternalCall,
17
+ FrameworkHint,
18
+ Route,
19
+ ScanResult,
20
+ SecretHint,
21
+ ServiceHint,
22
+ )
23
+
24
+ AttackMapAnalyzerProtocol = AnalyzerProtocol
25
+
26
+ __all__ = [
27
+ "AnalyzerMetadata",
28
+ "AttackMapAnalyzerProtocol",
29
+ "AuthHint",
30
+ "DatabaseHint",
31
+ "DependencyHint",
32
+ "EntrypointHint",
33
+ "ExternalCall",
34
+ "FrameworkHint",
35
+ "Route",
36
+ "ScanResult",
37
+ "SecretHint",
38
+ "ServiceHint",
39
+ ]
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: attackmap-analyzer-swift
3
+ Version: 0.1.0
4
+ Summary: Swift ecosystem analyzer plugin for AttackMap (SwiftPM projects, Package.resolved SBOM; server-side Vapor/Hummingbird and iOS/macOS app surface land in later slices).
5
+ Author: AttackMap Contributors
6
+ Author-email: Matthew Davis <matthewd@matthewd.xyz>
7
+ Maintainer-email: Matthew Davis <matthewd@matthewd.xyz>
8
+ License: MIT
9
+ Project-URL: Homepage, https://github.com/mlaify/attackmap-analyzer-swift
10
+ Project-URL: Repository, https://github.com/mlaify/attackmap-analyzer-swift
11
+ Project-URL: Issues, https://github.com/mlaify/attackmap-analyzer-swift/issues
12
+ Project-URL: Changelog, https://github.com/mlaify/attackmap-analyzer-swift/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,swift
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: License :: OSI Approved :: MIT License
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3 :: Only
26
+ Classifier: Topic :: Security
27
+ Classifier: Topic :: Software Development :: Quality Assurance
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.11
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Requires-Dist: pydantic>=2.7.0
33
+ Provides-Extra: core
34
+ Requires-Dist: attackmap>=0.4.29; 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.4.29; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # attackmap-analyzer-swift
42
+
43
+ Swift ecosystem analyzer plugin for [AttackMap](https://github.com/mlaify/AttackMap).
44
+ Auto-discovered via the `attackmap.analyzers` entry point once installed.
45
+
46
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
47
+
48
+ > **Status: early scaffold (v0.1.0).** SwiftPM project detection + a
49
+ > `Package.resolved` dependency SBOM. Server-side route extraction and the
50
+ > iOS/macOS app attack surface are on the roadmap (below).
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install attackmap-analyzer-swift # alongside attackmap>=0.4.29
56
+ ```
57
+
58
+ Then AttackMap picks it up automatically:
59
+
60
+ ```bash
61
+ attackmap analyze /path/to/swift/project
62
+ ```
63
+
64
+ ## What it does today
65
+
66
+ - **SwiftPM detection** — recognizes `Package.swift`, `.xcodeproj` / `.xcworkspace`,
67
+ and `.swift` sources; emits a `SwiftPM` framework hint.
68
+ - **Dependency SBOM** — parses `Package.resolved` (formats **v1**, **v2**, and **v3**,
69
+ including the copy Xcode stashes under `*.xcworkspace/xcshareddata/swiftpm/`) into
70
+ the scan's dependency inventory, marking direct vs. transitive by cross-referencing
71
+ the `Package.swift` manifest. Swift packages are inventoried but not CVE-matched —
72
+ OSV.dev has no SwiftPM ecosystem yet.
73
+ - **Framework hints** — flags server-side frameworks (Vapor, Hummingbird, Kitura,
74
+ Perfect) when declared in the manifest.
75
+ - **Entrypoints** — `main.swift` and `@main`.
76
+
77
+ ## Roadmap
78
+
79
+ - **Server-side routes** ([AttackMap#187](https://github.com/mlaify/AttackMap/issues/187)) — Vapor / Hummingbird route + auth-middleware extraction.
80
+ - **iOS/macOS app attack surface** ([AttackMap#188](https://github.com/mlaify/AttackMap/issues/188)) — URL schemes, universal links, `WKWebView` JS eval, Keychain / `UserDefaults` secrets, App Transport Security exceptions, pasteboard, XPC.
81
+
82
+ Part of the [Swift analyzer epic](https://github.com/mlaify/AttackMap/issues/204).
83
+
84
+ ## Development
85
+
86
+ ```bash
87
+ pip install -e ".[dev]"
88
+ pytest -q
89
+ ```
90
+
91
+ ## License
92
+
93
+ [MIT](LICENSE). Copyright (c) 2026 Matthew Davis and AttackMap Contributors.
@@ -0,0 +1,9 @@
1
+ attackmap_analyzer_swift/__init__.py,sha256=HWl7dOeWeXKWjEv33qWeioRMkzHct8u4aRU0ZmkkOB0,65
2
+ attackmap_analyzer_swift/analyzer.py,sha256=y9S7TqN6WO11WDt0Sm1ZnqXNuDWfkLRssNPVh4lTByY,8210
3
+ attackmap_analyzer_swift/contracts.py,sha256=G_cDkLCU_Uue4hErgzdiafFToi6rj0Mshq7txzzQmDk,894
4
+ attackmap_analyzer_swift-0.1.0.dist-info/licenses/LICENSE,sha256=vtPQVGUyxq359j0JmD3_eJaXyHmaHyjzaMOtaLwJc88,1097
5
+ attackmap_analyzer_swift-0.1.0.dist-info/METADATA,sha256=TSY9WDaeqLFebSfNHDVw0yfiaGErxfGFS-VZ56UHaDA,3962
6
+ attackmap_analyzer_swift-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
7
+ attackmap_analyzer_swift-0.1.0.dist-info/entry_points.txt,sha256=mK-fWLpGMGIXx34LpuTs8iinKAYfdE2eH4Uo9r0pjWw,69
8
+ attackmap_analyzer_swift-0.1.0.dist-info/top_level.txt,sha256=zEpqb9TzFle_vNrDOOdgaAnLxKCsWflwn-qx7-SZJR0,25
9
+ attackmap_analyzer_swift-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [attackmap.analyzers]
2
+ swift = attackmap_analyzer_swift:SwiftAnalyzer
@@ -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_swift