@vigolium/piolium 0.0.1

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 (271) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/agents/access-auditor.md +300 -0
  4. package/agents/assumption-breaker.md +154 -0
  5. package/agents/attack-designer.md +116 -0
  6. package/agents/code-scanner.md +139 -0
  7. package/agents/concurrency-auditor.md +238 -0
  8. package/agents/confirm-writer.md +257 -0
  9. package/agents/context-reviewer.md +274 -0
  10. package/agents/cross-verifier.md +165 -0
  11. package/agents/cve-scout.md +381 -0
  12. package/agents/env-builder.md +282 -0
  13. package/agents/env-profiler.md +205 -0
  14. package/agents/evidence-collector.md +140 -0
  15. package/agents/finding-grader.md +142 -0
  16. package/agents/finding-writer.md +148 -0
  17. package/agents/flow-tracer.md +106 -0
  18. package/agents/goal-backtracer.md +146 -0
  19. package/agents/history-miner.md +467 -0
  20. package/agents/independent-verifier.md +118 -0
  21. package/agents/intent-mapper.md +183 -0
  22. package/agents/longshot-collector.md +128 -0
  23. package/agents/longshot-prober.md +126 -0
  24. package/agents/patch-auditor.md +73 -0
  25. package/agents/poc-author.md +124 -0
  26. package/agents/poc-runner.md +194 -0
  27. package/agents/probe-lead.md +269 -0
  28. package/agents/red-challenger.md +101 -0
  29. package/agents/report-composer.md +208 -0
  30. package/agents/review-adjudicator.md +216 -0
  31. package/agents/spec-auditor.md +155 -0
  32. package/agents/taint-tracer.md +265 -0
  33. package/agents/test-locator.md +209 -0
  34. package/agents/threat-modeler.md +132 -0
  35. package/agents/variant-scanner.md +108 -0
  36. package/agents/variant-spotter.md +110 -0
  37. package/bin/piolium.mjs +376 -0
  38. package/extensions/piolium/_vendor/yaml.bundle.d.mts +6 -0
  39. package/extensions/piolium/_vendor/yaml.bundle.mjs +139 -0
  40. package/extensions/piolium/agent-runner.ts +322 -0
  41. package/extensions/piolium/agents.ts +266 -0
  42. package/extensions/piolium/audit-state.ts +522 -0
  43. package/extensions/piolium/bundled-resources.ts +97 -0
  44. package/extensions/piolium/candidate-scan.ts +966 -0
  45. package/extensions/piolium/command-target.ts +177 -0
  46. package/extensions/piolium/console-stream.ts +57 -0
  47. package/extensions/piolium/export-results.ts +380 -0
  48. package/extensions/piolium/findings.ts +448 -0
  49. package/extensions/piolium/heartbeat.ts +182 -0
  50. package/extensions/piolium/help.ts +234 -0
  51. package/extensions/piolium/index.ts +1865 -0
  52. package/extensions/piolium/longshot.ts +530 -0
  53. package/extensions/piolium/matcher-suggestions.ts +196 -0
  54. package/extensions/piolium/matcher-utils.ts +83 -0
  55. package/extensions/piolium/modes/balanced.ts +750 -0
  56. package/extensions/piolium/modes/confirm-bootstrap.ts +186 -0
  57. package/extensions/piolium/modes/confirm.ts +697 -0
  58. package/extensions/piolium/modes/deep.ts +917 -0
  59. package/extensions/piolium/modes/diff.ts +177 -0
  60. package/extensions/piolium/modes/lite.ts +540 -0
  61. package/extensions/piolium/modes/longshot.ts +595 -0
  62. package/extensions/piolium/modes/merge.ts +204 -0
  63. package/extensions/piolium/modes/phase-runner.ts +267 -0
  64. package/extensions/piolium/modes/reinvest.ts +546 -0
  65. package/extensions/piolium/modes/revisit.ts +279 -0
  66. package/extensions/piolium/modes.ts +48 -0
  67. package/extensions/piolium/phase-labels.ts +123 -0
  68. package/extensions/piolium/phase-status-strip.ts +92 -0
  69. package/extensions/piolium/prompt-prefix-editor.ts +39 -0
  70. package/extensions/piolium/providers/anthropic-vertex.ts +836 -0
  71. package/extensions/piolium/recon.ts +409 -0
  72. package/extensions/piolium/result-stats.ts +105 -0
  73. package/extensions/piolium/retry.ts +120 -0
  74. package/extensions/piolium/scheduler.ts +212 -0
  75. package/extensions/piolium/secrets.ts +368 -0
  76. package/extensions/piolium/tools/web-tools.ts +148 -0
  77. package/package.json +77 -0
  78. package/skills/agentic-actions-auditor/SKILL.md +327 -0
  79. package/skills/agentic-actions-auditor/references/action-profiles.md +186 -0
  80. package/skills/agentic-actions-auditor/references/cross-file-resolution.md +209 -0
  81. package/skills/agentic-actions-auditor/references/foundations.md +94 -0
  82. package/skills/agentic-actions-auditor/references/vector-a-env-var-intermediary.md +77 -0
  83. package/skills/agentic-actions-auditor/references/vector-b-direct-expression-injection.md +83 -0
  84. package/skills/agentic-actions-auditor/references/vector-c-cli-data-fetch.md +83 -0
  85. package/skills/agentic-actions-auditor/references/vector-d-pr-target-checkout.md +88 -0
  86. package/skills/agentic-actions-auditor/references/vector-e-error-log-injection.md +88 -0
  87. package/skills/agentic-actions-auditor/references/vector-f-subshell-expansion.md +82 -0
  88. package/skills/agentic-actions-auditor/references/vector-g-eval-of-ai-output.md +91 -0
  89. package/skills/agentic-actions-auditor/references/vector-h-dangerous-sandbox-configs.md +102 -0
  90. package/skills/agentic-actions-auditor/references/vector-i-wildcard-allowlists.md +88 -0
  91. package/skills/audit/SKILL.md +562 -0
  92. package/skills/audit/assets/icon.svg +7 -0
  93. package/skills/audit/hooks/scripts/validate_phase_output.py +550 -0
  94. package/skills/audit/references/adversarial-review.md +148 -0
  95. package/skills/audit/references/architecture-aware-sast.md +306 -0
  96. package/skills/audit/references/audit-workflow.md +737 -0
  97. package/skills/audit/references/chamber-protocol.md +384 -0
  98. package/skills/audit/references/creative-attack-modes.md +221 -0
  99. package/skills/audit/references/deep-analysis.md +273 -0
  100. package/skills/audit/references/domain-attack-playbooks.md +1129 -0
  101. package/skills/audit/references/knowledge-base-template.md +513 -0
  102. package/skills/audit/references/real-env-validation.md +191 -0
  103. package/skills/audit/references/report-templates.md +417 -0
  104. package/skills/audit/references/triage-and-prereqs.md +134 -0
  105. package/skills/audit/scripts/consolidate_drafts.py +554 -0
  106. package/skills/audit/scripts/partition_findings.py +152 -0
  107. package/skills/audit/scripts/rg-hotspots.sh +121 -0
  108. package/skills/audit/scripts/stamp_file_state.py +349 -0
  109. package/skills/code-reviewer/SKILL.md +65 -0
  110. package/skills/codeql/SKILL.md +281 -0
  111. package/skills/codeql/references/build-fixes.md +90 -0
  112. package/skills/codeql/references/diagnostic-query-templates.md +339 -0
  113. package/skills/codeql/references/extension-yaml-format.md +209 -0
  114. package/skills/codeql/references/important-only-suite.md +153 -0
  115. package/skills/codeql/references/language-details.md +207 -0
  116. package/skills/codeql/references/macos-arm64e-workaround.md +179 -0
  117. package/skills/codeql/references/performance-tuning.md +111 -0
  118. package/skills/codeql/references/quality-assessment.md +172 -0
  119. package/skills/codeql/references/ruleset-catalog.md +63 -0
  120. package/skills/codeql/references/run-all-suite.md +92 -0
  121. package/skills/codeql/references/sarif-processing.md +79 -0
  122. package/skills/codeql/references/threat-models.md +51 -0
  123. package/skills/codeql/workflows/build-database.md +280 -0
  124. package/skills/codeql/workflows/create-data-extensions.md +261 -0
  125. package/skills/codeql/workflows/run-analysis.md +301 -0
  126. package/skills/differential-review/SKILL.md +220 -0
  127. package/skills/differential-review/adversarial.md +203 -0
  128. package/skills/differential-review/methodology.md +234 -0
  129. package/skills/differential-review/patterns.md +300 -0
  130. package/skills/differential-review/reporting.md +369 -0
  131. package/skills/fp-check/SKILL.md +125 -0
  132. package/skills/fp-check/references/bug-class-verification.md +114 -0
  133. package/skills/fp-check/references/deep-verification.md +143 -0
  134. package/skills/fp-check/references/evidence-templates.md +91 -0
  135. package/skills/fp-check/references/false-positive-patterns.md +115 -0
  136. package/skills/fp-check/references/gate-reviews.md +27 -0
  137. package/skills/fp-check/references/standard-verification.md +78 -0
  138. package/skills/insecure-defaults/SKILL.md +117 -0
  139. package/skills/insecure-defaults/references/examples.md +409 -0
  140. package/skills/last30days/SKILL.md +444 -0
  141. package/skills/sarif-parsing/SKILL.md +483 -0
  142. package/skills/sarif-parsing/resources/jq-queries.md +162 -0
  143. package/skills/sarif-parsing/resources/sarif_helpers.py +331 -0
  144. package/skills/security-threat-model/LICENSE.txt +201 -0
  145. package/skills/security-threat-model/SKILL.md +81 -0
  146. package/skills/security-threat-model/agents/openai.yaml +4 -0
  147. package/skills/security-threat-model/references/prompt-template.md +255 -0
  148. package/skills/security-threat-model/references/security-controls-and-assets.md +32 -0
  149. package/skills/semgrep/SKILL.md +212 -0
  150. package/skills/semgrep/references/rulesets.md +162 -0
  151. package/skills/semgrep/references/scan-modes.md +110 -0
  152. package/skills/semgrep/references/scanner-task-prompt.md +140 -0
  153. package/skills/semgrep/scripts/merge_sarif.py +203 -0
  154. package/skills/semgrep/workflows/scan-workflow.md +311 -0
  155. package/skills/semgrep-rule-creator/SKILL.md +168 -0
  156. package/skills/semgrep-rule-creator/references/quick-reference.md +202 -0
  157. package/skills/semgrep-rule-creator/references/workflow.md +240 -0
  158. package/skills/semgrep-rule-variant-creator/SKILL.md +205 -0
  159. package/skills/semgrep-rule-variant-creator/references/applicability-analysis.md +250 -0
  160. package/skills/semgrep-rule-variant-creator/references/language-syntax-guide.md +324 -0
  161. package/skills/semgrep-rule-variant-creator/references/workflow.md +518 -0
  162. package/skills/sharp-edges/SKILL.md +292 -0
  163. package/skills/sharp-edges/references/auth-patterns.md +252 -0
  164. package/skills/sharp-edges/references/case-studies.md +274 -0
  165. package/skills/sharp-edges/references/config-patterns.md +333 -0
  166. package/skills/sharp-edges/references/crypto-apis.md +190 -0
  167. package/skills/sharp-edges/references/lang-c.md +205 -0
  168. package/skills/sharp-edges/references/lang-csharp.md +285 -0
  169. package/skills/sharp-edges/references/lang-go.md +270 -0
  170. package/skills/sharp-edges/references/lang-java.md +263 -0
  171. package/skills/sharp-edges/references/lang-javascript.md +269 -0
  172. package/skills/sharp-edges/references/lang-kotlin.md +265 -0
  173. package/skills/sharp-edges/references/lang-php.md +245 -0
  174. package/skills/sharp-edges/references/lang-python.md +274 -0
  175. package/skills/sharp-edges/references/lang-ruby.md +273 -0
  176. package/skills/sharp-edges/references/lang-rust.md +272 -0
  177. package/skills/sharp-edges/references/lang-swift.md +287 -0
  178. package/skills/sharp-edges/references/language-specific.md +588 -0
  179. package/skills/spec-to-code-compliance/SKILL.md +357 -0
  180. package/skills/spec-to-code-compliance/resources/COMPLETENESS_CHECKLIST.md +69 -0
  181. package/skills/spec-to-code-compliance/resources/IR_EXAMPLES.md +417 -0
  182. package/skills/spec-to-code-compliance/resources/OUTPUT_REQUIREMENTS.md +105 -0
  183. package/skills/supply-chain-risk-auditor/SKILL.md +67 -0
  184. package/skills/supply-chain-risk-auditor/resources/results-template.md +41 -0
  185. package/skills/variant-analysis/METHODOLOGY.md +327 -0
  186. package/skills/variant-analysis/SKILL.md +142 -0
  187. package/skills/variant-analysis/resources/codeql/cpp.ql +119 -0
  188. package/skills/variant-analysis/resources/codeql/go.ql +69 -0
  189. package/skills/variant-analysis/resources/codeql/java.ql +71 -0
  190. package/skills/variant-analysis/resources/codeql/javascript.ql +63 -0
  191. package/skills/variant-analysis/resources/codeql/python.ql +80 -0
  192. package/skills/variant-analysis/resources/semgrep/cpp.yaml +98 -0
  193. package/skills/variant-analysis/resources/semgrep/go.yaml +63 -0
  194. package/skills/variant-analysis/resources/semgrep/java.yaml +61 -0
  195. package/skills/variant-analysis/resources/semgrep/javascript.yaml +60 -0
  196. package/skills/variant-analysis/resources/semgrep/python.yaml +72 -0
  197. package/skills/variant-analysis/resources/variant-report-template.md +75 -0
  198. package/skills/vuln-report/SKILL.md +137 -0
  199. package/skills/vuln-report/agents/openai.yaml +4 -0
  200. package/skills/vuln-report/references/report-template.md +135 -0
  201. package/skills/wooyun-legacy/SKILL.md +367 -0
  202. package/skills/wooyun-legacy/references/bank-penetration.md +222 -0
  203. package/skills/wooyun-legacy/references/checklists/command-execution-checklist.md +119 -0
  204. package/skills/wooyun-legacy/references/checklists/csrf-checklist.md +74 -0
  205. package/skills/wooyun-legacy/references/checklists/file-upload-checklist.md +108 -0
  206. package/skills/wooyun-legacy/references/checklists/info-disclosure-checklist.md +114 -0
  207. package/skills/wooyun-legacy/references/checklists/logic-flaws-checklist.md +95 -0
  208. package/skills/wooyun-legacy/references/checklists/misconfig-checklist.md +124 -0
  209. package/skills/wooyun-legacy/references/checklists/path-traversal-checklist.md +87 -0
  210. package/skills/wooyun-legacy/references/checklists/rce-checklist.md +93 -0
  211. package/skills/wooyun-legacy/references/checklists/sql-injection-checklist.md +97 -0
  212. package/skills/wooyun-legacy/references/checklists/ssrf-checklist.md +99 -0
  213. package/skills/wooyun-legacy/references/checklists/unauthorized-access-checklist.md +89 -0
  214. package/skills/wooyun-legacy/references/checklists/weak-password-checklist.md +115 -0
  215. package/skills/wooyun-legacy/references/checklists/xss-checklist.md +103 -0
  216. package/skills/wooyun-legacy/references/checklists/xxe-checklist.md +130 -0
  217. package/skills/wooyun-legacy/references/info-disclosure.md +975 -0
  218. package/skills/wooyun-legacy/references/logic-flaws.md +721 -0
  219. package/skills/wooyun-legacy/references/path-traversal.md +1191 -0
  220. package/skills/wooyun-legacy/references/telecom-penetration.md +156 -0
  221. package/skills/wooyun-legacy/references/unauthorized-access.md +980 -0
  222. package/skills/wooyun-legacy/references/xss.md +746 -0
  223. package/skills/zeroize-audit/SKILL.md +371 -0
  224. package/skills/zeroize-audit/configs/c.yaml +21 -0
  225. package/skills/zeroize-audit/configs/default.yaml +128 -0
  226. package/skills/zeroize-audit/configs/rust.yaml +83 -0
  227. package/skills/zeroize-audit/prompts/report_template.md +238 -0
  228. package/skills/zeroize-audit/prompts/system.md +163 -0
  229. package/skills/zeroize-audit/prompts/task.md +97 -0
  230. package/skills/zeroize-audit/references/compile-commands.md +231 -0
  231. package/skills/zeroize-audit/references/detection-strategy.md +191 -0
  232. package/skills/zeroize-audit/references/ir-analysis.md +252 -0
  233. package/skills/zeroize-audit/references/mcp-analysis.md +221 -0
  234. package/skills/zeroize-audit/references/poc-generation.md +470 -0
  235. package/skills/zeroize-audit/references/rust-zeroization-patterns.md +867 -0
  236. package/skills/zeroize-audit/schemas/input.json +83 -0
  237. package/skills/zeroize-audit/schemas/output.json +140 -0
  238. package/skills/zeroize-audit/tools/analyze_asm.sh +202 -0
  239. package/skills/zeroize-audit/tools/analyze_cfg.py +381 -0
  240. package/skills/zeroize-audit/tools/analyze_heap.sh +211 -0
  241. package/skills/zeroize-audit/tools/analyze_ir_semantic.py +429 -0
  242. package/skills/zeroize-audit/tools/diff_ir.sh +135 -0
  243. package/skills/zeroize-audit/tools/diff_rust_mir.sh +189 -0
  244. package/skills/zeroize-audit/tools/emit_asm.sh +67 -0
  245. package/skills/zeroize-audit/tools/emit_ir.sh +77 -0
  246. package/skills/zeroize-audit/tools/emit_rust_asm.sh +178 -0
  247. package/skills/zeroize-audit/tools/emit_rust_ir.sh +150 -0
  248. package/skills/zeroize-audit/tools/emit_rust_mir.sh +158 -0
  249. package/skills/zeroize-audit/tools/extract_compile_flags.py +284 -0
  250. package/skills/zeroize-audit/tools/generate_poc.py +1329 -0
  251. package/skills/zeroize-audit/tools/mcp/apply_confidence_gates.py +113 -0
  252. package/skills/zeroize-audit/tools/mcp/check_mcp.sh +68 -0
  253. package/skills/zeroize-audit/tools/mcp/normalize_mcp_evidence.py +125 -0
  254. package/skills/zeroize-audit/tools/scripts/check_llvm_patterns.py +481 -0
  255. package/skills/zeroize-audit/tools/scripts/check_mir_patterns.py +554 -0
  256. package/skills/zeroize-audit/tools/scripts/check_rust_asm.py +424 -0
  257. package/skills/zeroize-audit/tools/scripts/check_rust_asm_aarch64.py +300 -0
  258. package/skills/zeroize-audit/tools/scripts/check_rust_asm_x86.py +283 -0
  259. package/skills/zeroize-audit/tools/scripts/find_dangerous_apis.py +375 -0
  260. package/skills/zeroize-audit/tools/scripts/semantic_audit.py +923 -0
  261. package/skills/zeroize-audit/tools/track_dataflow.sh +196 -0
  262. package/skills/zeroize-audit/tools/validate_rust_toolchain.sh +298 -0
  263. package/skills/zeroize-audit/workflows/phase-0-preflight.md +150 -0
  264. package/skills/zeroize-audit/workflows/phase-1-source-analysis.md +144 -0
  265. package/skills/zeroize-audit/workflows/phase-2-compiler-analysis.md +139 -0
  266. package/skills/zeroize-audit/workflows/phase-3-interim-report.md +46 -0
  267. package/skills/zeroize-audit/workflows/phase-4-poc-generation.md +46 -0
  268. package/skills/zeroize-audit/workflows/phase-5-poc-validation.md +136 -0
  269. package/skills/zeroize-audit/workflows/phase-6-final-report.md +44 -0
  270. package/skills/zeroize-audit/workflows/phase-7-test-generation.md +42 -0
  271. package/themes/piolium-srcery.json +94 -0
