From 3facd5a97dd423e87dcc840c76b8be71433794ac Mon Sep 17 00:00:00 2001 From: Danite Date: Mon, 6 Apr 2026 19:19:05 +0200 Subject: [PATCH 1/4] fix: security hardening --- .github/workflows/ci.yml | 15 +++++++++++++-- .github/workflows/release.yml | 9 +++++---- Cargo.lock | 28 +++++++++++++++++----------- Cargo.toml | 15 ++++++++++++--- deny.toml | 21 +++++++++++++++++++++ src/detectors/ports.rs | 18 +++++++++++++----- src/doctor.rs | 17 +++++++++++++++-- src/generate/compose.rs | 15 +++++++++++++-- src/infra/compose.rs | 4 ++-- src/output/html.rs | 10 ++++++---- 10 files changed, 117 insertions(+), 35 deletions(-) create mode 100644 deny.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d53f58e..2294bc5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + env: CARGO_TERM_COLOR: always @@ -13,11 +16,11 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Build run: cargo build @@ -30,3 +33,11 @@ jobs: - name: Format run: cargo fmt -- --check + + deny: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2.0.15 + with: + command: check advisories licenses diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6a92de9..2909486 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,18 +4,19 @@ on: release: types: [published] +permissions: + contents: read + env: CARGO_TERM_COLOR: always jobs: publish: runs-on: ubuntu-latest - permissions: - contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Extract version from tag id: version diff --git a/Cargo.lock b/Cargo.lock index 444eebf..f5af828 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -651,6 +651,16 @@ version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +[[package]] +name = "libyml" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -687,7 +697,7 @@ dependencies = [ "regex", "serde", "serde_json", - "serde_yaml", + "serde_yml", "tempfile", "url", "which", @@ -1244,16 +1254,18 @@ dependencies = [ ] [[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" +name = "serde_yml" +version = "0.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" dependencies = [ "indexmap", "itoa", + "libyml", + "memchr", "ryu", "serde", - "unsafe-libyaml", + "version_check", ] [[package]] @@ -1439,12 +1451,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "url" version = "2.5.8" diff --git a/Cargo.toml b/Cargo.toml index c229ebf..1a5e455 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,13 @@ rust-version = "1.92.0" repository = "https://github.com/Danite/looper" homepage = "https://github.com/Danite/looper" readme = "README.md" -keywords = ["static-analysis", "shadow-dependencies", "javascript", "typescript", "devops"] +keywords = [ + "static-analysis", + "shadow-dependencies", + "javascript", + "typescript", + "devops", +] categories = ["command-line-utilities", "development-tools"] exclude = [".beads/", ".cursor/", ".github/", "sandbox/", "working/"] @@ -28,13 +34,16 @@ oxc_allocator = "0.123" clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } +chrono = { version = "0.4", default-features = false, features = [ + "clock", + "serde", +] } ignore = "0.4" colored = "2" rayon = "1.10" url = "2" regex = "1" -serde_yaml = "0.9" +serde_yml = "0.0.12" which = "8" xxhash-rust = { version = "0.8", features = ["xxh3"] } diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..95d318b --- /dev/null +++ b/deny.toml @@ -0,0 +1,21 @@ +[advisories] +vulnerability = "deny" +unmaintained = "warn" +yanked = "warn" + +[licenses] +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "BSL-1.0", + "ISC", + "MPL-2.0", + "Unicode-3.0", + "Unicode-DFS-2016", + "Unlicense", + "Zlib", +] +confidence-threshold = 0.8 diff --git a/src/detectors/ports.rs b/src/detectors/ports.rs index cd830b8..dc37ea5 100644 --- a/src/detectors/ports.rs +++ b/src/detectors/ports.rs @@ -228,10 +228,18 @@ impl PortCollector<'_> { } } +fn f64_to_port(v: f64) -> Option { + if (1.0..=65535.0).contains(&v) && v.fract() == 0.0 { + Some(v as u16) + } else { + None + } +} + fn extract_port_from_first_arg(call: &CallExpression<'_>) -> Option { let arg = call.arguments.first()?; match arg { - Argument::NumericLiteral(lit) => Some(lit.value as u16), + Argument::NumericLiteral(lit) => f64_to_port(lit.value), _ => { let expr = arg.as_expression()?; extract_port_from_logical_expr(expr) @@ -242,14 +250,14 @@ fn extract_port_from_first_arg(call: &CallExpression<'_>) -> Option { fn extract_port_from_logical_expr(expr: &Expression<'_>) -> Option { if let Expression::LogicalExpression(logical) = expr { if let Expression::NumericLiteral(lit) = &logical.right { - return Some(lit.value as u16); + return f64_to_port(lit.value); } if let Expression::NumericLiteral(lit) = &logical.left { - return Some(lit.value as u16); + return f64_to_port(lit.value); } } if let Expression::NumericLiteral(lit) = expr { - return Some(lit.value as u16); + return f64_to_port(lit.value); } None } @@ -270,7 +278,7 @@ fn extract_port_from_options_arg(call: &CallExpression<'_>) -> Option { }; if key_name == "port" { if let Expression::NumericLiteral(lit) = &p.value { - return Some(lit.value as u16); + return f64_to_port(lit.value); } } } diff --git a/src/doctor.rs b/src/doctor.rs index e094b7f..db5d762 100644 --- a/src/doctor.rs +++ b/src/doctor.rs @@ -5,7 +5,19 @@ use std::net::{TcpListener, TcpStream, ToSocketAddrs}; use std::path::Path; use std::time::Duration; -const SECRET_KEYWORDS: &[&str] = &["SECRET", "KEY", "TOKEN", "PASSWORD", "CREDENTIAL", "AUTH"]; +const SECRET_KEYWORDS: &[&str] = &[ + "SECRET", + "KEY", + "TOKEN", + "PASSWORD", + "CREDENTIAL", + "AUTH", + "PRIVATE", + "SIGNING", + "DATABASE_URL", + "DSN", + "CONNECTION_STRING", +]; #[derive(Debug, Serialize)] pub struct DoctorReport { @@ -80,7 +92,8 @@ fn check_env_var(name: &str, metadata: &crate::types::Metadata) -> DoctorCheck { let display = if is_secret_name(name) { "set (*****)".to_string() } else if val.len() > 30 { - format!("set ({}...)", &val[..27]) + let truncated: String = val.chars().take(27).collect(); + format!("set ({truncated}...)") } else { format!("set ({val})") }; diff --git a/src/generate/compose.rs b/src/generate/compose.rs index 985e154..307cb1a 100644 --- a/src/generate/compose.rs +++ b/src/generate/compose.rs @@ -125,7 +125,7 @@ pub fn generate_compose(deps: &[ShadowDep]) -> String { let mut out = String::new(); writeln!(out, "# Generated by looper — review before use\n").unwrap(); - // serde_yaml adds unwanted formatting; hand-build for clean output + // serde_yml adds unwanted formatting; hand-build for clean output writeln!(out, "services:").unwrap(); // App service @@ -167,7 +167,11 @@ pub fn generate_compose(deps: &[ShadowDep]) -> String { if !svc.env_vars.is_empty() { writeln!(out, " environment:").unwrap(); for (key, val) in &svc.env_vars { - writeln!(out, " {key}: {val}").unwrap(); + if is_sensitive_key(key) { + writeln!(out, " {key}: {val} # CHANGE ME").unwrap(); + } else { + writeln!(out, " {key}: {val}").unwrap(); + } } } } @@ -196,3 +200,10 @@ fn lookup_service(protocol_hint: &str) -> Option { fn extract_port(name: &str) -> Option { name.strip_prefix(':')?.parse().ok() } + +fn is_sensitive_key(key: &str) -> bool { + let upper = key.to_uppercase(); + ["PASSWORD", "SECRET", "KEY", "TOKEN", "CREDENTIAL"] + .iter() + .any(|kw| upper.contains(kw)) +} diff --git a/src/infra/compose.rs b/src/infra/compose.rs index 7914611..bc8185c 100644 --- a/src/infra/compose.rs +++ b/src/infra/compose.rs @@ -31,7 +31,7 @@ pub fn parse_compose_file(path: &Path) -> Result { let content = std::fs::read_to_string(path) .map_err(|e| format!("could not read {}: {e}", path.display()))?; - let raw: RawCompose = serde_yaml::from_str(&content) + let raw: RawCompose = serde_yml::from_str(&content) .map_err(|e| format!("invalid YAML in {}: {e}", path.display()))?; let source_file = path.to_string_lossy().into_owned(); @@ -103,7 +103,7 @@ enum RawEnvFile { #[derive(Deserialize)] #[serde(untagged)] enum RawEnvironment { - Map(HashMap), + Map(HashMap), List(Vec), } diff --git a/src/output/html.rs b/src/output/html.rs index 9bf3c66..5c36b0b 100644 --- a/src/output/html.rs +++ b/src/output/html.rs @@ -86,16 +86,18 @@ function render() { tbody.innerHTML = rows.map(d => { const loc = d.locations[0]||{}; return ` - ${esc(d.name)} - ${d.category} - ${d.confidence} - ${esc(loc.file||'')} + ${esc(d.name)} + ${esc(d.category)} + ${esc(d.confidence)} + ${esc(loc.file||'')} ${loc.line||''} `; }).join(''); } function esc(s) { const d=document.createElement('div'); d.textContent=s; return d.innerHTML; } +function escAttr(s) { return s.replace(/&/g,'&').replace(/"/g,'"').replace(//g,'>'); } function copy(t) { navigator.clipboard.writeText(t); const toast=document.getElementById('toast'); toast.classList.add('show'); setTimeout(()=>toast.classList.remove('show'),1500); } +document.getElementById('tbody').addEventListener('click', e => { const td=e.target.closest('[data-copy]'); if(td) copy(td.dataset.copy); }); document.querySelectorAll('th').forEach(th => th.addEventListener('click', () => { const c=th.dataset.col; if(sortCol===c) sortDir*=-1; else { sortCol=c; sortDir=1; } render(); })); filterCat.addEventListener('change', render); document.getElementById('filterConf').addEventListener('change', render); From 770073a79b4018d8311f883dbc90c127e8d89602 Mon Sep 17 00:00:00 2001 From: Danite Date: Mon, 6 Apr 2026 19:20:55 +0200 Subject: [PATCH 2/4] fix deny config --- deny.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/deny.toml b/deny.toml index 95d318b..2855644 100644 --- a/deny.toml +++ b/deny.toml @@ -1,7 +1,6 @@ [advisories] -vulnerability = "deny" -unmaintained = "warn" -yanked = "warn" +unmaintained = "workspace" +yanked = "workspace" [licenses] allow = [ From 00e583fbba83291b57271a843108faec8c09dc03 Mon Sep 17 00:00:00 2001 From: Danite Date: Mon, 6 Apr 2026 19:23:18 +0200 Subject: [PATCH 3/4] fix deny config --- deny.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deny.toml b/deny.toml index 2855644..f2a5169 100644 --- a/deny.toml +++ b/deny.toml @@ -1,6 +1,6 @@ [advisories] unmaintained = "workspace" -yanked = "workspace" +yanked = "warn" [licenses] allow = [ From ba665f04c8970ff51299adffb70e9e50871d405d Mon Sep 17 00:00:00 2001 From: Danite Date: Mon, 6 Apr 2026 19:26:14 +0200 Subject: [PATCH 4/4] fix unmaintaned dep --- Cargo.lock | 28 +++++++++++----------------- Cargo.toml | 2 +- deny.toml | 5 ----- src/generate/compose.rs | 2 +- src/infra/compose.rs | 4 ++-- 5 files changed, 15 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5af828..9863e9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -651,16 +651,6 @@ version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" -[[package]] -name = "libyml" -version = "0.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" -dependencies = [ - "anyhow", - "version_check", -] - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -697,7 +687,7 @@ dependencies = [ "regex", "serde", "serde_json", - "serde_yml", + "serde_yaml_ng", "tempfile", "url", "which", @@ -1254,18 +1244,16 @@ dependencies = [ ] [[package]] -name = "serde_yml" -version = "0.0.12" +name = "serde_yaml_ng" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" +checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" dependencies = [ "indexmap", "itoa", - "libyml", - "memchr", "ryu", "serde", - "version_check", + "unsafe-libyaml", ] [[package]] @@ -1451,6 +1439,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "url" version = "2.5.8" diff --git a/Cargo.toml b/Cargo.toml index 1a5e455..787a8fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ colored = "2" rayon = "1.10" url = "2" regex = "1" -serde_yml = "0.0.12" +serde_yaml_ng = "0.10" which = "8" xxhash-rust = { version = "0.8", features = ["xxh3"] } diff --git a/deny.toml b/deny.toml index f2a5169..320e9ad 100644 --- a/deny.toml +++ b/deny.toml @@ -7,14 +7,9 @@ allow = [ "MIT", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", - "BSD-2-Clause", - "BSD-3-Clause", "BSL-1.0", - "ISC", "MPL-2.0", "Unicode-3.0", - "Unicode-DFS-2016", "Unlicense", - "Zlib", ] confidence-threshold = 0.8 diff --git a/src/generate/compose.rs b/src/generate/compose.rs index 307cb1a..a92d692 100644 --- a/src/generate/compose.rs +++ b/src/generate/compose.rs @@ -125,7 +125,7 @@ pub fn generate_compose(deps: &[ShadowDep]) -> String { let mut out = String::new(); writeln!(out, "# Generated by looper — review before use\n").unwrap(); - // serde_yml adds unwanted formatting; hand-build for clean output + // serde_yaml_ng adds unwanted formatting; hand-build for clean output writeln!(out, "services:").unwrap(); // App service diff --git a/src/infra/compose.rs b/src/infra/compose.rs index bc8185c..1e797c6 100644 --- a/src/infra/compose.rs +++ b/src/infra/compose.rs @@ -31,7 +31,7 @@ pub fn parse_compose_file(path: &Path) -> Result { let content = std::fs::read_to_string(path) .map_err(|e| format!("could not read {}: {e}", path.display()))?; - let raw: RawCompose = serde_yml::from_str(&content) + let raw: RawCompose = serde_yaml_ng::from_str(&content) .map_err(|e| format!("invalid YAML in {}: {e}", path.display()))?; let source_file = path.to_string_lossy().into_owned(); @@ -103,7 +103,7 @@ enum RawEnvFile { #[derive(Deserialize)] #[serde(untagged)] enum RawEnvironment { - Map(HashMap), + Map(HashMap), List(Vec), }