Skip to content

Commit 6b8f959

Browse files
Merge branch 'main' into chore/license-uniform-MPL-2.0
2 parents 1a7ac11 + 7f2e05d commit 6b8f959

10 files changed

Lines changed: 325 additions & 1 deletion

bots/panicbot/src/scanner.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ pub struct WeakPoint {
7171
/// Suppressed items are excluded from fleet counts and CI gates.
7272
#[serde(default)]
7373
pub suppressed: bool,
74+
/// Test-vs-production classification of the finding's location
75+
/// (panic-attack v2.5.5+). `None` means the scanner predates the
76+
/// classification field; treat as "Production" for downstream
77+
/// routing decisions. `"test_only"` and `"doc"` indicate the
78+
/// finding is in test or documentation code respectively — usually
79+
/// the panic-attack engine has ALREADY set `suppressed: true` for
80+
/// these, but the metadata is preserved so the translator can emit
81+
/// audit-trail context (e.g. "PanicPath in tests/foo.rs — accepted
82+
/// by test_context rule").
83+
#[serde(default)]
84+
pub test_context: Option<String>,
7485
}
7586

7687
/// Report from `panic-attack adjudicate` — cross-report verdict.

bots/panicbot/src/translator.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,9 +400,25 @@ pub fn translate_all(
400400
// Skip weak points suppressed by panic-attack's context-aware FP engine.
401401
// Suppressed = the logic engine found a defensive pattern (e.g. mutex guard,
402402
// RAII, schema validation) that makes this finding likely a false positive.
403+
//
404+
// v2.5.5+ classifications (test_context field):
405+
// * `"test_only"` / `"doc"` → typically also `suppressed: true`
406+
// (set by `apply_v255_context_suppression` in panic-attack
407+
// after the kanren rule pass). Defensive: even if a future
408+
// scanner version preserves `suppressed: false` while
409+
// classifying as TestOnly, drop the finding here so test code
410+
// never reaches the fleet.
411+
// * `"production"` → forwarded normally.
412+
// * `None` → predates v2.5.5; treat as production.
403413
if wp.suppressed {
404414
return None;
405415
}
416+
if matches!(
417+
wp.test_context.as_deref(),
418+
Some("test_only") | Some("doc")
419+
) {
420+
return None;
421+
}
406422

407423
// Apply severity filter
408424
let severity_value = match wp.severity.to_lowercase().as_str() {
@@ -498,6 +514,7 @@ mod tests {
498514
description: "AWS_SECRET_KEY found in source".to_string(),
499515
recommended_attack: vec![],
500516
suppressed: false,
517+
test_context: None,
501518
};
502519
let config = PanicbotConfig::default();
503520
let finding = translate_weak_point(&wp, &config);
@@ -522,6 +539,7 @@ mod tests {
522539
description: "Something new".to_string(),
523540
recommended_attack: vec![],
524541
suppressed: false,
542+
test_context: None,
525543
};
526544
let config = PanicbotConfig::default();
527545
let finding = translate_weak_point(&wp, &config);
@@ -541,6 +559,7 @@ mod tests {
541559
description: "unsafe block".to_string(),
542560
recommended_attack: vec![],
543561
suppressed: false,
562+
test_context: None,
544563
};
545564
let config = PanicbotConfig {
546565
confidence_overrides: vec![crate::config::ConfidenceOverride {
@@ -563,6 +582,7 @@ mod tests {
563582
description: "low severity".to_string(),
564583
recommended_attack: vec![],
565584
suppressed: false,
585+
test_context: None,
566586
},
567587
WeakPoint {
568588
category: "CommandInjection".to_string(),
@@ -571,6 +591,7 @@ mod tests {
571591
description: "critical severity".to_string(),
572592
recommended_attack: vec![],
573593
suppressed: false,
594+
test_context: None,
574595
},
575596
];
576597
let config = PanicbotConfig {
@@ -596,6 +617,7 @@ mod tests {
596617
description: "fixable".to_string(),
597618
recommended_attack: vec![],
598619
suppressed: false,
620+
test_context: None,
599621
},
600622
&config,
601623
),
@@ -607,6 +629,7 @@ mod tests {
607629
description: "not fixable".to_string(),
608630
recommended_attack: vec![],
609631
suppressed: false,
632+
test_context: None,
610633
},
611634
&config,
612635
),
@@ -643,4 +666,75 @@ mod tests {
643666
}
644667
assert_eq!(rule_ids.len(), 25);
645668
}
669+
670+
// ─────────────────────────────────────────────────────────────────────
671+
// v2.5.5 test_context tests
672+
// ─────────────────────────────────────────────────────────────────────
673+
674+
fn make_wp(category: &str, test_context: Option<&str>, suppressed: bool) -> WeakPoint {
675+
WeakPoint {
676+
category: category.to_string(),
677+
location: Some("src/foo.rs:1".to_string()),
678+
severity: "Critical".to_string(),
679+
description: "test".to_string(),
680+
recommended_attack: vec![],
681+
suppressed,
682+
test_context: test_context.map(String::from),
683+
}
684+
}
685+
686+
#[test]
687+
fn translate_all_drops_test_only_findings_even_if_not_suppressed() {
688+
// Defensive: even when `suppressed: false`, a `test_context: test_only`
689+
// finding should not reach the fleet.
690+
let wps = vec![
691+
make_wp("UnsafeCode", Some("test_only"), false),
692+
make_wp("UnsafeCode", Some("production"), false),
693+
];
694+
let config = PanicbotConfig::default();
695+
let findings = translate_all(&wps, &config);
696+
assert_eq!(
697+
findings.len(),
698+
1,
699+
"test_only finding must be dropped regardless of suppressed flag"
700+
);
701+
}
702+
703+
#[test]
704+
fn translate_all_drops_doc_findings_even_if_not_suppressed() {
705+
let wps = vec![
706+
make_wp("PanicPath", Some("doc"), false),
707+
make_wp("PanicPath", Some("production"), false),
708+
];
709+
let config = PanicbotConfig::default();
710+
let findings = translate_all(&wps, &config);
711+
assert_eq!(findings.len(), 1, "doc finding must be dropped");
712+
}
713+
714+
#[test]
715+
fn translate_all_keeps_production_test_context() {
716+
let wps = vec![make_wp("UnsafeCode", Some("production"), false)];
717+
let config = PanicbotConfig::default();
718+
let findings = translate_all(&wps, &config);
719+
assert_eq!(findings.len(), 1);
720+
}
721+
722+
#[test]
723+
fn translate_all_treats_none_test_context_as_production() {
724+
// Pre-v2.5.5 panic-attack reports don't have the field; they should
725+
// still be routed.
726+
let wps = vec![make_wp("UnsafeCode", None, false)];
727+
let config = PanicbotConfig::default();
728+
let findings = translate_all(&wps, &config);
729+
assert_eq!(findings.len(), 1);
730+
}
731+
732+
#[test]
733+
fn translate_all_respects_suppressed_independently() {
734+
// `suppressed: true` should drop regardless of test_context.
735+
let wps = vec![make_wp("UnsafeCode", Some("production"), true)];
736+
let config = PanicbotConfig::default();
737+
let findings = translate_all(&wps, &config);
738+
assert_eq!(findings.len(), 0);
739+
}
646740
}

bots/panicbot/tests/integration_test.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ fn mock_assail_report() -> AssailReport {
2929
description: "3 unsafe blocks in FFI boundary".to_string(),
3030
recommended_attack: vec!["Memory".to_string()],
3131
suppressed: false,
32+
test_context: None,
3233
},
3334
WeakPoint {
3435
category: "HardcodedSecret".to_string(),
@@ -37,6 +38,7 @@ fn mock_assail_report() -> AssailReport {
3738
description: "AWS_SECRET_ACCESS_KEY found in source code".to_string(),
3839
recommended_attack: vec![],
3940
suppressed: false,
41+
test_context: None,
4042
},
4143
WeakPoint {
4244
category: "PanicPath".to_string(),
@@ -45,6 +47,7 @@ fn mock_assail_report() -> AssailReport {
4547
description: "unwrap() on user-provided input".to_string(),
4648
recommended_attack: vec!["CPU".to_string()],
4749
suppressed: false,
50+
test_context: None,
4851
},
4952
WeakPoint {
5053
category: "RaceCondition".to_string(),
@@ -53,6 +56,7 @@ fn mock_assail_report() -> AssailReport {
5356
description: "Shared mutable state without synchronisation".to_string(),
5457
recommended_attack: vec!["Concurrency".to_string()],
5558
suppressed: false,
59+
test_context: None,
5660
},
5761
WeakPoint {
5862
category: "UncheckedError".to_string(),
@@ -61,6 +65,7 @@ fn mock_assail_report() -> AssailReport {
6165
description: "Result ignored from database query".to_string(),
6266
recommended_attack: vec![],
6367
suppressed: false,
68+
test_context: None,
6469
},
6570
],
6671
statistics: serde_json::json!({"total_lines": 5000}),

bots/panicbot/tests/translator_test.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ fn weak_point(category: &str, severity: &str) -> WeakPoint {
2626
description: format!("Test finding for {}", category),
2727
recommended_attack: vec![],
2828
suppressed: false,
29+
test_context: None,
2930
}
3031
}
3132

@@ -38,6 +39,7 @@ fn weak_point_at(category: &str, severity: &str, location: &str) -> WeakPoint {
3839
description: format!("Test finding at {}", location),
3940
recommended_attack: vec![],
4041
suppressed: false,
42+
test_context: None,
4143
}
4244
}
4345

@@ -310,6 +312,7 @@ fn test_translate_metadata_includes_panic_attack_category() {
310312
description: "os.system with user input".to_string(),
311313
recommended_attack: vec!["Memory".to_string(), "Concurrency".to_string()],
312314
suppressed: false,
315+
test_context: None,
313316
};
314317
let finding = translator::translate_weak_point(&wp, &config);
315318

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/bin/bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
#
4+
# fix-chapel-chpl-llvm-export.sh — ensure CHPL_LLVM is exported on Chapel jobs
5+
#
6+
# Driven by hypatia WF024 (chapel-chpl-llvm-export recipe). Sharp edge #2 in
7+
# panic-attack wiki Chapel-Metalayer.md: CHPL_LLVM is required by `chpl`'s
8+
# diagnostic + build steps; unset means the toolchain falls back to a different
9+
# code path that doesn't link against libllvm14 (the .deb's only supported
10+
# binding).
11+
#
12+
# Inserts `export CHPL_LLVM=bundled` (the only supported value for the
13+
# precompiled .deb path) into the install step.
14+
#
15+
# Idempotent.
16+
17+
set -euo pipefail
18+
19+
REPO_PATH="$1"
20+
FINDING_FILE="${2:-}"
21+
22+
cd "$REPO_PATH"
23+
24+
CHANGED=0
25+
for f in $(find .github/workflows -name '*.yml' -o -name '*.yaml' 2>/dev/null); do
26+
if ! grep -qE 'CHAPEL_DEB_URL|chapel-[0-9]+\.[0-9]+' "$f"; then
27+
continue
28+
fi
29+
if grep -qE 'CHPL_LLVM[[:space:]]*=' "$f"; then
30+
continue # already exported / set
31+
fi
32+
if grep -qE 'chpl --version|sudo apt-get install.*chapel' "$f"; then
33+
sed -i -E '0,/(chpl --version|sudo apt-get install.*chapel)/{s|^([[:space:]]*)(chpl --version\|sudo apt-get install.*chapel)|\1export CHPL_LLVM=bundled\n\1\2|}' "$f"
34+
echo " CHPL_LLVM=bundled exported: $f"
35+
CHANGED=$((CHANGED + 1))
36+
fi
37+
done
38+
39+
echo "fix-chapel-chpl-llvm-export: $CHANGED change(s) applied"
40+
exit 0
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
#
4+
# fix-chapel-manpath-guard.sh — guard against unset MANPATH on Chapel install
5+
#
6+
# Driven by hypatia WF024 (chapel-manpath-guard recipe). One of the 5 sharp
7+
# edges in panic-attack wiki Chapel-Metalayer.md: the chpl install step
8+
# references $MANPATH unconditionally; unset MANPATH explodes under `set -u`.
9+
#
10+
# Inserts `: \${MANPATH:=}` BEFORE the chapel install runs.
11+
#
12+
# Idempotent.
13+
14+
set -euo pipefail
15+
16+
REPO_PATH="$1"
17+
FINDING_FILE="${2:-}"
18+
19+
cd "$REPO_PATH"
20+
21+
CHANGED=0
22+
for f in $(find .github/workflows -name '*.yml' -o -name '*.yaml' 2>/dev/null); do
23+
if ! grep -qE 'CHAPEL_DEB_URL|chapel-[0-9]+\.[0-9]+' "$f"; then
24+
continue
25+
fi
26+
if grep -qF ': ${MANPATH:=}' "$f"; then
27+
continue # already guarded
28+
fi
29+
# Insert guard before the first `chpl --version` or chapel install line
30+
if grep -qE 'chpl --version|sudo apt-get install.*chapel' "$f"; then
31+
sed -i -E '0,/(chpl --version|sudo apt-get install.*chapel)/{s|^([[:space:]]*)(chpl --version\|sudo apt-get install.*chapel)|\1: ${MANPATH:=}\n\1\2|}' "$f"
32+
echo " MANPATH guard inserted: $f"
33+
CHANGED=$((CHANGED + 1))
34+
fi
35+
done
36+
37+
echo "fix-chapel-manpath-guard: $CHANGED change(s) applied"
38+
exit 0
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/bin/bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
#
4+
# fix-chapel-replace-chpl-about.sh — replace dropped `chpl --about` with
5+
# the 2.8-compatible equivalent.
6+
#
7+
# Driven by hypatia WF024 (chapel-replace-chpl-about-with-version recipe).
8+
# Sharp edge #3 in panic-attack wiki Chapel-Metalayer.md: `chpl --about` was
9+
# removed in Chapel 2.8 — workflows that rely on it for diagnostic output
10+
# break silently (no `--about` flag is a no-op + exits 0 on some versions).
11+
#
12+
# Replace with `chpl --version && printchplenv` which gives the same
13+
# information across the 2.7/2.8 split.
14+
#
15+
# Idempotent.
16+
17+
set -euo pipefail
18+
19+
REPO_PATH="$1"
20+
FINDING_FILE="${2:-}"
21+
22+
cd "$REPO_PATH"
23+
24+
CHANGED=0
25+
for f in $(find .github/workflows -name '*.yml' -o -name '*.yaml' 2>/dev/null); do
26+
if ! grep -qF 'chpl --about' "$f"; then
27+
continue
28+
fi
29+
sed -i 's|chpl --about|chpl --version \&\& printchplenv|g' "$f"
30+
echo " chpl --about → chpl --version && printchplenv: $f"
31+
CHANGED=$((CHANGED + 1))
32+
done
33+
34+
echo "fix-chapel-replace-chpl-about: $CHANGED change(s) applied"
35+
exit 0

scripts/fix-chapel-runs-on-pin.sh

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/bin/bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
#
4+
# fix-chapel-runs-on-pin.sh — pin runs-on: ubuntu-22.04 on any Chapel-using job
5+
#
6+
# Driven by hypatia WF024 (chapel-runs-on-pin-22-04 recipe). The Chapel-2.8.0
7+
# .deb (chapel-X.Y.Z-N.ubuntu22.amd64.deb) is ABI-linked to libclang-cpp.so.14
8+
# / LLVM-14 — only present on Ubuntu 22.04. ubuntu-latest / ubuntu-24.04
9+
# resolves the unmet dep with libclang-cpp-18 → `chpl` exit 127 at startup.
10+
#
11+
# Idempotent: skips files already pinned to ubuntu-22.04.
12+
13+
set -euo pipefail
14+
15+
REPO_PATH="$1"
16+
FINDING_FILE="${2:-}"
17+
18+
cd "$REPO_PATH"
19+
20+
CHANGED=0
21+
for f in $(find .github/workflows -name '*.yml' -o -name '*.yaml' 2>/dev/null); do
22+
# Only patch files that actually reference Chapel
23+
if ! grep -qE 'CHAPEL_DEB_URL|chapel-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+\.ubuntu22|chpl --(version|about)|printchplenv' "$f"; then
24+
continue
25+
fi
26+
27+
if grep -qE '^[[:space:]]+runs-on:[[:space:]]+ubuntu-latest' "$f"; then
28+
sed -i -E 's|^([[:space:]]+)runs-on:[[:space:]]+ubuntu-latest|\1runs-on: ubuntu-22.04|' "$f"
29+
echo " pinned ubuntu-22.04: $f"
30+
CHANGED=$((CHANGED + 1))
31+
fi
32+
33+
# Also normalise the apt install pattern (declarative resolution beats dpkg+fix loop)
34+
if grep -qF 'sudo dpkg -i /tmp/chapel.deb || sudo apt-get install -f -y' "$f"; then
35+
sed -i 's|sudo dpkg -i /tmp/chapel.deb || sudo apt-get install -f -y|sudo apt-get install -y /tmp/chapel.deb|' "$f"
36+
echo " apt-get install replaces dpkg fallback: $f"
37+
CHANGED=$((CHANGED + 1))
38+
fi
39+
done
40+
41+
echo "fix-chapel-runs-on-pin: $CHANGED change(s) applied in $REPO_PATH"
42+
exit 0

0 commit comments

Comments
 (0)