@@ -0,0 +1,1329 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.9"
4
+ # dependencies = ["pyyaml>=6.0"]
5
+ # ///
6
+ """
7
+ Generate proof-of-concept C programs from zeroize-audit findings.
8
+
9
+ Each PoC demonstrates that a finding is exploitable by reading sensitive
10
+ data that should have been zeroized. PoCs exit 0 when the secret persists
11
+ (exploitable) and exit 1 when the data has been wiped (not exploitable).
12
+
13
+ Usage:
14
+ python generate_poc.py \\
15
+ --findings <findings.json> \\
16
+ --compile-db <compile_commands.json> \\
17
+ --out <output_dir> \\
18
+ [--categories CAT1,CAT2,...] \\
19
+ [--config <config.yaml>]
20
+
21
+ Exit codes:
22
+ 0 PoCs generated successfully
23
+ 1 Invalid input (bad JSON, missing required fields)
24
+ 2 No exploitable findings in the selected categories
25
+ 3 Output directory error
26
+ """
27
+
28
+ import argparse
29
+ import json
30
+ import os
31
+ import re
32
+ import subprocess
33
+ import sys
34
+ import textwrap
35
+ from pathlib import Path
36
+ from typing import Any
37
+
38
+ try:
39
+ import yaml
40
+ except ImportError:
41
+ yaml = None # type: ignore[assignment]
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Categories that support PoC generation
45
+ # ---------------------------------------------------------------------------
46
+ EXPLOITABLE_CATEGORIES = frozenset(
47
+ [
48
+ "MISSING_SOURCE_ZEROIZE",
49
+ "OPTIMIZED_AWAY_ZEROIZE",
50
+ "STACK_RETENTION",
51
+ "REGISTER_SPILL",
52
+ "SECRET_COPY",
53
+ "MISSING_ON_ERROR_PATH",
54
+ "PARTIAL_WIPE",
55
+ "NOT_ON_ALL_PATHS",
56
+ "INSECURE_HEAP_ALLOC",
57
+ "LOOP_UNROLLED_INCOMPLETE",
58
+ "NOT_DOMINATING_EXITS",
59
+ ]
60
+ )
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # Defaults
64
+ # ---------------------------------------------------------------------------
65
+ _DEFAULT_SECRET_FILL: int = 0xAA
66
+ _DEFAULT_SOURCE_INCLUSION_THRESHOLD: int = 5000
67
+ _DEFAULT_STACK_PROBE_MAX: int = 4096
68
+ _DEFAULT_MIN_CONFIDENCE: str = "likely"
69
+
70
+ _CONFIDENCE_ORDER = {"confirmed": 0, "likely": 1, "needs_review": 2}
71
+
72
+ _TOOLS_DIR = Path(__file__).resolve().parent
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Helpers
77
+ # ---------------------------------------------------------------------------
78
+
79
+
80
+ def _load_config(config_path: str | None) -> dict[str, Any]:
81
+ """Load a YAML config file and return the poc_generation section."""
82
+ if not config_path:
83
+ return {}
84
+ path = Path(config_path)
85
+ if yaml is None:
86
+ sys.stderr.write("Error: --config requires pyyaml. Install with: pip install pyyaml\n")
87
+ sys.exit(1)
88
+ if not path.exists():
89
+ sys.stderr.write(f"Error: config file not found: {path}\n")
90
+ sys.exit(1)
91
+ with open(path) as f:
92
+ data = yaml.safe_load(f) or {}
93
+ return data.get("poc_generation", {})
94
+
95
+
96
+ def _get_compile_flags(compile_db: str, src_file: str) -> list[str] | None:
97
+ """Call extract_compile_flags.py and return flags as a list, or None on failure."""
98
+ script = _TOOLS_DIR / "extract_compile_flags.py"
99
+ if not script.exists():
100
+ sys.stderr.write(f"Warning: compile flag extractor not found: {script}\n")
101
+ return None
102
+ try:
103
+ result = subprocess.run(
104
+ [
105
+ sys.executable,
106
+ str(script),
107
+ "--compile-db",
108
+ compile_db,
109
+ "--src",
110
+ src_file,
111
+ "--format",
112
+ "json",
113
+ ],
114
+ capture_output=True,
115
+ text=True,
116
+ timeout=30,
117
+ )
118
+ if result.returncode != 0:
119
+ sys.stderr.write(
120
+ f"Warning: extract_compile_flags.py exited with code {result.returncode}"
121
+ f" for {src_file}\n"
122
+ )
123
+ return None
124
+ return json.loads(result.stdout)
125
+ except subprocess.TimeoutExpired:
126
+ sys.stderr.write(f"Warning: extract_compile_flags.py timed out for {src_file}\n")
127
+ return None
128
+ except json.JSONDecodeError as exc:
129
+ sys.stderr.write(
130
+ f"Warning: extract_compile_flags.py returned invalid JSON for {src_file}: {exc}\n"
131
+ )
132
+ return None
133
+ except OSError as exc:
134
+ sys.stderr.write(f"Warning: failed to run extract_compile_flags.py for {src_file}: {exc}\n")
135
+ return None
136
+
137
+
138
+ def _count_lines(path: str) -> int:
139
+ """Return the number of lines in a file, or 0 if unreadable."""
140
+ try:
141
+ with open(path) as f:
142
+ return sum(1 for _ in f)
143
+ except OSError:
144
+ return 0
145
+
146
+
147
+ def _extract_function_signature(src_file: str, line: int) -> str | None:
148
+ """
149
+ Attempt to extract the function signature surrounding the given line number.
150
+ Returns the function name if found, or None.
151
+ """
152
+ try:
153
+ with open(src_file) as f:
154
+ lines = f.readlines()
155
+ except OSError:
156
+ return None
157
+
158
+ # Search backwards from the finding line to find a function definition
159
+ start = max(0, line - 30)
160
+ end = min(len(lines), line + 5)
161
+ region = "".join(lines[start:end])
162
+
163
+ # Match C/C++ function definitions: return_type func_name(params) {
164
+ pattern = re.compile(
165
+ r"(?:^|\n)\s*"
166
+ r"(?:static\s+|inline\s+|extern\s+|__attribute__\s*\([^)]*\)\s+)*"
167
+ r"(?:(?:const\s+|unsigned\s+|signed\s+|volatile\s+)*\w[\w\s*&]*?)\s+"
168
+ r"(\w+)\s*\([^)]*\)\s*(?:\{|$)",
169
+ re.MULTILINE,
170
+ )
171
+ matches = list(pattern.finditer(region))
172
+ if matches:
173
+ return matches[-1].group(1)
174
+ return None
175
+
176
+
177
+ def _is_cpp_file(src_file: str) -> bool:
178
+ """Return True if the source file appears to be C++."""
179
+ ext = Path(src_file).suffix.lower()
180
+ return ext in (".cpp", ".cxx", ".cc", ".C", ".hpp", ".hxx")
181
+
182
+
183
+ def _is_rust_file(src_file: str) -> bool:
184
+ """Return True if the source file appears to be Rust."""
185
+ return Path(src_file).suffix.lower() == ".rs"
186
+
187
+
188
+ def _relative_source_path(src_file: str, out_dir: str) -> str:
189
+ """Compute a relative path from out_dir to src_file."""
190
+ try:
191
+ return os.path.relpath(src_file, out_dir)
192
+ except ValueError:
193
+ return src_file
194
+
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # poc_common.h generation
198
+ # ---------------------------------------------------------------------------
199
+
200
+
201
+ def _generate_common_header(
202
+ secret_fill: int = _DEFAULT_SECRET_FILL, stack_probe_max: int = _DEFAULT_STACK_PROBE_MAX
203
+ ) -> str:
204
+ return textwrap.dedent(f"""\
205
+ #ifndef POC_COMMON_H
206
+ #define POC_COMMON_H
207
+
208
+ #include <stdio.h>
209
+ #include <stdlib.h>
210
+ #include <string.h>
211
+ #include <stdint.h>
212
+
213
+ #define SECRET_FILL_BYTE 0x{secret_fill:02X}
214
+ #define STACK_PROBE_MAX {stack_probe_max}
215
+
216
+ #define POC_PASS() do {{ \\
217
+ fprintf(stderr, "POC PASS: secret persists (exploitable)\\n"); \\
218
+ exit(0); \\
219
+ }} while (0)
220
+
221
+ #define POC_FAIL() do {{ \\
222
+ fprintf(stderr, "POC FAIL: secret was wiped (not exploitable)\\n"); \\
223
+ exit(1); \\
224
+ }} while (0)
225
+
226
+ /* Read through a volatile pointer to prevent the compiler from
227
+ optimizing away the verification read. Returns non-zero if any
228
+ byte in [ptr, ptr+len) is non-zero. */
229
+ static int volatile_read_nonzero(const void *ptr, size_t len) {{
230
+ const volatile unsigned char *p = (const volatile unsigned char *)ptr;
231
+ int found = 0;
232
+ for (size_t i = 0; i < len; i++) {{
233
+ if (p[i] != 0) {{
234
+ found = 1;
235
+ }}
236
+ }}
237
+ return found;
238
+ }}
239
+
240
+ /* Read through volatile pointer checking for the secret fill pattern. */
241
+ static int volatile_read_has_secret(const void *ptr, size_t len) {{
242
+ const volatile unsigned char *p = (const volatile unsigned char *)ptr;
243
+ int count = 0;
244
+ for (size_t i = 0; i < len; i++) {{
245
+ if (p[i] == SECRET_FILL_BYTE) {{
246
+ count++;
247
+ }}
248
+ }}
249
+ /* Consider it a match if >= 50% of bytes are the fill pattern */
250
+ return count >= (int)(len / 2);
251
+ }}
252
+
253
+ /* Dump hex to stderr for diagnostics. */
254
+ static void hex_dump(const char *label, const void *ptr, size_t len) {{
255
+ const unsigned char *p = (const unsigned char *)ptr;
256
+ fprintf(stderr, "%s (%zu bytes):", label, len);
257
+ for (size_t i = 0; i < len && i < 64; i++) {{
258
+ if (i % 16 == 0) fprintf(stderr, "\\n ");
259
+ fprintf(stderr, "%02x ", p[i]);
260
+ }}
261
+ if (len > 64) fprintf(stderr, "\\n ... (%zu more bytes)", len - 64);
262
+ fprintf(stderr, "\\n");
263
+ }}
264
+
265
+ /* Probe the stack for residual secret data from a prior call frame.
266
+ Must be __attribute__((noinline, noclone)) so the compiler cannot
267
+ merge this frame with the caller. */
268
+ __attribute__((noinline))
269
+ #if defined(__GNUC__) && !defined(__clang__)
270
+ __attribute__((noclone))
271
+ #endif
272
+ static int stack_probe(size_t frame_size) {{
273
+ if (frame_size > STACK_PROBE_MAX) frame_size = STACK_PROBE_MAX;
274
+ volatile unsigned char probe[STACK_PROBE_MAX];
275
+ /* Do NOT initialize — we want to read whatever is on the stack */
276
+ int count = 0;
277
+ for (size_t i = 0; i < frame_size; i++) {{
278
+ if (probe[i] == SECRET_FILL_BYTE) {{
279
+ count++;
280
+ }}
281
+ }}
282
+ return count >= (int)(frame_size / 4); /* 25% threshold */
283
+ }}
284
+
285
+ /* Fill a buffer with the secret marker pattern. */
286
+ static void fill_secret(void *buf, size_t len) {{
287
+ memset(buf, SECRET_FILL_BYTE, len);
288
+ }}
289
+
290
+ /* Check whether heap memory retains secret data after free+realloc.
291
+ Do NOT compile with ASan — it poisons freed memory and hides the bug. */
292
+ static int heap_residue_check(size_t alloc_size) {{
293
+ void *ptr = malloc(alloc_size);
294
+ if (!ptr) return 0;
295
+ fill_secret(ptr, alloc_size);
296
+ free(ptr);
297
+ void *ptr2 = malloc(alloc_size);
298
+ if (!ptr2) return 0;
299
+ int found = volatile_read_has_secret(ptr2, alloc_size);
300
+ hex_dump("Heap residue after free+realloc", ptr2,
301
+ alloc_size > 64 ? 64 : alloc_size);
302
+ free(ptr2);
303
+ return found;
304
+ }}
305
+
306
+ #endif /* POC_COMMON_H */
307
+ """)
308
+
309
+
310
+ # ---------------------------------------------------------------------------
311
+ # Per-category PoC generators
312
+ # ---------------------------------------------------------------------------
313
+
314
+
315
+ class PoCGenerator:
316
+ """Base class for per-category PoC generators."""
317
+
318
+ category: str = ""
319
+ opt_level: str = "-O0"
320
+
321
+ def __init__(
322
+ self, finding: dict[str, Any], compile_db: str, out_dir: str, config: dict[str, Any]
323
+ ):
324
+ self.finding = finding
325
+ self.compile_db = compile_db
326
+ self.out_dir = out_dir
327
+ self.config = config
328
+ self.finding_id = finding.get("id", "unknown")
329
+ self.src_file = finding.get("file", "")
330
+ self.line = finding.get("line", 0)
331
+ self.symbol = finding.get("symbol")
332
+ self.requires_manual = False
333
+ self.adjustment_notes: str | None = None
334
+
335
+ def _func_name(self) -> str | None:
336
+ if self.symbol:
337
+ return self.symbol
338
+ return _extract_function_signature(self.src_file, self.line)
339
+
340
+ def _source_include_path(self) -> str:
341
+ return _relative_source_path(self.src_file, self.out_dir)
342
+
343
+ def _use_source_inclusion(self) -> bool:
344
+ threshold = self.config.get(
345
+ "source_inclusion_threshold", _DEFAULT_SOURCE_INCLUSION_THRESHOLD
346
+ )
347
+ return _count_lines(self.src_file) <= threshold
348
+
349
+ def _flags_str(self) -> str:
350
+ flags = _get_compile_flags(self.compile_db, self.src_file)
351
+ if flags is None:
352
+ return ""
353
+ # Filter out optimization flags — we set our own
354
+ return " ".join(f for f in flags if not re.match(r"^-O[0-3sg]$", f))
355
+
356
+ def _poc_filename(self) -> str:
357
+ safe_id = re.sub(r"[^a-zA-Z0-9_-]", "_", self.finding_id)
358
+ ext = ".cpp" if _is_cpp_file(self.src_file) else ".c"
359
+ return f"poc_{safe_id}_{self.category.lower()}{ext}"
360
+
361
+ def _compiler_var(self) -> str:
362
+ return "$(CXX)" if _is_cpp_file(self.src_file) else "$(CC)"
363
+
364
+ def _include_directive(self) -> str:
365
+ func = self._func_name()
366
+ if self._use_source_inclusion():
367
+ return f'#include "{self._source_include_path()}"'
368
+ return f"/* Link against object file containing {func or 'target function'} */"
369
+
370
+ def _build_poc_source(self, comment_lines: list[str], body_lines: list[str]) -> str:
371
+ """Assemble a PoC C source file with correct indentation."""
372
+ parts: list[str] = []
373
+ parts.append("/* " + comment_lines[0])
374
+ for cl in comment_lines[1:]:
375
+ parts.append(" * " + cl)
376
+ parts.append(" */")
377
+ parts.append('#include "poc_common.h"')
378
+ parts.append(self._include_directive())
379
+ parts.append("")
380
+ parts.append("int main(void) {")
381
+ for bl in body_lines:
382
+ if bl == "":
383
+ parts.append("")
384
+ else:
385
+ parts.append(" " + bl)
386
+ parts.append("}")
387
+ parts.append("")
388
+ return "\n".join(parts)
389
+
390
+ def generate(self) -> tuple[str, str]:
391
+ """Generate PoC source code. Returns (filename, source_code)."""
392
+ raise NotImplementedError
393
+
394
+ def makefile_target(self, filename: str) -> str:
395
+ """Return a Makefile target string for this PoC."""
396
+ binary = Path(filename).stem
397
+ flags = self._flags_str()
398
+ compiler = self._compiler_var()
399
+ return (
400
+ f"{binary}: {filename} poc_common.h\n\t{compiler} {self.opt_level} {flags} -o $@ $<\n"
401
+ )
402
+
403
+ def manifest_entry(self, filename: str) -> dict[str, Any]:
404
+ """Return a manifest entry for this PoC."""
405
+ entry: dict[str, Any] = {
406
+ "finding_id": self.finding_id,
407
+ "category": self.category,
408
+ "file": filename,
409
+ "makefile_target": Path(filename).stem,
410
+ "compile_opt": self.opt_level,
411
+ "requires_manual_adjustment": self.requires_manual,
412
+ }
413
+ if self.adjustment_notes:
414
+ entry["adjustment_notes"] = self.adjustment_notes
415
+ return entry
416
+
417
+
418
+ class MissingSourceZeroizePoC(PoCGenerator):
419
+ category = "MISSING_SOURCE_ZEROIZE"
420
+ opt_level = "-O0"
421
+
422
+ def generate(self) -> tuple[str, str]:
423
+ func = self._func_name()
424
+ filename = self._poc_filename()
425
+ comment = [
426
+ f"PoC for finding {self.finding_id}: {self.category}",
427
+ f"Source: {self.src_file}:{self.line}",
428
+ "Strategy: Call function at -O0, volatile-read buffer after return,",
429
+ " verify secret persists.",
430
+ ]
431
+
432
+ if func:
433
+ body = [
434
+ "unsigned char secret_buf[256];",
435
+ "fill_secret(secret_buf, sizeof(secret_buf));",
436
+ "",
437
+ "/* Call the function that handles the secret */",
438
+ f"{func}(/* TODO: fill in arguments */);",
439
+ "",
440
+ "/* Check if the secret buffer still contains data */",
441
+ "if (volatile_read_nonzero(secret_buf, sizeof(secret_buf)))",
442
+ " POC_PASS();",
443
+ "else",
444
+ " POC_FAIL();",
445
+ ]
446
+ self.requires_manual = True
447
+ self.adjustment_notes = (
448
+ f"Fill in arguments for {func}() call and adjust "
449
+ "secret_buf to point to the actual sensitive variable."
450
+ )
451
+ else:
452
+ body = [
453
+ "/* TODO: call the function that handles the secret */",
454
+ "/* TODO: volatile-read the secret buffer after return */",
455
+ "/* if (volatile_read_nonzero(ptr, len)) POC_PASS(); else POC_FAIL(); */",
456
+ 'fprintf(stderr, "PoC requires manual adjustment\\n");',
457
+ "exit(1);",
458
+ ]
459
+ self.requires_manual = True
460
+ self.adjustment_notes = (
461
+ "Could not determine function signature. "
462
+ "Fill in function call and secret buffer check."
463
+ )
464
+
465
+ return filename, self._build_poc_source(comment, body)
466
+
467
+
468
+ class OptimizedAwayZeroizePoC(PoCGenerator):
469
+ category = "OPTIMIZED_AWAY_ZEROIZE"
470
+
471
+ def __init__(self, *args: Any, **kwargs: Any):
472
+ super().__init__(*args, **kwargs)
473
+ compiler_ev = self.finding.get("compiler_evidence", {}) or {}
474
+ diff_summary = compiler_ev.get("diff_summary", "")
475
+ match = re.search(r"O([1-3s])", diff_summary)
476
+ if match:
477
+ self.opt_level = f"-O{match.group(1)}"
478
+ else:
479
+ self.opt_level = "-O2"
480
+
481
+ def generate(self) -> tuple[str, str]:
482
+ func = self._func_name()
483
+ filename = self._poc_filename()
484
+ comment = [
485
+ f"PoC for finding {self.finding_id}: {self.category}",
486
+ f"Source: {self.src_file}:{self.line}",
487
+ f"Strategy: Compile at {self.opt_level} where the wipe vanishes,",
488
+ " call function, volatile-read buffer.",
489
+ ]
490
+
491
+ if func:
492
+ body = [
493
+ "unsigned char secret_buf[256];",
494
+ "fill_secret(secret_buf, sizeof(secret_buf));",
495
+ "",
496
+ "/* Call function that contains the wipe the compiler removes */",
497
+ f"{func}(/* TODO: fill in arguments */);",
498
+ "",
499
+ "/* At this opt level the compiler has removed the wipe.",
500
+ " Volatile-read the buffer to see if secret persists. */",
501
+ "if (volatile_read_nonzero(secret_buf, sizeof(secret_buf)))",
502
+ " POC_PASS();",
503
+ "else",
504
+ " POC_FAIL();",
505
+ ]
506
+ self.requires_manual = True
507
+ self.adjustment_notes = (
508
+ f"Fill in arguments for {func}(). "
509
+ f"Compile at {self.opt_level} where the wipe disappears."
510
+ )
511
+ else:
512
+ body = [
513
+ "/* TODO: call function whose wipe is optimized away */",
514
+ 'fprintf(stderr, "PoC requires manual adjustment\\n");',
515
+ "exit(1);",
516
+ ]
517
+ self.requires_manual = True
518
+ self.adjustment_notes = "Could not determine function signature."
519
+
520
+ return filename, self._build_poc_source(comment, body)
521
+
522
+
523
+ class StackRetentionPoC(PoCGenerator):
524
+ category = "STACK_RETENTION"
525
+ opt_level = "-O2"
526
+
527
+ def generate(self) -> tuple[str, str]:
528
+ func = self._func_name()
529
+ filename = self._poc_filename()
530
+ evidence = self.finding.get("evidence", "")
531
+ frame_match = re.search(r"(\d+)\s*bytes?\s*(?:frame|stack|alloc)", evidence)
532
+ frame_size = frame_match.group(1) if frame_match else "256"
533
+
534
+ comment = [
535
+ f"PoC for finding {self.finding_id}: {self.category}",
536
+ f"Source: {self.src_file}:{self.line}",
537
+ "Strategy: Call function, immediately call stack_probe() with",
538
+ " matching frame size to detect residual secrets.",
539
+ ]
540
+
541
+ if func:
542
+ body = [
543
+ "/* Call the function that leaves secrets on the stack */",
544
+ f"{func}(/* TODO: fill in arguments */);",
545
+ "",
546
+ "/* Immediately probe the stack for residual secret data */",
547
+ f"if (stack_probe({frame_size}))",
548
+ " POC_PASS();",
549
+ "else",
550
+ " POC_FAIL();",
551
+ ]
552
+ self.requires_manual = True
553
+ self.adjustment_notes = (
554
+ f"Fill in arguments for {func}(). "
555
+ f"Frame size {frame_size} is estimated from evidence; adjust if needed."
556
+ )
557
+ else:
558
+ body = [
559
+ "/* TODO: call the function that retains secrets on stack */",
560
+ f"if (stack_probe({frame_size}))",
561
+ " POC_PASS();",
562
+ "else",
563
+ " POC_FAIL();",
564
+ ]
565
+ self.requires_manual = True
566
+ self.adjustment_notes = "Could not determine function signature."
567
+
568
+ return filename, self._build_poc_source(comment, body)
569
+
570
+
571
+ class RegisterSpillPoC(PoCGenerator):
572
+ category = "REGISTER_SPILL"
573
+ opt_level = "-O2"
574
+
575
+ def generate(self) -> tuple[str, str]:
576
+ func = self._func_name()
577
+ filename = self._poc_filename()
578
+ evidence = self.finding.get("evidence", "")
579
+ offset_match = re.search(r"-(\d+)\(%[re][sb]p\)", evidence)
580
+ spill_offset = offset_match.group(1) if offset_match else "64"
581
+
582
+ comment = [
583
+ f"PoC for finding {self.finding_id}: {self.category}",
584
+ f"Source: {self.src_file}:{self.line}",
585
+ "Strategy: Like stack retention but probe the specific spill",
586
+ " offset region from ASM evidence.",
587
+ ]
588
+
589
+ if func:
590
+ body = [
591
+ "/* Call the function that spills secrets to stack */",
592
+ f"{func}(/* TODO: fill in arguments */);",
593
+ "",
594
+ "/* Probe the specific spill offset region */",
595
+ f"if (stack_probe({spill_offset}))",
596
+ " POC_PASS();",
597
+ "else",
598
+ " POC_FAIL();",
599
+ ]
600
+ self.requires_manual = True
601
+ self.adjustment_notes = (
602
+ f"Fill in arguments for {func}(). "
603
+ f"Spill offset {spill_offset} from ASM evidence; adjust if needed."
604
+ )
605
+ else:
606
+ body = [
607
+ "/* TODO: call the function that spills registers to stack */",
608
+ f"if (stack_probe({spill_offset}))",
609
+ " POC_PASS();",
610
+ "else",
611
+ " POC_FAIL();",
612
+ ]
613
+ self.requires_manual = True
614
+ self.adjustment_notes = "Could not determine function signature."
615
+
616
+ return filename, self._build_poc_source(comment, body)
617
+
618
+
619
+ class SecretCopyPoC(PoCGenerator):
620
+ category = "SECRET_COPY"
621
+ opt_level = "-O0"
622
+
623
+ def generate(self) -> tuple[str, str]:
624
+ func = self._func_name()
625
+ filename = self._poc_filename()
626
+ comment = [
627
+ f"PoC for finding {self.finding_id}: {self.category}",
628
+ f"Source: {self.src_file}:{self.line}",
629
+ "Strategy: Call function at -O0, verify original may be wiped,",
630
+ " volatile-read the copy destination.",
631
+ ]
632
+
633
+ if func:
634
+ body = [
635
+ "/* Call function; it copies the secret internally */",
636
+ f"{func}(/* TODO: fill in arguments */);",
637
+ "",
638
+ "/* The original may be wiped, but the copy destination persists.",
639
+ " TODO: point this at the actual copy destination buffer. */",
640
+ "unsigned char *copy_dest = NULL; /* TODO: set to copy destination */",
641
+ "if (copy_dest && volatile_read_has_secret(copy_dest, 256))",
642
+ " POC_PASS();",
643
+ "else",
644
+ " POC_FAIL();",
645
+ ]
646
+ self.requires_manual = True
647
+ self.adjustment_notes = (
648
+ f"Fill in arguments for {func}() and set copy_dest to "
649
+ "point to the buffer where the secret is copied."
650
+ )
651
+ else:
652
+ body = [
653
+ "/* TODO: call the function that copies the secret */",
654
+ "/* TODO: volatile-read the copy destination after return */",
655
+ 'fprintf(stderr, "PoC requires manual adjustment\\n");',
656
+ "exit(1);",
657
+ ]
658
+ self.requires_manual = True
659
+ self.adjustment_notes = "Could not determine function signature or copy destination."
660
+
661
+ return filename, self._build_poc_source(comment, body)
662
+
663
+
664
+ class MissingOnErrorPathPoC(PoCGenerator):
665
+ category = "MISSING_ON_ERROR_PATH"
666
+ opt_level = "-O0"
667
+
668
+ def generate(self) -> tuple[str, str]:
669
+ func = self._func_name()
670
+ filename = self._poc_filename()
671
+ comment = [
672
+ f"PoC for finding {self.finding_id}: {self.category}",
673
+ f"Source: {self.src_file}:{self.line}",
674
+ "Strategy: Force the error path via controlled input,",
675
+ " volatile-read buffer after error return.",
676
+ ]
677
+
678
+ if func:
679
+ body = [
680
+ "unsigned char secret_buf[256];",
681
+ "fill_secret(secret_buf, sizeof(secret_buf));",
682
+ "",
683
+ "/* Force the error path via controlled input.",
684
+ " TODO: set up inputs that trigger the error return. */",
685
+ f"int ret = {func}(/* TODO: error-triggering arguments */);",
686
+ "",
687
+ 'fprintf(stderr, "Function returned: %d\\n", ret);',
688
+ 'hex_dump("Secret buffer after error return", secret_buf,',
689
+ " sizeof(secret_buf));",
690
+ "",
691
+ "/* After error return the secret should have been wiped */",
692
+ "if (volatile_read_has_secret(secret_buf, sizeof(secret_buf)))",
693
+ " POC_PASS();",
694
+ "else",
695
+ " POC_FAIL();",
696
+ ]
697
+ self.requires_manual = True
698
+ self.adjustment_notes = (
699
+ f"Fill in error-triggering arguments for {func}(). "
700
+ "The error path must be taken to demonstrate missing cleanup."
701
+ )
702
+ else:
703
+ body = [
704
+ "/* TODO: call function with error-triggering inputs */",
705
+ "/* TODO: volatile-read buffer after error return */",
706
+ 'fprintf(stderr, "PoC requires manual adjustment\\n");',
707
+ "exit(1);",
708
+ ]
709
+ self.requires_manual = True
710
+ self.adjustment_notes = "Could not determine function signature."
711
+
712
+ return filename, self._build_poc_source(comment, body)
713
+
714
+
715
+ class PartialWipePoC(PoCGenerator):
716
+ category = "PARTIAL_WIPE"
717
+ opt_level = "-O0"
718
+
719
+ def generate(self) -> tuple[str, str]:
720
+ func = self._func_name()
721
+ filename = self._poc_filename()
722
+ evidence = self.finding.get("evidence", "")
723
+
724
+ # Try to extract wiped vs full sizes from evidence
725
+ size_matches = re.findall(r"(\d+)\s*bytes?", evidence)
726
+ if len(size_matches) >= 2:
727
+ wiped_size = size_matches[0]
728
+ full_size = size_matches[1]
729
+ else:
730
+ wiped_size = "8"
731
+ full_size = "256"
732
+
733
+ comment = [
734
+ f"PoC for finding {self.finding_id}: {self.category}",
735
+ f"Source: {self.src_file}:{self.line}",
736
+ "Strategy: Fill full buffer with secret, call function, volatile-read",
737
+ " the tail beyond the incorrectly-sized wipe.",
738
+ ]
739
+
740
+ if func:
741
+ body = [
742
+ f"unsigned char buf[{full_size}];",
743
+ f"fill_secret(buf, {full_size});",
744
+ "",
745
+ "/* Call function that partially wipes the buffer */",
746
+ f"{func}(/* TODO: fill in arguments */);",
747
+ "",
748
+ f"/* The wipe covers only {wiped_size} bytes of {full_size}.",
749
+ " Check the tail beyond the wiped region. */",
750
+ f"if (volatile_read_has_secret(buf + {wiped_size}, {full_size} - {wiped_size}))",
751
+ " POC_PASS();",
752
+ "else",
753
+ " POC_FAIL();",
754
+ ]
755
+ self.requires_manual = True
756
+ self.adjustment_notes = (
757
+ f"Fill in arguments for {func}(). "
758
+ f"Wiped size {wiped_size} and full size {full_size} are estimated "
759
+ "from evidence; adjust if needed."
760
+ )
761
+ else:
762
+ body = [
763
+ f"unsigned char buf[{full_size}];",
764
+ f"fill_secret(buf, {full_size});",
765
+ "",
766
+ "/* TODO: call the function that partially wipes the buffer */",
767
+ "",
768
+ f"/* Check tail beyond the {wiped_size}-byte wipe */",
769
+ f"if (volatile_read_has_secret(buf + {wiped_size}, {full_size} - {wiped_size}))",
770
+ " POC_PASS();",
771
+ "else",
772
+ " POC_FAIL();",
773
+ ]
774
+ self.requires_manual = True
775
+ self.adjustment_notes = (
776
+ "Could not determine function signature. "
777
+ f"Wiped size {wiped_size} and full size {full_size} are estimated; "
778
+ "adjust if needed."
779
+ )
780
+
781
+ return filename, self._build_poc_source(comment, body)
782
+
783
+
784
+ class NotOnAllPathsPoC(PoCGenerator):
785
+ category = "NOT_ON_ALL_PATHS"
786
+ opt_level = "-O0"
787
+
788
+ def generate(self) -> tuple[str, str]:
789
+ func = self._func_name()
790
+ filename = self._poc_filename()
791
+ evidence = self.finding.get("evidence", "")
792
+
793
+ # Try to extract uncovered path line from evidence
794
+ line_match = re.search(r"line (\d+)", evidence)
795
+ uncovered_line = line_match.group(1) if line_match else "unknown"
796
+
797
+ comment = [
798
+ f"PoC for finding {self.finding_id}: {self.category}",
799
+ f"Source: {self.src_file}:{self.line}",
800
+ "Strategy: Force execution down the uncovered path that lacks the wipe,",
801
+ " then volatile-read the secret buffer.",
802
+ ]
803
+
804
+ if func:
805
+ body = [
806
+ "unsigned char secret_buf[256];",
807
+ "fill_secret(secret_buf, sizeof(secret_buf));",
808
+ "",
809
+ "/* Force the uncovered path (no wipe).",
810
+ f" TODO: set up inputs that take the path at line {uncovered_line}. */",
811
+ f"{func}(/* TODO: path-forcing arguments */);",
812
+ "",
813
+ "/* After taking the uncovered path the secret should persist */",
814
+ "if (volatile_read_has_secret(secret_buf, sizeof(secret_buf)))",
815
+ " POC_PASS();",
816
+ "else",
817
+ " POC_FAIL();",
818
+ ]
819
+ self.requires_manual = True
820
+ self.adjustment_notes = (
821
+ f"Fill in arguments for {func}() that force execution through "
822
+ f"the uncovered path (line {uncovered_line}). "
823
+ "Identify which inputs bypass the wipe."
824
+ )
825
+ else:
826
+ body = [
827
+ "/* TODO: call function with inputs that take the uncovered path */",
828
+ "/* TODO: volatile-read buffer after return */",
829
+ 'fprintf(stderr, "PoC requires manual adjustment\\n");',
830
+ "exit(1);",
831
+ ]
832
+ self.requires_manual = True
833
+ self.adjustment_notes = (
834
+ "Could not determine function signature. "
835
+ "Identify inputs that force the uncovered path."
836
+ )
837
+
838
+ return filename, self._build_poc_source(comment, body)
839
+
840
+
841
+ class InsecureHeapAllocPoC(PoCGenerator):
842
+ category = "INSECURE_HEAP_ALLOC"
843
+ opt_level = "-O0"
844
+
845
+ def generate(self) -> tuple[str, str]:
846
+ func = self._func_name()
847
+ filename = self._poc_filename()
848
+ evidence = self.finding.get("evidence", "")
849
+
850
+ # Extract allocation size and allocator from evidence
851
+ size_match = re.search(r"(\d+)", evidence)
852
+ alloc_size = size_match.group(1) if size_match else "256"
853
+ alloc_match = re.search(r"(malloc|calloc|realloc)", evidence)
854
+ allocator = alloc_match.group(1) if alloc_match else "malloc"
855
+
856
+ comment = [
857
+ f"PoC for finding {self.finding_id}: {self.category}",
858
+ f"Source: {self.src_file}:{self.line}",
859
+ "Strategy: Demonstrate heap residue — allocate, fill with secret, free,",
860
+ " re-allocate same size, check if secret persists.",
861
+ "NOTE: Do NOT compile with ASan (it poisons freed memory).",
862
+ ]
863
+
864
+ body = [
865
+ f"/* Demonstrate that {allocator}() leaves secret residue after free */",
866
+ f"if (heap_residue_check({alloc_size}))",
867
+ " POC_PASS();",
868
+ "else",
869
+ " POC_FAIL();",
870
+ ]
871
+
872
+ if func:
873
+ body.extend(
874
+ [
875
+ "",
876
+ "/* Additionally, call the function that uses the insecure allocator",
877
+ " and verify residue after it returns. */",
878
+ f"/* {func}(/ * TODO: fill in arguments * /); */",
879
+ ]
880
+ )
881
+ self.requires_manual = False # Self-contained heap check works
882
+ self.adjustment_notes = (
883
+ f"The self-contained heap_residue_check() demonstrates the "
884
+ f"vulnerability. Optionally uncomment and fill in {func}() "
885
+ "for a function-specific test."
886
+ )
887
+ else:
888
+ self.requires_manual = False
889
+ self.adjustment_notes = (
890
+ f"Self-contained PoC using heap_residue_check({alloc_size}). "
891
+ "Optionally add a call to the target function for specificity."
892
+ )
893
+
894
+ return filename, self._build_poc_source(comment, body)
895
+
896
+
897
+ class LoopUnrolledIncompletePoC(PoCGenerator):
898
+ category = "LOOP_UNROLLED_INCOMPLETE"
899
+ opt_level = "-O2"
900
+
901
+ def generate(self) -> tuple[str, str]:
902
+ func = self._func_name()
903
+ filename = self._poc_filename()
904
+ evidence = self.finding.get("evidence", "")
905
+
906
+ # Extract covered bytes and object size from evidence
907
+ covered_match = re.search(r"(\d+)\s*consecutive", evidence)
908
+ covered_bytes = covered_match.group(1) if covered_match else "16"
909
+ size_match = re.search(r"object size is (\d+)", evidence)
910
+ full_size = size_match.group(1) if size_match else "256"
911
+
912
+ comment = [
913
+ f"PoC for finding {self.finding_id}: {self.category}",
914
+ f"Source: {self.src_file}:{self.line}",
915
+ "Strategy: Compile at -O2 where incomplete loop unrolling occurs.",
916
+ f" Fill buffer, call function, check tail beyond {covered_bytes}",
917
+ f" unrolled bytes (object size: {full_size}).",
918
+ ]
919
+
920
+ if func:
921
+ body = [
922
+ f"unsigned char buf[{full_size}];",
923
+ f"fill_secret(buf, {full_size});",
924
+ "",
925
+ "/* Call function whose wipe loop is incompletely unrolled at -O2 */",
926
+ f"{func}(/* TODO: fill in arguments */);",
927
+ "",
928
+ f"/* The compiler unrolled {covered_bytes} bytes of the wipe loop",
929
+ f" but the object is {full_size} bytes. Check the tail. */",
930
+ (
931
+ f"if (volatile_read_has_secret(buf + {covered_bytes},"
932
+ f" {full_size} - {covered_bytes}))"
933
+ ),
934
+ " POC_PASS();",
935
+ "else",
936
+ " POC_FAIL();",
937
+ ]
938
+ self.requires_manual = True
939
+ self.adjustment_notes = (
940
+ f"Fill in arguments for {func}(). "
941
+ f"Covered bytes {covered_bytes} and object size {full_size} are "
942
+ "estimated from IR evidence; adjust if needed. "
943
+ "Must compile at -O2 for unrolling to occur."
944
+ )
945
+ else:
946
+ body = [
947
+ f"unsigned char buf[{full_size}];",
948
+ f"fill_secret(buf, {full_size});",
949
+ "",
950
+ "/* TODO: call function with incompletely unrolled wipe loop */",
951
+ "",
952
+ f"/* Check tail beyond the {covered_bytes}-byte unrolled region */",
953
+ (
954
+ f"if (volatile_read_has_secret(buf + {covered_bytes},"
955
+ f" {full_size} - {covered_bytes}))"
956
+ ),
957
+ " POC_PASS();",
958
+ "else",
959
+ " POC_FAIL();",
960
+ ]
961
+ self.requires_manual = True
962
+ self.adjustment_notes = (
963
+ "Could not determine function signature. "
964
+ f"Covered bytes {covered_bytes} and object size {full_size} are "
965
+ "estimated; adjust if needed."
966
+ )
967
+
968
+ return filename, self._build_poc_source(comment, body)
969
+
970
+
971
+ class NotDominatingExitsPoC(PoCGenerator):
972
+ category = "NOT_DOMINATING_EXITS"
973
+ opt_level = "-O0"
974
+
975
+ def generate(self) -> tuple[str, str]:
976
+ func = self._func_name()
977
+ filename = self._poc_filename()
978
+ evidence = self.finding.get("evidence", "")
979
+
980
+ # Extract exit line or path count from CFG evidence
981
+ exit_match = re.search(r"exit at line (\d+)", evidence)
982
+ path_match = re.search(r"(\d+) of (\d+) exit paths", evidence)
983
+ if exit_match:
984
+ exit_info = f"line {exit_match.group(1)}"
985
+ elif path_match:
986
+ exit_info = f"{path_match.group(1)} of {path_match.group(2)} exit paths"
987
+ else:
988
+ exit_info = "an exit path that bypasses the wipe"
989
+
990
+ comment = [
991
+ f"PoC for finding {self.finding_id}: {self.category}",
992
+ f"Source: {self.src_file}:{self.line}",
993
+ "Strategy: Force execution through an exit path that bypasses the wipe",
994
+ f" (CFG evidence: {exit_info}), then volatile-read the secret.",
995
+ ]
996
+
997
+ if func:
998
+ body = [
999
+ "unsigned char secret_buf[256];",
1000
+ "fill_secret(secret_buf, sizeof(secret_buf));",
1001
+ "",
1002
+ "/* Force execution through the exit path that bypasses the wipe.",
1003
+ f" CFG shows the wipe does not dominate {exit_info}.",
1004
+ " TODO: set up inputs that reach this exit path. */",
1005
+ f"{func}(/* TODO: exit-path-forcing arguments */);",
1006
+ "",
1007
+ "/* After taking the non-dominated exit the secret should persist */",
1008
+ "if (volatile_read_has_secret(secret_buf, sizeof(secret_buf)))",
1009
+ " POC_PASS();",
1010
+ "else",
1011
+ " POC_FAIL();",
1012
+ ]
1013
+ self.requires_manual = True
1014
+ self.adjustment_notes = (
1015
+ f"Fill in arguments for {func}() that force execution through "
1016
+ f"{exit_info} (the exit not dominated by the wipe). "
1017
+ "Requires understanding of the function's control flow."
1018
+ )
1019
+ else:
1020
+ body = [
1021
+ "/* TODO: call function with inputs that reach the non-dominated exit */",
1022
+ "/* TODO: volatile-read buffer after return */",
1023
+ 'fprintf(stderr, "PoC requires manual adjustment\\n");',
1024
+ "exit(1);",
1025
+ ]
1026
+ self.requires_manual = True
1027
+ self.adjustment_notes = (
1028
+ "Could not determine function signature. "
1029
+ "Identify inputs that reach the exit path bypassing the wipe."
1030
+ )
1031
+
1032
+ return filename, self._build_poc_source(comment, body)
1033
+
1034
+
1035
+ # ---------------------------------------------------------------------------
1036
+ # Category -> generator mapping
1037
+ # ---------------------------------------------------------------------------
1038
+ _GENERATORS: dict[str, type] = {
1039
+ "MISSING_SOURCE_ZEROIZE": MissingSourceZeroizePoC,
1040
+ "OPTIMIZED_AWAY_ZEROIZE": OptimizedAwayZeroizePoC,
1041
+ "STACK_RETENTION": StackRetentionPoC,
1042
+ "REGISTER_SPILL": RegisterSpillPoC,
1043
+ "SECRET_COPY": SecretCopyPoC,
1044
+ "MISSING_ON_ERROR_PATH": MissingOnErrorPathPoC,
1045
+ "PARTIAL_WIPE": PartialWipePoC,
1046
+ "NOT_ON_ALL_PATHS": NotOnAllPathsPoC,
1047
+ "INSECURE_HEAP_ALLOC": InsecureHeapAllocPoC,
1048
+ "LOOP_UNROLLED_INCOMPLETE": LoopUnrolledIncompletePoC,
1049
+ "NOT_DOMINATING_EXITS": NotDominatingExitsPoC,
1050
+ }
1051
+
1052
+
1053
+ # ---------------------------------------------------------------------------
1054
+ # Makefile generation
1055
+ # ---------------------------------------------------------------------------
1056
+
1057
+
1058
+ def _generate_makefile(targets: list[dict[str, str]]) -> str:
1059
+ """Generate a Makefile for all PoC targets."""
1060
+ lines = [
1061
+ "# Auto-generated by generate_poc.py",
1062
+ "# Build: make all",
1063
+ "# Run: make run",
1064
+ "",
1065
+ "CC ?= cc",
1066
+ "CXX ?= c++",
1067
+ "CFLAGS ?= -Wall -Wextra",
1068
+ "CXXFLAGS ?= -Wall -Wextra",
1069
+ "",
1070
+ "BINARIES =",
1071
+ ]
1072
+
1073
+ binary_names = []
1074
+ target_blocks = []
1075
+
1076
+ for t in targets:
1077
+ binary = t["binary"]
1078
+ binary_names.append(binary)
1079
+ target_blocks.append(t["rule"])
1080
+
1081
+ lines[9] = "BINARIES = " + " ".join(binary_names)
1082
+ lines.append("")
1083
+ lines.append(".PHONY: all run clean")
1084
+ lines.append("")
1085
+ lines.append("all: $(BINARIES)")
1086
+ lines.append("")
1087
+
1088
+ # Run target
1089
+ lines.append("run: all")
1090
+ for name in binary_names:
1091
+ lines.append(f"\t@echo '--- Running {name} ---'")
1092
+ lines.append(f"\t@./{name} && echo 'RESULT: EXPLOITABLE' || echo 'RESULT: NOT EXPLOITABLE'")
1093
+ lines.append("")
1094
+
1095
+ # Per-target rules
1096
+ for block in target_blocks:
1097
+ lines.append(block)
1098
+ lines.append("")
1099
+
1100
+ lines.append("clean:")
1101
+ lines.append("\trm -f $(BINARIES)")
1102
+ lines.append("")
1103
+
1104
+ return "\n".join(lines)
1105
+
1106
+
1107
+ # ---------------------------------------------------------------------------
1108
+ # Main logic
1109
+ # ---------------------------------------------------------------------------
1110
+
1111
+
1112
+ def _filter_findings(
1113
+ findings: list[dict[str, Any]], categories: frozenset, min_confidence: str | None
1114
+ ) -> list[dict[str, Any]]:
1115
+ """Filter findings to only exploitable categories above confidence threshold.
1116
+
1117
+ When min_confidence is None, all findings in the selected categories are
1118
+ returned regardless of confidence level.
1119
+ """
1120
+ result = []
1121
+ for f in findings:
1122
+ cat = f.get("category", "")
1123
+ if cat not in categories:
1124
+ continue
1125
+ if min_confidence is None:
1126
+ result.append(f)
1127
+ continue
1128
+ threshold = _CONFIDENCE_ORDER.get(min_confidence, 2)
1129
+ # Map needs_review boolean to confidence string
1130
+ conf = "needs_review" if f.get("needs_review", False) else "likely"
1131
+ # Check evidence/compiler_evidence for confirmed signals
1132
+ if f.get("compiler_evidence"):
1133
+ conf = "confirmed"
1134
+ # CFG-backed findings use evidence_source instead of compiler_evidence
1135
+ evidence_sources = f.get("evidence_source", [])
1136
+ if isinstance(evidence_sources, list) and "cfg" in evidence_sources:
1137
+ conf = "confirmed"
1138
+ if _CONFIDENCE_ORDER.get(conf, 2) <= threshold:
1139
+ result.append(f)
1140
+ return result
1141
+
1142
+
1143
+ def run(
1144
+ findings_path: str,
1145
+ compile_db: str,
1146
+ out_dir: str,
1147
+ categories: list[str] | None = None,
1148
+ config_path: str | None = None,
1149
+ no_confidence_filter: bool = False,
1150
+ ) -> int:
1151
+ """Main entry point. Returns exit code.
1152
+
1153
+ Args:
1154
+ no_confidence_filter: When True, generate PoCs for all findings
1155
+ regardless of confidence level.
1156
+ """
1157
+
1158
+ # Load findings
1159
+ try:
1160
+ with open(findings_path) as f:
1161
+ data = json.load(f)
1162
+ except (OSError, json.JSONDecodeError) as exc:
1163
+ sys.stderr.write(f"Error: cannot read findings: {exc}\n")
1164
+ return 1
1165
+
1166
+ # Support both top-level array and {findings: [...]} format
1167
+ if isinstance(data, list):
1168
+ findings = data
1169
+ elif isinstance(data, dict):
1170
+ findings = data.get("findings", [])
1171
+ else:
1172
+ sys.stderr.write("Error: findings must be a JSON array or object with 'findings' key\n")
1173
+ return 1
1174
+
1175
+ # Load config
1176
+ config = _load_config(config_path)
1177
+ min_confidence: str | None = (
1178
+ None if no_confidence_filter else config.get("min_confidence", _DEFAULT_MIN_CONFIDENCE)
1179
+ )
1180
+ secret_fill = config.get("secret_fill_byte", _DEFAULT_SECRET_FILL)
1181
+ stack_probe_max = config.get("stack_probe_max_size", _DEFAULT_STACK_PROBE_MAX)
1182
+
1183
+ # Determine categories
1184
+ if categories:
1185
+ selected = frozenset(categories) & EXPLOITABLE_CATEGORIES
1186
+ else:
1187
+ selected = EXPLOITABLE_CATEGORIES
1188
+
1189
+ # Filter findings
1190
+ exploitable = _filter_findings(findings, selected, min_confidence)
1191
+ if not exploitable:
1192
+ sys.stderr.write("No exploitable findings found in selected categories.\n")
1193
+ return 2
1194
+
1195
+ # Create output directory
1196
+ try:
1197
+ os.makedirs(out_dir, exist_ok=True)
1198
+ except OSError as exc:
1199
+ sys.stderr.write(f"Error: cannot create output directory: {exc}\n")
1200
+ return 3
1201
+
1202
+ # Write poc_common.h
1203
+ common_h = _generate_common_header(secret_fill, stack_probe_max)
1204
+ with open(os.path.join(out_dir, "poc_common.h"), "w") as f:
1205
+ f.write(common_h)
1206
+
1207
+ # Generate PoCs
1208
+ makefile_targets: list[dict[str, str]] = []
1209
+ manifest_entries: list[dict[str, Any]] = []
1210
+ generated_count = 0
1211
+ manual_count = 0
1212
+
1213
+ for finding in exploitable:
1214
+ cat = finding.get("category", "")
1215
+ gen_cls = _GENERATORS.get(cat)
1216
+ if gen_cls is None:
1217
+ continue
1218
+
1219
+ gen = gen_cls(finding, compile_db, out_dir, config)
1220
+ filename, source = gen.generate()
1221
+
1222
+ # Write PoC source
1223
+ poc_path = os.path.join(out_dir, filename)
1224
+ with open(poc_path, "w") as f:
1225
+ f.write(source)
1226
+
1227
+ # Collect Makefile target
1228
+ binary = Path(filename).stem
1229
+ makefile_targets.append(
1230
+ {
1231
+ "binary": binary,
1232
+ "rule": gen.makefile_target(filename),
1233
+ }
1234
+ )
1235
+
1236
+ # Collect manifest entry
1237
+ manifest_entries.append(gen.manifest_entry(filename))
1238
+
1239
+ generated_count += 1
1240
+ if gen.requires_manual:
1241
+ manual_count += 1
1242
+
1243
+ # Write Makefile
1244
+ makefile_content = _generate_makefile(makefile_targets)
1245
+ with open(os.path.join(out_dir, "Makefile"), "w") as f:
1246
+ f.write(makefile_content)
1247
+
1248
+ # Write manifest
1249
+ manifest = {
1250
+ "pocs_generated": generated_count,
1251
+ "pocs_requiring_adjustment": manual_count,
1252
+ "output_dir": out_dir,
1253
+ "categories_covered": sorted(set(e["category"] for e in manifest_entries)),
1254
+ "entries": manifest_entries,
1255
+ }
1256
+ with open(os.path.join(out_dir, "poc_manifest.json"), "w") as f:
1257
+ json.dump(manifest, f, indent=2)
1258
+ f.write("\n")
1259
+
1260
+ # Summary
1261
+ sys.stderr.write(
1262
+ f"Generated {generated_count} PoC(s) in {out_dir}/ "
1263
+ f"({manual_count} requiring manual adjustment)\n"
1264
+ )
1265
+ return 0
1266
+
1267
+
1268
+ def main() -> None:
1269
+ parser = argparse.ArgumentParser(
1270
+ description="Generate proof-of-concept programs from zeroize-audit findings.",
1271
+ formatter_class=argparse.RawDescriptionHelpFormatter,
1272
+ epilog=__doc__,
1273
+ )
1274
+ parser.add_argument(
1275
+ "--findings",
1276
+ required=True,
1277
+ metavar="PATH",
1278
+ help="Path to findings JSON (array or {findings: [...]})",
1279
+ )
1280
+ parser.add_argument(
1281
+ "--compile-db",
1282
+ required=True,
1283
+ metavar="PATH",
1284
+ help="Path to compile_commands.json",
1285
+ )
1286
+ parser.add_argument(
1287
+ "--out",
1288
+ required=True,
1289
+ metavar="DIR",
1290
+ help="Output directory for generated PoCs",
1291
+ )
1292
+ parser.add_argument(
1293
+ "--categories",
1294
+ metavar="CAT1,CAT2,...",
1295
+ default=None,
1296
+ help="Comma-separated list of finding categories (default: all exploitable)",
1297
+ )
1298
+ parser.add_argument(
1299
+ "--config",
1300
+ metavar="PATH",
1301
+ default=None,
1302
+ help="Path to config YAML with poc_generation section",
1303
+ )
1304
+ parser.add_argument(
1305
+ "--no-confidence-filter",
1306
+ action="store_true",
1307
+ default=False,
1308
+ help="Generate PoCs for all findings regardless of confidence level",
1309
+ )
1310
+ args = parser.parse_args()
1311
+
1312
+ categories = None
1313
+ if args.categories:
1314
+ categories = [c.strip() for c in args.categories.split(",")]
1315
+
1316
+ sys.exit(
1317
+ run(
1318
+ args.findings,
1319
+ args.compile_db,
1320
+ args.out,
1321
+ categories=categories,
1322
+ config_path=args.config,
1323
+ no_confidence_filter=args.no_confidence_filter,
1324
+ )
1325
+ )
1326
+
1327
+
1328
+ if __name__ == "__main__":
1329
+ main()