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.
Files changed (51) hide show
  1. ecdat/__init__.py +10 -0
  2. ecdat/__main__.py +5 -0
  3. ecdat/cli/__init__.py +203 -0
  4. ecdat/cli/commands/__init__.py +0 -0
  5. ecdat/cli/commands/about.py +130 -0
  6. ecdat/cli/commands/demo.py +116 -0
  7. ecdat/cli/commands/doctor.py +296 -0
  8. ecdat/cli/commands/help_cmd.py +205 -0
  9. ecdat/cli/commands/scan.py +228 -0
  10. ecdat/cli/commands/version_cmd.py +48 -0
  11. ecdat/cli/parser.py +87 -0
  12. ecdat/demo_project/auth/login.py +75 -0
  13. ecdat/demo_project/certs/cert_verify.go +81 -0
  14. ecdat/demo_project/keyexchange/channel.go +48 -0
  15. ecdat/demo_project/legacy/LegacyCrypto.java +78 -0
  16. ecdat/demo_project/payments/payment.py +64 -0
  17. ecdat/demo_project/quantum/pqc_utils.py +67 -0
  18. ecdat/demo_project/quantum/slh_signer.py +40 -0
  19. ecdat/demo_project/tokens/signing.js +54 -0
  20. ecdat/py.typed +0 -0
  21. ecdat/services/__init__.py +1 -0
  22. ecdat/services/crashlog.py +109 -0
  23. ecdat/services/demo.py +85 -0
  24. ecdat/services/paths.py +52 -0
  25. ecdat/services/scanner.py +248 -0
  26. ecdat/services/viewmodel.py +326 -0
  27. ecdat/ui/__init__.py +1 -0
  28. ecdat/ui/art3d.py +136 -0
  29. ecdat/ui/art_static.py +65 -0
  30. ecdat/ui/art_text.py +81 -0
  31. ecdat/ui/banner.py +148 -0
  32. ecdat/ui/console.py +119 -0
  33. ecdat/ui/motion.py +64 -0
  34. ecdat/ui/render.py +486 -0
  35. ecdat/ui/theme.py +173 -0
  36. ecdat-0.2.0.dist-info/METADATA +142 -0
  37. ecdat-0.2.0.dist-info/RECORD +51 -0
  38. ecdat-0.2.0.dist-info/WHEEL +5 -0
  39. ecdat-0.2.0.dist-info/entry_points.txt +2 -0
  40. ecdat-0.2.0.dist-info/licenses/LICENSE +21 -0
  41. ecdat-0.2.0.dist-info/top_level.txt +2 -0
  42. ecdat_core/__init__.py +6 -0
  43. ecdat_core/cbom_export.py +287 -0
  44. ecdat_core/cli.py +202 -0
  45. ecdat_core/detector.py +273 -0
  46. ecdat_core/ingestion.py +581 -0
  47. ecdat_core/models.py +145 -0
  48. ecdat_core/recommender.py +74 -0
  49. ecdat_core/risk_engine.py +264 -0
  50. ecdat_core/signature_loader.py +204 -0
  51. 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ecdat = ecdat.cli:main
@@ -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.
@@ -0,0 +1,2 @@
1
+ ecdat
2
+ ecdat_core
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]