diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 8f31bb1..681da1c 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -18,11 +18,11 @@ workflows: - 'actions/upload-pages-artifact@v5.0.0' - 'haskell-actions/setup@v2.12.1' '.github/workflows/chapel-ci.yml': - - 'actions/cache@v6.1.0' - - 'actions/checkout@v7.0.1' - - 'actions/download-artifact@v8.0.2' - - 'actions/upload-artifact@v7.0.2' - - 'dtolnay/rust-toolchain@v1' + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333' + - 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' + - 'dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8' '.github/workflows/codeql.yml': - 'actions/checkout@v7.0.1' - 'github/codeql-action@v4.39.0' @@ -114,6 +114,11 @@ dependencies: commit: 'sha1-368f82528645a54fb793d4d04e342629a3f51346' owner_id: 44036562 repo_id: 438112499 + 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333': + ref: 'v8.0.2' + commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333' + owner_id: 44036562 + repo_id: 192626254 'actions/download-artifact@v8.0.2': ref: 'v8.0.2' commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333' diff --git a/.github/workflows/chapel-ci.yml b/.github/workflows/chapel-ci.yml index 2a6eb88..3ae7720 100644 --- a/.github/workflows/chapel-ci.yml +++ b/.github/workflows/chapel-ci.yml @@ -72,7 +72,7 @@ jobs: outputs: relevant: ${{ steps.f.outputs.relevant }} steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 2 - id: f @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Chapel ${{ env.CHAPEL_VERSION }} run: | set -euo pipefail @@ -128,7 +128,7 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Chapel ${{ env.CHAPEL_VERSION }} run: | set -euo pipefail @@ -144,7 +144,7 @@ jobs: chpl src/MassPanic.chpl src/Protocol.chpl src/Imaging.chpl src/Temporal.chpl -o mass-panic chpl smoke/two_repo_smoke.chpl src/Protocol.chpl src/Imaging.chpl -o smoke/two_repo_smoke - name: Upload Chapel artefacts - uses: actions/upload-artifact@v7.0.2 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: chapel-binaries path: | @@ -159,7 +159,7 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Chapel ${{ env.CHAPEL_VERSION }} run: | set -euo pipefail @@ -168,7 +168,7 @@ jobs: curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}" sudo apt-get install -y /tmp/chapel.deb - name: Download Chapel artefacts - uses: actions/download-artifact@v8.0.2 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: chapel-binaries path: chapel/ @@ -184,7 +184,7 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Chapel ${{ env.CHAPEL_VERSION }} run: | set -euo pipefail @@ -193,7 +193,7 @@ jobs: curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}" sudo apt-get install -y /tmp/chapel.deb - name: Download Chapel artefacts - uses: actions/download-artifact@v8.0.2 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: chapel-binaries path: chapel/ @@ -221,11 +221,11 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - - uses: actions/checkout@v7.0.1 - - uses: dtolnay/rust-toolchain@v1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1 with: toolchain: stable - - uses: actions/cache@v6.1.0 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cargo/registry @@ -244,11 +244,11 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - - uses: actions/checkout@v7.0.1 - - uses: dtolnay/rust-toolchain@v1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1 with: toolchain: stable - - uses: actions/cache@v6.1.0 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cargo/registry @@ -263,7 +263,7 @@ jobs: curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}" sudo apt-get install -y /tmp/chapel.deb - name: Download Chapel artefacts - uses: actions/download-artifact@v8.0.2 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: chapel-binaries path: chapel/ @@ -283,7 +283,7 @@ jobs: env: CHPL_HOME: /opt/chapel-multilocale steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Cache the entire built-from-source Chapel tree. Key is stable across # PRs as long as the version, conduit, launcher and cache-gen marker @@ -291,7 +291,7 @@ jobs: # restore is ~30s. - name: Cache multilocale Chapel ($CHPL_HOME) id: chapel-cache - uses: actions/cache@v6.1.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.CHAPEL_MULTILOCALE_HOME }} key: ${{ runner.os }}-chapel-multilocale-${{ env.CHAPEL_VERSION }}-gasnet-smp-${{ env.CHAPEL_MULTILOCALE_CACHE_GEN }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index a389a51..d1ca432 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -17,7 +17,7 @@ jobs: scorecard: uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@333b06a01db1874ffedb2de1bfabb195640927c7 permissions: - actions: read + actions: read # required by scorecard-reusable.yml to read workflow metadata contents: read security-events: write id-token: write diff --git a/src/aggregate/proven_tests.rs b/src/aggregate/proven_tests.rs index 60c0819..9799cad 100644 --- a/src/aggregate/proven_tests.rs +++ b/src/aggregate/proven_tests.rs @@ -119,7 +119,10 @@ impl ReportTier { impl FromStr for ProvenTestsReport { type Err = anyhow::Error; - /// Parse a run report from JSON text. + /// Parse a run report from JSON text, requiring schema version 1. + /// + /// # Errors + /// Returns JSON deserialisation errors or an error if `schema_version` is not 1. fn from_str(text: &str) -> Result { let report: ProvenTestsReport = serde_json::from_str(text).context("parsing proven-tests run report")?; @@ -134,7 +137,11 @@ impl FromStr for ProvenTestsReport { } impl ProvenTestsReport { - /// Parse a run report from a file path. + /// Read a UTF-8 JSON run report from `path` and parse it as schema version 1. + /// + /// # Errors + /// Returns file read errors (including invalid UTF-8), JSON deserialisation + /// errors or an error if `schema_version` is not 1. pub fn from_path(path: &Path) -> Result { let text = std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?; diff --git a/src/assail/analyzer.rs b/src/assail/analyzer.rs index 1b75185..4b86ad1 100644 --- a/src/assail/analyzer.rs +++ b/src/assail/analyzer.rs @@ -263,18 +263,16 @@ static RE_HARDCODED_SECRET: OnceLock = OnceLock::new(); /// Does not handle OCaml `(* *)` or Forth `\` — edge cases for later. static RE_TODO_COMMENT: OnceLock = OnceLock::new(); -/// Heuristic: is the `malloc` on `lines[idx]` NULL-checked within a short -/// window of following lines? +/// Heuristically recognise a null check for the allocation on `lines[idx]`. /// -/// An "UncheckedAllocation" finding should only fire when the result is *not* -/// guarded. This recognises the common C idioms (`if (p == NULL)`, -/// `if (!p)`, `if (p == nullptr)`) on the lvalue assigned from `malloc`. +/// Searches the allocation line and up to six following lines for `NULL` or +/// `nullptr` alongside the assigned identifier, or an `if` line containing `!` +/// immediately followed by that identifier. If no identifier can be extracted, +/// any `NULL` or `nullptr` substring in the window suffices. Substring matches +/// do not establish that the allocation is guarded. /// -/// Deliberately conservative to avoid false negatives in a security scanner: -/// a site is treated as checked only on a clear NULL-guard signal that -/// references the allocated variable (or, when no lvalue can be parsed, any -/// NULL/nullptr token in the window). Genuinely-unchecked allocations — the -/// real finding — are still reported. +/// # Panics +/// Panics if the zero-based `idx` is outside `lines`. fn c_alloc_result_is_null_checked(lines: &[&str], idx: usize) -> bool { let line = lines[idx]; // Parse the lvalue assigned from malloc: the last identifier left of '='. diff --git a/src/assail/mod.rs b/src/assail/mod.rs index 7ec19ad..aaf369e 100644 --- a/src/assail/mod.rs +++ b/src/assail/mod.rs @@ -411,7 +411,8 @@ pub fn apply_suppression(report: &mut AssailReport) { /// 3. UnsafeFFI finding where the file's [[ffi_kind]] is /// `BuildSystem` or `TestMock` (audit-accepted by default). /// -/// JIT context handling remains inline in analyzer.rs:1117..1129. +/// JIT context handling remains in the transmute classification block of +/// `Analyzer::analyze_rust` in `analyzer.rs`. /// /// Fills missing `wp.test_context` for findings with a known file path so /// downstream audit consumers can render it. Returns the number of diff --git a/src/diagnostics.rs b/src/diagnostics.rs index f89ec6b..5890014 100644 --- a/src/diagnostics.rs +++ b/src/diagnostics.rs @@ -11,6 +11,12 @@ const HYPATIA_ENV: &str = "HYPATIA_API_KEY"; const GITBOT_FLEET_ENV: &str = "GITBOT_FLEET_ENDPOINT"; const PANICBOT_DIRECTIVE_PATH: &str = ".machine_readable/bot_directives/panicbot.scm"; +/// Print readiness checks using `manifest`, paths relative to the current +/// working directory and the configured integration environment variables. +/// +/// # Errors +/// Returns an error after printing all results if any check has error severity. +/// Warnings alone do not cause failure. pub fn run_self_diagnostics(manifest: &Manifest) -> Result<()> { println!("panic-attack self-diagnostics"); diff --git a/src/main.rs b/src/main.rs index d09d67a..adede31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,36 +7,34 @@ //! concurrency) and detecting bug signatures using logic programming techniques inspired by //! Mozart/Oz and Datalog. -mod a2ml; -mod abduct; -mod adjudicate; -mod aggregate; -mod ambush; -mod amuck; -mod assail; -mod assay; -mod assemblyline; -mod attack; -mod attestation; -mod axial; +use panic_attack::a2ml; +use panic_attack::abduct; +use panic_attack::adjudicate; +use panic_attack::aggregate; +use panic_attack::ambush; +use panic_attack::amuck; +use panic_attack::assail; +use panic_attack::assay; +use panic_attack::assemblyline; +use panic_attack::attack; +use panic_attack::attestation; +use panic_attack::axial; #[cfg(feature = "http")] -mod bridge; -mod campaign; +use panic_attack::bridge; +use panic_attack::campaign; mod diagnostics; mod groove; -mod i18n; -mod kanren; +use panic_attack::i18n; mod kin; -mod mass_panic; -mod notify; -mod panll; -mod query; -mod report; -mod signatures; -mod storage; -mod sweep_tracker; -mod test_context; -mod types; +use panic_attack::mass_panic; +use panic_attack::notify; +use panic_attack::panll; +use panic_attack::query; +use panic_attack::report; +use panic_attack::signatures; +use panic_attack::storage; +use panic_attack::sweep_tracker; +use panic_attack::types; extern crate walkdir; @@ -57,7 +55,6 @@ use anyhow::{anyhow, Context, Result}; use clap::{CommandFactory, Parser, Subcommand}; use clap_complete::{generate, Shell}; use clap_complete_nushell::Nushell; -use panic_attack::{comment_marker, ffi_kind}; use std::collections::HashMap; use std::fs::{self, File}; use std::io::{self, Read, Write}; diff --git a/src/report/gui.rs b/src/report/gui.rs index 014c3fa..549560b 100644 --- a/src/report/gui.rs +++ b/src/report/gui.rs @@ -120,8 +120,11 @@ impl ReportGui { } impl App for ReportGui { + /// Render the file loader, navigation and selected report tab. + /// A load-button click or Enter after the path field loses focus attempts + /// to read the specified JSON file, updating the viewer or its status message. fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut Frame) { - egui::Panel::top("header").show_inside(ui, |ui| { + egui::Panel::top("header").show(ui, |ui| { ui.horizontal(|ui| { ui.heading("panic-attack report"); ui.separator(); @@ -138,7 +141,7 @@ impl App for ReportGui { }); }); - egui::Panel::left("nav").show_inside(ui, |ui| { + egui::Panel::left("nav").show(ui, |ui| { ui.selectable_value(&mut self.tab, ReportTab::Summary, "Summary"); ui.selectable_value(&mut self.tab, ReportTab::Assail, "Assail"); ui.selectable_value(&mut self.tab, ReportTab::Matrix, "Matrix"); @@ -159,7 +162,7 @@ impl App for ReportGui { ui.selectable_value(&mut self.tab, ReportTab::Temporal, temporal_label); }); - egui::CentralPanel::default().show_inside(ui, |ui| match self.tab { + egui::CentralPanel::default().show(ui, |ui| match self.tab { ReportTab::Summary => self.render_summary(ui), ReportTab::Assail => self.render_assail(ui), ReportTab::Matrix => self.render_matrix(ui), diff --git a/src/safe_path.rs b/src/safe_path.rs index 12e008d..ec06273 100644 --- a/src/safe_path.rs +++ b/src/safe_path.rs @@ -100,17 +100,24 @@ pub fn sanitize_filename(filename: &str) -> String { } } +#[cfg(test)] +/// Returns true if `c` is a character `sanitize_filename` would keep. +fn is_safe_char(c: char) -> bool { + c.is_ascii_alphanumeric() || SAFE_PUNCT.contains(&c) +} + +#[cfg(test)] +/// Returns true if every character in `s` is safe. Used by the +/// idempotence + non-empty proptest invariants. +fn all_chars_safe(s: &str) -> bool { + s.chars().all(is_safe_char) +} + #[cfg(test)] mod tests { use super::*; use proptest::prelude::*; - /// Property-test predicate kept separate from the sanitizer implementation. - fn all_chars_safe(s: &str) -> bool { - s.chars() - .all(|c| c.is_ascii_alphanumeric() || SAFE_PUNCT.contains(&c)) - } - // ───────────────────────────────────────────────────────────────── // has_traversal — concrete cases // ───────────────────────────────────────────────────────────────── diff --git a/src/scan/cargodeny.rs b/src/scan/cargodeny.rs index 59ee87e..e002feb 100644 --- a/src/scan/cargodeny.rs +++ b/src/scan/cargodeny.rs @@ -182,10 +182,11 @@ pub fn run_cargo_deny(config: &CargoDenyConfig) -> Result> { .unwrap_or(WeakPointCategory::Advisory); let location = diagnostic.location.and_then(|loc| { - loc.file.map(|file| match loc.line { - Some(line) => format!("{}:{}", file, line), - None => file, - }) + if let (Some(ref file), Some(line)) = (&loc.file, loc.line) { + Some(format!("{}:{}", file, line)) + } else { + loc.file.clone() + } }); // Parse file and line from location string diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 1c551ba..3680f52 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -229,7 +229,10 @@ pub struct MigrationSemantic { pub snapshot_label: Option, } -/// Build a VerisimDB hexad from an assault report +/// Build a VerisimDB hexad from an assault report. +/// +/// # Errors +/// Propagates errors from serialising the report into its JSON document. fn build_hexad(report: &AssaultReport) -> Result { let now = Utc::now(); let id = format!( @@ -375,6 +378,9 @@ pub fn persist_report( /// /// Unlike single-repo hexads which wrap an AssaultReport, assemblyline /// hexads capture the batch scan results across many repos. +/// +/// # Errors +/// Propagates errors from serialising the report into its JSON document. fn build_assemblyline_hexad( report: &crate::assemblyline::AssemblylineReport, ) -> Result { @@ -492,13 +498,13 @@ fn severity_label(severity: &crate::types::Severity) -> &'static str { } } -/// Build one hexad per `WeakPoint` across all repo results in an -/// assemblyline report (issue #33 S1). +/// Build one hexad per unsuppressed `WeakPoint` across repo results with an +/// AssailReport in an assemblyline report (issue #33 S1). /// /// Subject identity lives in `semantic.finding.finding_id`; each emitted -/// hexad's top-level `id` remains per-run-unique so two runs of the same -/// finding produce two distinct hexad files (the join key is the -/// `finding_id`, not the hexad id). +/// hexad's top-level `id` includes a millisecond timestamp and repo/finding +/// indices. IDs are distinct within a call but may repeat across calls in +/// the same millisecond; use `finding_id` as the join key. /// /// `run_id` is shared across every finding-hexad in this run and stamped /// into both `first_seen_run` and `last_seen_run` (S1 has no prior-run @@ -662,10 +668,10 @@ fn derive_crosslang_interactions_for_report( /// repo in an assemblyline report (issue #33 kanren-crosslang follow-up). /// /// Subject identity lives in `semantic.crosslang.interaction_id`; the -/// top-level hexad `id` is per-run-unique so two runs of the same -/// interaction produce two distinct hexad files (the join key is the -/// `interaction_id`, not the hexad id). Returns an empty `Vec` when the -/// report has no AssailReport-bearing results or no derivable interactions. +/// top-level hexad `id` includes a millisecond timestamp and repo/interaction +/// indices. IDs are distinct within a call but may repeat across calls in +/// the same millisecond; use `interaction_id` as the join key. Returns an empty +/// `Vec` when the report has no AssailReport-bearing results or no derivable interactions. pub fn build_crosslang_hexads( report: &crate::assemblyline::AssemblylineReport, ) -> Result> { @@ -772,9 +778,10 @@ const HEXAD_FILE_READ_LIMIT: u64 = 16 * 1024 * 1024; /// Build a campaign-state hexad for one lifecycle event (issue #33 S2). /// -/// Append-only: each call produces a fresh hexad with a unique id. The -/// `finding_id` is carried as the semantic subject so the newest hexad -/// per finding is the current state. +/// The id is derived from the current millisecond timestamp, so calls in the +/// same millisecond can produce the same id. This only constructs a hexad; +/// it does not write to storage. The `finding_id` is carried as the semantic +/// subject so stored events can be grouped by finding. pub fn build_campaign_hexad(semantic: CampaignSemantic) -> PanicAttackHexad { let now = Utc::now(); let hexad_id = format!(