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.
Files changed (51) hide show
  1. cmmc2_toolkit-0.2.0.dist-info/METADATA +109 -0
  2. cmmc2_toolkit-0.2.0.dist-info/RECORD +51 -0
  3. cmmc2_toolkit-0.2.0.dist-info/WHEEL +5 -0
  4. cmmc2_toolkit-0.2.0.dist-info/entry_points.txt +3 -0
  5. cmmc2_toolkit-0.2.0.dist-info/top_level.txt +2 -0
  6. cmmc2agent/__init__.py +2 -0
  7. cmmc2agent/cli.py +94 -0
  8. cmmc2agent/config.py +64 -0
  9. cmmc2agent/poster.py +43 -0
  10. cmmc2toolkit/__init__.py +2 -0
  11. cmmc2toolkit/adapters/__init__.py +3 -0
  12. cmmc2toolkit/adapters/adapter_django.py +129 -0
  13. cmmc2toolkit/adapters/adapter_express.py +173 -0
  14. cmmc2toolkit/adapters/adapter_fastapi.py +156 -0
  15. cmmc2toolkit/adapters/adapter_flask.py +54 -0
  16. cmmc2toolkit/adapters/adapter_spring.py +181 -0
  17. cmmc2toolkit/adapters/base.py +48 -0
  18. cmmc2toolkit/auth/__init__.py +12 -0
  19. cmmc2toolkit/auth/models.py +28 -0
  20. cmmc2toolkit/auth/passwords.py +48 -0
  21. cmmc2toolkit/auth/store.py +189 -0
  22. cmmc2toolkit/auth/totp.py +26 -0
  23. cmmc2toolkit/checks/__init__.py +0 -0
  24. cmmc2toolkit/checks/access.py +33 -0
  25. cmmc2toolkit/checks/audit.py +11 -0
  26. cmmc2toolkit/checks/auth.py +51 -0
  27. cmmc2toolkit/checks/config.py +48 -0
  28. cmmc2toolkit/checks/integrity.py +93 -0
  29. cmmc2toolkit/checks/transport.py +16 -0
  30. cmmc2toolkit/cli.py +94 -0
  31. cmmc2toolkit/controls.py +352 -0
  32. cmmc2toolkit/dashboard/__init__.py +0 -0
  33. cmmc2toolkit/dashboard/app.py +543 -0
  34. cmmc2toolkit/dashboard/templates/audit_log.html +77 -0
  35. cmmc2toolkit/dashboard/templates/dashboard.html +454 -0
  36. cmmc2toolkit/dashboard/templates/home.html +218 -0
  37. cmmc2toolkit/dashboard/templates/login.html +67 -0
  38. cmmc2toolkit/dashboard/templates/mfa.html +55 -0
  39. cmmc2toolkit/dashboard/templates/poam.html +81 -0
  40. cmmc2toolkit/dashboard/templates/register.html +83 -0
  41. cmmc2toolkit/dashboard/templates/setup_mfa.html +49 -0
  42. cmmc2toolkit/detector/__init__.py +4 -0
  43. cmmc2toolkit/detector/base.py +79 -0
  44. cmmc2toolkit/detector/fingerprints.py +36 -0
  45. cmmc2toolkit/detector/profile.py +14 -0
  46. cmmc2toolkit/export.py +111 -0
  47. cmmc2toolkit/models.py +50 -0
  48. cmmc2toolkit/store/__init__.py +5 -0
  49. cmmc2toolkit/store/base.py +25 -0
  50. cmmc2toolkit/store/registry.py +180 -0
  51. 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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cmmc2 = cmmc2toolkit.cli:main
3
+ cmmc2agent = cmmc2agent.cli:main
@@ -0,0 +1,2 @@
1
+ cmmc2agent
2
+ cmmc2toolkit
cmmc2agent/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """cmmc2-agent — lightweight scanner agent for cmmc2-toolkit."""
2
+ __version__ = "0.1.0"
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
@@ -0,0 +1,2 @@
1
+ """cmmc2-toolkit — CMMC 2.0 Level 2 compliance scanner and dashboard."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from cmmc2toolkit.adapters.base import AdapterBase, get_adapter
2
+
3
+ __all__ = ["AdapterBase", "get_adapter"]
@@ -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