cmmc2-toolkit 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.
- cmmc2_toolkit-0.2.0.dist-info/METADATA +109 -0
- cmmc2_toolkit-0.2.0.dist-info/RECORD +51 -0
- cmmc2_toolkit-0.2.0.dist-info/WHEEL +5 -0
- cmmc2_toolkit-0.2.0.dist-info/entry_points.txt +3 -0
- cmmc2_toolkit-0.2.0.dist-info/top_level.txt +2 -0
- cmmc2agent/__init__.py +2 -0
- cmmc2agent/cli.py +94 -0
- cmmc2agent/config.py +64 -0
- cmmc2agent/poster.py +43 -0
- cmmc2toolkit/__init__.py +2 -0
- cmmc2toolkit/adapters/__init__.py +3 -0
- cmmc2toolkit/adapters/adapter_django.py +129 -0
- cmmc2toolkit/adapters/adapter_express.py +173 -0
- cmmc2toolkit/adapters/adapter_fastapi.py +156 -0
- cmmc2toolkit/adapters/adapter_flask.py +54 -0
- cmmc2toolkit/adapters/adapter_spring.py +181 -0
- cmmc2toolkit/adapters/base.py +48 -0
- cmmc2toolkit/auth/__init__.py +12 -0
- cmmc2toolkit/auth/models.py +28 -0
- cmmc2toolkit/auth/passwords.py +48 -0
- cmmc2toolkit/auth/store.py +189 -0
- cmmc2toolkit/auth/totp.py +26 -0
- cmmc2toolkit/checks/__init__.py +0 -0
- cmmc2toolkit/checks/access.py +33 -0
- cmmc2toolkit/checks/audit.py +11 -0
- cmmc2toolkit/checks/auth.py +51 -0
- cmmc2toolkit/checks/config.py +48 -0
- cmmc2toolkit/checks/integrity.py +93 -0
- cmmc2toolkit/checks/transport.py +16 -0
- cmmc2toolkit/cli.py +94 -0
- cmmc2toolkit/controls.py +352 -0
- cmmc2toolkit/dashboard/__init__.py +0 -0
- cmmc2toolkit/dashboard/app.py +543 -0
- cmmc2toolkit/dashboard/templates/audit_log.html +77 -0
- cmmc2toolkit/dashboard/templates/dashboard.html +454 -0
- cmmc2toolkit/dashboard/templates/home.html +218 -0
- cmmc2toolkit/dashboard/templates/login.html +67 -0
- cmmc2toolkit/dashboard/templates/mfa.html +55 -0
- cmmc2toolkit/dashboard/templates/poam.html +81 -0
- cmmc2toolkit/dashboard/templates/register.html +83 -0
- cmmc2toolkit/dashboard/templates/setup_mfa.html +49 -0
- cmmc2toolkit/detector/__init__.py +4 -0
- cmmc2toolkit/detector/base.py +79 -0
- cmmc2toolkit/detector/fingerprints.py +36 -0
- cmmc2toolkit/detector/profile.py +14 -0
- cmmc2toolkit/export.py +111 -0
- cmmc2toolkit/models.py +50 -0
- cmmc2toolkit/store/__init__.py +5 -0
- cmmc2toolkit/store/base.py +25 -0
- cmmc2toolkit/store/registry.py +180 -0
- cmmc2toolkit/store/store_sqlite.py +206 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cmmc2-toolkit
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: CMMC 2.0 Level 2 compliance scanner and admin dashboard for web application teams
|
|
5
|
+
Author-email: David Moorhead <david.moorhead37@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: cmmc,compliance,nist,800-171,security
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Security
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: flask>=3.0
|
|
15
|
+
Requires-Dist: click>=8.0
|
|
16
|
+
Requires-Dist: openpyxl>=3.1
|
|
17
|
+
Requires-Dist: bcrypt>=4.0
|
|
18
|
+
Requires-Dist: flask-login>=0.6
|
|
19
|
+
Requires-Dist: flask-wtf>=1.2
|
|
20
|
+
Requires-Dist: pyotp>=2.9
|
|
21
|
+
Requires-Dist: qrcode[pil]>=7.4
|
|
22
|
+
Requires-Dist: flask-limiter>=3.5
|
|
23
|
+
Requires-Dist: waitress>=3.0
|
|
24
|
+
Provides-Extra: mongo
|
|
25
|
+
Requires-Dist: pymongo>=4.0; extra == "mongo"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
29
|
+
|
|
30
|
+
# cmmc2-toolkit
|
|
31
|
+
|
|
32
|
+
A self-contained Python package that gives any web application team a **CMMC 2.0 Level 2** compliance scanner and admin dashboard — installable via pip, zero infrastructure required.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **62 controls** — all CMMC Level 1 (17), Level 2 practices, and optional Level 3 (24) controls
|
|
37
|
+
- **Auto-scan** for 5 platforms: Flask, Django, FastAPI, Express/Node.js, Spring Boot
|
|
38
|
+
- **Dashboard** — per-project compliance tracker with status, progress, target dates, and audit log
|
|
39
|
+
- **POA&M** — Plan of Action & Milestones view for Gap / Partial / Manual controls
|
|
40
|
+
- **Excel export** — one-click 3-tab workbook (Dashboard, POA&M, Audit Log)
|
|
41
|
+
- **Audit log** — every status change timestamped and attributed to a user
|
|
42
|
+
- **REST API** — agent ingest endpoint for CI/CD pipeline integration
|
|
43
|
+
|
|
44
|
+
## Quickstart
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install cmmc2-toolkit
|
|
48
|
+
|
|
49
|
+
cmmc2 serve # launch dashboard at http://localhost:5050
|
|
50
|
+
cmmc2 scan --path /path/to/project # run programmatic checks (CLI mode)
|
|
51
|
+
cmmc2 export --out poam.xlsx # export POA&M to Excel
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
On first launch the dashboard creates an admin account automatically (`admin` / `BWIadmin!2026` — change it immediately).
|
|
55
|
+
|
|
56
|
+
## Supported platforms
|
|
57
|
+
|
|
58
|
+
| Platform | Auto-detected by | Auto-checks |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| Flask | `flask` in requirements / `app.py` | 25 controls |
|
|
61
|
+
| Django | `django` in requirements / `manage.py` | 25 controls |
|
|
62
|
+
| FastAPI | `fastapi` in requirements / `main.py` | 25 controls |
|
|
63
|
+
| Express / Next.js | `express` or `next` in `package.json` | 25 controls |
|
|
64
|
+
| Spring Boot / WildFly | `spring-boot` in `pom.xml` / `WEB-INF` | 25 controls |
|
|
65
|
+
|
|
66
|
+
Unsupported platforms fall back to full manual tracking mode.
|
|
67
|
+
|
|
68
|
+
## Dashboard
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
cmmc2 serve --port 5050
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- Create projects by URL or local path
|
|
75
|
+
- Run a live scan against a local codebase
|
|
76
|
+
- Track status (Pass / Gap / Partial / Manual) per control
|
|
77
|
+
- Every status change requires a note — written to the immutable audit log
|
|
78
|
+
- Level 3 controls (NIST SP 800-172) are opt-in per project
|
|
79
|
+
- Export the full workbook (Dashboard + POA&M + Audit Log tabs) with one click
|
|
80
|
+
|
|
81
|
+
On Windows, use the included `serve.ps1` launcher if `cmmc2` is not yet on your PATH.
|
|
82
|
+
|
|
83
|
+
## Architecture
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
cmmc2toolkit/
|
|
87
|
+
├── controls.py # 62 CMMC control definitions (L1 + L2 + L3)
|
|
88
|
+
├── models.py # Control, Finding dataclasses
|
|
89
|
+
├── cli.py # click CLI (scan | serve | export | init)
|
|
90
|
+
├── detector/ # auto-detect platform from project files
|
|
91
|
+
├── adapters/ # platform-specific check runners
|
|
92
|
+
├── checks/ # atomic grep-based check functions
|
|
93
|
+
├── store/ # SQLiteStore + StoreBase ABC
|
|
94
|
+
├── auth/ # password hashing, TOTP, session management
|
|
95
|
+
├── dashboard/ # Flask micro-app (routes + Jinja2 templates)
|
|
96
|
+
└── export.py # openpyxl 3-tab Excel export
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Contributing an adapter
|
|
100
|
+
|
|
101
|
+
1. Subclass `AdapterBase` in `cmmc2toolkit/adapters/adapter_<platform>.py`
|
|
102
|
+
2. Implement `check_all() -> list[Finding]`
|
|
103
|
+
3. Register it in `adapters/base.py` → `get_adapter()` registry
|
|
104
|
+
4. Add fingerprint rules in `detector/fingerprints.py`
|
|
105
|
+
5. Open a PR
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
cmmc2agent/__init__.py,sha256=lo8AMhMpp0dcllCcc_lCfPHjMs3zSKqVTBmeCCpFPps,89
|
|
2
|
+
cmmc2agent/cli.py,sha256=fVa_m6NcyXZnvSFh85K6kAY2CNyXTovoJ6022jfiLgw,3455
|
|
3
|
+
cmmc2agent/config.py,sha256=ydnDYBAVdcZ4yRYUUpgvW3U_zbP18c1JCEBwvl3Eq3Y,1649
|
|
4
|
+
cmmc2agent/poster.py,sha256=X8ZSIzKuW51xlIyAjASzzMog0jvrZgUtdqcRoTAoWQk,1365
|
|
5
|
+
cmmc2toolkit/__init__.py,sha256=6nt07LUM6qqUB9DgZtwlA2mT84qX5xX_NGAUEMQahBM,97
|
|
6
|
+
cmmc2toolkit/cli.py,sha256=iKYxfMs5X1Sn8_uiUh3aiENY0vrZr9uMIWzp0QsDJCE,3316
|
|
7
|
+
cmmc2toolkit/controls.py,sha256=2l_cJwJjCjMP-6aG67M2EYgOJRdB8IRfhjfaGmKhFLQ,16343
|
|
8
|
+
cmmc2toolkit/export.py,sha256=AYJvtvwD1wn8V3oDs7usLvBPfCV_Nm-ss63yf0G51Pg,4125
|
|
9
|
+
cmmc2toolkit/models.py,sha256=zmLNtFRJu0QpVKFzT_WYBk7RU6M4SRywCMIM5rqkjNo,1489
|
|
10
|
+
cmmc2toolkit/adapters/__init__.py,sha256=24DMsY1UDbTt0BtVlirW8rUvqkEZMEDwBwGn7_nmXQw,106
|
|
11
|
+
cmmc2toolkit/adapters/adapter_django.py,sha256=ifyUyq2Gv0Rmrbft34w3fCVm4_NIDisco0wZpms9h-k,6630
|
|
12
|
+
cmmc2toolkit/adapters/adapter_express.py,sha256=nJSQ9j7HNUMt57CjAtPXhdq_ffLq_FGxKwxvPW7zWps,8337
|
|
13
|
+
cmmc2toolkit/adapters/adapter_fastapi.py,sha256=viD1jw8jc30OftQ4Btv6ja26WSEzqi-nrWwrPfoEjEs,8263
|
|
14
|
+
cmmc2toolkit/adapters/adapter_flask.py,sha256=xcwUzZblW0h3NpqQL1IvtCSkS9KemQPF2K0YV8Q-Fvs,2875
|
|
15
|
+
cmmc2toolkit/adapters/adapter_spring.py,sha256=mMBiRsFTz0mnbeBMoImkKDcFReSiZgfcZHWl1OQQivE,8898
|
|
16
|
+
cmmc2toolkit/adapters/base.py,sha256=1Qy8sr2kdvT6Cw4o60rflzwm9cSQvnvVpkaW_sfHp1E,1904
|
|
17
|
+
cmmc2toolkit/auth/__init__.py,sha256=X9aGekY2x65Q6Pwx-ahirDmR1ud8PBpxvkU2vuS4w5k,402
|
|
18
|
+
cmmc2toolkit/auth/models.py,sha256=qkPWXFxslG97XSU7Bsq-2wo57m5HKH2J6Lpm2qwoDI0,1060
|
|
19
|
+
cmmc2toolkit/auth/passwords.py,sha256=nq_Gpp1DPQ5VOGnWBeaF2msl8nySvmLXaHglh-XMKtU,1628
|
|
20
|
+
cmmc2toolkit/auth/store.py,sha256=Fz_KajaARgnZdPwrTwyHwpunUyPZvRs6Ud_X7MlOjJc,7761
|
|
21
|
+
cmmc2toolkit/auth/totp.py,sha256=Gu9xSUVjh37_YxKY1JU14sL5VTPqp2Zx4dKA_Kjatro,707
|
|
22
|
+
cmmc2toolkit/checks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
cmmc2toolkit/checks/access.py,sha256=rzLpvDDfLtiU_XIhZofp_7lKn1mPNPIgqlTiGJTWcv4,1766
|
|
24
|
+
cmmc2toolkit/checks/audit.py,sha256=i_HxtBuWAaoJ5hnyn8ThRfvfM_EP8PIt2ZuW5yfGN3E,431
|
|
25
|
+
cmmc2toolkit/checks/auth.py,sha256=p7ihcVJXPbFfh7lNKOoTVCDprFPhLwuWH60etmGxvQM,1960
|
|
26
|
+
cmmc2toolkit/checks/config.py,sha256=zEEIOz5x30onC86eVo6H_OEQCDAuRz9gP74EzAZ5wZg,1837
|
|
27
|
+
cmmc2toolkit/checks/integrity.py,sha256=IC-pAK_TRqT6zqHuB2WJxmbEPdjhj4TV51Rgp0-gzLg,4413
|
|
28
|
+
cmmc2toolkit/checks/transport.py,sha256=K_E0KCK8IAPIbLbJNPwpaFcIHIkJ4ZY44POXXgI_SBQ,623
|
|
29
|
+
cmmc2toolkit/dashboard/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
+
cmmc2toolkit/dashboard/app.py,sha256=97iOfk5FRN3s2PYyjcfx1x7CoJO0A-qpMUj6zpQ7_Ss,24760
|
|
31
|
+
cmmc2toolkit/dashboard/templates/audit_log.html,sha256=n4RW3QEgGr7SAhV_cw6ER4G1MvAsgw04xFjrijPL0H0,3162
|
|
32
|
+
cmmc2toolkit/dashboard/templates/dashboard.html,sha256=Qz02FEc9vMo7f23je1psoF5d2E3BIkblfMgB0d0UCMw,22542
|
|
33
|
+
cmmc2toolkit/dashboard/templates/home.html,sha256=hvQmwVjlVf-pNaZCPkHtvkP5tFjqssuUPwSNkhIHEUA,12312
|
|
34
|
+
cmmc2toolkit/dashboard/templates/login.html,sha256=xloh3KNhdn2gSvYJCQN0EwkU8LDA-gdnOq6GBTdPKnc,3347
|
|
35
|
+
cmmc2toolkit/dashboard/templates/mfa.html,sha256=LHVBffijdP0-CEVGgQHIMg2ZazFoYRPiiNDxTYvYaBk,2834
|
|
36
|
+
cmmc2toolkit/dashboard/templates/poam.html,sha256=kdYkMV1JtyoB2aABy84C_PM90ANSvixnHcAUM8A7aDM,3333
|
|
37
|
+
cmmc2toolkit/dashboard/templates/register.html,sha256=XMuz7uNGtBz0fQRcfuoKGOq8z-_L5cWFll95plBlQkg,4052
|
|
38
|
+
cmmc2toolkit/dashboard/templates/setup_mfa.html,sha256=H8bheFPnA7SH08nQ4uh5CRE3yQrNxYa_Z2db7-8PQ5Y,2319
|
|
39
|
+
cmmc2toolkit/detector/__init__.py,sha256=Hm4JD2Ards-453GjV1tdR1yPnjxIxlotsDJzHVfEzYo,163
|
|
40
|
+
cmmc2toolkit/detector/base.py,sha256=nmdVIgtA9EB20kwuMCJ2rXFuasiOYsrkg_7v4lbIXYs,2390
|
|
41
|
+
cmmc2toolkit/detector/fingerprints.py,sha256=B0quxOE9AhFrcAFi8ApSpm3cMx1OIxINF0npsm8eDmE,1647
|
|
42
|
+
cmmc2toolkit/detector/profile.py,sha256=WwXEqO1vB72IvLZdWi4Usk2dL3_uDzs8b_HQCrnBWmo,543
|
|
43
|
+
cmmc2toolkit/store/__init__.py,sha256=hh6rTTTnGktVGPsTkCIOIlp8pwHPkKoXp_3aGT7nTy4,217
|
|
44
|
+
cmmc2toolkit/store/base.py,sha256=-omdiv4Mg-29fQLkx39rtHcOrzuK6x2M085J-4jJ2TI,631
|
|
45
|
+
cmmc2toolkit/store/registry.py,sha256=YlPSqWUehbiUie4SMelhQO4tvnRk1jCenjSxzt2eDi4,6841
|
|
46
|
+
cmmc2toolkit/store/store_sqlite.py,sha256=y1XRfXXHmD0mgqppnxROQlylvtnEvERmaKd7RYwrJ0E,8642
|
|
47
|
+
cmmc2_toolkit-0.2.0.dist-info/METADATA,sha256=tQKoJy9XRFvZDRM2_jNvx9hIh-ElrpYLIEUpaDtMQDM,4308
|
|
48
|
+
cmmc2_toolkit-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
49
|
+
cmmc2_toolkit-0.2.0.dist-info/entry_points.txt,sha256=ButJtfhSa82YSBgNddNBMU4Lh4SO68-vQ_bIKbqcqW8,81
|
|
50
|
+
cmmc2_toolkit-0.2.0.dist-info/top_level.txt,sha256=T_vE6nTZCeibBZ3qYBlfUJ-bMiei3TM3bC_tTeN4caw,24
|
|
51
|
+
cmmc2_toolkit-0.2.0.dist-info/RECORD,,
|
cmmc2agent/__init__.py
ADDED
cmmc2agent/cli.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""cmmc2agent CLI entry point."""
|
|
2
|
+
import click
|
|
3
|
+
from cmmc2agent.config import load_config, TOML_TEMPLATE
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@click.group()
|
|
7
|
+
def main():
|
|
8
|
+
"""CMMC 2.0 scanner agent — posts results to a central dashboard."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@main.command()
|
|
12
|
+
@click.option("--config", default=None, help="Path to cmmc2agent.toml")
|
|
13
|
+
@click.option("--dry-run", is_flag=True, help="Run checks but do not post results.")
|
|
14
|
+
def scan(config, dry_run):
|
|
15
|
+
"""Run compliance checks and POST results to the dashboard."""
|
|
16
|
+
from cmmc2toolkit.detector import detect_platform
|
|
17
|
+
from cmmc2toolkit.adapters import get_adapter
|
|
18
|
+
from cmmc2agent.poster import post_findings
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
cfg = load_config(config)
|
|
22
|
+
except (FileNotFoundError, RuntimeError) as e:
|
|
23
|
+
click.secho(str(e), fg="red")
|
|
24
|
+
raise SystemExit(1)
|
|
25
|
+
|
|
26
|
+
click.echo(f"Scanning: {cfg.scan_path}")
|
|
27
|
+
profile = detect_platform(cfg.scan_path)
|
|
28
|
+
click.echo(f"Platform: {profile.platform} Language: {profile.language}")
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
adapter = get_adapter(profile)
|
|
32
|
+
findings = adapter.check_all()
|
|
33
|
+
except NotImplementedError as e:
|
|
34
|
+
click.secho(str(e), fg="yellow")
|
|
35
|
+
raise SystemExit(1)
|
|
36
|
+
|
|
37
|
+
passed = [f for f in findings if f.passed]
|
|
38
|
+
failed = [f for f in findings if not f.passed]
|
|
39
|
+
click.echo(f"Results : {len(passed)} passed / {len(failed)} failed")
|
|
40
|
+
|
|
41
|
+
for f in failed:
|
|
42
|
+
click.secho(f" ✗ {f.practice_id} {f.detail[:80]}", fg="red")
|
|
43
|
+
for f in passed:
|
|
44
|
+
click.secho(f" ✓ {f.practice_id} {f.detail[:80]}", fg="green")
|
|
45
|
+
|
|
46
|
+
if dry_run:
|
|
47
|
+
click.secho("\nDry run — results not posted.", fg="yellow")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
click.echo(f"\nPosting to: {cfg.ingest_url}")
|
|
51
|
+
try:
|
|
52
|
+
result = post_findings(
|
|
53
|
+
ingest_url=cfg.ingest_url,
|
|
54
|
+
api_key=cfg.api_key,
|
|
55
|
+
project_id=cfg.project_id,
|
|
56
|
+
platform=profile.platform,
|
|
57
|
+
scan_path=cfg.scan_path,
|
|
58
|
+
findings=findings,
|
|
59
|
+
)
|
|
60
|
+
click.secho(
|
|
61
|
+
f"Dashboard updated — "
|
|
62
|
+
f"{result.get('passed',0)} passed, "
|
|
63
|
+
f"{result.get('failed',0)} failed, "
|
|
64
|
+
f"{result.get('skipped',0)} skipped.",
|
|
65
|
+
fg="green", bold=True,
|
|
66
|
+
)
|
|
67
|
+
except RuntimeError as e:
|
|
68
|
+
click.secho(f"Post failed: {e}", fg="red")
|
|
69
|
+
raise SystemExit(1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@main.command("init")
|
|
73
|
+
@click.option("--dashboard-url", prompt="Central dashboard URL",
|
|
74
|
+
help="e.g. http://cmmc2.mycompany.com:5050")
|
|
75
|
+
@click.option("--api-key", prompt="API key",
|
|
76
|
+
help="Issued when you created the project on the dashboard.")
|
|
77
|
+
@click.option("--project-id", prompt="Project ID",
|
|
78
|
+
help="Shown on the dashboard home page after project creation.")
|
|
79
|
+
@click.option("--scan-path", prompt="Local scan path",
|
|
80
|
+
default=".", show_default=True,
|
|
81
|
+
help="Directory containing the app source code.")
|
|
82
|
+
@click.option("--output", default="cmmc2agent.toml", show_default=True)
|
|
83
|
+
def init_cmd(dashboard_url, api_key, project_id, scan_path, output):
|
|
84
|
+
"""Interactively create a cmmc2agent.toml config file."""
|
|
85
|
+
from pathlib import Path
|
|
86
|
+
content = TOML_TEMPLATE.format(
|
|
87
|
+
dashboard_url=dashboard_url,
|
|
88
|
+
api_key=api_key,
|
|
89
|
+
project_id=project_id,
|
|
90
|
+
scan_path=scan_path,
|
|
91
|
+
)
|
|
92
|
+
Path(output).write_text(content)
|
|
93
|
+
click.secho(f"Config written to {output}", fg="green")
|
|
94
|
+
click.echo("Run `cmmc2agent scan` to send your first results to the dashboard.")
|
cmmc2agent/config.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Load and validate cmmc2agent.toml config."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import tomllib
|
|
7
|
+
except ImportError:
|
|
8
|
+
try:
|
|
9
|
+
import tomli as tomllib # pip install tomli on Python < 3.11
|
|
10
|
+
except ImportError:
|
|
11
|
+
tomllib = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_CONFIG_PATHS = [
|
|
15
|
+
Path("cmmc2agent.toml"),
|
|
16
|
+
Path.home() / ".cmmc2" / "agent.toml",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class AgentConfig:
|
|
22
|
+
dashboard_url: str
|
|
23
|
+
api_key: str
|
|
24
|
+
project_id: str
|
|
25
|
+
scan_path: str
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def ingest_url(self) -> str:
|
|
29
|
+
return self.dashboard_url.rstrip("/") + "/api/v1/ingest"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_config(path: str | Path | None = None) -> AgentConfig:
|
|
33
|
+
if tomllib is None:
|
|
34
|
+
raise RuntimeError(
|
|
35
|
+
"TOML support not available. On Python < 3.11, run: pip install tomli"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
candidates = [Path(path)] if path else DEFAULT_CONFIG_PATHS
|
|
39
|
+
for candidate in candidates:
|
|
40
|
+
if candidate.exists():
|
|
41
|
+
with open(candidate, "rb") as f:
|
|
42
|
+
data = tomllib.load(f)
|
|
43
|
+
return AgentConfig(
|
|
44
|
+
dashboard_url=data["dashboard_url"],
|
|
45
|
+
api_key=data["api_key"],
|
|
46
|
+
project_id=data["project_id"],
|
|
47
|
+
scan_path=data["scan_path"],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
raise FileNotFoundError(
|
|
51
|
+
"No cmmc2agent.toml found. Run `cmmc2agent init` to create one, "
|
|
52
|
+
"or specify --config <path>."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
TOML_TEMPLATE = """\
|
|
57
|
+
# cmmc2agent.toml — Scanner agent configuration
|
|
58
|
+
# Generated by `cmmc2agent init`
|
|
59
|
+
|
|
60
|
+
dashboard_url = "{dashboard_url}"
|
|
61
|
+
api_key = "{api_key}"
|
|
62
|
+
project_id = "{project_id}"
|
|
63
|
+
scan_path = "{scan_path}"
|
|
64
|
+
"""
|
cmmc2agent/poster.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""HTTP poster — sends scan results to the central dashboard."""
|
|
2
|
+
import json
|
|
3
|
+
import urllib.request
|
|
4
|
+
import urllib.error
|
|
5
|
+
from cmmc2toolkit.models import Finding
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def post_findings(ingest_url: str, api_key: str, project_id: str,
|
|
9
|
+
platform: str, scan_path: str,
|
|
10
|
+
findings: list[Finding]) -> dict:
|
|
11
|
+
payload = json.dumps({
|
|
12
|
+
"project_id": project_id,
|
|
13
|
+
"platform": platform,
|
|
14
|
+
"path": scan_path,
|
|
15
|
+
"findings": [
|
|
16
|
+
{
|
|
17
|
+
"practice_id": f.practice_id,
|
|
18
|
+
"passed": f.passed,
|
|
19
|
+
"detail": f.detail,
|
|
20
|
+
"evidence": f.evidence,
|
|
21
|
+
}
|
|
22
|
+
for f in findings
|
|
23
|
+
],
|
|
24
|
+
}).encode()
|
|
25
|
+
|
|
26
|
+
req = urllib.request.Request(
|
|
27
|
+
ingest_url,
|
|
28
|
+
data=payload,
|
|
29
|
+
headers={
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"X-CMMC2-Key": api_key,
|
|
32
|
+
},
|
|
33
|
+
method="POST",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
38
|
+
return json.loads(resp.read())
|
|
39
|
+
except urllib.error.HTTPError as e:
|
|
40
|
+
body = e.read().decode(errors="replace")
|
|
41
|
+
raise RuntimeError(f"Dashboard returned {e.code}: {body}") from e
|
|
42
|
+
except urllib.error.URLError as e:
|
|
43
|
+
raise RuntimeError(f"Could not reach dashboard at {ingest_url}: {e.reason}") from e
|
cmmc2toolkit/__init__.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Django adapter — maps CMMC 2.0 controls to Django-specific checks."""
|
|
2
|
+
from cmmc2toolkit.adapters.base import AdapterBase
|
|
3
|
+
from cmmc2toolkit.models import Finding
|
|
4
|
+
from cmmc2toolkit.checks import auth, transport, config, audit, access, integrity
|
|
5
|
+
from cmmc2toolkit.checks.auth import grep
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ── Django-specific checks ────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
def check_django_auth(root: str) -> tuple[bool, str]:
|
|
11
|
+
found, ev = grep(root, r"django\.contrib\.auth|authenticate\(|login\(request")
|
|
12
|
+
return found, ev or "No Django auth framework usage found"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_django_login_required(root: str) -> tuple[bool, str]:
|
|
16
|
+
found, ev = grep(root, r"@login_required|LoginRequiredMixin|PermissionRequiredMixin")
|
|
17
|
+
return found, ev or "No @login_required or LoginRequiredMixin found in views"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_django_password_validators(root: str) -> tuple[bool, str]:
|
|
21
|
+
found, ev = grep(root, r"AUTH_PASSWORD_VALIDATORS|MinimumLengthValidator|"
|
|
22
|
+
r"CommonPasswordValidator|NumericPasswordValidator")
|
|
23
|
+
return found, ev or "No AUTH_PASSWORD_VALIDATORS in Django settings"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def check_django_session_timeout(root: str) -> tuple[bool, str]:
|
|
27
|
+
found, ev = grep(root, r"SESSION_COOKIE_AGE|SESSION_EXPIRE_AT_BROWSER_CLOSE|"
|
|
28
|
+
r"set_expiry\(")
|
|
29
|
+
return found, ev or "No session timeout config found (set SESSION_COOKIE_AGE)"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_django_csrf(root: str) -> tuple[bool, str]:
|
|
33
|
+
found, ev = grep(root, r"CsrfViewMiddleware|csrf_protect|csrf_token")
|
|
34
|
+
return found, ev or "No Django CSRF middleware or {% csrf_token %} found"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check_django_https(root: str) -> tuple[bool, str]:
|
|
38
|
+
found, ev = grep(root, r"SECURE_SSL_REDIRECT|SECURE_HSTS_SECONDS|"
|
|
39
|
+
r"SESSION_COOKIE_SECURE|CSRF_COOKIE_SECURE")
|
|
40
|
+
return found, ev or "No HTTPS enforcement settings found (set SECURE_SSL_REDIRECT = True)"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def check_django_security_headers(root: str) -> tuple[bool, str]:
|
|
44
|
+
found, ev = grep(root, r"SecurityMiddleware|SECURE_CONTENT_TYPE_NOSNIFF|"
|
|
45
|
+
r"X_FRAME_OPTIONS|SECURE_BROWSER_XSS_FILTER|"
|
|
46
|
+
r"SECURE_REFERRER_POLICY")
|
|
47
|
+
return found, ev or "No Django SecurityMiddleware or security header settings found"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_django_secret_key(root: str) -> tuple[bool, str]:
|
|
51
|
+
bad, ev = grep(root, r"SECRET_KEY\s*=\s*['\"](?!os\.|environ|get_secret|config|decouple)")
|
|
52
|
+
if bad:
|
|
53
|
+
return False, f"Hardcoded SECRET_KEY detected: {ev}"
|
|
54
|
+
return True, "SECRET_KEY loaded from environment (not hardcoded)"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check_django_audit_log(root: str) -> tuple[bool, str]:
|
|
58
|
+
found, ev = grep(root, r"auditlog|AuditlogHistoricalRecords|HistoricalRecords|"
|
|
59
|
+
r"django_simple_history|LogEntry|post_save.*signal|"
|
|
60
|
+
r"pre_delete.*signal")
|
|
61
|
+
return found, ev or "No audit logging found (consider django-auditlog or django-simple-history)"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check_django_rate_limiting(root: str) -> tuple[bool, str]:
|
|
65
|
+
found, ev = grep(root, r"django.ratelimit|ratelimit|RateThrottle|"
|
|
66
|
+
r"UserRateThrottle|AnonRateThrottle|DEFAULT_THROTTLE_RATES")
|
|
67
|
+
return found, ev or "No rate limiting found (consider django-ratelimit or DRF throttling)"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def check_django_permissions(root: str) -> tuple[bool, str]:
|
|
71
|
+
found, ev = grep(root, r"has_perm|permission_required|PermissionRequiredMixin|"
|
|
72
|
+
r"user\.groups|Meta.*permissions\s*=|DjangoModelPermissions")
|
|
73
|
+
return found, ev or "No Django permission checks found — add object-level permissions"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def check_django_mfa(root: str) -> tuple[bool, str]:
|
|
77
|
+
found, ev = grep(root, r"django_otp|django.mfa|allauth\.mfa|OTPDevice|"
|
|
78
|
+
r"TOTPDevice|totp|two.?factor")
|
|
79
|
+
return found, ev or "No MFA library found (consider django-otp or django-mfa2)"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── Adapter ───────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
class DjangoAdapter(AdapterBase):
|
|
85
|
+
platform = "django"
|
|
86
|
+
|
|
87
|
+
def check_all(self) -> list[Finding]:
|
|
88
|
+
r = self.root
|
|
89
|
+
results: list[Finding] = []
|
|
90
|
+
|
|
91
|
+
def f(pid, fn, *args):
|
|
92
|
+
passed, detail = fn(*args)
|
|
93
|
+
results.append(self._finding(pid, passed, detail))
|
|
94
|
+
|
|
95
|
+
# Identification & Authentication
|
|
96
|
+
f("IA.L1.076", check_django_auth, r)
|
|
97
|
+
f("IA.L1.077", check_django_auth, r)
|
|
98
|
+
f("IA.L2.078", check_django_password_validators, r)
|
|
99
|
+
f("IA.L2.079", auth.check_password_history, r)
|
|
100
|
+
f("IA.L2.081", auth.check_bcrypt, r)
|
|
101
|
+
f("IA.L2.082", check_django_mfa, r)
|
|
102
|
+
# Access Control (Level 1 + Level 2)
|
|
103
|
+
f("AC.L1.001", check_django_login_required, r)
|
|
104
|
+
f("AC.L1.002", access.check_rbac, r)
|
|
105
|
+
f("AC.L1.003", access.check_external_connections, r)
|
|
106
|
+
f("AC.L1.004", access.check_cui_exposure, r)
|
|
107
|
+
f("AC.L2.005", config.check_login_banner, r)
|
|
108
|
+
f("AC.L2.007", check_django_permissions, r)
|
|
109
|
+
f("AC.L2.013", check_django_session_timeout, r)
|
|
110
|
+
# Audit & Accountability
|
|
111
|
+
f("AU.L2.041", check_django_audit_log, r)
|
|
112
|
+
f("AU.L2.042", check_django_audit_log, r)
|
|
113
|
+
f("AU.L2.043", check_django_audit_log, r)
|
|
114
|
+
# System & Comms Protection (Level 1 + Level 2)
|
|
115
|
+
f("SC.L1.175", check_django_https, r)
|
|
116
|
+
f("SC.L2.178", check_django_csrf, r)
|
|
117
|
+
f("SC.L2.179", check_django_security_headers, r)
|
|
118
|
+
# Configuration Management
|
|
119
|
+
f("CM.L2.061", config.check_env_vars, r)
|
|
120
|
+
f("CM.L2.062", check_django_secret_key, r)
|
|
121
|
+
f("CM.L2.064", config.check_requirements_pinned, r)
|
|
122
|
+
# System & Info Integrity (Level 1 + Level 2)
|
|
123
|
+
f("SI.L1.210", check_django_rate_limiting, r)
|
|
124
|
+
f("SI.L1.211", integrity.check_malicious_code_protection, r)
|
|
125
|
+
f("SI.L1.212", integrity.check_dependency_update_policy, r)
|
|
126
|
+
f("SI.L1.213", integrity.check_scan_policy, r)
|
|
127
|
+
f("SI.L2.214", config.check_file_upload, r)
|
|
128
|
+
|
|
129
|
+
return results
|