dockerdna 1.0.2__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.
@@ -0,0 +1,139 @@
1
+ """
2
+ SARIF 2.1.0 output for GitHub Advanced Security integration.
3
+
4
+ DockerDNA ships with full SARIF support out of the box, enabling
5
+ GitHub Security tab annotations on pull requests.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ _TOOL = {
15
+ "driver": {
16
+ "name": "DockerDNA",
17
+ "version": "1.0.2",
18
+ "informationUri": "https://github.com/sunilgentyala/DockerDNA",
19
+ "rules": [],
20
+ }
21
+ }
22
+
23
+ _SEVERITY_MAP = {
24
+ "CRITICAL": "error",
25
+ "HIGH": "error",
26
+ "MEDIUM": "warning",
27
+ "LOW": "note",
28
+ }
29
+
30
+
31
+ def generate_sarif(
32
+ dockerfile_findings: list[Any],
33
+ compose_findings: list[Any],
34
+ secret_findings: list[Any],
35
+ supply_chain_findings: list[Any],
36
+ base_path: str = "",
37
+ ) -> dict:
38
+ rules: dict[str, dict] = {}
39
+ results: list[dict] = []
40
+
41
+ def _add_rule(rule_id: str, name: str, description: str, severity: str):
42
+ if rule_id not in rules:
43
+ rules[rule_id] = {
44
+ "id": rule_id,
45
+ "name": name,
46
+ "shortDescription": {"text": name},
47
+ "fullDescription": {"text": description},
48
+ "defaultConfiguration": {
49
+ "level": _SEVERITY_MAP.get(severity, "warning")
50
+ },
51
+ "helpUri": f"https://github.com/sunilgentyala/DockerDNA/wiki/{rule_id}",
52
+ }
53
+
54
+ def _add_result(
55
+ rule_id: str, message: str, filepath: str, line: int, severity: str
56
+ ):
57
+ uri = Path(filepath).as_posix() if filepath else "unknown"
58
+ if base_path:
59
+ try:
60
+ uri = Path(filepath).relative_to(base_path).as_posix()
61
+ except ValueError:
62
+ pass
63
+ results.append(
64
+ {
65
+ "ruleId": rule_id,
66
+ "level": _SEVERITY_MAP.get(severity, "warning"),
67
+ "message": {"text": message},
68
+ "locations": [
69
+ {
70
+ "physicalLocation": {
71
+ "artifactLocation": {"uri": uri, "uriBaseId": "%SRCROOT%"},
72
+ "region": {"startLine": max(1, line)},
73
+ }
74
+ }
75
+ ],
76
+ }
77
+ )
78
+
79
+ # Dockerfile findings
80
+ for f in dockerfile_findings:
81
+ _add_rule(f.cis_id, f.title, f.remediation, f.severity)
82
+ _add_result(f.cis_id, f.detail, "Dockerfile", f.line_number, f.severity)
83
+
84
+ # Compose findings
85
+ for f in compose_findings:
86
+ _add_rule(f.cis_id, f.title, f.remediation, f.severity)
87
+ _add_result(
88
+ f.cis_id,
89
+ f"{f.service}: {f.detail}",
90
+ "docker-compose.yml",
91
+ f.line_hint or 1,
92
+ f.severity,
93
+ )
94
+
95
+ # Secret findings
96
+ for f in secret_findings:
97
+ _add_rule(f.cis_id, f"Secret: {f.secret_type}", f.remediation, f.severity)
98
+ _add_result(
99
+ f.cis_id,
100
+ f"{f.secret_type} detected (method: {f.detection_method})",
101
+ f.file,
102
+ f.line_number,
103
+ f.severity,
104
+ )
105
+
106
+ # Supply chain findings
107
+ for f in supply_chain_findings:
108
+ if f.risk_score >= 15:
109
+ rule_id = f"{f.cis_id}-SUPPLY-CHAIN"
110
+ _add_rule(
111
+ rule_id, f"Supply chain risk: {f.image}", f.remediation, f.severity
112
+ )
113
+ _add_result(
114
+ rule_id,
115
+ f"Risk score {f.risk_score}/100 — {'; '.join(f.factors)}",
116
+ "Dockerfile",
117
+ 1,
118
+ f.severity,
119
+ )
120
+
121
+ tool = dict(_TOOL)
122
+ tool["driver"]["rules"] = list(rules.values())
123
+
124
+ return {
125
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
126
+ "version": "2.1.0",
127
+ "runs": [
128
+ {
129
+ "tool": tool,
130
+ "results": results,
131
+ "invocations": [
132
+ {
133
+ "executionSuccessful": True,
134
+ "endTimeUtc": datetime.now(timezone.utc).isoformat(),
135
+ }
136
+ ],
137
+ }
138
+ ],
139
+ }
@@ -0,0 +1,176 @@
1
+ """
2
+ CycloneDX SBOM (Software Bill of Materials) generator.
3
+
4
+ Parses Dockerfile package install instructions to produce a
5
+ CycloneDX 1.5 JSON SBOM documenting what is installed in each layer,
6
+ enabling supply-chain transparency and NTIA minimum-elements compliance.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ import uuid
13
+ from dataclasses import dataclass
14
+ from datetime import datetime, timezone
15
+ from typing import Any
16
+
17
+
18
+ @dataclass
19
+ class Component:
20
+ name: str
21
+ version: str
22
+ pkg_type: str # "deb" | "rpm" | "apk" | "pip" | "npm" | "gem" | "unknown"
23
+ source_layer: int
24
+ stage: str
25
+ purl: str = ""
26
+
27
+ def to_cyclonedx(self) -> dict:
28
+ comp: dict = {
29
+ "type": "library",
30
+ "name": self.name,
31
+ "version": self.version or "unknown",
32
+ "properties": [
33
+ {"name": "dockerdna:layer", "value": str(self.source_layer)},
34
+ {"name": "dockerdna:stage", "value": self.stage},
35
+ {"name": "dockerdna:pkg_type", "value": self.pkg_type},
36
+ ],
37
+ }
38
+ if self.purl:
39
+ comp["purl"] = self.purl
40
+ return comp
41
+
42
+
43
+ # Package manager install patterns
44
+ _INSTALLERS: list[tuple[str, str, re.Pattern]] = [
45
+ (
46
+ "deb",
47
+ "apt",
48
+ re.compile(
49
+ r"apt(?:-get)?\s+install\s+(?:-[^\s]+\s+)*(.+?)(?:\s*&&|\s*$|\s*\\)",
50
+ re.DOTALL,
51
+ ),
52
+ ),
53
+ (
54
+ "apk",
55
+ "apk",
56
+ re.compile(r"apk\s+add\s+(?:--[^\s]+\s+)*(.+?)(?:\s*&&|\s*$|\s*\\)", re.DOTALL),
57
+ ),
58
+ (
59
+ "rpm",
60
+ "yum",
61
+ re.compile(
62
+ r"yum\s+install\s+(?:-[^\s]+\s+)*(.+?)(?:\s*&&|\s*$|\s*\\)", re.DOTALL
63
+ ),
64
+ ),
65
+ (
66
+ "rpm",
67
+ "dnf",
68
+ re.compile(
69
+ r"dnf\s+install\s+(?:-[^\s]+\s+)*(.+?)(?:\s*&&|\s*$|\s*\\)", re.DOTALL
70
+ ),
71
+ ),
72
+ (
73
+ "pip",
74
+ "pip",
75
+ re.compile(
76
+ r"pip(?:3)?\s+install\s+(?:--[^\s]+\s+)*(.+?)(?:\s*&&|\s*$|\s*\\)",
77
+ re.DOTALL,
78
+ ),
79
+ ),
80
+ (
81
+ "npm",
82
+ "npm",
83
+ re.compile(
84
+ r"npm\s+install\s+(?:-[^\s]+\s+)*(.+?)(?:\s*&&|\s*$|\s*\\)", re.DOTALL
85
+ ),
86
+ ),
87
+ (
88
+ "gem",
89
+ "gem",
90
+ re.compile(
91
+ r"gem\s+install\s+(?:--[^\s]+\s+)*(.+?)(?:\s*&&|\s*$|\s*\\)", re.DOTALL
92
+ ),
93
+ ),
94
+ ]
95
+
96
+ _PKG_TOKEN = re.compile(r"[A-Za-z0-9_\-\.]+(?:=[^\s]+)?")
97
+ _VERSION_SEP = re.compile(r"[=><~!]+")
98
+
99
+
100
+ def _parse_pkg(token: str, pkg_type: str) -> tuple[str, str]:
101
+ """Split 'pkg=1.2.3' into (name, version)."""
102
+ parts = _VERSION_SEP.split(token, maxsplit=1)
103
+ name = parts[0].strip()
104
+ version = parts[1].strip() if len(parts) > 1 else ""
105
+ return name, version
106
+
107
+
108
+ def _make_purl(pkg_type: str, name: str, version: str) -> str:
109
+ if not name:
110
+ return ""
111
+ v = f"@{version}" if version else ""
112
+ return f"pkg:{pkg_type}/{name}{v}"
113
+
114
+
115
+ def extract_components(layers: list[Any]) -> list[Component]:
116
+ components: list[Component] = []
117
+ for layer in layers:
118
+ if layer.instruction != "RUN":
119
+ continue
120
+ args = layer.arguments
121
+ for pkg_type, _, pattern in _INSTALLERS:
122
+ for match in pattern.finditer(args):
123
+ raw_pkgs = match.group(1)
124
+ for token in _PKG_TOKEN.findall(raw_pkgs):
125
+ if token.startswith("-"):
126
+ continue
127
+ name, version = _parse_pkg(token, pkg_type)
128
+ if len(name) < 2:
129
+ continue
130
+ components.append(
131
+ Component(
132
+ name=name,
133
+ version=version,
134
+ pkg_type=pkg_type,
135
+ source_layer=layer.index,
136
+ stage=layer.stage,
137
+ purl=_make_purl(pkg_type, name, version),
138
+ )
139
+ )
140
+ return components
141
+
142
+
143
+ def generate_cyclonedx(
144
+ components: list[Component],
145
+ image_name: str = "unknown",
146
+ dockerfile_path: str = "Dockerfile",
147
+ ) -> dict:
148
+ """Return a CycloneDX 1.5 BOM as a Python dict (serialise to JSON)."""
149
+ now = datetime.now(timezone.utc).isoformat()
150
+ bom_ref = str(uuid.uuid4())
151
+
152
+ return {
153
+ "bomFormat": "CycloneDX",
154
+ "specVersion": "1.5",
155
+ "serialNumber": f"urn:uuid:{bom_ref}",
156
+ "version": 1,
157
+ "metadata": {
158
+ "timestamp": now,
159
+ "tools": [
160
+ {
161
+ "vendor": "DockerDNA",
162
+ "name": "DockerDNA SBOM Generator",
163
+ "version": "1.0.2",
164
+ }
165
+ ],
166
+ "component": {
167
+ "type": "container",
168
+ "name": image_name,
169
+ "version": "unknown",
170
+ "properties": [
171
+ {"name": "dockerdna:dockerfile", "value": dockerfile_path}
172
+ ],
173
+ },
174
+ },
175
+ "components": [c.to_cyclonedx() for c in components],
176
+ }
dockerdna/scanner.py ADDED
@@ -0,0 +1,234 @@
1
+ """
2
+ DockerDNA main orchestrator.
3
+
4
+ Runs all sub-scanners and assembles a unified report.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ from dockerdna.reports.html import generate_html
15
+ from dockerdna.reports.json_report import generate_json_report
16
+ from dockerdna.reports.sarif import generate_sarif
17
+ from dockerdna.reports.sbom import extract_components, generate_cyclonedx
18
+ from dockerdna.scanners.compliance import ComplianceMapper
19
+ from dockerdna.scanners.compose import ComposeScanner
20
+ from dockerdna.scanners.dockerfile import DockerfileScanner
21
+ from dockerdna.scanners.secrets import SecretsScanner
22
+ from dockerdna.scanners.supply_chain import SupplyChainScanner
23
+
24
+
25
+ def scan(
26
+ dockerfile: Optional[str] = None,
27
+ compose_file: Optional[str] = None,
28
+ extra_files: Optional[list[str]] = None,
29
+ scan_directory: Optional[str] = None,
30
+ ai_remediation: bool = False,
31
+ ai_model: str = "claude-sonnet-4-6",
32
+ output_dir: str = "dockerdna-results",
33
+ formats: list[str] | None = None,
34
+ threshold: Optional[str] = None, # CRITICAL | HIGH | MEDIUM | LOW
35
+ redact_secrets: bool = True,
36
+ verbose: bool = False,
37
+ ) -> dict:
38
+ """
39
+ Run all DockerDNA scanners and write reports.
40
+
41
+ Returns the full report dict. Raises SystemExit with code 1 if
42
+ ``threshold`` is set and findings at that severity or above are found.
43
+ """
44
+ if formats is None:
45
+ formats = ["json", "html"]
46
+
47
+ # ------------------------------------------------------------------ #
48
+ # Auto-discover files
49
+ # ------------------------------------------------------------------ #
50
+ search_roots: list[Path] = []
51
+ if scan_directory:
52
+ search_roots.append(Path(scan_directory))
53
+ if dockerfile:
54
+ search_roots.append(Path(dockerfile).parent)
55
+
56
+ if dockerfile is None and scan_directory:
57
+ for candidate in ["Dockerfile", "dockerfile"]:
58
+ p = Path(scan_directory) / candidate
59
+ if p.exists():
60
+ dockerfile = str(p)
61
+ break
62
+
63
+ if compose_file is None and scan_directory:
64
+ for candidate in [
65
+ "docker-compose.yml",
66
+ "docker-compose.yaml",
67
+ "compose.yml",
68
+ "compose.yaml",
69
+ ]:
70
+ p = Path(scan_directory) / candidate
71
+ if p.exists():
72
+ compose_file = str(p)
73
+ break
74
+
75
+ # ------------------------------------------------------------------ #
76
+ # Run scanners
77
+ # ------------------------------------------------------------------ #
78
+ secrets_scanner = SecretsScanner(redact=redact_secrets)
79
+ compose_scanner = ComposeScanner()
80
+ dockerfile_scanner = DockerfileScanner(redact=redact_secrets)
81
+ supply_chain_scanner = SupplyChainScanner()
82
+ compliance_mapper = ComplianceMapper()
83
+
84
+ # Secrets
85
+ secret_findings = []
86
+ if dockerfile:
87
+ secret_findings.extend(secrets_scanner.scan_file(dockerfile))
88
+ if compose_file:
89
+ secret_findings.extend(secrets_scanner.scan_file(compose_file))
90
+ if scan_directory:
91
+ secret_findings.extend(secrets_scanner.scan_directory(scan_directory))
92
+ if extra_files:
93
+ for ef in extra_files:
94
+ secret_findings.extend(secrets_scanner.scan_file(ef))
95
+
96
+ # Deduplicate secrets by (file, line)
97
+ seen_secrets: set[tuple[str, int]] = set()
98
+ deduped_secrets = []
99
+ for sf in secret_findings:
100
+ key = (sf.file, sf.line_number)
101
+ if key not in seen_secrets:
102
+ seen_secrets.add(key)
103
+ deduped_secrets.append(sf)
104
+ secret_findings = deduped_secrets
105
+
106
+ # Compose
107
+ compose_findings = []
108
+ if compose_file:
109
+ compose_findings = compose_scanner.scan(compose_file)
110
+
111
+ # Dockerfile + layers
112
+ layers: list = []
113
+ dockerfile_findings = []
114
+ if dockerfile:
115
+ layers, dockerfile_findings = dockerfile_scanner.scan(dockerfile)
116
+
117
+ # Supply chain
118
+ supply_chain_findings = supply_chain_scanner.analyze_base_images(layers)
119
+
120
+ # Compliance
121
+ compliance_report = compliance_mapper.generate(
122
+ dockerfile_findings,
123
+ compose_findings,
124
+ secret_findings,
125
+ supply_chain_findings,
126
+ )
127
+
128
+ # SBOM
129
+ components = extract_components(layers)
130
+ image_name = Path(dockerfile).stem if dockerfile else "unknown"
131
+ sbom = generate_cyclonedx(components, image_name, dockerfile or "Dockerfile")
132
+
133
+ # Metadata
134
+ metadata = {
135
+ "scanned_path": dockerfile or scan_directory or ".",
136
+ "compose_file": compose_file or "",
137
+ "dockerfile": dockerfile or "",
138
+ "files_scanned": (
139
+ ([dockerfile] if dockerfile else [])
140
+ + ([compose_file] if compose_file else [])
141
+ + (extra_files or [])
142
+ ),
143
+ }
144
+
145
+ # ------------------------------------------------------------------ #
146
+ # Assemble report
147
+ # ------------------------------------------------------------------ #
148
+ report = generate_json_report(
149
+ dockerfile_findings,
150
+ compose_findings,
151
+ secret_findings,
152
+ supply_chain_findings,
153
+ compliance_report,
154
+ sbom,
155
+ metadata,
156
+ )
157
+
158
+ # ------------------------------------------------------------------ #
159
+ # AI remediation (optional)
160
+ # ------------------------------------------------------------------ #
161
+ if ai_remediation:
162
+ from dockerdna.ai.remediation import get_ai_remediation
163
+
164
+ ai_result = get_ai_remediation(
165
+ dockerfile,
166
+ compose_file,
167
+ dockerfile_findings,
168
+ compose_findings,
169
+ secret_findings,
170
+ supply_chain_findings,
171
+ compliance_report.score,
172
+ model=ai_model,
173
+ )
174
+ report["ai_remediation"] = ai_result
175
+
176
+ # ------------------------------------------------------------------ #
177
+ # Write outputs
178
+ # ------------------------------------------------------------------ #
179
+ out = Path(output_dir)
180
+ out.mkdir(parents=True, exist_ok=True)
181
+
182
+ if "json" in formats:
183
+ json_path = out / "report.json"
184
+ json_path.write_text(
185
+ json.dumps(report, indent=2, default=str), encoding="utf-8"
186
+ )
187
+ if verbose:
188
+ print(f"[DockerDNA] JSON report: {json_path}")
189
+
190
+ if "html" in formats:
191
+ html_path = out / "report.html"
192
+ html_path.write_text(generate_html(report), encoding="utf-8")
193
+ if verbose:
194
+ print(f"[DockerDNA] HTML report: {html_path}")
195
+
196
+ if "sarif" in formats:
197
+ sarif_data = generate_sarif(
198
+ dockerfile_findings,
199
+ compose_findings,
200
+ secret_findings,
201
+ supply_chain_findings,
202
+ base_path=str(Path(dockerfile).parent) if dockerfile else "",
203
+ )
204
+ sarif_path = out / "report.sarif"
205
+ sarif_path.write_text(json.dumps(sarif_data, indent=2), encoding="utf-8")
206
+ if verbose:
207
+ print(f"[DockerDNA] SARIF report: {sarif_path}")
208
+
209
+ if "sbom" in formats:
210
+ sbom_path = out / "sbom.cyclonedx.json"
211
+ sbom_path.write_text(json.dumps(sbom, indent=2), encoding="utf-8")
212
+ if verbose:
213
+ print(f"[DockerDNA] SBOM: {sbom_path}")
214
+
215
+ # ------------------------------------------------------------------ #
216
+ # Threshold gate (CI/CD)
217
+ # ------------------------------------------------------------------ #
218
+ if threshold:
219
+ order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
220
+ threshold_level = order.get(threshold.upper(), 3)
221
+ by_sev = report["summary"]["by_severity"]
222
+ should_fail = any(
223
+ count > 0
224
+ for sev, count in by_sev.items()
225
+ if order.get(sev, 99) <= threshold_level
226
+ )
227
+ if should_fail:
228
+ print(
229
+ f"[DockerDNA] FAIL: findings at or above {threshold} severity detected.",
230
+ file=sys.stderr,
231
+ )
232
+ sys.exit(1)
233
+
234
+ return report
File without changes
@@ -0,0 +1,149 @@
1
+ """
2
+ CIS Docker Benchmark v1.6 compliance report generator.
3
+
4
+ Aggregates findings from all scanners and maps them to specific CIS Docker
5
+ Benchmark control IDs, producing a structured pass/fail/not-checked scorecard.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+ from dockerdna.utils.patterns import CIS_RULES, CISRule
14
+
15
+
16
+ @dataclass
17
+ class ControlResult:
18
+ rule: CISRule
19
+ status: str # PASS | FAIL | NOT_CHECKED
20
+ findings: list[dict] = field(default_factory=list)
21
+
22
+ def to_dict(self) -> dict:
23
+ return {
24
+ "id": self.rule.id,
25
+ "level": self.rule.level,
26
+ "title": self.rule.title,
27
+ "severity": self.rule.severity,
28
+ "status": self.status,
29
+ "findings_count": len(self.findings),
30
+ "remediation": self.rule.remediation if self.status == "FAIL" else "",
31
+ }
32
+
33
+
34
+ @dataclass
35
+ class ComplianceReport:
36
+ total: int
37
+ passed: int
38
+ failed: int
39
+ not_checked: int
40
+ score: float # 0–100 compliance score
41
+ controls: list[ControlResult]
42
+
43
+ def to_dict(self) -> dict:
44
+ return {
45
+ "summary": {
46
+ "total_controls": self.total,
47
+ "passed": self.passed,
48
+ "failed": self.failed,
49
+ "not_checked": self.not_checked,
50
+ "compliance_score": round(self.score, 1),
51
+ },
52
+ "controls": [c.to_dict() for c in self.controls],
53
+ }
54
+
55
+
56
+ class ComplianceMapper:
57
+ """Map raw findings to CIS Docker Benchmark controls."""
58
+
59
+ def generate(
60
+ self,
61
+ dockerfile_findings: list[Any],
62
+ compose_findings: list[Any],
63
+ secret_findings: list[Any],
64
+ supply_chain_findings: list[Any],
65
+ ) -> ComplianceReport:
66
+
67
+ # Build a map: check_key -> list of findings
68
+ hit_map: dict[str, list[dict]] = {}
69
+
70
+ for f in dockerfile_findings:
71
+ hit_map.setdefault(f.check_key, []).append(f.to_dict())
72
+
73
+ for f in compose_findings:
74
+ hit_map.setdefault(f.check_key, []).append(f.to_dict())
75
+
76
+ for f in secret_findings:
77
+ # map secret type to a check_key
78
+ key = (
79
+ "hardcoded_secret" if f.detection_method == "pattern" else "env_secret"
80
+ )
81
+ hit_map.setdefault(key, []).append(f.to_dict())
82
+
83
+ for f in supply_chain_findings:
84
+ if f.risk_score >= 15:
85
+ hit_map.setdefault("no_content_trust", []).append(f.to_dict())
86
+
87
+ results: list[ControlResult] = []
88
+ passed = failed = not_checked = 0
89
+
90
+ for rule in CIS_RULES:
91
+ findings_for_rule = hit_map.get(rule.check_key, [])
92
+ if findings_for_rule:
93
+ status = "FAIL"
94
+ failed += 1
95
+ else:
96
+ # We only mark PASS for checks we actually ran
97
+ if rule.check_key in self._checked_keys():
98
+ status = "PASS"
99
+ passed += 1
100
+ else:
101
+ status = "NOT_CHECKED"
102
+ not_checked += 1
103
+
104
+ results.append(
105
+ ControlResult(
106
+ rule=rule,
107
+ status=status,
108
+ findings=findings_for_rule,
109
+ )
110
+ )
111
+
112
+ total = len(CIS_RULES)
113
+ checkable = passed + failed
114
+ score = (passed / checkable * 100) if checkable else 0.0
115
+
116
+ return ComplianceReport(
117
+ total=total,
118
+ passed=passed,
119
+ failed=failed,
120
+ not_checked=not_checked,
121
+ score=score,
122
+ controls=results,
123
+ )
124
+
125
+ @staticmethod
126
+ def _checked_keys() -> set[str]:
127
+ """The set of check_keys that DockerDNA actually evaluates."""
128
+ return {
129
+ "no_user",
130
+ "latest_tag",
131
+ "unnecessary_packages",
132
+ "no_healthcheck",
133
+ "update_without_install",
134
+ "env_secret",
135
+ "hardcoded_secret",
136
+ "no_content_trust",
137
+ "privileged",
138
+ "excess_capabilities",
139
+ "sensitive_mount",
140
+ "docker_socket",
141
+ "host_network",
142
+ "no_memory_limit",
143
+ "no_cpu_limit",
144
+ "no_readonly_fs",
145
+ "no_new_privileges",
146
+ "privileged_ports",
147
+ "no_apparmor",
148
+ "ssh_in_container",
149
+ }