attackmap-analyzer-swift 0.1.0__tar.gz
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.
- attackmap_analyzer_swift-0.1.0/LICENSE +21 -0
- attackmap_analyzer_swift-0.1.0/PKG-INFO +93 -0
- attackmap_analyzer_swift-0.1.0/README.md +53 -0
- attackmap_analyzer_swift-0.1.0/pyproject.toml +75 -0
- attackmap_analyzer_swift-0.1.0/setup.cfg +4 -0
- attackmap_analyzer_swift-0.1.0/src/attackmap_analyzer_swift/__init__.py +3 -0
- attackmap_analyzer_swift-0.1.0/src/attackmap_analyzer_swift/analyzer.py +210 -0
- attackmap_analyzer_swift-0.1.0/src/attackmap_analyzer_swift/contracts.py +39 -0
- attackmap_analyzer_swift-0.1.0/src/attackmap_analyzer_swift.egg-info/PKG-INFO +93 -0
- attackmap_analyzer_swift-0.1.0/src/attackmap_analyzer_swift.egg-info/SOURCES.txt +13 -0
- attackmap_analyzer_swift-0.1.0/src/attackmap_analyzer_swift.egg-info/dependency_links.txt +1 -0
- attackmap_analyzer_swift-0.1.0/src/attackmap_analyzer_swift.egg-info/entry_points.txt +2 -0
- attackmap_analyzer_swift-0.1.0/src/attackmap_analyzer_swift.egg-info/requires.txt +9 -0
- attackmap_analyzer_swift-0.1.0/src/attackmap_analyzer_swift.egg-info/top_level.txt +1 -0
- attackmap_analyzer_swift-0.1.0/tests/test_swift_analyzer.py +127 -0
|
@@ -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,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)
|
|
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,53 @@
|
|
|
1
|
+
# attackmap-analyzer-swift
|
|
2
|
+
|
|
3
|
+
Swift ecosystem analyzer plugin for [AttackMap](https://github.com/mlaify/AttackMap).
|
|
4
|
+
Auto-discovered via the `attackmap.analyzers` entry point once installed.
|
|
5
|
+
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
> **Status: early scaffold (v0.1.0).** SwiftPM project detection + a
|
|
9
|
+
> `Package.resolved` dependency SBOM. Server-side route extraction and the
|
|
10
|
+
> iOS/macOS app attack surface are on the roadmap (below).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install attackmap-analyzer-swift # alongside attackmap>=0.4.29
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then AttackMap picks it up automatically:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
attackmap analyze /path/to/swift/project
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What it does today
|
|
25
|
+
|
|
26
|
+
- **SwiftPM detection** — recognizes `Package.swift`, `.xcodeproj` / `.xcworkspace`,
|
|
27
|
+
and `.swift` sources; emits a `SwiftPM` framework hint.
|
|
28
|
+
- **Dependency SBOM** — parses `Package.resolved` (formats **v1**, **v2**, and **v3**,
|
|
29
|
+
including the copy Xcode stashes under `*.xcworkspace/xcshareddata/swiftpm/`) into
|
|
30
|
+
the scan's dependency inventory, marking direct vs. transitive by cross-referencing
|
|
31
|
+
the `Package.swift` manifest. Swift packages are inventoried but not CVE-matched —
|
|
32
|
+
OSV.dev has no SwiftPM ecosystem yet.
|
|
33
|
+
- **Framework hints** — flags server-side frameworks (Vapor, Hummingbird, Kitura,
|
|
34
|
+
Perfect) when declared in the manifest.
|
|
35
|
+
- **Entrypoints** — `main.swift` and `@main`.
|
|
36
|
+
|
|
37
|
+
## Roadmap
|
|
38
|
+
|
|
39
|
+
- **Server-side routes** ([AttackMap#187](https://github.com/mlaify/AttackMap/issues/187)) — Vapor / Hummingbird route + auth-middleware extraction.
|
|
40
|
+
- **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.
|
|
41
|
+
|
|
42
|
+
Part of the [Swift analyzer epic](https://github.com/mlaify/AttackMap/issues/204).
|
|
43
|
+
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install -e ".[dev]"
|
|
48
|
+
pytest -q
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
[MIT](LICENSE). Copyright (c) 2026 Matthew Davis and AttackMap Contributors.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "attackmap-analyzer-swift"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Swift ecosystem analyzer plugin for AttackMap (SwiftPM projects, Package.resolved SBOM; server-side Vapor/Hummingbird and iOS/macOS app surface land in later slices)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Matthew Davis", email = "matthewd@matthewd.xyz"},
|
|
14
|
+
{name = "AttackMap Contributors"},
|
|
15
|
+
]
|
|
16
|
+
maintainers = [
|
|
17
|
+
{name = "Matthew Davis", email = "matthewd@matthewd.xyz"},
|
|
18
|
+
]
|
|
19
|
+
keywords = [
|
|
20
|
+
"security",
|
|
21
|
+
"static-analysis",
|
|
22
|
+
"appsec",
|
|
23
|
+
"defensive-security",
|
|
24
|
+
"attackmap",
|
|
25
|
+
"attackmap-plugin",
|
|
26
|
+
"swift",
|
|
27
|
+
]
|
|
28
|
+
classifiers = [
|
|
29
|
+
"Development Status :: 4 - Beta",
|
|
30
|
+
"Intended Audience :: Developers",
|
|
31
|
+
"Intended Audience :: Information Technology",
|
|
32
|
+
"Intended Audience :: System Administrators",
|
|
33
|
+
"License :: OSI Approved :: MIT License",
|
|
34
|
+
"Operating System :: OS Independent",
|
|
35
|
+
"Programming Language :: Python :: 3",
|
|
36
|
+
"Programming Language :: Python :: 3.11",
|
|
37
|
+
"Programming Language :: Python :: 3.12",
|
|
38
|
+
"Programming Language :: Python :: 3.13",
|
|
39
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
40
|
+
"Topic :: Security",
|
|
41
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
42
|
+
"Typing :: Typed",
|
|
43
|
+
]
|
|
44
|
+
dependencies = [
|
|
45
|
+
"pydantic>=2.7.0",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[project.optional-dependencies]
|
|
49
|
+
core = [
|
|
50
|
+
"attackmap>=0.4.29",
|
|
51
|
+
]
|
|
52
|
+
dev = [
|
|
53
|
+
"pytest>=8.0.0",
|
|
54
|
+
"build>=1.2.0",
|
|
55
|
+
"attackmap>=0.4.29",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[project.urls]
|
|
59
|
+
Homepage = "https://github.com/mlaify/attackmap-analyzer-swift"
|
|
60
|
+
Repository = "https://github.com/mlaify/attackmap-analyzer-swift"
|
|
61
|
+
Issues = "https://github.com/mlaify/attackmap-analyzer-swift/issues"
|
|
62
|
+
Changelog = "https://github.com/mlaify/attackmap-analyzer-swift/blob/main/CHANGELOG.md"
|
|
63
|
+
"AttackMap core" = "https://github.com/mlaify/AttackMap"
|
|
64
|
+
|
|
65
|
+
[project.entry-points."attackmap.analyzers"]
|
|
66
|
+
swift = "attackmap_analyzer_swift:SwiftAnalyzer"
|
|
67
|
+
|
|
68
|
+
[tool.setuptools]
|
|
69
|
+
package-dir = {"" = "src"}
|
|
70
|
+
|
|
71
|
+
[tool.setuptools.packages.find]
|
|
72
|
+
where = ["src"]
|
|
73
|
+
|
|
74
|
+
[tool.pytest.ini_options]
|
|
75
|
+
testpaths = ["tests"]
|
|
@@ -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)
|
|
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,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/attackmap_analyzer_swift/__init__.py
|
|
5
|
+
src/attackmap_analyzer_swift/analyzer.py
|
|
6
|
+
src/attackmap_analyzer_swift/contracts.py
|
|
7
|
+
src/attackmap_analyzer_swift.egg-info/PKG-INFO
|
|
8
|
+
src/attackmap_analyzer_swift.egg-info/SOURCES.txt
|
|
9
|
+
src/attackmap_analyzer_swift.egg-info/dependency_links.txt
|
|
10
|
+
src/attackmap_analyzer_swift.egg-info/entry_points.txt
|
|
11
|
+
src/attackmap_analyzer_swift.egg-info/requires.txt
|
|
12
|
+
src/attackmap_analyzer_swift.egg-info/top_level.txt
|
|
13
|
+
tests/test_swift_analyzer.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
attackmap_analyzer_swift
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from attackmap_analyzer_swift import SwiftAnalyzer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _write(root: Path, rel: str, content: str) -> None:
|
|
8
|
+
p = root / rel
|
|
9
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
10
|
+
p.write_text(content, encoding="utf-8")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
PACKAGE_SWIFT = """// swift-tools-version:5.9
|
|
14
|
+
import PackageDescription
|
|
15
|
+
|
|
16
|
+
let package = Package(
|
|
17
|
+
name: "MyServer",
|
|
18
|
+
dependencies: [
|
|
19
|
+
.package(url: "https://github.com/vapor/vapor.git", from: "4.89.0"),
|
|
20
|
+
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
|
|
21
|
+
],
|
|
22
|
+
targets: [.executableTarget(name: "MyServer", dependencies: ["Vapor"])]
|
|
23
|
+
)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Package.resolved v2/v3 (Xcode 13.3+ / SwiftPM): top-level "pins".
|
|
27
|
+
RESOLVED_V2 = json.dumps({
|
|
28
|
+
"pins": [
|
|
29
|
+
{"identity": "vapor", "kind": "remoteSourceControl",
|
|
30
|
+
"location": "https://github.com/vapor/vapor.git",
|
|
31
|
+
"state": {"version": "4.89.0", "revision": "abc"}},
|
|
32
|
+
{"identity": "swift-nio", "kind": "remoteSourceControl",
|
|
33
|
+
"location": "https://github.com/apple/swift-nio.git",
|
|
34
|
+
"state": {"version": "2.63.0", "revision": "def"}},
|
|
35
|
+
],
|
|
36
|
+
"version": 2,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
# Package.resolved v1 (Xcode 11–13): pins nested under "object".
|
|
40
|
+
RESOLVED_V1 = json.dumps({
|
|
41
|
+
"object": {"pins": [
|
|
42
|
+
{"package": "Alamofire", "repositoryURL": "https://github.com/Alamofire/Alamofire.git",
|
|
43
|
+
"state": {"version": "5.8.1", "revision": "xyz"}},
|
|
44
|
+
]},
|
|
45
|
+
"version": 1,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_detect_swiftpm_project(tmp_path: Path):
|
|
50
|
+
_write(tmp_path, "Package.swift", PACKAGE_SWIFT)
|
|
51
|
+
assert SwiftAnalyzer().detect(tmp_path) is True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_detect_by_source_file(tmp_path: Path):
|
|
55
|
+
_write(tmp_path, "Sources/App/main.swift", 'print("hi")\n')
|
|
56
|
+
assert SwiftAnalyzer().detect(tmp_path) is True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_detect_false_on_unrelated_repo(tmp_path: Path):
|
|
60
|
+
_write(tmp_path, "app.py", "print('hi')\n")
|
|
61
|
+
assert SwiftAnalyzer().detect(tmp_path) is False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_detect_ignores_build_dir(tmp_path: Path):
|
|
65
|
+
_write(tmp_path, ".build/checkouts/vapor/Package.swift", PACKAGE_SWIFT)
|
|
66
|
+
assert SwiftAnalyzer().detect(tmp_path) is False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_framework_and_swiftpm_hints(tmp_path: Path):
|
|
70
|
+
_write(tmp_path, "Package.swift", PACKAGE_SWIFT)
|
|
71
|
+
result = SwiftAnalyzer().analyze(tmp_path)
|
|
72
|
+
hints = [h.hint for h in result.framework_hints]
|
|
73
|
+
assert "SwiftPM" in hints
|
|
74
|
+
assert any("Vapor" in h for h in hints)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_resolved_v2_sbom(tmp_path: Path):
|
|
78
|
+
_write(tmp_path, "Package.swift", PACKAGE_SWIFT)
|
|
79
|
+
_write(tmp_path, "Package.resolved", RESOLVED_V2)
|
|
80
|
+
result = SwiftAnalyzer().analyze(tmp_path)
|
|
81
|
+
deps = {d.name: d for d in result.dependencies}
|
|
82
|
+
assert deps["vapor"].version == "4.89.0"
|
|
83
|
+
assert deps["vapor"].ecosystem == "swiftpm"
|
|
84
|
+
assert deps["vapor"].resolved is True
|
|
85
|
+
assert deps["vapor"].direct is True # declared in Package.swift
|
|
86
|
+
assert deps["swift-nio"].version == "2.63.0"
|
|
87
|
+
assert deps["swift-nio"].direct is True # apple/swift-nio is declared
|
|
88
|
+
assert deps["vapor"].source_analyzer == "swift"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_resolved_v1_sbom(tmp_path: Path):
|
|
92
|
+
# No Package.swift → no direct set → everything transitive/false.
|
|
93
|
+
_write(tmp_path, "Package.resolved", RESOLVED_V1)
|
|
94
|
+
result = SwiftAnalyzer().analyze(tmp_path)
|
|
95
|
+
deps = {d.name: d for d in result.dependencies}
|
|
96
|
+
assert deps["alamofire"].version == "5.8.1"
|
|
97
|
+
assert deps["alamofire"].direct is False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_resolved_in_xcode_workspace_path(tmp_path: Path):
|
|
101
|
+
# Xcode stashes Package.resolved under the workspace's xcshareddata.
|
|
102
|
+
_write(tmp_path, "App.xcworkspace/xcshareddata/swiftpm/Package.resolved", RESOLVED_V2)
|
|
103
|
+
result = SwiftAnalyzer().analyze(tmp_path)
|
|
104
|
+
assert {d.name for d in result.dependencies} == {"vapor", "swift-nio"}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_malformed_resolved_does_not_crash(tmp_path: Path):
|
|
108
|
+
_write(tmp_path, "Package.swift", PACKAGE_SWIFT)
|
|
109
|
+
_write(tmp_path, "Package.resolved", "{ this is : not json ]")
|
|
110
|
+
result = SwiftAnalyzer().analyze(tmp_path) # must not raise
|
|
111
|
+
assert result.dependencies == []
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_entrypoints(tmp_path: Path):
|
|
115
|
+
_write(tmp_path, "Sources/App/main.swift", 'import Foundation\nprint("run")\n')
|
|
116
|
+
_write(tmp_path, "Sources/App/App.swift", "@main\nstruct App {}\n")
|
|
117
|
+
result = SwiftAnalyzer().analyze(tmp_path)
|
|
118
|
+
hints = " ".join(h.hint for h in result.entrypoint_hints)
|
|
119
|
+
assert "main.swift" in hints
|
|
120
|
+
assert "@main" in hints
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_metadata_contract():
|
|
124
|
+
m = SwiftAnalyzer().metadata
|
|
125
|
+
assert m.name == "swift"
|
|
126
|
+
assert "swift" in m.languages
|
|
127
|
+
assert SwiftAnalyzer().name == "swift"
|