ecdat 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.
- ecdat/__init__.py +10 -0
- ecdat/__main__.py +5 -0
- ecdat/cli/__init__.py +203 -0
- ecdat/cli/commands/__init__.py +0 -0
- ecdat/cli/commands/about.py +130 -0
- ecdat/cli/commands/demo.py +116 -0
- ecdat/cli/commands/doctor.py +296 -0
- ecdat/cli/commands/help_cmd.py +205 -0
- ecdat/cli/commands/scan.py +228 -0
- ecdat/cli/commands/version_cmd.py +48 -0
- ecdat/cli/parser.py +87 -0
- ecdat/demo_project/auth/login.py +75 -0
- ecdat/demo_project/certs/cert_verify.go +81 -0
- ecdat/demo_project/keyexchange/channel.go +48 -0
- ecdat/demo_project/legacy/LegacyCrypto.java +78 -0
- ecdat/demo_project/payments/payment.py +64 -0
- ecdat/demo_project/quantum/pqc_utils.py +67 -0
- ecdat/demo_project/quantum/slh_signer.py +40 -0
- ecdat/demo_project/tokens/signing.js +54 -0
- ecdat/py.typed +0 -0
- ecdat/services/__init__.py +1 -0
- ecdat/services/crashlog.py +109 -0
- ecdat/services/demo.py +85 -0
- ecdat/services/paths.py +52 -0
- ecdat/services/scanner.py +248 -0
- ecdat/services/viewmodel.py +326 -0
- ecdat/ui/__init__.py +1 -0
- ecdat/ui/art3d.py +136 -0
- ecdat/ui/art_static.py +65 -0
- ecdat/ui/art_text.py +81 -0
- ecdat/ui/banner.py +148 -0
- ecdat/ui/console.py +119 -0
- ecdat/ui/motion.py +64 -0
- ecdat/ui/render.py +486 -0
- ecdat/ui/theme.py +173 -0
- ecdat-0.2.0.dist-info/METADATA +142 -0
- ecdat-0.2.0.dist-info/RECORD +51 -0
- ecdat-0.2.0.dist-info/WHEEL +5 -0
- ecdat-0.2.0.dist-info/entry_points.txt +2 -0
- ecdat-0.2.0.dist-info/licenses/LICENSE +21 -0
- ecdat-0.2.0.dist-info/top_level.txt +2 -0
- ecdat_core/__init__.py +6 -0
- ecdat_core/cbom_export.py +287 -0
- ecdat_core/cli.py +202 -0
- ecdat_core/detector.py +273 -0
- ecdat_core/ingestion.py +581 -0
- ecdat_core/models.py +145 -0
- ecdat_core/recommender.py +74 -0
- ecdat_core/risk_engine.py +264 -0
- ecdat_core/signature_loader.py +204 -0
- ecdat_core/signatures.json +692 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ecdat
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Cryptographic discovery and post-quantum readiness scanner — CLI with a gradient ASCII banner and an animated 3D globe (CBOM, CycloneDX 1.6)
|
|
5
|
+
Author: ECDAT Team
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/profm0r14rty/ecdat
|
|
8
|
+
Project-URL: Repository, https://github.com/profm0r14rty/ecdat
|
|
9
|
+
Project-URL: Issues, https://github.com/profm0r14rty/ecdat/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/profm0r14rty/ecdat/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: cbom,cli,cryptography,cyclonedx,post-quantum,pqc,quantum,sbom,security
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Information Technology
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Security
|
|
24
|
+
Classifier: Topic :: Security :: Cryptography
|
|
25
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: pydantic>=2
|
|
30
|
+
Requires-Dist: rich>=13.7
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
34
|
+
Requires-Dist: jsonschema; extra == "dev"
|
|
35
|
+
Requires-Dist: build; extra == "dev"
|
|
36
|
+
Requires-Dist: twine; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# ecdat
|
|
40
|
+
|
|
41
|
+
**ECDAT** is a [CycloneDX](https://cyclonedx.org/) **Cryptography Bill of
|
|
42
|
+
Materials (CBOM) scanner for post-quantum readiness**. One command finds the
|
|
43
|
+
cryptographic artefacts in a codebase — RSA, ECC, DH, DSA, MD5, SHA-1, DES,
|
|
44
|
+
3DES, RC4, AES, and others — scores each one's post-quantum risk with Mosca's
|
|
45
|
+
inequality, recommends NIST post-quantum replacements, and emits a real
|
|
46
|
+
CycloneDX 1.6 CBOM next to a dashboard-friendly risk summary.
|
|
47
|
+
|
|
48
|
+
It is a small, self-contained Python package (runtime dependencies: `pydantic`
|
|
49
|
+
and `rich`) — no FastAPI, Postgres, Redis, or Docker required. It runs fully
|
|
50
|
+
offline: the only network access is `git clone` for an explicit URL scan.
|
|
51
|
+
|
|
52
|
+
Why it is more than a lookalike scanner:
|
|
53
|
+
|
|
54
|
+
- **Real CBOM output.** The report is genuine CycloneDX 1.6, validated against
|
|
55
|
+
the official JSON Schema on every scan in the test suite, so Dependency-Track,
|
|
56
|
+
Syft, Grype, and other CycloneDX tooling parse it unchanged.
|
|
57
|
+
- **"Classically broken" is not "quantum vulnerable".** MD5, SHA-1, DES, 3DES,
|
|
58
|
+
and RC4 are reported as broken *today*; RSA, ECC, DH, and DSA as broken *by
|
|
59
|
+
Shor's algorithm*. An actively exploitable hash is never mislabeled
|
|
60
|
+
"quantum-safe".
|
|
61
|
+
- **Offline and private.** No telemetry, no update checks.
|
|
62
|
+
- **Crash-safe.** Expected errors print a clear message and exit code;
|
|
63
|
+
unexpected errors are written to a local crash log instead of a raw
|
|
64
|
+
traceback.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pipx install ecdat # recommended: isolated and on PATH
|
|
70
|
+
uv tool install ecdat # recommended if you use uv
|
|
71
|
+
pip install ecdat # into the active Python environment
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If plain `pip` reports `externally-managed-environment`, that is
|
|
75
|
+
[PEP 668](https://peps.python.org/pep-0668/) protecting your OS Python — use
|
|
76
|
+
`pipx` or `uv tool` (or a virtualenv) instead of forcing the install. On
|
|
77
|
+
Windows, install with `py -m pip install ecdat`; if `ecdat` is not on `PATH`,
|
|
78
|
+
run the CLI as `py -m ecdat …`.
|
|
79
|
+
|
|
80
|
+
## 60-second tour
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
ecdat demo # scan the bundled sample project
|
|
84
|
+
ecdat scan . # scan the current directory
|
|
85
|
+
ecdat scan https://github.com/<user>/<repo> # scan a public https:// repo
|
|
86
|
+
ecdat about # credits and an animated 3D globe
|
|
87
|
+
ecdat doctor # check your environment
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Commands
|
|
91
|
+
|
|
92
|
+
| Command | Description |
|
|
93
|
+
|---------|-------------|
|
|
94
|
+
| `ecdat scan <path-or-url>` | Scan a local directory, or an `https://` Git repository |
|
|
95
|
+
| `ecdat demo` | Scan the bundled, deliberately-insecure sample project (zero setup) |
|
|
96
|
+
| `ecdat doctor` | Environment self-check with a one-line fix per problem |
|
|
97
|
+
| `ecdat about` | Credits, project links, and a spinning 3D globe |
|
|
98
|
+
| `ecdat help [command]` | Command overview, or full help for one command |
|
|
99
|
+
| `ecdat version` | Version, engine, signature count, Python, and OS |
|
|
100
|
+
|
|
101
|
+
Global flags: `--version`, `--no-color`, and `--debug`.
|
|
102
|
+
|
|
103
|
+
## Formats
|
|
104
|
+
|
|
105
|
+
`ecdat scan --format <pretty|json|cbom|summary>`:
|
|
106
|
+
|
|
107
|
+
| Format | Contents |
|
|
108
|
+
|--------|----------|
|
|
109
|
+
| `pretty` | Colourised terminal report (default) |
|
|
110
|
+
| `json` | The full `ScanResult` |
|
|
111
|
+
| `cbom` | A CycloneDX 1.6 CBOM |
|
|
112
|
+
| `summary` | The risk rollup used by dashboards |
|
|
113
|
+
|
|
114
|
+
Payload formats write **only** the payload to stdout; progress, warnings, and
|
|
115
|
+
errors go to stderr — so `ecdat scan . -f cbom > cbom.json` is safe to pipe.
|
|
116
|
+
`ecdat demo` supports `pretty`, `json`, and `summary`.
|
|
117
|
+
|
|
118
|
+
For CI, `ecdat scan . --fail-on high` exits `1` when a finding is at or above
|
|
119
|
+
that level (`quantum-safe` findings never count) and `0` when clean. The other
|
|
120
|
+
exit codes are `2` (usage/validation error), `3` (scan/runtime/unexpected
|
|
121
|
+
error), and `130` (interrupted with `Ctrl-C`).
|
|
122
|
+
|
|
123
|
+
## Guarantees
|
|
124
|
+
|
|
125
|
+
- **Offline by default.** Only `git clone` for an explicit `https://` URL scan
|
|
126
|
+
touches the network; local scans never do.
|
|
127
|
+
- **No telemetry and no update checks.**
|
|
128
|
+
- **Injection-safe rendering.** File paths, matched snippets, and algorithm
|
|
129
|
+
names from scanned (untrusted) code are rendered as plain text and cannot
|
|
130
|
+
inject terminal escapes or Rich/HTML/Markdown markup.
|
|
131
|
+
- **Validated Git-URL scanning.** Only `https://` URLs are cloned, and the host
|
|
132
|
+
is resolved and checked against private/reserved address ranges before any
|
|
133
|
+
clone runs.
|
|
134
|
+
|
|
135
|
+
## Links
|
|
136
|
+
|
|
137
|
+
- Source, issues, and documentation: <https://github.com/profm0r14rty/ecdat>
|
|
138
|
+
- Security policy: <https://github.com/profm0r14rty/ecdat/blob/main/SECURITY.md>
|
|
139
|
+
- Changelog: <https://github.com/profm0r14rty/ecdat/blob/main/CHANGELOG.md>
|
|
140
|
+
- Live demo dashboard: <https://ecdat-web.onrender.com>
|
|
141
|
+
- Live demo API: <https://ecdat-api.onrender.com>
|
|
142
|
+
- License (MIT): <https://github.com/profm0r14rty/ecdat/blob/main/LICENSE>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
ecdat/__init__.py,sha256=BOTeAsSSsyvPoPc3UcLBTGX1oRgO_36k1NpCsxQLMYU,255
|
|
2
|
+
ecdat/__main__.py,sha256=CJz3NsJwYFPOuY4zrLVg6TEh18GHKabXuHz_3apmqT0,99
|
|
3
|
+
ecdat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
ecdat/cli/__init__.py,sha256=k9MqA6JYPNvSGmSmNuSgFRBgJI5oqxaXLENvn8eg1nQ,6235
|
|
5
|
+
ecdat/cli/parser.py,sha256=7wRLlRsA4apj5lcOTTF0sjubEFdY4A8RiLQr7Z4NlzQ,3065
|
|
6
|
+
ecdat/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
ecdat/cli/commands/about.py,sha256=WqhlTylTU3lUSn5mqkEnGQo2nJbdYo4HjQRBQsjEb4Y,4306
|
|
8
|
+
ecdat/cli/commands/demo.py,sha256=xn9oSqY4ssfyTlxqlIXFfKFXxLxpU9KBm9qtepJaJYQ,3831
|
|
9
|
+
ecdat/cli/commands/doctor.py,sha256=aUVuXzn-KFQCSgPOupO5cD13lF2vWy4i_oKIPC80_Hk,8696
|
|
10
|
+
ecdat/cli/commands/help_cmd.py,sha256=ezPxwpPc_yNknO-tInUpA2jDTqsmz-AibC36JnBAKkk,6281
|
|
11
|
+
ecdat/cli/commands/scan.py,sha256=8veZUGvuadiHNtroqX_lorX_xIz24bf4Wu28AlXlqKo,7021
|
|
12
|
+
ecdat/cli/commands/version_cmd.py,sha256=jcvnp70vLiH5getr9N67YJxVgblZP1KdA7WQmNBp6Ds,1480
|
|
13
|
+
ecdat/demo_project/auth/login.py,sha256=rZ4zI7vX5c4eMWeF1rpKdIfuEjU_2S9PeO9JMCKZKTI,2496
|
|
14
|
+
ecdat/demo_project/certs/cert_verify.go,sha256=6Xhj5rCH9FtLD8xMaIdpI1UfEBVQp45wphOpPisXgBE,2381
|
|
15
|
+
ecdat/demo_project/keyexchange/channel.go,sha256=j_6VmbMfSIKvNbsf2F2yKls8G4otVe2wdd22ErWM3K8,1441
|
|
16
|
+
ecdat/demo_project/legacy/LegacyCrypto.java,sha256=qIja8rRhe9_k715Kid_pJm8drtEF7A7Iq9no5rZ2lA4,2624
|
|
17
|
+
ecdat/demo_project/payments/payment.py,sha256=ZNaL5bB9W6gXgzQ0lovMEM6R4MnmINFcDsqcBgiTtt4,2076
|
|
18
|
+
ecdat/demo_project/quantum/pqc_utils.py,sha256=wQQ0grfyAeRyrVNjJJRdtYEzE4UOvTqT6SLiL0fHIak,2233
|
|
19
|
+
ecdat/demo_project/quantum/slh_signer.py,sha256=AOuS0VVyw2PvTEz_UZGZ8tKmgOa2_xHwF9bLoZDW2u4,1258
|
|
20
|
+
ecdat/demo_project/tokens/signing.js,sha256=GEKuJsOgi40tHMztx0AhN4c08N2lOjaYsi5iddtrao4,1575
|
|
21
|
+
ecdat/services/__init__.py,sha256=AlwMW3oG-5vx_ygyd4d_0ekYqw_ACERqtSUeF3zCL38,31
|
|
22
|
+
ecdat/services/crashlog.py,sha256=CEBdSBwKg-qQpTjULFka2ag1EmbCWXpIVKG1SFuaX5Y,3243
|
|
23
|
+
ecdat/services/demo.py,sha256=Yhqci4jgzEacBOLgW_Bz5yUzHrvcAL4eIhk4y6mNeLc,3001
|
|
24
|
+
ecdat/services/paths.py,sha256=rOlu2kq_gZZMhE6rQ2ApC_oLs0RY6biuHgBVhQjDFPc,1649
|
|
25
|
+
ecdat/services/scanner.py,sha256=ybfCcp1LKA9xmz3bddjIzPkPqlRCF-NO7Z3BLYpj3k0,7886
|
|
26
|
+
ecdat/services/viewmodel.py,sha256=NLRpbPV8nZGDp1CKVucXCC_JGzYUeXSvLdQ0_YL6O0E,10150
|
|
27
|
+
ecdat/ui/__init__.py,sha256=_-6m7QQDrN6HW4FCW8RzjvnfQWLcsOrYuhER0Xnh4gM,74
|
|
28
|
+
ecdat/ui/art3d.py,sha256=kbtd8ErI26jx5Qq3j57V6v-ZqHFgi15x5ESp9foCtTM,5799
|
|
29
|
+
ecdat/ui/art_static.py,sha256=sC5XaAGBnl9WAJ47ebpW7ePekZubHa8qQ7NB2eKxhrg,1980
|
|
30
|
+
ecdat/ui/art_text.py,sha256=MrCWwXeYS_vvbm3LXD93CvwPMMSRW2o95C-koX3EZXM,2320
|
|
31
|
+
ecdat/ui/banner.py,sha256=baIb1LetcHIPCBm06GYSytdUyE-RSKHwMYcMjThWJMA,5204
|
|
32
|
+
ecdat/ui/console.py,sha256=lpv4deyTM_PS-GwibFQXL5wWswFmkkpsk4peCTlW_K0,3815
|
|
33
|
+
ecdat/ui/motion.py,sha256=TJlJdHEsG_MWZo76zJO2OPhqqiq93ejNIrJhu8YDu_0,1837
|
|
34
|
+
ecdat/ui/render.py,sha256=9x_i9JAnaIyiIBtsm5JHarpNC0j3vfN6p6E2lzjkOpI,14846
|
|
35
|
+
ecdat/ui/theme.py,sha256=vVJXUj9i0fakirAKiCzjafX6TsuUYwOIPF8IENm-n5g,5773
|
|
36
|
+
ecdat-0.2.0.dist-info/licenses/LICENSE,sha256=dMfAf8nlYhDH6bgNsWVgX_Fbsz3urRxtIinmgnn687Y,1071
|
|
37
|
+
ecdat_core/__init__.py,sha256=1O7qWp5bcCESexCZmARnuvPd0r5TRbqCNQPBOF04iJU,314
|
|
38
|
+
ecdat_core/cbom_export.py,sha256=fqN7qCfXUZ9ge9qsPqj1gJ3OLpIRsH3bp40Mk-hvEVU,9849
|
|
39
|
+
ecdat_core/cli.py,sha256=2pJVk7J5RV5qOcNsAPGLwZ1kKMKDlk3Ep9eQ9jMIC8k,7285
|
|
40
|
+
ecdat_core/detector.py,sha256=F-xuL6i3Qk_IDp2wxJnyEoqFXyuMrkpWLtXaOjMV5Rk,10596
|
|
41
|
+
ecdat_core/ingestion.py,sha256=ADbxEL60UG0oH6aPqRiO_e1T-t28ebJNiy4X0pl3KRA,20918
|
|
42
|
+
ecdat_core/models.py,sha256=mtIz8LFTO1Ove-agL04C82SdXAmzrHHNbhp1LdNpl6o,5456
|
|
43
|
+
ecdat_core/recommender.py,sha256=6CYpVHlFUZU5_4oMt9mPpgHAIddIpbw2KnEPCP6oihk,2940
|
|
44
|
+
ecdat_core/risk_engine.py,sha256=5tX7fyxjFzmh4Twp4QnWg9hc0xXoVhPLOIZAxca1Oe4,10663
|
|
45
|
+
ecdat_core/signature_loader.py,sha256=YyWS53wVswP7Vs8RWcWe0NIyHPxrzlK270HE2AzvRPQ,7648
|
|
46
|
+
ecdat_core/signatures.json,sha256=LcPyquVkaJMSNQdOIPgbeReFtpfG7eORkaw19DoGh0Y,24736
|
|
47
|
+
ecdat-0.2.0.dist-info/METADATA,sha256=hb61C3OOa1LN13El9W78VWu_q7aaQBfZNYUNyBjZhqg,6347
|
|
48
|
+
ecdat-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
49
|
+
ecdat-0.2.0.dist-info/entry_points.txt,sha256=lYK7UoQ8ncEZghNNdeRH1Cao2zkUcqjb-7lxh_wXzIg,41
|
|
50
|
+
ecdat-0.2.0.dist-info/top_level.txt,sha256=qKYEK2ZH4q1NHurvUFQBLetpx7XqH9NJtqtRIQv9zeY,17
|
|
51
|
+
ecdat-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Errorists Team
|
|
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.
|
ecdat_core/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""ECDAT - Enterprise Cryptographic Discovery & Analysis Tool.
|
|
2
|
+
|
|
3
|
+
A Cryptography Bill of Materials (CBOM) scanner for post-quantum-readiness assessment.
|
|
4
|
+
Discovers cryptographic artefacts in source code, assesses their quantum-computing risk
|
|
5
|
+
using Mosca's algorithm, and recommends NIST post-quantum replacements.
|
|
6
|
+
"""
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""CycloneDX 1.6 CBOM export module for ECDAT.
|
|
2
|
+
|
|
3
|
+
Converts an ECDAT :class:`ScanResult` into a dict matching the real
|
|
4
|
+
CycloneDX 1.6 BOM JSON structure, and provides a lightweight summary
|
|
5
|
+
format optimised for frontend consumption.
|
|
6
|
+
|
|
7
|
+
Public API:
|
|
8
|
+
- :func:`export_cbom` -> full CycloneDX 1.6 BOM dict.
|
|
9
|
+
- :func:`export_summary` -> frontend-friendly summary dict.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections import Counter
|
|
15
|
+
from uuid import uuid4
|
|
16
|
+
|
|
17
|
+
from ecdat_core.models import Detection, Recommendation, RiskAssessment, ScanResult
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# ECDAT tool identity embedded in every CBOM.
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
_TOOL_NAME = "ECDAT"
|
|
23
|
+
_TOOL_VERSION = "0.1.0"
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Algorithm family keywords → CycloneDX ``algorithmProperties.primitive``.
|
|
27
|
+
# Checked case-insensitively against ``algorithm_family``.
|
|
28
|
+
#
|
|
29
|
+
# The values MUST be members of the official CycloneDX 1.6
|
|
30
|
+
# ``algorithmProperties.primitive`` enum (verified against
|
|
31
|
+
# bom-1.6.schema.json): ``signature``, ``hash``, ``block-cipher``,
|
|
32
|
+
# ``key-agree``, ``kem``, ``pke``, ... — note ``key-agree``, NOT
|
|
33
|
+
# ``key-agreement``.
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
_PRIMITIVE_RULES: list[tuple[str, str]] = [
|
|
36
|
+
("sign", "signature"),
|
|
37
|
+
("dsa", "signature"),
|
|
38
|
+
("ecdsa", "signature"),
|
|
39
|
+
("sha", "hash"),
|
|
40
|
+
("md5", "hash"),
|
|
41
|
+
("hash", "hash"),
|
|
42
|
+
("aes", "block-cipher"),
|
|
43
|
+
("des", "block-cipher"),
|
|
44
|
+
("blowfish", "block-cipher"),
|
|
45
|
+
("cast", "block-cipher"),
|
|
46
|
+
("idea", "block-cipher"),
|
|
47
|
+
("rc4", "block-cipher"),
|
|
48
|
+
("chacha", "block-cipher"),
|
|
49
|
+
("salsa", "block-cipher"),
|
|
50
|
+
("rsa", "pke"),
|
|
51
|
+
("dh", "key-agree"),
|
|
52
|
+
("ecdh", "key-agree"),
|
|
53
|
+
("key-agreement", "key-agree"),
|
|
54
|
+
("key-exchange", "key-agree"),
|
|
55
|
+
("kem", "kem"),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Public API
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def export_cbom(scan_result: ScanResult) -> dict:
|
|
65
|
+
"""Produce a CycloneDX 1.6 BOM dict from a :class:`ScanResult`.
|
|
66
|
+
|
|
67
|
+
The returned dict conforms to the real CycloneDX 1.6 specification with
|
|
68
|
+
``bomFormat`` set to ``"CycloneDX"``, ``specVersion`` set to ``"1.6"``,
|
|
69
|
+
and each detection mapped to a ``cryptographic-asset`` component.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
scan_result: The complete scan result to export.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
A dict matching the CycloneDX 1.6 BOM structure.
|
|
76
|
+
"""
|
|
77
|
+
risk_lookup: dict[str, RiskAssessment] = {
|
|
78
|
+
ra.detection_id: ra for ra in scan_result.risk_assessments
|
|
79
|
+
}
|
|
80
|
+
rec_lookup: dict[str, Recommendation] = {
|
|
81
|
+
rec.detection_id: rec for rec in scan_result.recommendations
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
components = [
|
|
85
|
+
_build_component(det, risk_lookup, rec_lookup)
|
|
86
|
+
for det in scan_result.detections
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"bomFormat": "CycloneDX",
|
|
91
|
+
"specVersion": "1.6",
|
|
92
|
+
"serialNumber": f"urn:uuid:{uuid4()}",
|
|
93
|
+
"version": 1,
|
|
94
|
+
"metadata": {
|
|
95
|
+
"timestamp": scan_result.scanned_at,
|
|
96
|
+
"tools": {
|
|
97
|
+
"components": [
|
|
98
|
+
{
|
|
99
|
+
"type": "application",
|
|
100
|
+
"name": _TOOL_NAME,
|
|
101
|
+
"version": _TOOL_VERSION,
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"component": {
|
|
106
|
+
"type": "application",
|
|
107
|
+
"name": scan_result.target,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
"components": components,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def export_summary(scan_result: ScanResult) -> dict:
|
|
115
|
+
"""Produce a frontend-friendly summary dict from a :class:`ScanResult`.
|
|
116
|
+
|
|
117
|
+
Includes detection counts, quantum-vulnerability statistics, risk-level
|
|
118
|
+
breakdown, algorithm-family distribution, and the five highest-urgency
|
|
119
|
+
detections.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
scan_result: The complete scan result to summarise.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
A dict with aggregated statistics ready for frontend consumption.
|
|
126
|
+
"""
|
|
127
|
+
total = len(scan_result.detections)
|
|
128
|
+
vuln_count = sum(
|
|
129
|
+
1 for det in scan_result.detections if det.quantum_vulnerable
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
risk_lookup: dict[str, RiskAssessment] = {
|
|
133
|
+
ra.detection_id: ra for ra in scan_result.risk_assessments
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Risk level counts (initialise all buckets so the frontend always sees
|
|
137
|
+
# every level even when the count is zero).
|
|
138
|
+
risk_level_counts: dict[str, int] = {
|
|
139
|
+
"critical": 0,
|
|
140
|
+
"high": 0,
|
|
141
|
+
"medium": 0,
|
|
142
|
+
"low": 0,
|
|
143
|
+
"quantum-safe": 0,
|
|
144
|
+
}
|
|
145
|
+
for ra in scan_result.risk_assessments:
|
|
146
|
+
risk_level_counts[ra.risk_level] = risk_level_counts.get(ra.risk_level, 0) + 1
|
|
147
|
+
|
|
148
|
+
# Algorithm family counts.
|
|
149
|
+
family_counter: Counter[str] = Counter(
|
|
150
|
+
det.algorithm_family for det in scan_result.detections
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Top-5 highest-urgency detections.
|
|
154
|
+
top_5 = _top_urgency(scan_result.detections, risk_lookup, limit=5)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"total_detections": total,
|
|
158
|
+
"quantum_vulnerable_count": vuln_count,
|
|
159
|
+
"quantum_vulnerable_percentage": (vuln_count / total * 100.0) if total else 0.0,
|
|
160
|
+
"risk_level_counts": risk_level_counts,
|
|
161
|
+
"algorithm_family_counts": dict(family_counter),
|
|
162
|
+
"top_5_urgency": top_5,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
# Private helpers
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _classify_primitive(algorithm_family: str) -> str | None:
|
|
172
|
+
"""Best-effort classification of an algorithm family to a CycloneDX primitive.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
algorithm_family: The algorithm family name (e.g. ``"RSA"``, ``"SHA"``).
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
One of ``"signature"``, ``"hash"``, ``"block-cipher"``,
|
|
179
|
+
``"key-agree"``, ``"kem"``, ``"pke"``, or ``None`` if unclassifiable.
|
|
180
|
+
Returned values are members of the official CycloneDX 1.6
|
|
181
|
+
``algorithmProperties.primitive`` enum.
|
|
182
|
+
"""
|
|
183
|
+
family_lower = algorithm_family.lower()
|
|
184
|
+
for keyword, primitive in _PRIMITIVE_RULES:
|
|
185
|
+
if keyword in family_lower:
|
|
186
|
+
return primitive
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _build_component(
|
|
191
|
+
detection: Detection,
|
|
192
|
+
risk_lookup: dict[str, RiskAssessment],
|
|
193
|
+
rec_lookup: dict[str, Recommendation],
|
|
194
|
+
) -> dict:
|
|
195
|
+
"""Build a single CycloneDX component dict for a detection.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
detection: The detection to represent as a component.
|
|
199
|
+
risk_lookup: Map from detection ID to its risk assessment.
|
|
200
|
+
rec_lookup: Map from detection ID to its recommendation.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
A dict matching the CycloneDX 1.6 component structure for
|
|
204
|
+
``cryptographic-asset`` components.
|
|
205
|
+
"""
|
|
206
|
+
ra = risk_lookup.get(detection.id)
|
|
207
|
+
rec = rec_lookup.get(detection.id)
|
|
208
|
+
|
|
209
|
+
# --- cryptoProperties ---
|
|
210
|
+
crypto_props: dict = {"assetType": detection.asset_type}
|
|
211
|
+
if detection.asset_type == "algorithm":
|
|
212
|
+
algo_props: dict = {}
|
|
213
|
+
primitive = _classify_primitive(detection.algorithm_family)
|
|
214
|
+
# The official CycloneDX 1.6 schema types ``primitive`` as a string
|
|
215
|
+
# enum without ``null``: an unclassifiable family is emitted as the
|
|
216
|
+
# schema-sanctioned "unknown" rather than null (which the schema
|
|
217
|
+
# rejects) or an omitted key.
|
|
218
|
+
algo_props["primitive"] = primitive if primitive is not None else "unknown"
|
|
219
|
+
if detection.key_size_bits is not None:
|
|
220
|
+
algo_props["parameterSetIdentifier"] = str(detection.key_size_bits)
|
|
221
|
+
crypto_props["algorithmProperties"] = algo_props
|
|
222
|
+
|
|
223
|
+
# --- properties ---
|
|
224
|
+
properties = [
|
|
225
|
+
{
|
|
226
|
+
"name": "ecdat:quantumVulnerable",
|
|
227
|
+
"value": "true" if detection.quantum_vulnerable else "false",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "ecdat:classicallyBroken",
|
|
231
|
+
"value": "true" if detection.classically_broken else "false",
|
|
232
|
+
},
|
|
233
|
+
{"name": "ecdat:riskLevel", "value": ra.risk_level if ra else ""},
|
|
234
|
+
{"name": "ecdat:confidence", "value": str(detection.confidence)},
|
|
235
|
+
{"name": "ecdat:recommendedAlgorithm", "value": rec.recommended_algorithm if rec else ""},
|
|
236
|
+
{"name": "ecdat:fipsReference", "value": rec.fips_reference if rec else ""},
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
"type": "cryptographic-asset",
|
|
241
|
+
"bom-ref": detection.id,
|
|
242
|
+
"name": detection.algorithm_family,
|
|
243
|
+
"cryptoProperties": crypto_props,
|
|
244
|
+
"evidence": {
|
|
245
|
+
"occurrences": [
|
|
246
|
+
{"location": detection.file_path, "line": detection.line_number}
|
|
247
|
+
]
|
|
248
|
+
},
|
|
249
|
+
"properties": properties,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _top_urgency(
|
|
254
|
+
detections: list[Detection],
|
|
255
|
+
risk_lookup: dict[str, RiskAssessment],
|
|
256
|
+
limit: int = 5,
|
|
257
|
+
) -> list[dict]:
|
|
258
|
+
"""Return the *limit* highest-urgency detections as a list of dicts.
|
|
259
|
+
|
|
260
|
+
Each dict contains ``detection_id``, ``algorithm_family``, ``risk_level``,
|
|
261
|
+
``urgency_ratio``, ``file_path``, and ``line_number``.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
detections: All detections to consider.
|
|
265
|
+
risk_lookup: Map from detection ID to its risk assessment.
|
|
266
|
+
limit: Maximum number of results (default 5).
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
A list of dicts sorted by ``urgency_ratio`` descending.
|
|
270
|
+
"""
|
|
271
|
+
paired = []
|
|
272
|
+
for det in detections:
|
|
273
|
+
ra = risk_lookup.get(det.id)
|
|
274
|
+
if ra is None:
|
|
275
|
+
continue
|
|
276
|
+
paired.append(
|
|
277
|
+
{
|
|
278
|
+
"detection_id": det.id,
|
|
279
|
+
"algorithm_family": det.algorithm_family,
|
|
280
|
+
"risk_level": ra.risk_level,
|
|
281
|
+
"urgency_ratio": ra.urgency_ratio,
|
|
282
|
+
"file_path": det.file_path,
|
|
283
|
+
"line_number": det.line_number,
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
paired.sort(key=lambda item: item["urgency_ratio"], reverse=True)
|
|
287
|
+
return paired[:limit]
|