cra-scanner 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.
- cra_scanner-0.1.0/PKG-INFO +63 -0
- cra_scanner-0.1.0/README.md +50 -0
- cra_scanner-0.1.0/cra_scanner/__init__.py +4 -0
- cra_scanner-0.1.0/cra_scanner/__main__.py +5 -0
- cra_scanner-0.1.0/cra_scanner/cli.py +52 -0
- cra_scanner-0.1.0/cra_scanner/discovery.py +60 -0
- cra_scanner-0.1.0/cra_scanner/reporters/__init__.py +1 -0
- cra_scanner-0.1.0/cra_scanner/reporters/json.py +9 -0
- cra_scanner-0.1.0/cra_scanner/reporters/text.py +53 -0
- cra_scanner-0.1.0/cra_scanner/sbom.py +74 -0
- cra_scanner-0.1.0/cra_scanner/scanner.py +67 -0
- cra_scanner-0.1.0/cra_scanner/scoring.py +93 -0
- cra_scanner-0.1.0/cra_scanner.egg-info/PKG-INFO +63 -0
- cra_scanner-0.1.0/cra_scanner.egg-info/SOURCES.txt +18 -0
- cra_scanner-0.1.0/cra_scanner.egg-info/dependency_links.txt +1 -0
- cra_scanner-0.1.0/cra_scanner.egg-info/entry_points.txt +2 -0
- cra_scanner-0.1.0/cra_scanner.egg-info/requires.txt +4 -0
- cra_scanner-0.1.0/cra_scanner.egg-info/top_level.txt +1 -0
- cra_scanner-0.1.0/pyproject.toml +26 -0
- cra_scanner-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cra-scanner
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open-source CRA Readiness Scanner CLI for assessing EU Cyber Resilience Act readiness from SBOMs and project signals.
|
|
5
|
+
Author-email: CyberCert <info@cybercert.example>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: click>=8.1
|
|
10
|
+
Requires-Dist: rich>=13.0
|
|
11
|
+
Requires-Dist: cyclonedx-python-lib>=7.6
|
|
12
|
+
Requires-Dist: spdx-tools>=0.8
|
|
13
|
+
|
|
14
|
+
# CRA Readiness Scanner (MVP)
|
|
15
|
+
|
|
16
|
+
The CRA Readiness Scanner is an open-source CLI tool that helps engineering teams quickly assess their readiness for the EU Cyber Resilience Act (CRA) from a single SBOM or project directory.
|
|
17
|
+
|
|
18
|
+
It focuses on three things:
|
|
19
|
+
|
|
20
|
+
- SBOM presence and basic quality
|
|
21
|
+
- Basic vulnerability exposure (stubbed for MVP)
|
|
22
|
+
- Signals of good vulnerability-handling practices
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Once published to PyPI:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install cra-scanner
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For local development from this repository:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd cli
|
|
36
|
+
pip install -e .
|
|
37
|
+
cra-scanner --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
Scan a project directory (auto-discover SBOMs and signals):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cra-scanner scan .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Scan using an explicit SBOM and emit JSON to a file:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cra-scanner scan . --sbom path/to/bom.json --format json --output report.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## What the CRA Readiness Score means
|
|
55
|
+
|
|
56
|
+
The scanner returns a score from 0–100 based on:
|
|
57
|
+
|
|
58
|
+
- **SBOM (40 pts)** – existence, coverage, presence of versions.
|
|
59
|
+
- **Vulnerabilities (30 pts)** – placeholder in MVP.
|
|
60
|
+
- **Practices (30 pts)** – presence of `SECURITY.md`, Dependabot, and basic documentation signals.
|
|
61
|
+
|
|
62
|
+
The score is a directional indicator, not legal advice. It is intended to highlight gaps and next steps, not certify compliance.
|
|
63
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# CRA Readiness Scanner (MVP)
|
|
2
|
+
|
|
3
|
+
The CRA Readiness Scanner is an open-source CLI tool that helps engineering teams quickly assess their readiness for the EU Cyber Resilience Act (CRA) from a single SBOM or project directory.
|
|
4
|
+
|
|
5
|
+
It focuses on three things:
|
|
6
|
+
|
|
7
|
+
- SBOM presence and basic quality
|
|
8
|
+
- Basic vulnerability exposure (stubbed for MVP)
|
|
9
|
+
- Signals of good vulnerability-handling practices
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Once published to PyPI:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install cra-scanner
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
For local development from this repository:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd cli
|
|
23
|
+
pip install -e .
|
|
24
|
+
cra-scanner --help
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
Scan a project directory (auto-discover SBOMs and signals):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cra-scanner scan .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Scan using an explicit SBOM and emit JSON to a file:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
cra-scanner scan . --sbom path/to/bom.json --format json --output report.json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What the CRA Readiness Score means
|
|
42
|
+
|
|
43
|
+
The scanner returns a score from 0–100 based on:
|
|
44
|
+
|
|
45
|
+
- **SBOM (40 pts)** – existence, coverage, presence of versions.
|
|
46
|
+
- **Vulnerabilities (30 pts)** – placeholder in MVP.
|
|
47
|
+
- **Practices (30 pts)** – presence of `SECURITY.md`, Dependabot, and basic documentation signals.
|
|
48
|
+
|
|
49
|
+
The score is a directional indicator, not legal advice. It is intended to highlight gaps and next steps, not certify compliance.
|
|
50
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from .scanner import ScanConfig, perform_scan
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
@click.version_option()
|
|
13
|
+
def cli() -> None:
|
|
14
|
+
"""CRA Readiness Scanner CLI."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@cli.command("scan")
|
|
18
|
+
@click.argument("project_root", type=click.Path(path_type=Path), default=Path("."))
|
|
19
|
+
@click.option("--sbom", "sbom_path", type=click.Path(path_type=Path), help="Path to an SBOM file.")
|
|
20
|
+
@click.option(
|
|
21
|
+
"--format",
|
|
22
|
+
"output_format",
|
|
23
|
+
type=click.Choice(["text", "json"], case_sensitive=False),
|
|
24
|
+
default="text",
|
|
25
|
+
show_default=True,
|
|
26
|
+
help="Output format.",
|
|
27
|
+
)
|
|
28
|
+
@click.option(
|
|
29
|
+
"--output",
|
|
30
|
+
"output_path",
|
|
31
|
+
type=click.Path(path_type=Path),
|
|
32
|
+
help="Optional path to write JSON report.",
|
|
33
|
+
)
|
|
34
|
+
def scan_command(
|
|
35
|
+
project_root: Path,
|
|
36
|
+
sbom_path: Optional[Path],
|
|
37
|
+
output_format: str,
|
|
38
|
+
output_path: Optional[Path],
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Run CRA readiness scan on a project."""
|
|
41
|
+
config = ScanConfig(
|
|
42
|
+
project_root=project_root,
|
|
43
|
+
sbom_path=sbom_path,
|
|
44
|
+
output_format=output_format,
|
|
45
|
+
output_path=output_path,
|
|
46
|
+
)
|
|
47
|
+
perform_scan(config)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main() -> None:
|
|
51
|
+
cli(prog_name="cra-scanner")
|
|
52
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from .sbom import ParsedSbomFile, parse_sbom_file
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class DiscoveryResult:
|
|
12
|
+
project_root: Path
|
|
13
|
+
sbom_found: bool
|
|
14
|
+
sbom_files: List[ParsedSbomFile]
|
|
15
|
+
has_security_md: bool
|
|
16
|
+
has_docs_security_section: bool
|
|
17
|
+
has_dependabot: bool
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def discover_project(project_root: Path, explicit_sbom: Optional[Path] = None) -> DiscoveryResult:
|
|
21
|
+
root = project_root.resolve()
|
|
22
|
+
|
|
23
|
+
sbom_files: list[ParsedSbomFile] = []
|
|
24
|
+
if explicit_sbom:
|
|
25
|
+
parsed = parse_sbom_file(explicit_sbom)
|
|
26
|
+
if parsed:
|
|
27
|
+
sbom_files.append(parsed)
|
|
28
|
+
else:
|
|
29
|
+
for path in root.rglob("*"):
|
|
30
|
+
if not path.is_file():
|
|
31
|
+
continue
|
|
32
|
+
if path.suffix.lower() in {".json", ".xml", ".spdx"} and any(
|
|
33
|
+
token in path.name.lower() for token in ("sbom", "bom", "cyclonedx", "spdx")
|
|
34
|
+
):
|
|
35
|
+
parsed = parse_sbom_file(path)
|
|
36
|
+
if parsed:
|
|
37
|
+
sbom_files.append(parsed)
|
|
38
|
+
|
|
39
|
+
# Simple signals for practices
|
|
40
|
+
has_security_md = (root / "SECURITY.md").is_file()
|
|
41
|
+
has_dependabot = (root / ".github" / "dependabot.yml").is_file()
|
|
42
|
+
|
|
43
|
+
has_docs_security_section = False
|
|
44
|
+
docs_root = root / "docs"
|
|
45
|
+
if docs_root.is_dir():
|
|
46
|
+
for md in docs_root.rglob("*.md"):
|
|
47
|
+
text = md.read_text(encoding="utf-8", errors="ignore").lower()
|
|
48
|
+
if "security" in text or "vulnerability" in text:
|
|
49
|
+
has_docs_security_section = True
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
return DiscoveryResult(
|
|
53
|
+
project_root=root,
|
|
54
|
+
sbom_found=len(sbom_files) > 0,
|
|
55
|
+
sbom_files=sbom_files,
|
|
56
|
+
has_security_md=has_security_md,
|
|
57
|
+
has_docs_security_section=has_docs_security_section,
|
|
58
|
+
has_dependabot=has_dependabot,
|
|
59
|
+
)
|
|
60
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
from ..discovery import DiscoveryResult
|
|
9
|
+
from ..scoring import ScoreBreakdown
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def render_text_report(report: Dict[str, Any], discovery: DiscoveryResult, scores: ScoreBreakdown) -> str:
|
|
13
|
+
console = Console(record=True)
|
|
14
|
+
|
|
15
|
+
console.rule(f"CRA Readiness Scanner v{report['meta']['scannerVersion']}")
|
|
16
|
+
|
|
17
|
+
console.print(f"[bold]Project:[/bold] {report['meta']['projectPath']}")
|
|
18
|
+
|
|
19
|
+
# SBOM section
|
|
20
|
+
console.print("\n[bold]SBOM Readiness[/bold]")
|
|
21
|
+
if not discovery.sbom_found:
|
|
22
|
+
console.print("No SBOM detected.")
|
|
23
|
+
else:
|
|
24
|
+
table = Table(show_header=True, header_style="bold")
|
|
25
|
+
table.add_column("Path")
|
|
26
|
+
table.add_column("Format")
|
|
27
|
+
table.add_column("Components", justify="right")
|
|
28
|
+
for f in discovery.sbom_files:
|
|
29
|
+
table.add_row(str(f.path), f.format, str(f.component_count))
|
|
30
|
+
console.print(table)
|
|
31
|
+
|
|
32
|
+
# Practices section
|
|
33
|
+
console.print("\n[bold]Vulnerability Handling Practices[/bold]")
|
|
34
|
+
console.print(f"SECURITY.md present: {'yes' if discovery.has_security_md else 'no'}")
|
|
35
|
+
console.print(f"Dependabot configured: {'yes' if discovery.has_dependabot else 'no'}")
|
|
36
|
+
console.print(f"Docs include security section: {'yes' if discovery.has_docs_security_section else 'no'}")
|
|
37
|
+
|
|
38
|
+
# Scores
|
|
39
|
+
console.print("\n[bold]CRA Readiness Score[/bold]")
|
|
40
|
+
console.print(f"Overall: [bold]{scores.overall}/100[/bold]")
|
|
41
|
+
console.print(f"SBOM: {scores.sbom_score}/40")
|
|
42
|
+
console.print(f"Vulnerabilities: {scores.vuln_score}/30")
|
|
43
|
+
console.print(f"Practices: {scores.practices_score}/30")
|
|
44
|
+
|
|
45
|
+
if scores.recommendations:
|
|
46
|
+
console.print("\n[bold]Top Recommendations[/bold]")
|
|
47
|
+
for rec in scores.recommendations:
|
|
48
|
+
console.print(f"- [bold]{rec['title']}[/bold]: {rec['description']} ({rec['craReference']})")
|
|
49
|
+
|
|
50
|
+
console.rule()
|
|
51
|
+
|
|
52
|
+
return console.export_text()
|
|
53
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from cyclonedx.model.bom import Bom
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ParsedSbomFile:
|
|
14
|
+
path: Path
|
|
15
|
+
format: str
|
|
16
|
+
component_count: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_cyclonedx_json(path: Path) -> Optional[ParsedSbomFile]:
|
|
20
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
21
|
+
if data.get("bomFormat") != "CycloneDX":
|
|
22
|
+
return None
|
|
23
|
+
bom = Bom.from_json(data=data)
|
|
24
|
+
count = len(bom.components or [])
|
|
25
|
+
return ParsedSbomFile(path=path, format="cyclonedx_json", component_count=count)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_cyclonedx_xml(path: Path) -> Optional[ParsedSbomFile]:
|
|
29
|
+
text = path.read_text(encoding="utf-8")
|
|
30
|
+
if "<bom" not in text:
|
|
31
|
+
return None
|
|
32
|
+
bom = Bom.from_xml(data=text)
|
|
33
|
+
count = len(bom.components or [])
|
|
34
|
+
return ParsedSbomFile(path=path, format="cyclonedx_xml", component_count=count)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _parse_spdx_json(path: Path) -> Optional[ParsedSbomFile]:
|
|
38
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
39
|
+
if not data.get("spdxVersion"):
|
|
40
|
+
return None
|
|
41
|
+
packages = data.get("packages") or []
|
|
42
|
+
return ParsedSbomFile(path=path, format="spdx_json", component_count=len(packages))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_spdx_tag_value(path: Path) -> Optional[ParsedSbomFile]:
|
|
46
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
47
|
+
count = 0
|
|
48
|
+
for raw_line in text.splitlines():
|
|
49
|
+
line = raw_line.strip()
|
|
50
|
+
if line.startswith("PackageName:"):
|
|
51
|
+
count += 1
|
|
52
|
+
if count == 0:
|
|
53
|
+
return None
|
|
54
|
+
return ParsedSbomFile(path=path, format="spdx_tag_value", component_count=count)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_sbom_file(path: Path) -> Optional[ParsedSbomFile]:
|
|
58
|
+
suffix = path.suffix.lower()
|
|
59
|
+
try:
|
|
60
|
+
if suffix == ".json":
|
|
61
|
+
# Try CycloneDX then SPDX JSON
|
|
62
|
+
parsed = _parse_cyclonedx_json(path)
|
|
63
|
+
if parsed:
|
|
64
|
+
return parsed
|
|
65
|
+
return _parse_spdx_json(path)
|
|
66
|
+
if suffix == ".xml":
|
|
67
|
+
return _parse_cyclonedx_xml(path)
|
|
68
|
+
if suffix == ".spdx":
|
|
69
|
+
# Tag-value format
|
|
70
|
+
return _parse_spdx_tag_value(path)
|
|
71
|
+
except Exception:
|
|
72
|
+
return None
|
|
73
|
+
return None
|
|
74
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from .discovery import DiscoveryResult, discover_project
|
|
10
|
+
from .reporters.json import render_json_report
|
|
11
|
+
from .reporters.text import render_text_report
|
|
12
|
+
from .scoring import ScoreBreakdown, compute_scores
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ScanConfig:
|
|
17
|
+
project_root: Path
|
|
18
|
+
sbom_path: Optional[Path]
|
|
19
|
+
output_format: str
|
|
20
|
+
output_path: Optional[Path]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def perform_scan(config: ScanConfig) -> None:
|
|
24
|
+
discovery: DiscoveryResult = discover_project(config.project_root, config.sbom_path)
|
|
25
|
+
scores: ScoreBreakdown = compute_scores(discovery)
|
|
26
|
+
|
|
27
|
+
report = {
|
|
28
|
+
"meta": {
|
|
29
|
+
"scannerVersion": __version__,
|
|
30
|
+
"projectPath": str(config.project_root),
|
|
31
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
32
|
+
},
|
|
33
|
+
"scores": {
|
|
34
|
+
"overall": scores.overall,
|
|
35
|
+
"sbom": scores.sbom_score,
|
|
36
|
+
"vulnerabilities": scores.vuln_score,
|
|
37
|
+
"practices": scores.practices_score,
|
|
38
|
+
},
|
|
39
|
+
"sbom": {
|
|
40
|
+
"found": discovery.sbom_found,
|
|
41
|
+
"files": [
|
|
42
|
+
{
|
|
43
|
+
"path": str(f.path),
|
|
44
|
+
"format": f.format,
|
|
45
|
+
"componentCount": f.component_count,
|
|
46
|
+
}
|
|
47
|
+
for f in discovery.sbom_files
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
"signals": {
|
|
51
|
+
"hasSecurityMd": discovery.has_security_md,
|
|
52
|
+
"hasDocsSecuritySection": discovery.has_docs_security_section,
|
|
53
|
+
"hasDependabot": discovery.has_dependabot,
|
|
54
|
+
},
|
|
55
|
+
"recommendations": scores.recommendations,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if config.output_format == "json":
|
|
59
|
+
output = render_json_report(report)
|
|
60
|
+
if config.output_path:
|
|
61
|
+
config.output_path.write_text(output, encoding="utf-8")
|
|
62
|
+
else:
|
|
63
|
+
print(output)
|
|
64
|
+
else:
|
|
65
|
+
text = render_text_report(report, discovery, scores)
|
|
66
|
+
print(text)
|
|
67
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
from .discovery import DiscoveryResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ScoreBreakdown:
|
|
11
|
+
overall: int
|
|
12
|
+
sbom_score: int
|
|
13
|
+
vuln_score: int
|
|
14
|
+
practices_score: int
|
|
15
|
+
recommendations: List[Dict[str, Any]]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compute_scores(discovery: DiscoveryResult) -> ScoreBreakdown:
|
|
19
|
+
recommendations: list[dict[str, Any]] = []
|
|
20
|
+
|
|
21
|
+
# SBOM score (40)
|
|
22
|
+
sbom_score = 0
|
|
23
|
+
if discovery.sbom_found:
|
|
24
|
+
sbom_score += 20
|
|
25
|
+
total_components = sum(f.component_count for f in discovery.sbom_files)
|
|
26
|
+
if total_components > 0:
|
|
27
|
+
sbom_score += 10 # placeholder for coverage
|
|
28
|
+
sbom_score += 10 # placeholder for version completeness
|
|
29
|
+
else:
|
|
30
|
+
recommendations.append(
|
|
31
|
+
{
|
|
32
|
+
"id": "sbom_missing",
|
|
33
|
+
"title": "Generate an SBOM for your product",
|
|
34
|
+
"description": "No SBOM was detected. Generate a CycloneDX or SPDX SBOM from your build pipeline.",
|
|
35
|
+
"category": "sbom",
|
|
36
|
+
"craReference": "Annex I, Section 2 – Vulnerability handling requirements",
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Vulnerability hygiene score (30) – placeholder mid-score for now
|
|
41
|
+
vuln_score = 15
|
|
42
|
+
|
|
43
|
+
# Practices score (30)
|
|
44
|
+
practices_score = 0
|
|
45
|
+
if discovery.has_security_md:
|
|
46
|
+
practices_score += 10
|
|
47
|
+
else:
|
|
48
|
+
recommendations.append(
|
|
49
|
+
{
|
|
50
|
+
"id": "security_md_missing",
|
|
51
|
+
"title": "Add a SECURITY.md file",
|
|
52
|
+
"description": "Document how users and researchers should report vulnerabilities.",
|
|
53
|
+
"category": "practices",
|
|
54
|
+
"craReference": "Annex I, Section 2 – Vulnerability handling and disclosure",
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if discovery.has_dependabot:
|
|
59
|
+
practices_score += 10
|
|
60
|
+
else:
|
|
61
|
+
recommendations.append(
|
|
62
|
+
{
|
|
63
|
+
"id": "dependabot_missing",
|
|
64
|
+
"title": "Enable automated dependency updates",
|
|
65
|
+
"description": "Configure Dependabot or a similar tool to keep dependencies up to date.",
|
|
66
|
+
"category": "practices",
|
|
67
|
+
"craReference": "Annex I, Section 1 – Secure development and maintenance",
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if discovery.has_docs_security_section:
|
|
72
|
+
practices_score += 10
|
|
73
|
+
else:
|
|
74
|
+
recommendations.append(
|
|
75
|
+
{
|
|
76
|
+
"id": "docs_security_section_missing",
|
|
77
|
+
"title": "Document security and vulnerability handling in your docs",
|
|
78
|
+
"description": "Add a section describing how you monitor, remediate, and communicate vulnerabilities.",
|
|
79
|
+
"category": "practices",
|
|
80
|
+
"craReference": "Annex I, Section 2 – Vulnerability handling requirements",
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
overall = sbom_score + vuln_score + practices_score
|
|
85
|
+
|
|
86
|
+
return ScoreBreakdown(
|
|
87
|
+
overall=overall,
|
|
88
|
+
sbom_score=sbom_score,
|
|
89
|
+
vuln_score=vuln_score,
|
|
90
|
+
practices_score=practices_score,
|
|
91
|
+
recommendations=recommendations,
|
|
92
|
+
)
|
|
93
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cra-scanner
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open-source CRA Readiness Scanner CLI for assessing EU Cyber Resilience Act readiness from SBOMs and project signals.
|
|
5
|
+
Author-email: CyberCert <info@cybercert.example>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: click>=8.1
|
|
10
|
+
Requires-Dist: rich>=13.0
|
|
11
|
+
Requires-Dist: cyclonedx-python-lib>=7.6
|
|
12
|
+
Requires-Dist: spdx-tools>=0.8
|
|
13
|
+
|
|
14
|
+
# CRA Readiness Scanner (MVP)
|
|
15
|
+
|
|
16
|
+
The CRA Readiness Scanner is an open-source CLI tool that helps engineering teams quickly assess their readiness for the EU Cyber Resilience Act (CRA) from a single SBOM or project directory.
|
|
17
|
+
|
|
18
|
+
It focuses on three things:
|
|
19
|
+
|
|
20
|
+
- SBOM presence and basic quality
|
|
21
|
+
- Basic vulnerability exposure (stubbed for MVP)
|
|
22
|
+
- Signals of good vulnerability-handling practices
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Once published to PyPI:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install cra-scanner
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For local development from this repository:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd cli
|
|
36
|
+
pip install -e .
|
|
37
|
+
cra-scanner --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
Scan a project directory (auto-discover SBOMs and signals):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cra-scanner scan .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Scan using an explicit SBOM and emit JSON to a file:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cra-scanner scan . --sbom path/to/bom.json --format json --output report.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## What the CRA Readiness Score means
|
|
55
|
+
|
|
56
|
+
The scanner returns a score from 0–100 based on:
|
|
57
|
+
|
|
58
|
+
- **SBOM (40 pts)** – existence, coverage, presence of versions.
|
|
59
|
+
- **Vulnerabilities (30 pts)** – placeholder in MVP.
|
|
60
|
+
- **Practices (30 pts)** – presence of `SECURITY.md`, Dependabot, and basic documentation signals.
|
|
61
|
+
|
|
62
|
+
The score is a directional indicator, not legal advice. It is intended to highlight gaps and next steps, not certify compliance.
|
|
63
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
cra_scanner/__init__.py
|
|
4
|
+
cra_scanner/__main__.py
|
|
5
|
+
cra_scanner/cli.py
|
|
6
|
+
cra_scanner/discovery.py
|
|
7
|
+
cra_scanner/sbom.py
|
|
8
|
+
cra_scanner/scanner.py
|
|
9
|
+
cra_scanner/scoring.py
|
|
10
|
+
cra_scanner.egg-info/PKG-INFO
|
|
11
|
+
cra_scanner.egg-info/SOURCES.txt
|
|
12
|
+
cra_scanner.egg-info/dependency_links.txt
|
|
13
|
+
cra_scanner.egg-info/entry_points.txt
|
|
14
|
+
cra_scanner.egg-info/requires.txt
|
|
15
|
+
cra_scanner.egg-info/top_level.txt
|
|
16
|
+
cra_scanner/reporters/__init__.py
|
|
17
|
+
cra_scanner/reporters/json.py
|
|
18
|
+
cra_scanner/reporters/text.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cra_scanner
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cra-scanner"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Open-source CRA Readiness Scanner CLI for assessing EU Cyber Resilience Act readiness from SBOMs and project signals."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "CyberCert", email = "info@cybercert.example" }]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"click>=8.1",
|
|
11
|
+
"rich>=13.0",
|
|
12
|
+
"cyclonedx-python-lib>=7.6",
|
|
13
|
+
"spdx-tools>=0.8",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
cra-scanner = "cra_scanner.cli:main"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["setuptools>=61.0"]
|
|
21
|
+
build-backend = "setuptools.build_meta"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["."]
|
|
25
|
+
include = ["cra_scanner*"]
|
|
26
|
+
|