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..9863e9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -687,7 +687,7 @@ dependencies = [ "regex", "serde", "serde_json", - "serde_yaml", + "serde_yaml_ng", "tempfile", "url", "which", @@ -1244,10 +1244,10 @@ dependencies = [ ] [[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" +name = "serde_yaml_ng" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" dependencies = [ "indexmap", "itoa", diff --git a/Cargo.toml b/Cargo.toml index c229ebf..787a8fb 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_yaml_ng = "0.10" which = "8" xxhash-rust = { version = "0.8", features = ["xxh3"] } diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..320e9ad --- /dev/null +++ b/deny.toml @@ -0,0 +1,15 @@ +[advisories] +unmaintained = "workspace" +yanked = "warn" + +[licenses] +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSL-1.0", + "MPL-2.0", + "Unicode-3.0", + "Unlicense", +] +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..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_yaml 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 @@ -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..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_yaml::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), } 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);