@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,923 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = []
5
+ # ///
6
+ """
7
+ semantic_audit.py — Rust trait-aware zeroization auditor.
8
+
9
+ Reads rustdoc JSON (generated by `cargo +nightly rustdoc --document-private-items
10
+ -- -Z unstable-options --output-format json`) and emits findings about missing or
11
+ incorrect zeroization of sensitive types.
12
+
13
+ Usage:
14
+ uv run semantic_audit.py --rustdoc <path.json> [--cargo-toml <Cargo.toml>] --out <findings.json>
15
+
16
+ Exit codes:
17
+ 0 — ran successfully (findings may be empty)
18
+ 1 — rustdoc JSON not found or unparseable
19
+ 2 — argument error
20
+ """
21
+
22
+ import argparse
23
+ import json
24
+ import re
25
+ import sys
26
+ import tomllib
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Sensitive type / field name patterns
32
+ # ---------------------------------------------------------------------------
33
+
34
+ SENSITIVE_TYPE_RE = re.compile(
35
+ r"(?i)(Key|PrivateKey|SecretKey|SigningKey|MasterKey|HmacKey|"
36
+ r"Password|Passphrase|Pin|Token|AuthToken|BearerToken|ApiKey|"
37
+ r"Secret|SharedSecret|PreSharedKey|Nonce|Seed|Entropy|"
38
+ r"Credential|SessionKey|DerivedKey)"
39
+ )
40
+
41
+ SENSITIVE_FIELD_RE = re.compile(
42
+ r"(?i)\b(key|secret|password|token|nonce|seed|private|master|credential)\b"
43
+ )
44
+
45
+ # Derives/traits that indicate zeroization intent
46
+ ZEROIZE_TRAITS = {"Zeroize", "ZeroizeOnDrop"}
47
+ DROP_TRAIT = "Drop"
48
+
49
+ # Traits / derives that create untracked copies
50
+ COPY_DERIVES = {"Copy"}
51
+ CLONE_DERIVES = {"Clone"}
52
+ DEBUG_DERIVES = {"Debug"}
53
+ SERIALIZE_DERIVES = {"Serialize"}
54
+
55
+ # Evidence tags used for conservative confidence mapping.
56
+ STRONG_EVIDENCE_TAGS = {
57
+ "trait_impl",
58
+ "resolved_path",
59
+ "drop_body_source",
60
+ "cargo_toml",
61
+ }
62
+
63
+ MEDIUM_EVIDENCE_TAGS = {
64
+ "source_scan",
65
+ "generic_traversal",
66
+ }
67
+
68
+ HEAP_TYPE_NAMES = {
69
+ "Vec",
70
+ "Box",
71
+ "String",
72
+ "HashMap",
73
+ "BTreeMap",
74
+ "VecDeque",
75
+ "BinaryHeap",
76
+ "LinkedList",
77
+ }
78
+
79
+ ZEROIZING_WRAPPER_NAMES = {
80
+ "Zeroizing",
81
+ }
82
+
83
+ MANUALLY_DROP_NAMES = {"ManuallyDrop"}
84
+
85
+ ZEROIZING_NAME_HINT_RE = re.compile(r"(?i)(Zeroiz|Protected|Secret|Sensitive)")
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Helper: is a type name sensitive?
90
+ # ---------------------------------------------------------------------------
91
+
92
+
93
+ def is_sensitive_name(name: str) -> bool:
94
+ return bool(SENSITIVE_TYPE_RE.search(name))
95
+
96
+
97
+ def has_sensitive_field(fields: list[dict]) -> bool:
98
+ for field in fields:
99
+ fname = field.get("name") or ""
100
+ if SENSITIVE_FIELD_RE.search(fname):
101
+ return True
102
+ return False
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Finding builder
107
+ # ---------------------------------------------------------------------------
108
+
109
+ _finding_counter = [0]
110
+
111
+
112
+ def make_finding(
113
+ category: str,
114
+ severity: str,
115
+ detail: str,
116
+ type_name: str,
117
+ file: str,
118
+ line: int | None,
119
+ confidence: str | None = None,
120
+ evidence_strength: list[str] | None = None,
121
+ ) -> dict:
122
+ _finding_counter[0] += 1
123
+ fid = f"F-RUST-SRC-{_finding_counter[0]:04d}"
124
+ evidence_strength = evidence_strength or ["heuristic"]
125
+ resolved_confidence = confidence or _confidence_from_evidence_strength(evidence_strength)
126
+ return {
127
+ "id": fid,
128
+ "language": "rust",
129
+ "category": category,
130
+ "severity": severity,
131
+ "confidence": resolved_confidence,
132
+ "evidence_strength": evidence_strength,
133
+ "detail": detail,
134
+ "symbol": type_name,
135
+ "object": {"name": type_name},
136
+ "location": {"file": file, "line": line or 1},
137
+ "evidence": [
138
+ {
139
+ "source": "rustdoc_json",
140
+ "detail": detail,
141
+ "strength": evidence_strength,
142
+ }
143
+ ],
144
+ }
145
+
146
+
147
+ def _confidence_from_evidence_strength(evidence_strength: list[str]) -> str:
148
+ strong_count = sum(1 for tag in evidence_strength if tag in STRONG_EVIDENCE_TAGS)
149
+ medium_count = sum(1 for tag in evidence_strength if tag in MEDIUM_EVIDENCE_TAGS)
150
+ if strong_count >= 2:
151
+ return "confirmed"
152
+ if strong_count == 1:
153
+ return "likely"
154
+ if medium_count >= 1 and not any(tag == "heuristic" for tag in evidence_strength):
155
+ return "likely"
156
+ return "needs_review"
157
+
158
+
159
+ # ---------------------------------------------------------------------------
160
+ # Rustdoc JSON helpers
161
+ # ---------------------------------------------------------------------------
162
+
163
+
164
+ def item_span(item: dict) -> tuple[str, int | None]:
165
+ """Return (file, line) from an item's span."""
166
+ span = item.get("span") or {}
167
+ filename = span.get("filename") or ""
168
+ begin = span.get("begin") or []
169
+ line = begin[0] if begin else None
170
+ return filename, line
171
+
172
+
173
+ def item_derives(item: dict) -> set[str]:
174
+ """Collect derive macro names from item attrs."""
175
+ derives: set[str] = set()
176
+ for attr in item.get("attrs") or []:
177
+ # attr is a string like '#[derive(Copy, Clone, Debug)]'
178
+ m = re.search(r"derive\(([^)]+)\)", attr)
179
+ if m:
180
+ for d in m.group(1).split(","):
181
+ derives.add(d.strip())
182
+ return derives
183
+
184
+
185
+ def item_impls(item: dict, index: dict) -> set[str]:
186
+ """Return trait names implemented by this struct/enum via its impl IDs."""
187
+ trait_names: set[str] = set()
188
+ for impl_id in item.get("impls") or []:
189
+ impl_item = index.get(str(impl_id)) or {}
190
+ inner = impl_item.get("inner") or {}
191
+ impl_data = inner.get("impl") or {}
192
+ trait_ref = impl_data.get("trait") or {}
193
+ tname = _trait_name(trait_ref)
194
+ if tname:
195
+ trait_names.add(tname)
196
+ return trait_names
197
+
198
+
199
+ def _trait_name(trait_ref: dict[str, Any]) -> str:
200
+ name = trait_ref.get("name")
201
+ if isinstance(name, str) and name:
202
+ return name.split("::")[-1]
203
+ resolved = trait_ref.get("resolved_path")
204
+ if isinstance(resolved, dict):
205
+ resolved_name = resolved.get("name")
206
+ if isinstance(resolved_name, str) and resolved_name:
207
+ return resolved_name.split("::")[-1]
208
+ return ""
209
+
210
+
211
+ def struct_fields(item: dict, index: dict) -> list[dict]:
212
+ """Return field items for a struct."""
213
+ fields: list[dict] = []
214
+ inner = item.get("inner") or {}
215
+ struct_data = inner.get("struct") or {}
216
+ kind = struct_data.get("kind") or {}
217
+ # plain struct: kind = {"plain": {"fields": [id, ...], ...}}
218
+ plain = kind.get("plain") or {}
219
+ field_ids = plain.get("fields") or []
220
+ for fid in field_ids:
221
+ fitem = index.get(str(fid)) or {}
222
+ fields.append(fitem)
223
+ return fields
224
+
225
+
226
+ # ---------------------------------------------------------------------------
227
+ # Core analysis
228
+ # ---------------------------------------------------------------------------
229
+
230
+
231
+ def analyze(rustdoc: dict, cargo_toml_path: str | None) -> list[dict]:
232
+ findings: list[dict] = []
233
+ index: dict = rustdoc.get("index") or {}
234
+
235
+ # Check whether zeroize crate is a dependency
236
+ has_zeroize_dep = _check_zeroize_dep(cargo_toml_path)
237
+
238
+ for _item_id, item in index.items():
239
+ kind = item.get("kind") or ""
240
+ if kind not in ("struct", "enum"):
241
+ continue
242
+
243
+ name = item.get("name") or ""
244
+ if not is_sensitive_name(name):
245
+ # Check fields too
246
+ fields = struct_fields(item, index) if kind == "struct" else []
247
+ if not has_sensitive_field(fields):
248
+ continue
249
+
250
+ file, line = item_span(item)
251
+ derives = item_derives(item)
252
+ trait_impls = item_impls(item, index)
253
+
254
+ # --- 1. Copy derive on sensitive type ---
255
+ if COPY_DERIVES & derives:
256
+ findings.append(
257
+ make_finding(
258
+ "SECRET_COPY",
259
+ "critical",
260
+ f"#[derive(Copy)] on sensitive type '{name}' — all assignments are "
261
+ "untracked duplicates, no Drop ever runs",
262
+ name,
263
+ file,
264
+ line,
265
+ evidence_strength=["attr_only", "sensitive_name_or_field"],
266
+ )
267
+ )
268
+
269
+ # --- 2. No Zeroize / ZeroizeOnDrop / Drop ---
270
+ # (Skip for Copy types: Copy and Drop are mutually exclusive in Rust.)
271
+ has_zeroize = bool(ZEROIZE_TRAITS & trait_impls)
272
+ has_drop = DROP_TRAIT in trait_impls
273
+ has_zeroize_on_drop = "ZeroizeOnDrop" in trait_impls or "ZeroizeOnDrop" in derives
274
+
275
+ if not (COPY_DERIVES & derives):
276
+ if not has_zeroize and not has_drop and not has_zeroize_on_drop:
277
+ findings.append(
278
+ make_finding(
279
+ "MISSING_SOURCE_ZEROIZE",
280
+ "high",
281
+ f"Sensitive type '{name}' has no Zeroize, ZeroizeOnDrop,"
282
+ " or Drop implementation",
283
+ name,
284
+ file,
285
+ line,
286
+ evidence_strength=["trait_impl", "sensitive_name_or_field"],
287
+ )
288
+ )
289
+ elif has_zeroize and not has_zeroize_on_drop and not has_drop:
290
+ # Zeroize implemented but never auto-triggered
291
+ findings.append(
292
+ make_finding(
293
+ "MISSING_SOURCE_ZEROIZE",
294
+ "high",
295
+ f"Sensitive type '{name}' implements Zeroize but has no "
296
+ "ZeroizeOnDrop or Drop to trigger it automatically",
297
+ name,
298
+ file,
299
+ line,
300
+ evidence_strength=["trait_impl", "sensitive_name_or_field"],
301
+ )
302
+ )
303
+
304
+ # --- 3. Partial Drop: Drop impl present but not all secret fields zeroed ---
305
+ if has_drop and kind == "struct":
306
+ fields = struct_fields(item, index)
307
+ secret_fields = [f for f in fields if SENSITIVE_FIELD_RE.search(f.get("name") or "")]
308
+ if secret_fields:
309
+ # Find the Drop impl and check whether it zeroes all secret fields.
310
+ drop_impls = _find_drop_impl_items(item, index)
311
+ if drop_impls:
312
+ secret_field_names = [f.get("name") or "" for f in secret_fields]
313
+ zeroed_names, evidence_strength = _zeroed_field_names_in_drop(
314
+ drop_impls[0], index, secret_field_names
315
+ )
316
+ unzeroed = [
317
+ f.get("name") for f in secret_fields if f.get("name") not in zeroed_names
318
+ ]
319
+ if unzeroed:
320
+ severity = "high" if "drop_body_source" in evidence_strength else "medium"
321
+ findings.append(
322
+ make_finding(
323
+ "PARTIAL_WIPE",
324
+ severity,
325
+ f"Drop impl for '{name}' does not zero all secret fields: "
326
+ f"missing {unzeroed}",
327
+ name,
328
+ file,
329
+ line,
330
+ evidence_strength=evidence_strength + ["trait_impl"],
331
+ )
332
+ )
333
+ elif "drop_body_source" not in evidence_strength:
334
+ findings.append(
335
+ make_finding(
336
+ "PARTIAL_WIPE",
337
+ "medium",
338
+ f"Drop impl for '{name}' found, but field-level "
339
+ "zeroization could not be confirmed from "
340
+ "function body; review manually",
341
+ name,
342
+ file,
343
+ line,
344
+ evidence_strength=evidence_strength + ["trait_impl"],
345
+ )
346
+ )
347
+
348
+ # --- 4. ZeroizeOnDrop with heap (Vec/Box) fields ---
349
+ if has_zeroize_on_drop and kind == "struct":
350
+ fields = struct_fields(item, index)
351
+ heap_fields = _heap_fields(fields, index, source_file=file)
352
+ alias_review = "__alias_review__" in heap_fields
353
+ real_heap_fields = [f for f in heap_fields if f != "__alias_review__"]
354
+ if real_heap_fields:
355
+ findings.append(
356
+ make_finding(
357
+ "PARTIAL_WIPE",
358
+ "medium",
359
+ f"ZeroizeOnDrop on '{name}' which has heap fields {real_heap_fields} — "
360
+ "capacity bytes beyond len may not be zeroed",
361
+ name,
362
+ file,
363
+ line,
364
+ evidence_strength=["resolved_path", "generic_traversal", "trait_impl"],
365
+ )
366
+ )
367
+ elif alias_review:
368
+ findings.append(
369
+ make_finding(
370
+ "PARTIAL_WIPE",
371
+ "medium",
372
+ f"ZeroizeOnDrop on '{name}' — source file contains type aliases that may "
373
+ "wrap heap types (Vec/Box/String); verify all heap fields are covered",
374
+ name,
375
+ file,
376
+ line,
377
+ evidence_strength=["alias_heuristic", "source_scan", "trait_impl"],
378
+ )
379
+ )
380
+
381
+ # --- 4b. ManuallyDrop<T> field on sensitive struct ---
382
+ if kind == "struct":
383
+ fields = struct_fields(item, index)
384
+ md_fields = _manually_drop_fields(fields, index)
385
+ if md_fields:
386
+ findings.append(
387
+ make_finding(
388
+ "MISSING_SOURCE_ZEROIZE",
389
+ "critical",
390
+ f"Sensitive struct '{name}' has ManuallyDrop<T> field(s) {md_fields} — "
391
+ "Drop does not run automatically on ManuallyDrop fields; "
392
+ "secret is not zeroed unless ManuallyDrop::drop() is called explicitly",
393
+ name,
394
+ file,
395
+ line,
396
+ evidence_strength=["resolved_path", "trait_impl"],
397
+ )
398
+ )
399
+
400
+ # --- 5. Clone on zeroizing type ---
401
+ if CLONE_DERIVES & derives and (has_zeroize or has_zeroize_on_drop or has_drop):
402
+ findings.append(
403
+ make_finding(
404
+ "SECRET_COPY",
405
+ "medium",
406
+ f"Clone on zeroizing type '{name}' — each clone is an independent allocation "
407
+ "that must be independently zeroed",
408
+ name,
409
+ file,
410
+ line,
411
+ evidence_strength=["attr_only", "trait_impl"],
412
+ )
413
+ )
414
+
415
+ # --- 6. From/Into returning non-zeroizing type ---
416
+ from_into_escapes = _find_from_into_non_zeroizing(item, index)
417
+ for escape, evidence_strength in from_into_escapes:
418
+ findings.append(
419
+ make_finding(
420
+ "SECRET_COPY",
421
+ "medium",
422
+ f"'{name}' has {escape} conversion returning a non-zeroizing type — "
423
+ "bytes escape into caller's ownership in a non-zeroizing container",
424
+ name,
425
+ file,
426
+ line,
427
+ evidence_strength=evidence_strength + ["trait_impl"],
428
+ )
429
+ )
430
+
431
+ # --- 7. ptr::write_bytes without compiler_fence ---
432
+ if _has_write_bytes_without_compiler_fence(file):
433
+ findings.append(
434
+ make_finding(
435
+ "OPTIMIZED_AWAY_ZEROIZE",
436
+ "medium",
437
+ f"'{name}' is defined in a file that uses ptr::write_bytes without "
438
+ "compiler_fence — wipe may be optimized away by the compiler",
439
+ name,
440
+ file,
441
+ line,
442
+ evidence_strength=["source_scan", "heuristic"],
443
+ )
444
+ )
445
+
446
+ # --- 8. cfg(feature) wrapping Drop/Zeroize ---
447
+ if _has_cfg_feature_on_cleanup(item, index):
448
+ findings.append(
449
+ make_finding(
450
+ "NOT_ON_ALL_PATHS",
451
+ "medium",
452
+ f"#[cfg(feature=...)] wraps Drop or Zeroize impl for '{name}' — "
453
+ "zeroing absent when feature flag is off",
454
+ name,
455
+ file,
456
+ line,
457
+ evidence_strength=["attr_only", "trait_impl"],
458
+ )
459
+ )
460
+
461
+ # --- 9. Debug derive ---
462
+ if DEBUG_DERIVES & derives:
463
+ findings.append(
464
+ make_finding(
465
+ "SECRET_COPY",
466
+ "low",
467
+ f"#[derive(Debug)] on sensitive type '{name}' — "
468
+ "secrets may appear in formatted output / log entries",
469
+ name,
470
+ file,
471
+ line,
472
+ evidence_strength=["attr_only"],
473
+ )
474
+ )
475
+
476
+ # --- 10. Serialize derive ---
477
+ if SERIALIZE_DERIVES & derives:
478
+ findings.append(
479
+ make_finding(
480
+ "SECRET_COPY",
481
+ "low",
482
+ f"#[derive(Serialize)] on sensitive type '{name}' — "
483
+ "serialization creates an uncontrolled copy of secret bytes",
484
+ name,
485
+ file,
486
+ line,
487
+ evidence_strength=["attr_only"],
488
+ )
489
+ )
490
+
491
+ # --- 11. No zeroize crate dependency ---
492
+ # Only emit when Cargo.toml was provided and successfully parsed but did
493
+ # not list zeroize. has_zeroize_dep is None when the path was omitted or
494
+ # the file could not be parsed, which must not trigger a false finding.
495
+ if has_zeroize_dep is False:
496
+ findings.append(
497
+ make_finding(
498
+ "MISSING_SOURCE_ZEROIZE",
499
+ "low",
500
+ "No 'zeroize' crate in Cargo.toml dependencies — "
501
+ "all manual zeroing lacks approved-API guarantee",
502
+ "<crate>",
503
+ str(cargo_toml_path or "Cargo.toml"),
504
+ 1,
505
+ evidence_strength=["cargo_toml"],
506
+ )
507
+ )
508
+
509
+ return findings
510
+
511
+
512
+ # ---------------------------------------------------------------------------
513
+ # Helpers
514
+ # ---------------------------------------------------------------------------
515
+
516
+
517
+ def _check_zeroize_dep(cargo_toml_path: str | None) -> bool | None:
518
+ """Return True/False if Cargo.toml was parsed, None if path absent or unreadable."""
519
+ if not cargo_toml_path:
520
+ return None
521
+ try:
522
+ content = Path(cargo_toml_path).read_text(encoding="utf-8")
523
+ manifest = tomllib.loads(content)
524
+ except OSError:
525
+ return None
526
+ except tomllib.TOMLDecodeError as e:
527
+ print(
528
+ f"semantic_audit.py: warning: cannot parse Cargo.toml {cargo_toml_path!r}: {e}",
529
+ file=sys.stderr,
530
+ )
531
+ return None
532
+ return _manifest_has_zeroize_dep(manifest)
533
+
534
+
535
+ def _manifest_has_zeroize_dep(manifest: dict) -> bool:
536
+ return any(_dep_table_has_zeroize(dep_table) for dep_table in _iter_dependency_tables(manifest))
537
+
538
+
539
+ def _iter_dependency_tables(manifest: dict) -> list[dict]:
540
+ dep_tables: list[dict] = []
541
+
542
+ dependencies = manifest.get("dependencies")
543
+ if isinstance(dependencies, dict):
544
+ dep_tables.append(dependencies)
545
+
546
+ workspace = manifest.get("workspace")
547
+ if isinstance(workspace, dict):
548
+ workspace_deps = workspace.get("dependencies")
549
+ if isinstance(workspace_deps, dict):
550
+ dep_tables.append(workspace_deps)
551
+
552
+ target = manifest.get("target")
553
+ if isinstance(target, dict):
554
+ for target_data in target.values():
555
+ if not isinstance(target_data, dict):
556
+ continue
557
+ target_deps = target_data.get("dependencies")
558
+ if isinstance(target_deps, dict):
559
+ dep_tables.append(target_deps)
560
+
561
+ return dep_tables
562
+
563
+
564
+ def _dep_table_has_zeroize(dep_table: dict) -> bool:
565
+ for dep_name, dep_spec in dep_table.items():
566
+ if isinstance(dep_name, str) and dep_name.lower() == "zeroize":
567
+ return True
568
+ if isinstance(dep_spec, dict):
569
+ package_name = dep_spec.get("package")
570
+ if isinstance(package_name, str) and package_name.lower() == "zeroize":
571
+ return True
572
+ return False
573
+
574
+
575
+ def _find_drop_impl_items(item: dict, index: dict) -> list[dict]:
576
+ result = []
577
+ for impl_id in item.get("impls") or []:
578
+ impl_item = index.get(str(impl_id)) or {}
579
+ inner = impl_item.get("inner") or {}
580
+ impl_data = inner.get("impl") or {}
581
+ trait_ref = impl_data.get("trait") or {}
582
+ if _trait_name(trait_ref) == "Drop":
583
+ result.append(impl_item)
584
+ return result
585
+
586
+
587
+ def _zeroed_field_names_in_drop(
588
+ drop_impl: dict, index: dict, secret_fields: list[str]
589
+ ) -> tuple[set[str], list[str]]:
590
+ """
591
+ Extract zeroed fields from Drop evidence.
592
+
593
+ Prefers parsing Drop::drop source span. Falls back to docs text when source
594
+ body is unavailable.
595
+ """
596
+ body = _extract_drop_body_from_impl(drop_impl, index)
597
+ if body:
598
+ return _zeroed_field_names_in_text(body, secret_fields), ["drop_body_source"]
599
+
600
+ docs = drop_impl.get("docs") or ""
601
+ if docs:
602
+ return _zeroed_field_names_in_text(docs, secret_fields), ["docs_heuristic"]
603
+
604
+ return set(), ["unavailable"]
605
+
606
+
607
+ def _extract_drop_body_from_impl(drop_impl: dict, index: dict) -> str:
608
+ inner = drop_impl.get("inner") or {}
609
+ impl_data = inner.get("impl") or {}
610
+ for method_id in impl_data.get("items") or []:
611
+ method_item = index.get(str(method_id)) or {}
612
+ if (method_item.get("kind") or "") != "function":
613
+ continue
614
+ if (method_item.get("name") or "") != "drop":
615
+ continue
616
+ source = _read_item_span_source(method_item)
617
+ if source:
618
+ return source
619
+ return ""
620
+
621
+
622
+ def _read_item_span_source(item: dict) -> str:
623
+ span = item.get("span") or {}
624
+ filename = span.get("filename")
625
+ begin = span.get("begin") or []
626
+ end = span.get("end") or []
627
+ if not filename or not begin or not end:
628
+ return ""
629
+ try:
630
+ lines = Path(filename).read_text(encoding="utf-8", errors="replace").splitlines()
631
+ except OSError as e:
632
+ print(
633
+ f"semantic_audit.py: warning: cannot read span source {filename!r}: {e}",
634
+ file=sys.stderr,
635
+ )
636
+ return ""
637
+
638
+ start_line = max(int(begin[0]), 1)
639
+ end_line = max(int(end[0]), start_line)
640
+ if start_line > len(lines):
641
+ return ""
642
+ snippet = lines[start_line - 1 : min(end_line, len(lines))]
643
+ return "\n".join(snippet)
644
+
645
+
646
+ def _zeroed_field_names_in_text(text: str, field_names: list[str]) -> set[str]:
647
+ zeroed: set[str] = set()
648
+ for field_name in field_names:
649
+ escaped = re.escape(field_name)
650
+ patterns = [
651
+ rf"\bself\.{escaped}\.zeroize\s*\(",
652
+ rf"\bzeroize\s*\(\s*&mut\s+self\.{escaped}\s*\)",
653
+ rf"\bself\.{escaped}\s*=\s*(?:0+|Default::default\(\)|\[[^]]+\])",
654
+ rf"\bself\.{escaped}\.fill\s*\(\s*0\s*\)",
655
+ ]
656
+ if any(re.search(pattern, text) for pattern in patterns):
657
+ zeroed.add(field_name)
658
+ return zeroed
659
+
660
+
661
+ # Matches type alias definitions like: type SecretBuffer = Vec<u8>;
662
+ _TYPE_ALIAS_RE = re.compile(
663
+ r"^\s*(?:pub\s+)?type\s+\w+\s*=\s*(?:Vec|Box|String|HashMap|BTreeMap)\b"
664
+ )
665
+
666
+
667
+ def _heap_fields(fields: list[dict], index: dict, source_file: str | None = None) -> list[str]:
668
+ heap: list[str] = []
669
+ for field in fields:
670
+ fname = field.get("name") or ""
671
+ inner = field.get("inner") or {}
672
+ struct_field = inner.get("struct_field") or {}
673
+ ty = struct_field.get("type") or {}
674
+ if _type_contains_heap(ty, index):
675
+ heap.append(fname)
676
+ # If no heap fields found via rustdoc, scan the source file for type aliases
677
+ # that may wrap heap types (e.g. `type SecretBuffer = Vec<u8>`). Emit a
678
+ # needs_review note by appending a sentinel value so callers can detect this.
679
+ if not heap and source_file:
680
+ try:
681
+ src = Path(source_file).read_text(encoding="utf-8", errors="replace")
682
+ if _TYPE_ALIAS_RE.search(src):
683
+ heap.append("__alias_review__")
684
+ except OSError as e:
685
+ print(
686
+ f"semantic_audit.py: warning: cannot read source"
687
+ f" for alias scan {source_file!r}: {e}",
688
+ file=sys.stderr,
689
+ )
690
+ return heap
691
+
692
+
693
+ def _manually_drop_fields(fields: list[dict], index: dict) -> list[str]:
694
+ """Return field names whose type is or contains ManuallyDrop<T>."""
695
+ result: list[str] = []
696
+ for field in fields:
697
+ fname = field.get("name") or ""
698
+ inner = field.get("inner") or {}
699
+ struct_field = inner.get("struct_field") or {}
700
+ ty = struct_field.get("type") or {}
701
+ names = _type_named_paths(ty, index, set())
702
+ if MANUALLY_DROP_NAMES & names:
703
+ result.append(fname)
704
+ return result
705
+
706
+
707
+ def _find_from_into_non_zeroizing(item: dict, index: dict) -> list[tuple[str, list[str]]]:
708
+ escapes: list[tuple[str, list[str]]] = []
709
+ for impl_id in item.get("impls") or []:
710
+ impl_item = index.get(str(impl_id)) or {}
711
+ inner = impl_item.get("inner") or {}
712
+ impl_data = inner.get("impl") or {}
713
+ trait_ref = impl_data.get("trait") or {}
714
+ tname = _trait_name(trait_ref)
715
+ if tname not in ("From", "Into"):
716
+ continue
717
+ for target_type in _iter_trait_type_args(trait_ref):
718
+ if _type_is_zeroizing(target_type, index):
719
+ continue
720
+ target_desc = _type_description(target_type, index)
721
+ evidence = (
722
+ ["resolved_path", "generic_traversal"]
723
+ if _type_has_resolved_path(target_type)
724
+ else ["alias_heuristic"]
725
+ )
726
+ escapes.append((f"{tname}<{target_desc}>", evidence))
727
+ return escapes
728
+
729
+
730
+ def _iter_trait_type_args(trait_ref: dict) -> list[dict]:
731
+ args = trait_ref.get("args") or {}
732
+ angle = args.get("angle_bracketed") or {}
733
+ out: list[dict] = []
734
+ for arg in angle.get("args") or []:
735
+ ty = arg.get("type")
736
+ if isinstance(ty, dict):
737
+ out.append(ty)
738
+ return out
739
+
740
+
741
+ def _type_contains_heap(ty: dict[str, Any], index: dict, seen: set[str] | None = None) -> bool:
742
+ seen = seen or set()
743
+ return any(name in HEAP_TYPE_NAMES for name in _type_named_paths(ty, index, seen))
744
+
745
+
746
+ def _type_is_zeroizing(ty: dict[str, Any], index: dict, seen: set[str] | None = None) -> bool:
747
+ seen = seen or set()
748
+ names = _type_named_paths(ty, index, seen)
749
+ if any(name in ZEROIZING_WRAPPER_NAMES for name in names):
750
+ return True
751
+ return any(ZEROIZING_NAME_HINT_RE.search(name) for name in names)
752
+
753
+
754
+ def _type_has_resolved_path(ty: dict[str, Any]) -> bool:
755
+ if not isinstance(ty, dict):
756
+ return False
757
+ if "resolved_path" in ty:
758
+ return True
759
+ return any(_type_has_resolved_path(nested) for nested in _iter_nested_types(ty))
760
+
761
+
762
+ def _type_description(ty: dict[str, Any], index: dict) -> str:
763
+ names = sorted(_type_named_paths(ty, index, set()))
764
+ if names:
765
+ return "::".join(names[:2]) if len(names) > 1 else names[0]
766
+ return "unknown"
767
+
768
+
769
+ def _type_named_paths(ty: dict[str, Any], index: dict, seen_alias_ids: set[str]) -> set[str]:
770
+ names: set[str] = set()
771
+ if not isinstance(ty, dict):
772
+ return names
773
+
774
+ resolved = ty.get("resolved_path")
775
+ if isinstance(resolved, dict):
776
+ raw_name = resolved.get("name")
777
+ if isinstance(raw_name, str) and raw_name:
778
+ names.add(raw_name.split("::")[-1])
779
+
780
+ alias_id = resolved.get("id")
781
+ alias_item = index.get(str(alias_id)) if alias_id is not None else None
782
+ alias_id_str = str(alias_id) if alias_id is not None else ""
783
+ if (
784
+ alias_id_str
785
+ and alias_id_str not in seen_alias_ids
786
+ and isinstance(alias_item, dict)
787
+ and (alias_item.get("kind") or "") == "typedef"
788
+ ):
789
+ seen_alias_ids.add(alias_id_str)
790
+ alias_type = ((alias_item.get("inner") or {}).get("type_alias") or {}).get("type") or {}
791
+ names |= _type_named_paths(alias_type, index, seen_alias_ids)
792
+
793
+ args = resolved.get("args") or {}
794
+ names |= _type_args_named_paths(args, index, seen_alias_ids)
795
+
796
+ for nested in _iter_nested_types(ty):
797
+ names |= _type_named_paths(nested, index, seen_alias_ids)
798
+ return names
799
+
800
+
801
+ def _type_args_named_paths(args: dict[str, Any], index: dict, seen_alias_ids: set[str]) -> set[str]:
802
+ names: set[str] = set()
803
+ angle = args.get("angle_bracketed") if isinstance(args, dict) else None
804
+ if not isinstance(angle, dict):
805
+ return names
806
+ for arg in angle.get("args") or []:
807
+ if isinstance(arg, dict):
808
+ ty = arg.get("type")
809
+ if isinstance(ty, dict):
810
+ names |= _type_named_paths(ty, index, seen_alias_ids)
811
+ return names
812
+
813
+
814
+ def _iter_nested_types(ty: dict[str, Any]) -> list[dict[str, Any]]:
815
+ nested: list[dict[str, Any]] = []
816
+
817
+ borrowed = ty.get("borrowed_ref")
818
+ if isinstance(borrowed, dict):
819
+ inner_ty = borrowed.get("type")
820
+ if isinstance(inner_ty, dict):
821
+ nested.append(inner_ty)
822
+
823
+ raw_ptr = ty.get("raw_pointer")
824
+ if isinstance(raw_ptr, dict):
825
+ inner_ty = raw_ptr.get("type")
826
+ if isinstance(inner_ty, dict):
827
+ nested.append(inner_ty)
828
+
829
+ array_ty = ty.get("array")
830
+ if isinstance(array_ty, dict):
831
+ inner_ty = array_ty.get("type")
832
+ if isinstance(inner_ty, dict):
833
+ nested.append(inner_ty)
834
+
835
+ slice_ty = ty.get("slice")
836
+ if isinstance(slice_ty, dict):
837
+ nested.append(slice_ty)
838
+
839
+ tuple_types = ty.get("tuple")
840
+ if isinstance(tuple_types, list):
841
+ for inner_ty in tuple_types:
842
+ if isinstance(inner_ty, dict):
843
+ nested.append(inner_ty)
844
+
845
+ qualified = ty.get("qualified_path")
846
+ if isinstance(qualified, dict):
847
+ qself = qualified.get("self_type")
848
+ if isinstance(qself, dict):
849
+ nested.append(qself)
850
+ qtrait = qualified.get("trait")
851
+ if isinstance(qtrait, dict):
852
+ nested.append(qtrait)
853
+
854
+ return nested
855
+
856
+
857
+ _COMPILER_FENCE_RE = re.compile(
858
+ r"\b(?:core::sync::atomic::|std::sync::atomic::)?compiler_fence\s*\("
859
+ )
860
+
861
+
862
+ def _has_write_bytes_without_compiler_fence(source_file: str | None) -> bool:
863
+ if not source_file:
864
+ return False
865
+ try:
866
+ src = Path(source_file).read_text(encoding="utf-8", errors="replace")
867
+ except OSError:
868
+ return False
869
+ return "write_bytes" in src and not _COMPILER_FENCE_RE.search(src)
870
+
871
+
872
+ def _has_cfg_feature_on_cleanup(item: dict, index: dict) -> bool:
873
+ for impl_id in item.get("impls") or []:
874
+ impl_item = index.get(str(impl_id)) or {}
875
+ inner = impl_item.get("inner") or {}
876
+ impl_data = inner.get("impl") or {}
877
+ trait_ref = impl_data.get("trait") or {}
878
+ tname = _trait_name(trait_ref)
879
+ if tname not in ("Drop", "Zeroize", "ZeroizeOnDrop"):
880
+ continue
881
+ for attr in impl_item.get("attrs") or []:
882
+ if "cfg" in attr and "feature" in attr:
883
+ return True
884
+ return False
885
+
886
+
887
+ # ---------------------------------------------------------------------------
888
+ # CLI
889
+ # ---------------------------------------------------------------------------
890
+
891
+
892
+ def main() -> int:
893
+ parser = argparse.ArgumentParser(
894
+ description="Rust trait-aware zeroization auditor (rustdoc JSON input)"
895
+ )
896
+ parser.add_argument("--rustdoc", required=True, help="Path to rustdoc JSON file")
897
+ parser.add_argument("--cargo-toml", help="Path to Cargo.toml (for dependency checks)")
898
+ parser.add_argument("--out", required=True, help="Output findings JSON path")
899
+ args = parser.parse_args()
900
+
901
+ rustdoc_path = Path(args.rustdoc)
902
+ if not rustdoc_path.exists():
903
+ print(f"semantic_audit.py: rustdoc JSON not found: {rustdoc_path}", file=sys.stderr)
904
+ return 1
905
+
906
+ try:
907
+ rustdoc = json.loads(rustdoc_path.read_text(encoding="utf-8"))
908
+ except (json.JSONDecodeError, OSError) as e:
909
+ print(f"semantic_audit.py: failed to parse rustdoc JSON: {e}", file=sys.stderr)
910
+ return 1
911
+
912
+ findings = analyze(rustdoc, args.cargo_toml)
913
+
914
+ out_path = Path(args.out)
915
+ out_path.parent.mkdir(parents=True, exist_ok=True)
916
+ out_path.write_text(json.dumps(findings, indent=2), encoding="utf-8")
917
+
918
+ print(f"semantic_audit.py: {len(findings)} finding(s) written to {out_path}")
919
+ return 0
920
+
921
+
922
+ if __name__ == "__main__":
923
+ sys.exit(main())