Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:
run: bash tests/automation-quarantine-test.sh
- name: Verify fix-outcome verdicts
run: bash tests/outcome-verdict-test.sh
- name: PA rule IDs agree with panic-attack's table
run: bash tests/panic-attack-rule-ids-test.sh
- name: Run E2E tests
run: bash tests/e2e.sh
- name: Verify canonical descriptile policy repair
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ jobs:
dir: dashboard
- module: rhodibot
dir: bots/rhodibot
# panicbot was built (panicbot-sweep.yml) but never tested or linted
# in CI, so its private PA numbering went unnoticed until 2026-11-04.
- module: panicbot
dir: bots/panicbot
- module: idris-dependents
dir: idris-dependents
defaults:
Expand Down
295 changes: 295 additions & 0 deletions bots/panicbot/canon/canonical-rule-ids.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
{
"schema_version": "1.0.0",
"title": "panic-attack canonical rule-ID table",
"description": "Public rule ID contract matching WeakPointCategory::rule_id at source commit 410a8f821ea8297833162565b50a298f787815da. CWE links are semantic associations, not assertions of exact CWE identity; [] means no direct CWE mapping is claimed. PA001 and PA001b intentionally share a semantic class and CWE and should deduplicate for class-level verdict grids.",
"cwe_catalog": "https://cwe.mitre.org/",
"source_commit": "410a8f821ea8297833162565b50a298f787815da",
"deduplication": {
"primary_key": "semantic_class",
"preserve_rule_ids": [
"PA001",
"PA001b"
],
"note": "Preserve the canonical rule ID for audit/report display; group findings by semantic_class for cross-tool verdict-grid comparison. Apply the first matching semantic_class_overrides entry (description_prefix matches the generated finding description) and use its semantic_class and cwe_ids before falling back to the rule metadata; this keeps PA011 DOM sinks separate from eval input. CWE associations are supporting metadata, not a unique deduplication key."
},
"rules": [
{
"rule_id": "PA001",
"category": "UncheckedAllocation",
"name": "unchecked-allocation",
"cwe_ids": [
"CWE-789"
],
"semantic_class": "unbounded-memory-allocation"
},
{
"rule_id": "PA001b",
"category": "UnboundedAllocation",
"name": "unbounded-allocation",
"cwe_ids": [
"CWE-789"
],
"semantic_class": "unbounded-memory-allocation"
},
{
"rule_id": "PA002",
"category": "UnboundedLoop",
"name": "unbounded-loop",
"cwe_ids": [
"CWE-834"
],
"semantic_class": "unbounded-iteration"
},
{
"rule_id": "PA003",
"category": "BlockingIO",
"name": "blocking-io",
"cwe_ids": [
"CWE-400"
],
"semantic_class": "blocking-resource-consumption"
},
{
"rule_id": "PA004",
"category": "UnsafeCode",
"name": "unsafe-code",
"cwe_ids": [],
"semantic_class": "unsafe-memory-or-type-operation"
},
{
"rule_id": "PA005",
"category": "PanicPath",
"name": "panic-path",
"cwe_ids": [
"CWE-248"
],
"semantic_class": "unhandled-runtime-panic"
},
{
"rule_id": "PA006",
"category": "RaceCondition",
"name": "race-condition",
"cwe_ids": [
"CWE-362"
],
"semantic_class": "concurrent-access-race"
},
{
"rule_id": "PA007",
"category": "DeadlockPotential",
"name": "deadlock-potential",
"cwe_ids": [
"CWE-833"
],
"semantic_class": "concurrency-deadlock"
},
{
"rule_id": "PA008",
"category": "ResourceLeak",
"name": "resource-leak",
"cwe_ids": [
"CWE-772"
],
"semantic_class": "unreleased-resource"
},
{
"rule_id": "PA009",
"category": "CommandInjection",
"name": "command-injection",
"cwe_ids": [
"CWE-78"
],
"semantic_class": "improper-neutralization-of-command-input"
},
{
"rule_id": "PA010",
"category": "UnsafeDeserialization",
"name": "unsafe-deserialization",
"cwe_ids": [
"CWE-502"
],
"semantic_class": "unsafe-deserialization"
},
{
"rule_id": "PA011",
"category": "DynamicCodeExecution",
"name": "dynamic-code-execution",
"cwe_ids": [
"CWE-95"
],
"semantic_class": "improper-neutralization-of-eval-input",
"semantic_class_overrides": [
{
"description_prefix": "DOM manipulation (innerHTML/document.write) in ",
"semantic_class": "improper-neutralization-of-html-input",
"cwe_ids": ["CWE-79"]
},
{
"description_prefix": "dangerouslySetInnerHTML (XSS risk) in ",
"semantic_class": "improper-neutralization-of-html-input",
"cwe_ids": ["CWE-79"]
}
]
},
{
"rule_id": "PA012",
"category": "UnsafeFFI",
"name": "unsafe-ffi",
"cwe_ids": [],
"semantic_class": "unsafe-foreign-function-boundary"
},
{
"rule_id": "PA013",
"category": "AtomExhaustion",
"name": "atom-exhaustion",
"cwe_ids": [
"CWE-400"
],
"semantic_class": "unbounded-resource-consumption"
},
{
"rule_id": "PA014",
"category": "InsecureProtocol",
"name": "insecure-protocol",
"cwe_ids": [
"CWE-319",
"CWE-326"
],
"semantic_class": "insecure-communications-or-cryptography"
},
{
"rule_id": "PA015",
"category": "ExcessivePermissions",
"name": "excessive-permissions",
"cwe_ids": [
"CWE-732"
],
"semantic_class": "incorrect-permission-assignment"
},
{
"rule_id": "PA016",
"category": "PathTraversal",
"name": "path-traversal",
"cwe_ids": [
"CWE-22"
],
"semantic_class": "improper-limitation-of-pathname"
},
{
"rule_id": "PA017",
"category": "HardcodedSecret",
"name": "hardcoded-secret",
"cwe_ids": [
"CWE-798"
],
"semantic_class": "hardcoded-credential"
},
{
"rule_id": "PA018",
"category": "UncheckedError",
"name": "unchecked-error",
"cwe_ids": [
"CWE-390",
"CWE-391"
],
"semantic_class": "ignored-or-unchecked-error-condition"
},
{
"rule_id": "PA019",
"category": "InfiniteRecursion",
"name": "infinite-recursion",
"cwe_ids": [
"CWE-674"
],
"semantic_class": "uncontrolled-recursion"
},
{
"rule_id": "PA020",
"category": "UnsafeTypeCoercion",
"name": "unsafe-type-coercion",
"cwe_ids": [
"CWE-704"
],
"semantic_class": "incorrect-type-conversion"
},
{
"rule_id": "PA021",
"category": "ProofDrift",
"name": "proof-drift",
"cwe_ids": [],
"semantic_class": "unverified-or-drifted-formal-proof"
},
{
"rule_id": "PA022",
"category": "CryptoMisuse",
"name": "crypto-misuse",
"cwe_ids": [
"CWE-327",
"CWE-328",
"CWE-330"
],
"semantic_class": "cryptographic-primitive-or-validation-misuse"
},
{
"rule_id": "PA023",
"category": "SupplyChain",
"name": "supply-chain",
"cwe_ids": [
"CWE-1104",
"CWE-1395"
],
"semantic_class": "untrusted-or-vulnerable-component"
},
{
"rule_id": "PA024",
"category": "InputBoundary",
"name": "input-boundary",
"cwe_ids": [
"CWE-20",
"CWE-502"
],
"semantic_class": "unvalidated-input-at-deserialization-boundary"
},
{
"rule_id": "PA025",
"category": "MutationGap",
"name": "mutation-gap",
"cwe_ids": [],
"semantic_class": "insufficient-test-or-mutation-assurance"
},
{
"rule_id": "PA026",
"category": "LicensePolicy",
"name": "license-policy",
"cwe_ids": [],
"semantic_class": "dependency-license-policy-violation"
},
{
"rule_id": "PA027",
"category": "Advisory",
"name": "advisory",
"cwe_ids": [
"CWE-1395"
],
"semantic_class": "known-vulnerable-component-version"
},
{
"rule_id": "PA028",
"category": "BannedDep",
"name": "banned-dep",
"cwe_ids": [
"CWE-1357"
],
"semantic_class": "prohibited-or-insufficiently-trustworthy-component"
},
{
"rule_id": "PA029",
"category": "UnknownSource",
"name": "unknown-source",
"cwe_ids": [
"CWE-1357"
],
"semantic_class": "unverified-component-provenance"
}
]
}
16 changes: 16 additions & 0 deletions bots/panicbot/canon/pin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# SPDX-License-Identifier: MPL-2.0
#
# panicbot's copy of panic-attack's published rule-ID table, taken verbatim
# and pinned by digest. panicbot's translator must use these IDs (enforced by
# tests/canonical_rule_ids_test.rs); the digest is checked by
# tests/panic-attack-rule-ids-test.sh. To re-pin: copy the upstream file over
# canonical-rule-ids.json, update `commit`, `pinned` and `sha256`, and run both
# tests. `tests/panic-attack-rule-ids-test.sh --upstream` compares against
# panic-attack main.

