audit-packs-mapping 0.1.1__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,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: audit-packs-mapping
3
+ Version: 0.1.1
4
+ Summary: Compliance mapping, coverage, and OSCAL export for audit-packs
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: audit-packs-core>=0.1.1
9
+ Requires-Dist: PyYAML>=6.0
10
+
11
+ # audit-packs-mapping
12
+
13
+ [![PyPI version](https://img.shields.io/pypi/v/audit-packs-mapping.svg)](https://pypi.org/project/audit-packs-mapping/)
14
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](../../LICENSE)
15
+
16
+ `audit-packs-mapping` is the compliance framework mapping and coverage calculation engine for the `audit-packs` ecosystem. It evaluates raw security scanner findings and maps them to control requirements in GRC frameworks (such as SOC 2, NIST 800-53, GDPR, HIPAA, and ISO 27001).
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install audit-packs-mapping
22
+ ```
23
+
24
+ ## Features
25
+
26
+ - **Framework Control Mapping**: Resolves raw scanner rule IDs (e.g. Checkov `CKV_AWS_19`, Semgrep rules) to specific compliance controls.
27
+ - **Coverage Engine**: Computes compliance pass/fail/manual rates across active control frameworks based on finding states.
28
+ - **OSCAL Export**: Generates NIST Open Security Controls Assessment Language (OSCAL) JSON representation of compliance postures.
29
+ - **Pack Registry Support**: Loads, validates, and installs compliance packs containing control-to-rule mappings.
30
+
31
+ ## Learn More
32
+
33
+ This library is part of the larger `audit-packs` Compliance Intelligence Engine. For the main command-line interface, GitHub Action integration, and framework mappings, see the [main repository](https://github.com/prakharsingh/audit-packs).
@@ -0,0 +1,23 @@
1
+ # audit-packs-mapping
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/audit-packs-mapping.svg)](https://pypi.org/project/audit-packs-mapping/)
4
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](../../LICENSE)
5
+
6
+ `audit-packs-mapping` is the compliance framework mapping and coverage calculation engine for the `audit-packs` ecosystem. It evaluates raw security scanner findings and maps them to control requirements in GRC frameworks (such as SOC 2, NIST 800-53, GDPR, HIPAA, and ISO 27001).
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pip install audit-packs-mapping
12
+ ```
13
+
14
+ ## Features
15
+
16
+ - **Framework Control Mapping**: Resolves raw scanner rule IDs (e.g. Checkov `CKV_AWS_19`, Semgrep rules) to specific compliance controls.
17
+ - **Coverage Engine**: Computes compliance pass/fail/manual rates across active control frameworks based on finding states.
18
+ - **OSCAL Export**: Generates NIST Open Security Controls Assessment Language (OSCAL) JSON representation of compliance postures.
19
+ - **Pack Registry Support**: Loads, validates, and installs compliance packs containing control-to-rule mappings.
20
+
21
+ ## Learn More
22
+
23
+ This library is part of the larger `audit-packs` Compliance Intelligence Engine. For the main command-line interface, GitHub Action integration, and framework mappings, see the [main repository](https://github.com/prakharsingh/audit-packs).
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "audit-packs-mapping"
3
+ version = "0.1.1"
4
+ description = "Compliance mapping, coverage, and OSCAL export for audit-packs"
5
+ readme = "README.md"
6
+ license = { text = "Apache-2.0" }
7
+ requires-python = ">=3.11"
8
+ dependencies = ["audit-packs-core>=0.1.1", "PyYAML>=6.0"]
9
+
10
+ [build-system]
11
+ requires = ["setuptools>=68"]
12
+ build-backend = "setuptools.build_meta"
13
+
14
+ [tool.setuptools.packages.find]
15
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,60 @@
1
+ """coverage.py — compute ControlStatus for every framework control.
2
+
3
+ Pure logic: no subprocess, no HTTP. Takes ControlFinding instances (already
4
+ mapped by packs.map_findings) and the pack directory, and returns a
5
+ ControlStatus per control — pass, fail, manual, or not_applicable.
6
+ """
7
+
8
+ from audit_packs_core.models import ControlFinding, ControlStatus, AssessmentStatus
9
+ from audit_packs_mapping.packs import iter_controls
10
+
11
+
12
+ def compute_coverage(
13
+ control_findings: list[ControlFinding],
14
+ packs_dir: str,
15
+ frameworks: list[str],
16
+ ) -> list[ControlStatus]:
17
+ """Compute a ControlStatus for every control in *frameworks*.
18
+
19
+ Args:
20
+ control_findings: All ControlFinding instances from map_findings()
21
+ (may come from diff-only or full-repo scan).
22
+ packs_dir: Path to the directory containing pack YAML files.
23
+ frameworks: List of framework ids (e.g. ["nist-800-53", "soc2"]).
24
+
25
+ Returns:
26
+ One ControlStatus per (framework, control_id) pair.
27
+ """
28
+ # Index findings by (framework, control_id) for O(1) lookup
29
+ findings_by_key: dict[tuple[str, str], list[ControlFinding]] = {}
30
+ for cf in control_findings:
31
+ key = (cf.framework, cf.control_id)
32
+ findings_by_key.setdefault(key, []).append(cf)
33
+
34
+ statuses: list[ControlStatus] = []
35
+ for fw in frameworks:
36
+ for ctrl in iter_controls(packs_dir, fw):
37
+ key = (fw, ctrl["id"])
38
+ matched = findings_by_key.get(key, [])
39
+ assessment_hint = ctrl.get("assessment")
40
+
41
+ if assessment_hint == "manual":
42
+ status = AssessmentStatus.MANUAL
43
+ elif matched:
44
+ status = AssessmentStatus.FAIL
45
+ else:
46
+ status = AssessmentStatus.PASS
47
+
48
+ statuses.append(
49
+ ControlStatus(
50
+ framework=fw,
51
+ control_id=ctrl["id"],
52
+ control_title=ctrl["title"],
53
+ status=status,
54
+ check_ids=tuple(ctrl["check_ids"]),
55
+ findings=tuple(matched),
56
+ evidence=tuple(cf.finding.evidence for cf in matched),
57
+ )
58
+ )
59
+
60
+ return statuses
@@ -0,0 +1,144 @@
1
+ """oscal.py — Emit NIST OSCAL assessment-results from ControlStatus objects.
2
+
3
+ Produces a JSON-serialisable dict that conforms to the OSCAL assessment-results
4
+ model (https://pages.nist.gov/OSCAL/). Uses stdlib only (no new dependencies).
5
+
6
+ Supports:
7
+ - One result entry per framework
8
+ - reviewed-controls with all assessed controls
9
+ - findings for FAIL controls with evidence links
10
+ - Manual controls flagged with state "not-satisfied" (human evidence required)
11
+ """
12
+
13
+ import uuid
14
+ from collections import defaultdict
15
+ from datetime import datetime, timezone
16
+
17
+ from audit_packs_core.models import AssessmentStatus, ControlStatus
18
+
19
+
20
+ def _now_iso() -> str:
21
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
22
+
23
+
24
+ def _uuid() -> str:
25
+ return str(uuid.uuid4())
26
+
27
+
28
+ def to_assessment_results(control_statuses: list[ControlStatus]) -> dict:
29
+ """Convert a list of ControlStatus objects to an OSCAL assessment-results dict.
30
+
31
+ Returns a JSON-serialisable dict with the OSCAL top-level structure:
32
+ {"assessment-results": {"uuid": ..., "metadata": ..., "results": [...]}}
33
+ """
34
+ if not control_statuses:
35
+ return {
36
+ "assessment-results": {
37
+ "uuid": _uuid(),
38
+ "metadata": _metadata(),
39
+ "results": [],
40
+ }
41
+ }
42
+
43
+ # Group by framework
44
+ by_framework: dict[str, list[ControlStatus]] = defaultdict(list)
45
+ for cs in control_statuses:
46
+ by_framework[cs.framework].append(cs)
47
+
48
+ results = []
49
+ for framework, statuses in by_framework.items():
50
+ results.append(_build_result(framework, statuses))
51
+
52
+ return {
53
+ "assessment-results": {
54
+ "uuid": _uuid(),
55
+ "metadata": _metadata(),
56
+ "results": results,
57
+ }
58
+ }
59
+
60
+
61
+ def _metadata() -> dict:
62
+ return {
63
+ "title": "audit-packs SOC 2 / NIST 800-53 Assessment Results",
64
+ "last-modified": _now_iso(),
65
+ "version": "1.0",
66
+ "oscal-version": "1.1.2",
67
+ "remarks": (
68
+ "Generated by audit-packs (https://github.com/prakharsingh/audit-packs). "
69
+ "Code-observable controls are assessed automatically; controls marked "
70
+ "assessment:manual require human evidence for SOC 2 attestation."
71
+ ),
72
+ }
73
+
74
+
75
+ def _build_result(framework: str, statuses: list[ControlStatus]) -> dict:
76
+ findings = []
77
+ control_selections: list[dict] = []
78
+
79
+ # Separate by status to build both the reviewed-controls list and findings
80
+ include_all = [{"control-id": s.control_id} for s in statuses]
81
+ if include_all:
82
+ control_selections.append({"include-controls": include_all})
83
+
84
+ for cs in statuses:
85
+ if cs.status == AssessmentStatus.FAIL:
86
+ for cf in cs.findings:
87
+ findings.append(
88
+ {
89
+ "uuid": _uuid(),
90
+ "title": f"[{cs.framework}] {cs.control_id} — {cs.control_title}",
91
+ "description": cf.finding.message,
92
+ "target": {
93
+ "type": "statement-id",
94
+ "target-id": cs.control_id,
95
+ "status": {
96
+ "state": "not-satisfied",
97
+ "reason": "fail",
98
+ },
99
+ },
100
+ "related-observations": [
101
+ {
102
+ "observation-uuid": _uuid(),
103
+ "description": (
104
+ f"Engine: {cf.finding.engine} ({cf.finding.check_id}) "
105
+ f"— {cf.finding.file}:{cf.finding.line}"
106
+ ),
107
+ "evidence": cf.finding.evidence,
108
+ "severity": cf.finding.severity,
109
+ }
110
+ ],
111
+ }
112
+ )
113
+ elif cs.status == AssessmentStatus.MANUAL:
114
+ # Manual controls appear in findings with state "not-satisfied" and
115
+ # a note that human evidence is required.
116
+ findings.append(
117
+ {
118
+ "uuid": _uuid(),
119
+ "title": f"[{cs.framework}] {cs.control_id} — {cs.control_title}",
120
+ "description": (
121
+ "This control requires manual attestation. "
122
+ "No automated checks are available for this criterion."
123
+ ),
124
+ "target": {
125
+ "type": "statement-id",
126
+ "target-id": cs.control_id,
127
+ "status": {
128
+ "state": "not-satisfied",
129
+ "reason": "manual-evidence-required",
130
+ },
131
+ },
132
+ "related-observations": [],
133
+ }
134
+ )
135
+
136
+ return {
137
+ "uuid": _uuid(),
138
+ "title": framework,
139
+ "description": f"Assessment results for {framework}",
140
+ "reviewed-controls": {
141
+ "control-selections": control_selections,
142
+ },
143
+ "findings": findings,
144
+ }
@@ -0,0 +1,160 @@
1
+ import os
2
+ import yaml
3
+ from audit_packs_core.models import Finding, ControlFinding
4
+
5
+
6
+ def load_pack(path: str) -> dict | None:
7
+ """Load a pack YAML. Returns None (instead of raising) when the file is missing."""
8
+ if not os.path.exists(path):
9
+ return None
10
+ with open(path) as fh:
11
+ data = yaml.safe_load(fh) or {}
12
+ framework_key = data.get("framework") or data.get("id")
13
+ if not framework_key or "controls" not in data:
14
+ raise ValueError(f"pack {path} missing required keys 'framework'/'controls'")
15
+ return data
16
+
17
+
18
+ def _pack_path(packs_dir: str, pack_id: str) -> str:
19
+ # 1. Check if packs_dir/pack_id exists
20
+ local_path = os.path.join(packs_dir, pack_id, "controls.yaml")
21
+ if os.path.exists(local_path):
22
+ return local_path
23
+
24
+ # 2. Check if it's installed in the user's home folder registry cache
25
+ installed_path = os.path.join(
26
+ os.path.expanduser("~"), ".audit-packs", "installed", pack_id, "controls.yaml"
27
+ )
28
+ if os.path.exists(installed_path):
29
+ return installed_path
30
+
31
+ return local_path
32
+
33
+
34
+ def _canonical_index(pack: dict) -> dict[tuple[str, str], list[tuple[str, str, tuple]]]:
35
+ """(engine, check_id) -> [(control_id, control_title, evidence_requirements), ...]"""
36
+ index: dict[tuple[str, str], list[tuple[str, str, tuple]]] = {}
37
+ for control in pack["controls"]:
38
+ ev_reqs: tuple = tuple(control.get("evidence_requirements", []))
39
+ for m in control.get("mappings", []):
40
+ key = (m["engine"], m["check_id"])
41
+ index.setdefault(key, []).append(
42
+ (
43
+ control["id"],
44
+ control.get("title", control["id"]),
45
+ ev_reqs,
46
+ )
47
+ )
48
+ return index
49
+
50
+
51
+ def _canonical_check_ids(pack: dict) -> dict[str, list[tuple[str, str]]]:
52
+ """control_id -> [(engine, check_id), ...]"""
53
+ result: dict[str, list[tuple[str, str]]] = {}
54
+ for control in pack["controls"]:
55
+ pairs = [(m["engine"], m["check_id"]) for m in control.get("mappings", [])]
56
+ result[control["id"]] = pairs
57
+ return result
58
+
59
+
60
+ def iter_controls(packs_dir: str, framework: str) -> list[dict]:
61
+ """Return every control in *framework* with its resolved check_ids."""
62
+ pack = load_pack(_pack_path(packs_dir, framework))
63
+ if pack is None:
64
+ return []
65
+ crosswalk_id = pack.get("crosswalk")
66
+
67
+ if crosswalk_id:
68
+ canonical = load_pack(_pack_path(packs_dir, crosswalk_id))
69
+ canon_checks = _canonical_check_ids(canonical)
70
+ result = []
71
+ for control in pack["controls"]:
72
+ maps_to = control.get("maps_to", [])
73
+ assessment = control.get("assessment", None)
74
+ check_ids: list[tuple[str, str]] = []
75
+ for nist_id in maps_to:
76
+ check_ids.extend(canon_checks.get(nist_id, []))
77
+ result.append(
78
+ {
79
+ "id": control["id"],
80
+ "title": control.get("title", control["id"]),
81
+ "assessment": assessment,
82
+ "check_ids": check_ids,
83
+ "maps_to": maps_to,
84
+ }
85
+ )
86
+ return result
87
+ else:
88
+ canon_checks = _canonical_check_ids(pack)
89
+ return [
90
+ {
91
+ "id": control["id"],
92
+ "title": control.get("title", control["id"]),
93
+ "assessment": None,
94
+ "check_ids": canon_checks.get(control["id"], []),
95
+ "maps_to": [],
96
+ }
97
+ for control in pack["controls"]
98
+ ]
99
+
100
+
101
+ def map_findings(
102
+ findings: list[Finding], packs_dir: str, frameworks: list[str]
103
+ ) -> list[ControlFinding]:
104
+ import sys
105
+
106
+ results: list[ControlFinding] = []
107
+ for fw in frameworks:
108
+ pack = load_pack(_pack_path(packs_dir, fw))
109
+ if pack is None:
110
+ print(
111
+ f"\n⚠️ pack not found for framework '{fw}' in {packs_dir!r} — skipping mapping.\n"
112
+ f" Install with: audit-packs pack install <source> "
113
+ f"or point --packs-dir at your packs directory.",
114
+ file=sys.stderr,
115
+ )
116
+ continue
117
+ crosswalk_id = pack.get("crosswalk")
118
+ canonical = (
119
+ load_pack(_pack_path(packs_dir, crosswalk_id)) if crosswalk_id else pack
120
+ )
121
+ if canonical is None:
122
+ print(
123
+ f"\n⚠️ crosswalk pack '{crosswalk_id}' not found for '{fw}' — skipping mapping.",
124
+ file=sys.stderr,
125
+ )
126
+ continue
127
+ check_index = _canonical_index(canonical)
128
+
129
+ if crosswalk_id:
130
+ cw: dict[str, list[tuple[str, str, tuple]]] = {}
131
+ for control in pack["controls"]:
132
+ ev_reqs: tuple = tuple(control.get("evidence_requirements", []))
133
+ for mapped in control.get("maps_to", []):
134
+ cw.setdefault(mapped, []).append(
135
+ (control["id"], control.get("title", control["id"]), ev_reqs)
136
+ )
137
+ has_manual = any(c.get("assessment") == "manual" for c in pack["controls"])
138
+ if not cw and not has_manual:
139
+ raise ValueError(
140
+ f"crosswalk pack '{fw}' has no 'maps_to' entries in any control; "
141
+ f"check that controls use 'maps_to' (not 'nist_ids' or similar)"
142
+ )
143
+
144
+ for f in findings:
145
+ hits = check_index.get((f.engine, f.check_id), [])
146
+ for canonical_control_id, canonical_title, canon_ev_reqs in hits:
147
+ if crosswalk_id:
148
+ for control_id, title, fw_ev_reqs in cw.get(
149
+ canonical_control_id, []
150
+ ):
151
+ results.append(
152
+ ControlFinding(f, fw, control_id, title, fw_ev_reqs)
153
+ )
154
+ else:
155
+ results.append(
156
+ ControlFinding(
157
+ f, fw, canonical_control_id, canonical_title, canon_ev_reqs
158
+ )
159
+ )
160
+ return results
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: audit-packs-mapping
3
+ Version: 0.1.1
4
+ Summary: Compliance mapping, coverage, and OSCAL export for audit-packs
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: audit-packs-core>=0.1.1
9
+ Requires-Dist: PyYAML>=6.0
10
+
11
+ # audit-packs-mapping
12
+
13
+ [![PyPI version](https://img.shields.io/pypi/v/audit-packs-mapping.svg)](https://pypi.org/project/audit-packs-mapping/)
14
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](../../LICENSE)
15
+
16
+ `audit-packs-mapping` is the compliance framework mapping and coverage calculation engine for the `audit-packs` ecosystem. It evaluates raw security scanner findings and maps them to control requirements in GRC frameworks (such as SOC 2, NIST 800-53, GDPR, HIPAA, and ISO 27001).
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install audit-packs-mapping
22
+ ```
23
+
24
+ ## Features
25
+
26
+ - **Framework Control Mapping**: Resolves raw scanner rule IDs (e.g. Checkov `CKV_AWS_19`, Semgrep rules) to specific compliance controls.
27
+ - **Coverage Engine**: Computes compliance pass/fail/manual rates across active control frameworks based on finding states.
28
+ - **OSCAL Export**: Generates NIST Open Security Controls Assessment Language (OSCAL) JSON representation of compliance postures.
29
+ - **Pack Registry Support**: Loads, validates, and installs compliance packs containing control-to-rule mappings.
30
+
31
+ ## Learn More
32
+
33
+ This library is part of the larger `audit-packs` Compliance Intelligence Engine. For the main command-line interface, GitHub Action integration, and framework mappings, see the [main repository](https://github.com/prakharsingh/audit-packs).
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/audit_packs_mapping/__init__.py
4
+ src/audit_packs_mapping/coverage.py
5
+ src/audit_packs_mapping/oscal.py
6
+ src/audit_packs_mapping/packs.py
7
+ src/audit_packs_mapping.egg-info/PKG-INFO
8
+ src/audit_packs_mapping.egg-info/SOURCES.txt
9
+ src/audit_packs_mapping.egg-info/dependency_links.txt
10
+ src/audit_packs_mapping.egg-info/requires.txt
11
+ src/audit_packs_mapping.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ audit-packs-core>=0.1.1
2
+ PyYAML>=6.0
@@ -0,0 +1 @@
1
+ audit_packs_mapping