cobalt-sbom 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.
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: cobalt-sbom
3
+ Version: 0.1.0
4
+ Summary: Cryptographic Bill of Materials generator — CycloneDX 1.5, ML-DSA signed
5
+ Author-email: QreativeLab / OMEGA <dominik@qreativelab.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/dom-omg/cobalt-sbom
8
+ Project-URL: Repository, https://github.com/dom-omg/cobalt-sbom
9
+ Keywords: cbom,sbom,pqc,cryptography,cyclonedx,quantum
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Information Technology
13
+ Classifier: Topic :: Security :: Cryptography
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: rich>=13.0
20
+ Requires-Dist: requests>=2.31
21
+ Requires-Dist: click>=8.1
22
+
23
+ # COBALT SBOM
24
+
25
+ **Cryptographic Bill of Materials generator — CycloneDX 1.6, ML-DSA-65 signed.**
26
+
27
+ Scan any codebase in seconds. Find every cryptographic algorithm. Know your quantum exposure.
28
+
29
+ ## What it does
30
+
31
+ - Detects 30+ crypto primitives across Python, TypeScript, JavaScript, Go, C/C++, Java, Rust
32
+ - Outputs a signed **CycloneDX 1.6 CBOM** (Cryptographic Bill of Materials)
33
+ - Scores your **quantum readiness (0-100)**
34
+ - Flags broken algorithms (DES, MD5, RC4) and deprecated ones (SHA-1, TLS-1.0)
35
+ - Integrates into CI/CD via GitHub Actions — gate PRs on quantum safety
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install cobalt-sbom
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```bash
46
+ # Scan a repo
47
+ cobalt-sbom scan ./myrepo --output cbom.cdx.json
48
+
49
+ # Scan + sign with ML-DSA-65 (via AXIOM)
50
+ cobalt-sbom scan ./myrepo --output cbom.cdx.json --sign
51
+
52
+ # CI mode — fail if quantum-unsafe algorithms present
53
+ cobalt-sbom scan ./myrepo --ci
54
+
55
+ # Fail if quantum readiness score below 80
56
+ cobalt-sbom scan ./myrepo --fail-score 80
57
+
58
+ # Display an existing CBOM
59
+ cobalt-sbom show cbom.cdx.json
60
+ ```
61
+
62
+ ## GitHub Actions
63
+
64
+ Add to `.github/workflows/cbom.yml`:
65
+
66
+ ```yaml
67
+ - name: Install cobalt-sbom
68
+ run: pip install cobalt-sbom
69
+
70
+ - name: Scan cryptographic assets
71
+ run: cobalt-sbom scan . --output cbom.cdx.json --sign --fail-score 60
72
+ ```
73
+
74
+ ## Output format
75
+
76
+ CycloneDX 1.6 JSON — compatible with all CBOM/SBOM toolchains:
77
+
78
+ ```json
79
+ {
80
+ "bomFormat": "CycloneDX",
81
+ "specVersion": "1.6",
82
+ "components": [
83
+ {
84
+ "type": "cryptographic-asset",
85
+ "name": "ML-DSA-65",
86
+ "cryptoProperties": {
87
+ "assetType": "algorithm",
88
+ "algorithmProperties": {
89
+ "primitive": "sign",
90
+ "nistQuantumSecurityLevel": 3,
91
+ "cryptoFunctions": ["sign", "verify"]
92
+ },
93
+ "quantumSafety": "quantum-safe"
94
+ }
95
+ }
96
+ ],
97
+ "summary": {
98
+ "quantum_readiness_score": 72,
99
+ "quantum_unsafe": 15,
100
+ "quantum_safe": 16,
101
+ "broken_algorithms": ["DES", "SHA-1"]
102
+ }
103
+ }
104
+ ```
105
+
106
+ ## Why CBOM
107
+
108
+ Governments (USA EO 14028, EU CRA, NIST IR 8547) are mandating cryptographic inventories. The CBOM Working Group (CycloneDX) is standardizing the format. **First movers own the toolchain.**
109
+
110
+ ---
111
+
112
+ Built on [OMEGA](https://omega-sovereign.fly.dev) — sovereign intelligence stack.
113
+ Signing powered by [AXIOM](https://axiom-trust.fly.dev) — ML-DSA-65 post-quantum certificates.
@@ -0,0 +1,91 @@
1
+ # COBALT SBOM
2
+
3
+ **Cryptographic Bill of Materials generator — CycloneDX 1.6, ML-DSA-65 signed.**
4
+
5
+ Scan any codebase in seconds. Find every cryptographic algorithm. Know your quantum exposure.
6
+
7
+ ## What it does
8
+
9
+ - Detects 30+ crypto primitives across Python, TypeScript, JavaScript, Go, C/C++, Java, Rust
10
+ - Outputs a signed **CycloneDX 1.6 CBOM** (Cryptographic Bill of Materials)
11
+ - Scores your **quantum readiness (0-100)**
12
+ - Flags broken algorithms (DES, MD5, RC4) and deprecated ones (SHA-1, TLS-1.0)
13
+ - Integrates into CI/CD via GitHub Actions — gate PRs on quantum safety
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install cobalt-sbom
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ # Scan a repo
25
+ cobalt-sbom scan ./myrepo --output cbom.cdx.json
26
+
27
+ # Scan + sign with ML-DSA-65 (via AXIOM)
28
+ cobalt-sbom scan ./myrepo --output cbom.cdx.json --sign
29
+
30
+ # CI mode — fail if quantum-unsafe algorithms present
31
+ cobalt-sbom scan ./myrepo --ci
32
+
33
+ # Fail if quantum readiness score below 80
34
+ cobalt-sbom scan ./myrepo --fail-score 80
35
+
36
+ # Display an existing CBOM
37
+ cobalt-sbom show cbom.cdx.json
38
+ ```
39
+
40
+ ## GitHub Actions
41
+
42
+ Add to `.github/workflows/cbom.yml`:
43
+
44
+ ```yaml
45
+ - name: Install cobalt-sbom
46
+ run: pip install cobalt-sbom
47
+
48
+ - name: Scan cryptographic assets
49
+ run: cobalt-sbom scan . --output cbom.cdx.json --sign --fail-score 60
50
+ ```
51
+
52
+ ## Output format
53
+
54
+ CycloneDX 1.6 JSON — compatible with all CBOM/SBOM toolchains:
55
+
56
+ ```json
57
+ {
58
+ "bomFormat": "CycloneDX",
59
+ "specVersion": "1.6",
60
+ "components": [
61
+ {
62
+ "type": "cryptographic-asset",
63
+ "name": "ML-DSA-65",
64
+ "cryptoProperties": {
65
+ "assetType": "algorithm",
66
+ "algorithmProperties": {
67
+ "primitive": "sign",
68
+ "nistQuantumSecurityLevel": 3,
69
+ "cryptoFunctions": ["sign", "verify"]
70
+ },
71
+ "quantumSafety": "quantum-safe"
72
+ }
73
+ }
74
+ ],
75
+ "summary": {
76
+ "quantum_readiness_score": 72,
77
+ "quantum_unsafe": 15,
78
+ "quantum_safe": 16,
79
+ "broken_algorithms": ["DES", "SHA-1"]
80
+ }
81
+ }
82
+ ```
83
+
84
+ ## Why CBOM
85
+
86
+ Governments (USA EO 14028, EU CRA, NIST IR 8547) are mandating cryptographic inventories. The CBOM Working Group (CycloneDX) is standardizing the format. **First movers own the toolchain.**
87
+
88
+ ---
89
+
90
+ Built on [OMEGA](https://omega-sovereign.fly.dev) — sovereign intelligence stack.
91
+ Signing powered by [AXIOM](https://axiom-trust.fly.dev) — ML-DSA-65 post-quantum certificates.
@@ -0,0 +1,2 @@
1
+ """COBALT SBOM — Cryptographic Bill of Materials generator."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,194 @@
1
+ """COBALT SBOM CLI."""
2
+
3
+ from __future__ import annotations
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+ from rich import box
12
+ from rich.panel import Panel
13
+ from rich.text import Text
14
+
15
+ from .detectors import scan_repo, CryptoAsset
16
+ from .cyclonedx import build_cbom
17
+ from .signer import sign_cbom
18
+
19
+ console = Console()
20
+
21
+ QUANTUM_SAFE_ALGOS = {"ML-KEM", "ML-DSA", "FN-DSA", "SLH-DSA", "AES", "ChaCha20", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-512", "HMAC-SHA256", "HMAC-SHA384", "HMAC-SHA512"}
22
+
23
+
24
+ def _safety_color(safety: str) -> str:
25
+ return {"quantum-safe": "green", "quantum-unsafe": "red", "hybrid": "yellow"}.get(safety, "dim")
26
+
27
+
28
+ @click.group()
29
+ def main() -> None:
30
+ """COBALT SBOM — Cryptographic Bill of Materials generator."""
31
+
32
+
33
+ @main.command()
34
+ @click.argument("target", type=click.Path(exists=True, file_okay=False))
35
+ @click.option("--output", "-o", default="cbom.cdx.json", help="Output file path")
36
+ @click.option("--sign", is_flag=True, default=False, help="Sign with ML-DSA-65 via AXIOM")
37
+ @click.option("--include-tests", is_flag=True, default=False, help="Include test files")
38
+ @click.option("--ci", is_flag=True, default=False, help="CI mode: exit 1 if quantum-unsafe algorithms detected")
39
+ @click.option("--fail-score", default=0, type=int, help="CI: fail if quantum readiness score below threshold (0-100)")
40
+ @click.option("--json-only", is_flag=True, default=False, help="Suppress terminal output, write JSON only")
41
+ def scan(
42
+ target: str,
43
+ output: str,
44
+ sign: bool,
45
+ include_tests: bool,
46
+ ci: bool,
47
+ fail_score: int,
48
+ json_only: bool,
49
+ ) -> None:
50
+ """Scan TARGET repo and generate a CycloneDX 1.6 CBOM."""
51
+ target_path = Path(target)
52
+ target = target_path # type: ignore[assignment]
53
+ if not json_only:
54
+ console.print(Panel(
55
+ f"[bold cyan]COBALT SBOM[/bold cyan] Cryptographic Bill of Materials\n"
56
+ f"Target: [cyan]{target.resolve()}[/cyan]",
57
+ box=box.ROUNDED,
58
+ ))
59
+
60
+ assets: list[CryptoAsset] = scan_repo(target, include_tests=include_tests)
61
+
62
+ if not json_only:
63
+ console.print(f" Scanned: [bold]{len(list(target.rglob('*')))}[/bold] files — "
64
+ f"[bold]{len(assets)}[/bold] crypto usages found\n")
65
+
66
+ signature = None
67
+ if sign:
68
+ if not json_only:
69
+ console.print(" Signing with ML-DSA-65 via AXIOM...", end=" ")
70
+ signature = sign_cbom({})
71
+ if not json_only:
72
+ console.print("[green]✓[/green]" if signature else "[yellow]⚠ AXIOM unreachable, unsigned[/yellow]")
73
+
74
+ cbom = build_cbom(assets, target, signature)
75
+ out_path = Path(output)
76
+ out_path.write_text(json.dumps(cbom, indent=2))
77
+
78
+ if not json_only:
79
+ _print_summary(cbom, assets)
80
+
81
+ if not json_only:
82
+ console.print(f"\n Output: [cyan]{out_path.resolve()}[/cyan]")
83
+
84
+ score: int = cbom["summary"]["quantum_readiness_score"]
85
+
86
+ if ci:
87
+ unsafe_algos = cbom["summary"]["quantum_unsafe"]
88
+ if unsafe_algos > 0:
89
+ console.print(f"\n[red]CI FAIL[/red] — {unsafe_algos} quantum-unsafe algorithm(s) detected", err=True)
90
+ sys.exit(1)
91
+
92
+ if fail_score and score < fail_score:
93
+ console.print(f"\n[red]CI FAIL[/red] — quantum readiness score {score} < threshold {fail_score}", err=True)
94
+ sys.exit(1)
95
+
96
+
97
+ @main.command()
98
+ @click.argument("cbom_file", type=click.Path(exists=True))
99
+ def show(cbom_file: str) -> None:
100
+ """Display a CBOM file in a human-readable table."""
101
+ cbom = json.loads(Path(cbom_file).read_text())
102
+ assets_raw = cbom.get("components", [])
103
+
104
+ table = Table(show_header=True, header_style="bold cyan", box=box.SIMPLE_HEAVY)
105
+ table.add_column("Algorithm", style="bold")
106
+ table.add_column("Type")
107
+ table.add_column("Primitive")
108
+ table.add_column("Library")
109
+ table.add_column("Quantum")
110
+ table.add_column("Bits")
111
+ table.add_column("NIST PQC Level")
112
+
113
+ for comp in assets_raw:
114
+ cp = comp.get("cryptoProperties", {})
115
+ ap = cp.get("algorithmProperties", {})
116
+ safety = cp.get("quantumSafety", "unknown")
117
+ color = _safety_color(safety)
118
+ table.add_row(
119
+ comp.get("name", ""),
120
+ cp.get("assetType", ""),
121
+ ap.get("primitive", ""),
122
+ ap.get("implementationPlatform", ""),
123
+ f"[{color}]{safety}[/{color}]",
124
+ str(ap.get("classicalSecurityLevel", "—")),
125
+ str(ap.get("nistQuantumSecurityLevel", "—")),
126
+ )
127
+
128
+ console.print(table)
129
+
130
+ summary = cbom.get("summary", {})
131
+ score = summary.get("quantum_readiness_score", 0)
132
+ score_color = "green" if score >= 80 else ("yellow" if score >= 50 else "red")
133
+ console.print(f"\nQuantum Readiness Score: [{score_color}]{score}/100[/{score_color}]")
134
+
135
+ broken = summary.get("broken_algorithms", [])
136
+ if broken:
137
+ console.print(f"[red]Broken algorithms (immediate risk):[/red] {', '.join(broken)}")
138
+
139
+ deprecated = summary.get("deprecated_algorithms", [])
140
+ if deprecated:
141
+ console.print(f"[yellow]Deprecated algorithms:[/yellow] {', '.join(deprecated)}")
142
+
143
+
144
+ def _print_summary(cbom: dict, assets: list[CryptoAsset]) -> None:
145
+ summary = cbom["summary"]
146
+ score: int = summary["quantum_readiness_score"]
147
+ score_color = "green" if score >= 80 else ("yellow" if score >= 50 else "red")
148
+
149
+ table = Table(show_header=True, header_style="bold", box=box.SIMPLE_HEAVY)
150
+ table.add_column("Algorithm", style="bold")
151
+ table.add_column("Primitive")
152
+ table.add_column("Library")
153
+ table.add_column("Quantum Safety")
154
+ table.add_column("Classical Bits")
155
+ table.add_column("NIST PQC Level")
156
+ table.add_column("Occurrences")
157
+
158
+ from collections import Counter
159
+ counts: Counter[str] = Counter(a.name for a in assets)
160
+
161
+ seen: set[str] = set()
162
+ for a in assets:
163
+ if a.name in seen:
164
+ continue
165
+ seen.add(a.name)
166
+ color = _safety_color(a.quantum_safety)
167
+ table.add_row(
168
+ a.name,
169
+ a.primitive,
170
+ a.library,
171
+ f"[{color}]{a.quantum_safety}[/{color}]",
172
+ str(a.classical_bits) if a.classical_bits else "—",
173
+ str(a.nist_quantum_level) if a.nist_quantum_level else "—",
174
+ str(counts[a.name]),
175
+ )
176
+
177
+ console.print(table)
178
+
179
+ broken = summary.get("broken_algorithms", [])
180
+ deprecated = summary.get("deprecated_algorithms", [])
181
+
182
+ score_text = Text(f" Quantum Readiness Score: {score}/100", style=f"bold {score_color}")
183
+ console.print(score_text)
184
+
185
+ if broken:
186
+ console.print(f" [bold red]CRITICAL — Broken algorithms:[/bold red] {', '.join(broken)}")
187
+ if deprecated:
188
+ console.print(f" [bold yellow]WARNING — Deprecated algorithms:[/bold yellow] {', '.join(deprecated)}")
189
+
190
+ console.print(
191
+ f"\n Quantum-safe: [green]{summary['quantum_safe']}[/green] "
192
+ f"Quantum-unsafe: [red]{summary['quantum_unsafe']}[/red] "
193
+ f"Total unique: [bold]{summary['unique_algorithms']}[/bold]"
194
+ )
@@ -0,0 +1,190 @@
1
+ """CycloneDX 1.5 CBOM serializer — spec-compliant."""
2
+
3
+ from __future__ import annotations
4
+ import json
5
+ import uuid
6
+ from collections import defaultdict
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from .detectors import CryptoAsset
13
+
14
+ # CycloneDX 1.5 spec: cryptoFunctions valid values
15
+ _CRYPTO_FUNCTIONS: dict[str, list[str]] = {
16
+ "ae": ["encrypt", "decrypt"],
17
+ "hash": ["digest"],
18
+ "ecc": ["sign", "verify"],
19
+ "dsa": ["sign", "verify"],
20
+ "rsa": ["sign", "verify", "encrypt", "decrypt"],
21
+ "ecDH": ["keyDerive"],
22
+ "ff-dh": ["keyDerive"],
23
+ "post-quantum": ["encapsulate", "decapsulate", "sign", "verify"],
24
+ "mac": ["generate", "verify"],
25
+ "kdf": ["derive"],
26
+ "drbg": ["generate"],
27
+ "stream-cipher": ["encrypt", "decrypt"],
28
+ "other": [],
29
+ }
30
+
31
+ # CycloneDX 1.5 spec valid values for executionEnvironment
32
+ _EXEC_ENV = "software-plain-ram"
33
+
34
+
35
+ def _aggregate(assets: list[CryptoAsset]) -> list[dict]:
36
+ """Group by algorithm name, aggregate all occurrences and libraries."""
37
+ groups: dict[str, dict] = {}
38
+
39
+ for a in assets:
40
+ key = a.name
41
+ if key not in groups:
42
+ groups[key] = {
43
+ "asset": a,
44
+ "occurrences": [],
45
+ "libraries": set(),
46
+ }
47
+ groups[key]["occurrences"].append({
48
+ "location": a.location,
49
+ "line": a.line,
50
+ "symbol": a.evidence[:100] if a.evidence else "",
51
+ })
52
+ if a.library and a.library != "unknown":
53
+ groups[key]["libraries"].add(a.library)
54
+
55
+ return list(groups.values())
56
+
57
+
58
+ def _component(group: dict, idx: int) -> dict:
59
+ a: CryptoAsset = group["asset"]
60
+ occurrences: list[dict] = group["occurrences"]
61
+ libraries: set[str] = group["libraries"]
62
+
63
+ crypto_funcs = _CRYPTO_FUNCTIONS.get(a.primitive, [])
64
+ # Post-quantum: distinguish KEM vs signature by name
65
+ if a.primitive == "post-quantum":
66
+ name_up = a.name.upper()
67
+ if "KEM" in name_up or "KYBER" in name_up:
68
+ crypto_funcs = ["encapsulate", "decapsulate"]
69
+ else:
70
+ crypto_funcs = ["sign", "verify"]
71
+
72
+ algo: dict = {
73
+ "primitive": a.primitive,
74
+ "executionEnvironment": _EXEC_ENV,
75
+ "cryptoFunctions": crypto_funcs,
76
+ }
77
+
78
+ if a.param_set:
79
+ algo["parameterSetIdentifier"] = a.param_set
80
+ if a.curve:
81
+ algo["curve"] = a.curve
82
+ if a.mode:
83
+ algo["mode"] = a.mode
84
+ if a.classical_bits:
85
+ algo["classicalSecurityLevel"] = a.classical_bits
86
+ if a.nist_quantum_level:
87
+ algo["nistQuantumSecurityLevel"] = a.nist_quantum_level
88
+
89
+ crypto_props: dict = {
90
+ "assetType": a.asset_type,
91
+ "algorithmProperties": algo,
92
+ }
93
+ if a.oid:
94
+ crypto_props["oid"] = a.oid
95
+
96
+ # Custom properties: quantum safety + library (not in spec, use properties array)
97
+ props = [
98
+ {"name": "cobalt:quantumSafety", "value": a.quantum_safety},
99
+ ]
100
+ if libraries:
101
+ props.append({"name": "cobalt:library", "value": ", ".join(sorted(libraries))})
102
+ props.append({"name": "cobalt:occurrenceCount", "value": str(len(occurrences))})
103
+
104
+ comp: dict = {
105
+ "type": "cryptographic-asset",
106
+ "bom-ref": f"crypto-{idx}",
107
+ "name": a.name,
108
+ "cryptoProperties": crypto_props,
109
+ # CycloneDX 1.5 spec: occurrences go in evidence.occurrences
110
+ "evidence": {
111
+ "occurrences": occurrences,
112
+ },
113
+ "properties": props,
114
+ }
115
+
116
+ return comp
117
+
118
+
119
+ def _metadata(root: Path) -> dict:
120
+ return {
121
+ "timestamp": datetime.now(timezone.utc).isoformat(),
122
+ "tools": [
123
+ {
124
+ "type": "application",
125
+ "vendor": "OMEGA / QreativeLab",
126
+ "name": "COBALT SBOM",
127
+ "version": "0.1.0",
128
+ "externalReferences": [
129
+ {"type": "website", "url": "https://axiom-trust.fly.dev"},
130
+ ],
131
+ }
132
+ ],
133
+ "component": {
134
+ "type": "library",
135
+ "name": root.name,
136
+ "bom-ref": "root-component",
137
+ },
138
+ }
139
+
140
+
141
+ def build_cbom(
142
+ assets: list[CryptoAsset],
143
+ root: Path,
144
+ signature: dict | None = None,
145
+ ) -> dict:
146
+ groups = _aggregate(assets)
147
+ serial = str(uuid.uuid4())
148
+
149
+ cbom: dict = {
150
+ "bomFormat": "CycloneDX",
151
+ "specVersion": "1.5",
152
+ "serialNumber": f"urn:uuid:{serial}",
153
+ "version": 1,
154
+ "metadata": _metadata(root),
155
+ "components": [_component(g, i) for i, g in enumerate(groups)],
156
+ "summary": _summary(assets),
157
+ }
158
+
159
+ if signature:
160
+ cbom["signature"] = signature
161
+
162
+ return cbom
163
+
164
+
165
+ def _summary(assets: list[CryptoAsset]) -> dict:
166
+ unsafe = {a.name for a in assets if a.quantum_safety == "quantum-unsafe"}
167
+ safe = {a.name for a in assets if a.quantum_safety == "quantum-safe"}
168
+ unique_names = {a.name for a in assets}
169
+ broken = {a.name for a in assets if 0 < a.classical_bits < 112}
170
+ deprecated = {"MD5", "SHA-1", "DES", "RC4", "3DES", "TLS-1.0", "TLS-1.2"}
171
+
172
+ return {
173
+ "total_findings": len(assets),
174
+ "unique_algorithms": len(unique_names),
175
+ "quantum_unsafe": len(unsafe),
176
+ "quantum_safe": len(safe),
177
+ "broken_algorithms": sorted(broken),
178
+ "deprecated_algorithms": sorted(deprecated & unique_names),
179
+ "quantum_readiness_score": _readiness_score(assets),
180
+ }
181
+
182
+
183
+ def _readiness_score(assets: list[CryptoAsset]) -> int:
184
+ if not assets:
185
+ return 100
186
+ total = len(assets)
187
+ unsafe = sum(1 for a in assets if a.quantum_safety == "quantum-unsafe")
188
+ broken = sum(1 for a in assets if 0 < a.classical_bits < 112)
189
+ score = max(0, 100 - int((unsafe / total) * 70) - int((broken / total) * 30))
190
+ return score
@@ -0,0 +1,390 @@
1
+ """Multi-language cryptographic asset detectors for CBOM generation."""
2
+
3
+ from __future__ import annotations
4
+ import re
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Literal
8
+
9
+ AssetType = Literal["algorithm", "protocol", "related-crypto-material", "certificate"]
10
+ # CycloneDX 1.5 CBOM primitive values
11
+ Primitive = Literal[
12
+ "ae", "dsa", "ecc", "ecDH", "ecMQV", "factoring", "ff-dh",
13
+ "hash", "integer-factorization", "kdf", "lattice", "mac",
14
+ "nestedEncryption", "other", "pke", "post-quantum", "rsa",
15
+ "stream-cipher", "xor", "drbg",
16
+ ]
17
+ QuantumSafety = Literal["quantum-safe", "quantum-unsafe", "hybrid", "unknown"]
18
+
19
+ NIST_QUANTUM_LEVELS: dict[str, int] = {
20
+ "ml-kem-512": 1, "kyber512": 1,
21
+ "ml-kem-768": 3, "kyber768": 3,
22
+ "ml-kem-1024": 5, "kyber1024": 5,
23
+ "ml-dsa-44": 2, "dilithium2": 2,
24
+ "ml-dsa-65": 3, "dilithium3": 3,
25
+ "ml-dsa-87": 5, "dilithium5": 5,
26
+ "slh-dsa-shake-128s": 1,
27
+ "slh-dsa-shake-192s": 3,
28
+ "slh-dsa-shake-256s": 5,
29
+ "sphincs+-shake-128s": 1,
30
+ "fn-dsa-512": 1, "falcon512": 1,
31
+ "fn-dsa-1024": 5, "falcon1024": 5,
32
+ }
33
+
34
+ CLASSICAL_SECURITY_BITS: dict[str, int] = {
35
+ "rsa-1024": 80, "rsa-2048": 112, "rsa-3072": 128, "rsa-4096": 140,
36
+ "ecdsa-p-256": 128, "ecdsa-p-384": 192, "ecdsa-p-521": 260,
37
+ "ecdsa-p256": 128, "ecdsa-p384": 192, "ecdsa-p521": 260,
38
+ "ecdsa-secp256k1": 128,
39
+ "ed25519": 128, "ed448": 224,
40
+ "ecdh-p-256": 128, "ecdh-p-384": 192, "ecdh-p-521": 260,
41
+ "ecdh-p256": 128, "ecdh-p384": 192,
42
+ "x25519": 128, "x448": 224,
43
+ "aes-128": 128, "aes-192": 192, "aes-256": 256,
44
+ "chacha20": 256, "chacha20-poly1305": 256,
45
+ "sha-1": 80, "sha-256": 128, "sha-384": 192, "sha-512": 256,
46
+ "sha3-256": 128, "sha3-512": 256,
47
+ "hmac-sha256": 128, "hmac-sha384": 192, "hmac-sha512": 256,
48
+ "3des": 112, "des": 56, "rc4": 0, "md5": 0,
49
+ }
50
+
51
+
52
+ @dataclass
53
+ class CryptoAsset:
54
+ name: str
55
+ asset_type: AssetType
56
+ primitive: Primitive
57
+ location: str
58
+ line: int
59
+ library: str
60
+ mode: str = ""
61
+ curve: str = ""
62
+ param_set: str = ""
63
+ classical_bits: int = 0
64
+ nist_quantum_level: int = 0
65
+ quantum_safety: QuantumSafety = "unknown"
66
+ oid: str = ""
67
+ evidence: str = ""
68
+
69
+ def normalize_name(self) -> str:
70
+ return self.name.lower().replace("_", "-")
71
+
72
+
73
+ ALGO_PATTERNS: list[dict] = [
74
+ # ── RSA ──────────────────────────────────────────────────────────────────
75
+ {"pattern": r"RSA[_\-]?(\d{3,4})|rsa\.generate\s*\(\s*(\d{3,4})|genrsa.*?(\d{3,4})|RSA\.generate\s*\((\d{3,4})",
76
+ "name_fn": lambda m: f"RSA-{m.group(1) or m.group(2) or m.group(3) or m.group(4)}",
77
+ "primitive": "rsa", "asset_type": "algorithm", "quantum_safety": "quantum-unsafe",
78
+ "oid": "1.2.840.113549.1.1.1"},
79
+
80
+ # ── ECDSA ─────────────────────────────────────────────────────────────────
81
+ {"pattern": r"(?:ECDSA|ecdsa)[_\-]?(P-?256|P-?384|P-?521|secp256k1|secp384r1|secp521r1)?|EC_KEY_new|ec\.generate_private_key\s*\(\s*(?:ec\.\s*)?(\w+)",
82
+ "name_fn": lambda m: f"ECDSA-{(m.group(1) or m.group(2) or 'P-256').upper()}",
83
+ "primitive": "ecc", "asset_type": "algorithm", "quantum_safety": "quantum-unsafe",
84
+ "oid": "1.2.840.10045.4.3.2"},
85
+
86
+ # ── ECDH ──────────────────────────────────────────────────────────────────
87
+ {"pattern": r"(?:ECDH|ecdh)[_\-]?(P-?256|P-?384|P-?521|X25519|X448)?|ec\.ECDH\(",
88
+ "name_fn": lambda m: f"ECDH-{(m.group(1) or 'P-256').upper()}",
89
+ "primitive": "ecDH", "asset_type": "algorithm", "quantum_safety": "quantum-unsafe",
90
+ "oid": "1.3.132.1.12"},
91
+
92
+ # ── Ed25519 / Ed448 ───────────────────────────────────────────────────────
93
+ {"pattern": r"Ed25519|ed25519|edwards25519|ed25519_dalek|ed25519-dalek",
94
+ "name_fn": lambda m: "Ed25519",
95
+ "primitive": "ecc", "asset_type": "algorithm", "quantum_safety": "quantum-unsafe",
96
+ "oid": "1.3.101.112"},
97
+
98
+ {"pattern": r"Ed448|ed448",
99
+ "name_fn": lambda m: "Ed448",
100
+ "primitive": "ecc", "asset_type": "algorithm", "quantum_safety": "quantum-unsafe",
101
+ "oid": "1.3.101.113"},
102
+
103
+ # ── X25519 / X448 ─────────────────────────────────────────────────────────
104
+ {"pattern": r"X25519|x25519|curve25519|Curve25519",
105
+ "name_fn": lambda m: "X25519",
106
+ "primitive": "ecDH", "asset_type": "algorithm", "quantum_safety": "quantum-unsafe",
107
+ "oid": "1.3.101.110"},
108
+
109
+ {"pattern": r"X448|x448",
110
+ "name_fn": lambda m: "X448",
111
+ "primitive": "ecDH", "asset_type": "algorithm", "quantum_safety": "quantum-unsafe",
112
+ "oid": "1.3.101.111"},
113
+
114
+ # ── AES ───────────────────────────────────────────────────────────────────
115
+ {"pattern": r"AES[_\-]?(128|192|256)?[_\-]?(GCM|CBC|CTR|CCM|SIV|ECB|CFB|OFB|XTS)?|AES\.new\(|EVP_aes_(\d+)_(\w+)|createCipheriv\(['\"]aes[_\-](\d+)[_\-](\w+)",
116
+ "name_fn": lambda m: f"AES-{m.group(1) or m.group(3) or m.group(5) or '256'}-{(m.group(2) or m.group(4) or m.group(6) or 'GCM').upper()}",
117
+ "primitive": "ae", "asset_type": "algorithm", "quantum_safety": "quantum-safe",
118
+ "oid": "2.16.840.1.101.3.4.1"},
119
+
120
+ # ── ChaCha20-Poly1305 ─────────────────────────────────────────────────────
121
+ {"pattern": r"ChaCha20[_\-]?Poly1305|chacha20[_\-]poly1305|chacha20_poly1305",
122
+ "name_fn": lambda m: "ChaCha20-Poly1305",
123
+ "primitive": "ae", "asset_type": "algorithm", "quantum_safety": "quantum-safe",
124
+ "oid": "1.2.840.113549.1.9.16.3.18"},
125
+
126
+ # ── SHA ───────────────────────────────────────────────────────────────────
127
+ {"pattern": r"SHA[_\-]?1\b|sha1\b|SHA1\b|hashlib\.sha1",
128
+ "name_fn": lambda m: "SHA-1",
129
+ "primitive": "hash", "asset_type": "algorithm", "quantum_safety": "quantum-unsafe",
130
+ "oid": "1.3.14.3.2.26"},
131
+
132
+ {"pattern": r"SHA[_\-]?256\b|sha256\b|SHA256\b|hashlib\.sha256|EVP_sha256",
133
+ "name_fn": lambda m: "SHA-256",
134
+ "primitive": "hash", "asset_type": "algorithm", "quantum_safety": "quantum-safe",
135
+ "oid": "2.16.840.1.101.3.4.2.1"},
136
+
137
+ {"pattern": r"SHA[_\-]?384\b|sha384\b|SHA384\b|hashlib\.sha384",
138
+ "name_fn": lambda m: "SHA-384",
139
+ "primitive": "hash", "asset_type": "algorithm", "quantum_safety": "quantum-safe",
140
+ "oid": "2.16.840.1.101.3.4.2.2"},
141
+
142
+ {"pattern": r"SHA[_\-]?512\b|sha512\b|SHA512\b|hashlib\.sha512",
143
+ "name_fn": lambda m: "SHA-512",
144
+ "primitive": "hash", "asset_type": "algorithm", "quantum_safety": "quantum-safe",
145
+ "oid": "2.16.840.1.101.3.4.2.3"},
146
+
147
+ {"pattern": r"SHA3[_\-]?256\b|sha3_256\b|SHA3_256\b|sha3[_\-]256",
148
+ "name_fn": lambda m: "SHA3-256",
149
+ "primitive": "hash", "asset_type": "algorithm", "quantum_safety": "quantum-safe",
150
+ "oid": "2.16.840.1.101.3.4.2.8"},
151
+
152
+ {"pattern": r"SHA3[_\-]?512\b|sha3_512\b|SHA3_512\b|sha3[_\-]512",
153
+ "name_fn": lambda m: "SHA3-512",
154
+ "primitive": "hash", "asset_type": "algorithm", "quantum_safety": "quantum-safe",
155
+ "oid": "2.16.840.1.101.3.4.2.10"},
156
+
157
+ {"pattern": r"\bMD5\b|hashlib\.md5\b|EVP_md5\b",
158
+ "name_fn": lambda m: "MD5",
159
+ "primitive": "hash", "asset_type": "algorithm", "quantum_safety": "quantum-unsafe",
160
+ "oid": "1.2.840.113549.2.5"},
161
+
162
+ # ── HMAC ──────────────────────────────────────────────────────────────────
163
+ {"pattern": r"HMAC[_\-]?SHA[_\-]?(256|384|512)|hmac\.new\(|createHmac\(['\"]sha(256|384|512)",
164
+ "name_fn": lambda m: f"HMAC-SHA{m.group(1) or m.group(2) or '256'}",
165
+ "primitive": "mac", "asset_type": "algorithm", "quantum_safety": "quantum-safe",
166
+ "oid": "1.2.840.113549.2.9"},
167
+
168
+ # ── 3DES / DES ────────────────────────────────────────────────────────────
169
+ {"pattern": r"3DES|TripleDES|DES3|des3\b|EVP_des_ede3",
170
+ "name_fn": lambda m: "3DES",
171
+ "primitive": "ae", "asset_type": "algorithm", "quantum_safety": "quantum-unsafe",
172
+ "oid": "1.2.840.113549.3.7"},
173
+
174
+ {"pattern": r"\bDES\b(?!3)|EVP_des_cbc\b",
175
+ "name_fn": lambda m: "DES",
176
+ "primitive": "ae", "asset_type": "algorithm", "quantum_safety": "quantum-unsafe",
177
+ "oid": "1.3.14.3.2.7"},
178
+
179
+ # ── RC4 ───────────────────────────────────────────────────────────────────
180
+ {"pattern": r"\bRC4\b|arcfour|EVP_rc4\b",
181
+ "name_fn": lambda m: "RC4",
182
+ "primitive": "stream-cipher", "asset_type": "algorithm", "quantum_safety": "quantum-unsafe",
183
+ "oid": "1.2.840.113549.3.4"},
184
+
185
+ # ── DH ────────────────────────────────────────────────────────────────────
186
+ {"pattern": r"\bDH\b|DHE\b|dh\.generate_parameters|EVP_PKEY_DH|DiffieHellman\(",
187
+ "name_fn": lambda m: "DH",
188
+ "primitive": "ff-dh", "asset_type": "algorithm", "quantum_safety": "quantum-unsafe",
189
+ "oid": "1.2.840.113549.1.3.1"},
190
+
191
+ # ── PQC: ML-KEM ───────────────────────────────────────────────────────────
192
+ {"pattern": r"ML[_\-]KEM[_\-]?(512|768|1024)?|Kyber(?:512|768|1024)?|kyber(?:512|768|1024)?|ml_kem|mlkem",
193
+ "name_fn": lambda m: f"ML-KEM-{m.group(1) or '768'}",
194
+ "primitive": "post-quantum", "asset_type": "algorithm", "quantum_safety": "quantum-safe",
195
+ "oid": "1.3.6.1.4.1.22554.5.6.1"},
196
+
197
+ # ── PQC: ML-DSA ───────────────────────────────────────────────────────────
198
+ {"pattern": r"ML[_\-]DSA[_\-]?(44|65|87)?|Dilithium(?:2|3|5)?|dilithium(?:2|3|5)?|ml_dsa|mldsa",
199
+ "name_fn": lambda m: f"ML-DSA-{m.group(1) or '65'}",
200
+ "primitive": "post-quantum", "asset_type": "algorithm", "quantum_safety": "quantum-safe",
201
+ "oid": "1.3.6.1.4.1.22554.5.6.2"},
202
+
203
+ # ── PQC: FN-DSA ───────────────────────────────────────────────────────────
204
+ {"pattern": r"FN[_\-]DSA[_\-]?(512|1024)?|Falcon(?:512|1024)?|falcon(?:512|1024)?|fn_dsa",
205
+ "name_fn": lambda m: f"FN-DSA-{m.group(1) or '512'}",
206
+ "primitive": "post-quantum", "asset_type": "algorithm", "quantum_safety": "quantum-safe",
207
+ "oid": "1.3.6.1.4.1.22554.5.6.3"},
208
+
209
+ # ── PQC: SLH-DSA ──────────────────────────────────────────────────────────
210
+ {"pattern": r"SLH[_\-]DSA|SPHINCS\+?|sphincs_plus|slh_dsa",
211
+ "name_fn": lambda m: "SLH-DSA-SHAKE-128s",
212
+ "primitive": "post-quantum", "asset_type": "algorithm", "quantum_safety": "quantum-safe",
213
+ "oid": "1.3.6.1.4.1.22554.5.6.4"},
214
+
215
+ # ── TLS ───────────────────────────────────────────────────────────────────
216
+ {"pattern": r"TLS[_\s]?1[_\.]?0\b|SSL[_\s]?3\b|TLSv1\b|SSLv3\b",
217
+ "name_fn": lambda m: "TLS-1.0",
218
+ "primitive": "other", "asset_type": "protocol", "quantum_safety": "quantum-unsafe",
219
+ "oid": ""},
220
+
221
+ {"pattern": r"TLS[_\s]?1[_\.]?2\b|TLSv1_2\b|TLS\.TLSv1_2",
222
+ "name_fn": lambda m: "TLS-1.2",
223
+ "primitive": "other", "asset_type": "protocol", "quantum_safety": "quantum-unsafe",
224
+ "oid": ""},
225
+
226
+ {"pattern": r"TLS[_\s]?1[_\.]?3\b|TLSv1_3\b|ssl\.PROTOCOL_TLS",
227
+ "name_fn": lambda m: "TLS-1.3",
228
+ "primitive": "other", "asset_type": "protocol", "quantum_safety": "quantum-unsafe",
229
+ "oid": ""},
230
+
231
+ # ── JWT ───────────────────────────────────────────────────────────────────
232
+ {"pattern": r"['\"]HS256['\"]|['\"]RS256['\"]|['\"]ES256['\"]|['\"]PS256['\"]|jwt\.sign\(|jwt\.verify\(|jose\.",
233
+ "name_fn": lambda m: f"JWT-{m.group(0).strip(chr(39)).strip(chr(34)) if m.group(0).startswith((chr(39), chr(34))) else 'RS256'}",
234
+ "primitive": "other", "asset_type": "protocol", "quantum_safety": "quantum-unsafe",
235
+ "oid": ""},
236
+
237
+ # ── KDF ───────────────────────────────────────────────────────────────────
238
+ {"pattern": r"PBKDF2|pbkdf2_hmac|bcrypt|argon2|scrypt",
239
+ "name_fn": lambda m: m.group(0).upper().replace("_HMAC", ""),
240
+ "primitive": "kdf", "asset_type": "algorithm", "quantum_safety": "quantum-safe",
241
+ "oid": "1.2.840.113549.1.5.12"},
242
+ ]
243
+
244
+ LIBRARY_PATTERNS: list[tuple[re.Pattern, str]] = [
245
+ (re.compile(r"#include\s+[<\"]openssl/|import\s+openssl"), "openssl"),
246
+ (re.compile(r"from\s+cryptography|import\s+cryptography"), "cryptography-py"),
247
+ (re.compile(r"require\(['\"]crypto['\"]\)|from\s+['\"]crypto['\"]|import\s+crypto\s+from"), "node-crypto"),
248
+ (re.compile(r"['\"]jose['\"]|['\"]jsonwebtoken['\"]|node-jose"), "jose"),
249
+ (re.compile(r"#include\s+[<\"]wolfssl/|import\s+wolfssl"), "wolfssl"),
250
+ (re.compile(r"mbedtls/|psa/crypto"), "mbedtls"),
251
+ (re.compile(r"liboqs|oqs\."), "liboqs"),
252
+ (re.compile(r"use\s+ring::|extern\s+crate\s+ring"), "ring-rust"),
253
+ (re.compile(r"ed25519[_\-]dalek|p256::|rsa::"), "rust-crypto"),
254
+ (re.compile(r"BoringSSL|boringssl"), "boringssl"),
255
+ (re.compile(r"import\s+javax\.crypto|import\s+java\.security"), "java-jce"),
256
+ (re.compile(r"\"crypto/rsa\"|\"crypto/ecdsa\"|\"x/crypto"), "go-stdlib"),
257
+ (re.compile(r"tweetnacl|libsodium|sodium\.random"), "libsodium"),
258
+ (re.compile(r"org\.bouncycastle"), "bouncycastle"),
259
+ (re.compile(r"aws-lc|aws_lc"), "aws-lc"),
260
+ (re.compile(r"nss\.|mozilla.*nss"), "nss"),
261
+ ]
262
+
263
+ LANG_EXTENSIONS: dict[str, list[str]] = {
264
+ "python": [".py"],
265
+ "typescript": [".ts", ".tsx"],
266
+ "javascript": [".js", ".jsx", ".mjs", ".cjs"],
267
+ "go": [".go"],
268
+ "c": [".c", ".h"],
269
+ "cpp": [".cpp", ".cc", ".cxx", ".hpp", ".hh"],
270
+ "java": [".java"],
271
+ "rust": [".rs"],
272
+ "kotlin": [".kt"],
273
+ "swift": [".swift"],
274
+ }
275
+
276
+ SKIP_DIRS: set[str] = {
277
+ "node_modules", ".git", ".next", "dist", "build", "__pycache__",
278
+ ".venv", "venv", "vendor", "target", ".cargo", "coverage",
279
+ }
280
+
281
+
282
+ def _detect_libraries(source: str) -> list[str]:
283
+ """Return all detected crypto libraries in a source file."""
284
+ found = [lib for pat, lib in LIBRARY_PATTERNS if pat.search(source)]
285
+ return found if found else []
286
+
287
+
288
+ def _extract_curve(name: str) -> str:
289
+ n = name.upper()
290
+ if "P-256" in n or "P256" in n or "SECP256R1" in n: return "P-256"
291
+ if "P-384" in n or "P384" in n or "SECP384R1" in n: return "P-384"
292
+ if "P-521" in n or "P521" in n or "SECP521R1" in n: return "P-521"
293
+ if "SECP256K1" in n: return "secp256k1"
294
+ if "25519" in name: return "Curve25519"
295
+ if "448" in name: return "Curve448"
296
+ return ""
297
+
298
+
299
+ def _extract_param_set(name: str) -> str:
300
+ """Extract key/parameter size from algorithm name."""
301
+ import re as _re
302
+ m = _re.search(r"[-_](\d{2,4})(?:[-_]|$)", name)
303
+ return m.group(1) if m else ""
304
+
305
+
306
+ def _extract_mode(name: str) -> str:
307
+ n = name.upper()
308
+ for mode in ("GCM", "CBC", "CTR", "CCM", "SIV", "ECB", "CFB", "OFB", "XTS"):
309
+ if mode in n:
310
+ return mode.lower()
311
+ return ""
312
+
313
+
314
+ def scan_file(path: Path) -> list[CryptoAsset]:
315
+ try:
316
+ source = path.read_text(encoding="utf-8", errors="ignore")
317
+ except (OSError, PermissionError):
318
+ return []
319
+
320
+ libraries = _detect_libraries(source)
321
+ primary_lib = libraries[0] if libraries else "unknown"
322
+ lines = source.splitlines()
323
+ assets: list[CryptoAsset] = []
324
+ seen: set[tuple[str, int]] = set()
325
+
326
+ for detector in ALGO_PATTERNS:
327
+ pat = re.compile(detector["pattern"], re.IGNORECASE | re.MULTILINE)
328
+ for m in pat.finditer(source):
329
+ try:
330
+ name = detector["name_fn"](m)
331
+ except (IndexError, AttributeError):
332
+ continue
333
+
334
+ line_no = source[: m.start()].count("\n") + 1
335
+ key = (name.lower(), line_no)
336
+ if key in seen:
337
+ continue
338
+ seen.add(key)
339
+
340
+ norm = name.lower().replace("_", "-")
341
+ # Try exact match, then prefix match for classical_bits
342
+ classical_bits = CLASSICAL_SECURITY_BITS.get(norm, 0)
343
+ if not classical_bits:
344
+ for prefix in ("aes-128", "aes-192", "aes-256"):
345
+ if norm.startswith(prefix):
346
+ classical_bits = CLASSICAL_SECURITY_BITS[prefix]
347
+ break
348
+
349
+ nist_level = NIST_QUANTUM_LEVELS.get(norm, 0)
350
+ evidence = lines[line_no - 1].strip()[:120] if line_no <= len(lines) else ""
351
+
352
+ assets.append(CryptoAsset(
353
+ name=name,
354
+ asset_type=detector["asset_type"],
355
+ primitive=detector["primitive"],
356
+ location=str(path),
357
+ line=line_no,
358
+ library=primary_lib,
359
+ mode=_extract_mode(name),
360
+ curve=_extract_curve(name),
361
+ param_set=_extract_param_set(name),
362
+ classical_bits=classical_bits,
363
+ nist_quantum_level=nist_level,
364
+ quantum_safety=detector["quantum_safety"],
365
+ oid=detector.get("oid", ""),
366
+ evidence=evidence,
367
+ ))
368
+
369
+ return assets
370
+
371
+
372
+ def scan_repo(root: Path, include_tests: bool = False) -> list[CryptoAsset]:
373
+ all_assets: list[CryptoAsset] = []
374
+ extensions = {ext for exts in LANG_EXTENSIONS.values() for ext in exts}
375
+
376
+ for file_path in root.rglob("*"):
377
+ if not file_path.is_file():
378
+ continue
379
+ if any(part in SKIP_DIRS for part in file_path.parts):
380
+ continue
381
+ if not include_tests and any(
382
+ p in ("test", "tests", "spec", "__tests__", "__test__") for p in file_path.parts
383
+ ):
384
+ continue
385
+ if file_path.suffix.lower() not in extensions:
386
+ continue
387
+
388
+ all_assets.extend(scan_file(file_path))
389
+
390
+ return all_assets
@@ -0,0 +1,63 @@
1
+ """ML-DSA-65 signing via AXIOM API."""
2
+
3
+ from __future__ import annotations
4
+ import hashlib
5
+ import json
6
+ from typing import TYPE_CHECKING
7
+
8
+ try:
9
+ import requests
10
+ HAS_REQUESTS = True
11
+ except ImportError:
12
+ HAS_REQUESTS = False
13
+
14
+ AXIOM_URL = "https://axiom-trust.fly.dev"
15
+
16
+
17
+ def sign_cbom(cbom: dict) -> dict | None:
18
+ """Sign the CBOM payload via AXIOM ML-DSA-65 endpoint. Returns signature block or None."""
19
+ if not HAS_REQUESTS:
20
+ return None
21
+
22
+ # Sign the actual CBOM content, not an empty dict
23
+ cbom_without_sig = {k: v for k, v in cbom.items() if k != "signature"}
24
+ payload_bytes = json.dumps(cbom_without_sig, separators=(",", ":"), sort_keys=True).encode()
25
+ digest = hashlib.sha3_256(payload_bytes).hexdigest()
26
+
27
+ try:
28
+ resp = requests.post(
29
+ f"{AXIOM_URL}/api/sign",
30
+ json={"payload": digest, "algorithm": "ML-DSA-65"},
31
+ timeout=10,
32
+ )
33
+ if resp.status_code == 200:
34
+ data = resp.json()
35
+ return {
36
+ "algorithm": "ML-DSA-65",
37
+ "publicKey": data.get("publicKey", ""),
38
+ "value": data.get("signature", ""),
39
+ "digest": digest,
40
+ "certRef": data.get("certRef", ""),
41
+ }
42
+ except Exception:
43
+ pass
44
+ return None
45
+
46
+
47
+ def verify_cbom(cbom: dict, signature_block: dict) -> bool:
48
+ if not HAS_REQUESTS:
49
+ return False
50
+ try:
51
+ resp = requests.post(
52
+ f"{AXIOM_URL}/api/verify",
53
+ json={
54
+ "digest": signature_block["digest"],
55
+ "signature": signature_block["value"],
56
+ "publicKey": signature_block["publicKey"],
57
+ "algorithm": "ML-DSA-65",
58
+ },
59
+ timeout=10,
60
+ )
61
+ return resp.status_code == 200 and resp.json().get("valid") is True
62
+ except Exception:
63
+ return False
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: cobalt-sbom
3
+ Version: 0.1.0
4
+ Summary: Cryptographic Bill of Materials generator — CycloneDX 1.5, ML-DSA signed
5
+ Author-email: QreativeLab / OMEGA <dominik@qreativelab.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/dom-omg/cobalt-sbom
8
+ Project-URL: Repository, https://github.com/dom-omg/cobalt-sbom
9
+ Keywords: cbom,sbom,pqc,cryptography,cyclonedx,quantum
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Information Technology
13
+ Classifier: Topic :: Security :: Cryptography
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: rich>=13.0
20
+ Requires-Dist: requests>=2.31
21
+ Requires-Dist: click>=8.1
22
+
23
+ # COBALT SBOM
24
+
25
+ **Cryptographic Bill of Materials generator — CycloneDX 1.6, ML-DSA-65 signed.**
26
+
27
+ Scan any codebase in seconds. Find every cryptographic algorithm. Know your quantum exposure.
28
+
29
+ ## What it does
30
+
31
+ - Detects 30+ crypto primitives across Python, TypeScript, JavaScript, Go, C/C++, Java, Rust
32
+ - Outputs a signed **CycloneDX 1.6 CBOM** (Cryptographic Bill of Materials)
33
+ - Scores your **quantum readiness (0-100)**
34
+ - Flags broken algorithms (DES, MD5, RC4) and deprecated ones (SHA-1, TLS-1.0)
35
+ - Integrates into CI/CD via GitHub Actions — gate PRs on quantum safety
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install cobalt-sbom
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```bash
46
+ # Scan a repo
47
+ cobalt-sbom scan ./myrepo --output cbom.cdx.json
48
+
49
+ # Scan + sign with ML-DSA-65 (via AXIOM)
50
+ cobalt-sbom scan ./myrepo --output cbom.cdx.json --sign
51
+
52
+ # CI mode — fail if quantum-unsafe algorithms present
53
+ cobalt-sbom scan ./myrepo --ci
54
+
55
+ # Fail if quantum readiness score below 80
56
+ cobalt-sbom scan ./myrepo --fail-score 80
57
+
58
+ # Display an existing CBOM
59
+ cobalt-sbom show cbom.cdx.json
60
+ ```
61
+
62
+ ## GitHub Actions
63
+
64
+ Add to `.github/workflows/cbom.yml`:
65
+
66
+ ```yaml
67
+ - name: Install cobalt-sbom
68
+ run: pip install cobalt-sbom
69
+
70
+ - name: Scan cryptographic assets
71
+ run: cobalt-sbom scan . --output cbom.cdx.json --sign --fail-score 60
72
+ ```
73
+
74
+ ## Output format
75
+
76
+ CycloneDX 1.6 JSON — compatible with all CBOM/SBOM toolchains:
77
+
78
+ ```json
79
+ {
80
+ "bomFormat": "CycloneDX",
81
+ "specVersion": "1.6",
82
+ "components": [
83
+ {
84
+ "type": "cryptographic-asset",
85
+ "name": "ML-DSA-65",
86
+ "cryptoProperties": {
87
+ "assetType": "algorithm",
88
+ "algorithmProperties": {
89
+ "primitive": "sign",
90
+ "nistQuantumSecurityLevel": 3,
91
+ "cryptoFunctions": ["sign", "verify"]
92
+ },
93
+ "quantumSafety": "quantum-safe"
94
+ }
95
+ }
96
+ ],
97
+ "summary": {
98
+ "quantum_readiness_score": 72,
99
+ "quantum_unsafe": 15,
100
+ "quantum_safe": 16,
101
+ "broken_algorithms": ["DES", "SHA-1"]
102
+ }
103
+ }
104
+ ```
105
+
106
+ ## Why CBOM
107
+
108
+ Governments (USA EO 14028, EU CRA, NIST IR 8547) are mandating cryptographic inventories. The CBOM Working Group (CycloneDX) is standardizing the format. **First movers own the toolchain.**
109
+
110
+ ---
111
+
112
+ Built on [OMEGA](https://omega-sovereign.fly.dev) — sovereign intelligence stack.
113
+ Signing powered by [AXIOM](https://axiom-trust.fly.dev) — ML-DSA-65 post-quantum certificates.
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ cobalt_sbom/__init__.py
4
+ cobalt_sbom/cli.py
5
+ cobalt_sbom/cyclonedx.py
6
+ cobalt_sbom/detectors.py
7
+ cobalt_sbom/signer.py
8
+ cobalt_sbom.egg-info/PKG-INFO
9
+ cobalt_sbom.egg-info/SOURCES.txt
10
+ cobalt_sbom.egg-info/dependency_links.txt
11
+ cobalt_sbom.egg-info/entry_points.txt
12
+ cobalt_sbom.egg-info/requires.txt
13
+ cobalt_sbom.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cobalt-sbom = cobalt_sbom.cli:main
@@ -0,0 +1,3 @@
1
+ rich>=13.0
2
+ requests>=2.31
3
+ click>=8.1
@@ -0,0 +1 @@
1
+ cobalt_sbom
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cobalt-sbom"
7
+ version = "0.1.0"
8
+ description = "Cryptographic Bill of Materials generator — CycloneDX 1.5, ML-DSA signed"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "QreativeLab / OMEGA", email = "dominik@qreativelab.io"}]
13
+ keywords = ["cbom", "sbom", "pqc", "cryptography", "cyclonedx", "quantum"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Information Technology",
18
+ "Topic :: Security :: Cryptography",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ ]
23
+ dependencies = [
24
+ "rich>=13.0",
25
+ "requests>=2.31",
26
+ "click>=8.1",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/dom-omg/cobalt-sbom"
31
+ Repository = "https://github.com/dom-omg/cobalt-sbom"
32
+
33
+ [project.scripts]
34
+ cobalt-sbom = "cobalt_sbom.cli:main"
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["."]
38
+ include = ["cobalt_sbom*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+