[source]
repo = "hyperpolymath/panic-attack"
path = "data/canonical-rule-ids.json"
commit = "5175a681f9a0c82313f06e3c2cba8089f4ecfb6b"
pinned = "2026-11-04"
sha256 = "28ffe510535e0bc6cccc92845411d6496fe9270cce58364f4d3507c5c08337d4"
16 changes: 9 additions & 7 deletions bots/panicbot/src/a2ml_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,7 @@ fn finding_to_a2ml(finding: &Finding) -> A2mlFinding {
}

/// Build the summary statistics from fixable and unfixable findings.
fn build_summary(
fixable: &[&Finding],
unfixable: &[&Finding],
) -> FindingsSummary {
fn build_summary(fixable: &[&Finding], unfixable: &[&Finding]) -> FindingsSummary {
let total = fixable.len() + unfixable.len();
let all_findings: Vec<&&Finding> = fixable.iter().chain(unfixable.iter()).collect();

Expand Down Expand Up @@ -187,8 +184,8 @@ pub fn write_a2ml(repo_path: &Path, document: &A2mlDocument) -> Result<std::path
.with_context(|| format!("Failed to create {}", panicbot_dir.display()))?;

let output_path = panicbot_dir.join("PANICBOT-FINDINGS.a2ml");
let json = serde_json::to_string_pretty(document)
.context("Failed to serialize A2ML document")?;
let json =
serde_json::to_string_pretty(document).context("Failed to serialize A2ML document")?;

std::fs::write(&output_path, &json)
.with_context(|| format!("Failed to write {}", output_path.display()))?;
Expand All @@ -204,7 +201,12 @@ mod tests {
use gitbot_shared_context::finding::Severity;
use gitbot_shared_context::triangle::TriangleTier;

fn make_finding(rule_id: &str, severity: Severity, fixable: bool, tier: TriangleTier) -> Finding {
fn make_finding(
rule_id: &str,
severity: Severity,
fixable: bool,
tier: TriangleTier,
) -> Finding {
let mut f = Finding::new(BotId::Panicbot, rule_id, severity, "test finding")
.with_category("static-analysis/test")
.with_triangle_tier(tier)
Expand Down
6 changes: 4 additions & 2 deletions bots/panicbot/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ use std::path::PathBuf;
#[command(name = "panicbot")]
#[command(version)]
#[command(about = "Targeted audit bot wrapping panic-attack static analysis")]
#[command(long_about = "Panicbot invokes panic-attack as a subprocess, translates its findings \
#[command(
long_about = "Panicbot invokes panic-attack as a subprocess, translates its findings \
into gitbot-fleet Finding structs, classifies them as fixable or unfixable, and writes \
unfixable findings to per-repo A2ML debt registers.")]
unfixable findings to per-repo A2ML debt registers."
)]
pub struct Cli {
/// Subcommand to run.
#[command(subcommand)]
Expand Down
Loading
Loading