Skip to content
Merged
3 changes: 3 additions & 0 deletions src/report/sarif.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ pub fn build_sarif(findings: &[Finding]) -> serde_json::Value {
if let Some(deadline) = &f.cnsa2_deadline {
props.insert("cnsa2Deadline".to_string(), json!(deadline));
}
if let Some(dep) = &f.dep_name {
props.insert("depName".to_string(), json!(dep));
}

// SARIF `rank` is a native 0.0..=100.0 ordering hint. Map
// confidence linearly so 1.0 → 100 and 0.0 → 0.
Expand Down
39 changes: 39 additions & 0 deletions src/rules/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ pub fn shannon_entropy(s: &str) -> f32 {
entropy
}

/// Base regex pattern shared by all `*/no-hardcoded-secret` rules.
///
/// Each language rule file should use this constant (or
/// [`CSHARP_HARDCODED_SECRET_PATTERN`] for C#) instead of inlining its
/// own copy of the pattern.
pub const HARDCODED_SECRET_PATTERN: &str =
r"(?i)(password|secret|api_?key|token|auth|credential|private_?key)";

/// Extended variant for C# that adds `connection_?string` /
/// `connectionstring` to the base keyword set.
pub const CSHARP_HARDCODED_SECRET_PATTERN: &str = r"(?i)(password|secret|api_?key|token|auth|credential|private_?key|connection_?string|connectionstring)";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant connectionstring alternative

connection_?string (with _? making the underscore optional) already matches connectionstring, so the explicit connectionstring alternative at the end is redundant. This was present in the original csharp.rs inline pattern too, but now that it's being lifted into a shared constant it's worth cleaning up.

Suggested change
pub const CSHARP_HARDCODED_SECRET_PATTERN: &str = r"(?i)(password|secret|api_?key|token|auth|credential|private_?key|connection_?string|connectionstring)";
pub const CSHARP_HARDCODED_SECRET_PATTERN: &str = r"(?i)(password|secret|api_?key|token|auth|credential|private_?key|connection_?string)";


/// Default minimum length for strings flagged as a hardcoded secret.
///
/// Historically this was hardcoded as `>= 4` across every language-specific
Expand Down Expand Up @@ -338,4 +350,31 @@ mod tests {
assert_eq!(confidence_for_hops(3), 0.6);
assert_eq!(confidence_for_hops(10), 0.6);
}

#[test]
fn csharp_pattern_is_superset_of_base() {
let base = regex::Regex::new(HARDCODED_SECRET_PATTERN).unwrap();
let extended = regex::Regex::new(CSHARP_HARDCODED_SECRET_PATTERN).unwrap();

// Every keyword the base pattern matches must also match the C# pattern.
for kw in &[
"password",
"secret",
"api_key",
"apikey",
"token",
"auth",
"credential",
"private_key",
"privatekey",
] {
assert!(base.is_match(kw), "base should match {kw}");
assert!(extended.is_match(kw), "csharp should match {kw}");
}
// C#-specific extras
for kw in &["connection_string", "connectionstring"] {
assert!(!base.is_match(kw), "base should NOT match {kw}");
assert!(extended.is_match(kw), "csharp should match {kw}");
}
}
}
9 changes: 4 additions & 5 deletions src/rules/csharp.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::impl_rule;
use crate::rules::common::{is_secret_value_long_enough, make_finding, walk_tree};
use crate::rules::common::{
is_secret_value_long_enough, make_finding, walk_tree, CSHARP_HARDCODED_SECRET_PATTERN,
};
use crate::{Language, Severity};
use regex::Regex;

Expand Down Expand Up @@ -443,10 +445,7 @@ impl_rule! {
fn check(_self, source, tree) {

let mut findings = Vec::new();
let secret_pattern = Regex::new(
r"(?i)(password|secret|api_?key|apikey|token|credential|private_?key|connection_?string|connectionstring)",
)
.unwrap();
let secret_pattern = Regex::new(CSHARP_HARDCODED_SECRET_PATTERN).unwrap();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 C# rule silently gains auth keyword

The old inline C# pattern was password|secret|api_?key|apikey|token|credential|private_?key|connection_?string|connectionstringauth was intentionally absent. CSHARP_HARDCODED_SECRET_PATTERN inherits auth from the shared base, so this rule will now fire on C# variables like authToken, authKey, authHeader, etc. that it never flagged before. The PR description only mentions PHP gaining keywords; this C# expansion is undocumented. If it's intentional (makes sense given the normalisation goal), a brief note in the description or a changelog entry would help reviewers and users understand the wider diff in findings.


walk_tree(tree.root_node(), source, &mut |node, src| {
// variable_declarator: string password = "hardcoded"
Expand Down
4 changes: 2 additions & 2 deletions src/rules/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::impl_rule;
use crate::rules::common::AliasTable;
use crate::rules::common::{
get_source_line, is_secret_value_long_enough, make_finding, make_finding_from_offsets,
walk_tree,
walk_tree, HARDCODED_SECRET_PATTERN,
};
use crate::rules::go_taint::{
self, go_aliases_from_tree, go_taint_sources, NodeMatcher as GoNodeMatcher,
Expand Down Expand Up @@ -149,7 +149,7 @@ impl_rule! {

let mut findings = Vec::new();
let secret_pattern =
Regex::new(r"(?i)(password|secret|api_?key|token|auth|credential|private_?key)")
Regex::new(HARDCODED_SECRET_PATTERN)
.unwrap();

walk_tree(tree.root_node(), source, &mut |node, src| {
Expand Down
3 changes: 2 additions & 1 deletion src/rules/java.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::impl_rule;
use crate::rules::common::{
is_secret_value_long_enough, make_finding, make_finding_from_offsets, walk_tree,
HARDCODED_SECRET_PATTERN,
};
use crate::{Language, Severity};
use regex::Regex;
Expand Down Expand Up @@ -604,7 +605,7 @@ impl_rule! {

let mut findings = Vec::new();
let secret_pattern =
Regex::new(r"(?i)(password|secret|api_?key|apiKey|token|auth|credential|private_?key)")
Regex::new(HARDCODED_SECRET_PATTERN)
.unwrap();

walk_tree(tree.root_node(), source, &mut |node, src| {
Expand Down
7 changes: 5 additions & 2 deletions src/rules/javascript.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use crate::impl_rule;
use crate::rules::common::{get_source_line, is_secret_value_long_enough, make_finding, walk_tree};
use crate::rules::common::{
get_source_line, is_secret_value_long_enough, make_finding, walk_tree,
HARDCODED_SECRET_PATTERN,
};
use crate::rules::FileContext;
use crate::{Finding, Language, Severity};
use regex::Regex;
Expand Down Expand Up @@ -56,7 +59,7 @@ impl_rule! {

let mut findings = Vec::new();
let secret_pattern =
Regex::new(r"(?i)(password|secret|api_?key|token|auth|credential|private_?key)")
Regex::new(HARDCODED_SECRET_PATTERN)
.unwrap();

walk_tree(tree.root_node(), source, &mut |node, src| {
Expand Down
3 changes: 2 additions & 1 deletion src/rules/kotlin.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::impl_rule;
use crate::rules::common::{
is_secret_value_long_enough, make_finding, make_finding_from_offsets, walk_tree,
HARDCODED_SECRET_PATTERN,
};
use crate::{Finding, Language, Severity};
use regex::Regex;
Expand Down Expand Up @@ -521,7 +522,7 @@ impl_rule! {

let mut findings = Vec::new();
let secret_pattern =
Regex::new(r"(?i)(password|secret|api_?key|apiKey|token|auth|credential|private_?key)")
Regex::new(HARDCODED_SECRET_PATTERN)
.unwrap();

walk_tree(tree.root_node(), source, &mut |node, src| {
Expand Down
110 changes: 105 additions & 5 deletions src/rules/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct SeedEntry {

const MANIFEST_PQ_CWE: &str = "CWE-327";
const MANIFEST_PQ_DESC: &str = "Dependency uses quantum-vulnerable cryptographic algorithm";
Comment on lines 15 to 17
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unrelated changes bundled into a regex-dedup PR

The PR title and description are scoped to deduplicating HARDCODED_SECRET_PATTERN, but manifest.rs carries several orthogonal changes: new k256/secp256k1/ed448-goldilocks/pyjwt/etc. seed entries, a BFS deduplication fix (VecHashMap), a fabric algorithm reclassification, a new CARGO_PQ_DESC constant, and SARIF depName emission in sarif.rs. Bundling these makes the diff harder to bisect if a regression appears in the manifest or SARIF output. Consider splitting into a separate PR (or at minimum updating the summary to describe all changed behaviour).

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

const CARGO_PQ_DESC: &str =
"Dependency uses quantum-vulnerable cryptographic algorithm (dev-dependencies not distinguished)";
const MANIFEST_PQ_DEADLINE: &str = "2033";

/// Apply shared PQ fields to a manifest finding.
Expand Down Expand Up @@ -61,6 +63,26 @@ const CARGO_SEEDS: &[SeedEntry] = &[
crypto_algorithm: Some("ECDSA"),
confidence: 0.9,
},
SeedEntry {
name: "k256",
crypto_algorithm: Some("ECDSA"),
confidence: 0.9,
},
SeedEntry {
name: "secp256k1",
crypto_algorithm: Some("ECDSA"),
confidence: 0.9,
},
SeedEntry {
name: "libsecp256k1",
crypto_algorithm: Some("ECDSA"),
confidence: 0.9,
},
SeedEntry {
name: "ed448-goldilocks",
crypto_algorithm: Some("Ed448"),
confidence: 0.9,
},
// Tier 2 — confidence 0.6, mixed algorithms
SeedEntry {
name: "ring",
Expand All @@ -72,6 +94,11 @@ const CARGO_SEEDS: &[SeedEntry] = &[
crypto_algorithm: None,
confidence: 0.6,
},
SeedEntry {
name: "openssl",
crypto_algorithm: None,
confidence: 0.6,
},
SeedEntry {
name: "aws-lc-rs",
crypto_algorithm: None,
Expand Down Expand Up @@ -112,11 +139,36 @@ const PIP_PACKAGES: &[SeedEntry] = &[
crypto_algorithm: Some("RSA"),
confidence: 0.8,
},
SeedEntry {
name: "pyjwt",
crypto_algorithm: None,
confidence: 0.8,
},
SeedEntry {
name: "authlib",
crypto_algorithm: None,
confidence: 0.8,
},
SeedEntry {
name: "python-jose",
crypto_algorithm: None,
confidence: 0.8,
},
SeedEntry {
name: "jwcrypto",
crypto_algorithm: None,
confidence: 0.8,
},
SeedEntry {
name: "fabric",
crypto_algorithm: Some("RSA"),
crypto_algorithm: None,
confidence: 0.7,
},
SeedEntry {
name: "m2crypto",
crypto_algorithm: None,
confidence: 0.6,
},
SeedEntry {
name: "cryptography",
crypto_algorithm: None,
Expand Down Expand Up @@ -154,7 +206,7 @@ impl Rule for CargoLockPqCrypto {
Some(MANIFEST_PQ_CWE)
}
fn description(&self) -> &str {
MANIFEST_PQ_DESC
CARGO_PQ_DESC
}
fn language(&self) -> Language {
Language::Manifest
Expand Down Expand Up @@ -241,7 +293,7 @@ impl Rule for CargoLockPqCrypto {
visited.insert(i);
queue.push_back(i);

let mut reached_seeds: Vec<&SeedEntry> = Vec::new();
let mut reached_seeds: HashMap<&str, &SeedEntry> = HashMap::new();

while let Some(node) = queue.pop_front() {
for &neighbor in &graph[node] {
Expand All @@ -255,7 +307,14 @@ impl Rule for CargoLockPqCrypto {
continue;
};
if let Some(entry) = seed_map.get(neighbor_name) {
reached_seeds.push(entry);
reached_seeds
.entry(entry.name)
.and_modify(|existing| {
if entry.confidence > existing.confidence {
*existing = entry;
}
})
.or_insert(entry);
} else {
queue.push_back(neighbor);
}
Expand All @@ -268,7 +327,7 @@ impl Rule for CargoLockPqCrypto {

// Pick the highest-confidence seed
let best = reached_seeds
.iter()
.values()
.max_by(|a, b| a.confidence.total_cmp(&b.confidence))
.unwrap();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

Expand Down Expand Up @@ -818,4 +877,45 @@ version = \"0.17.0\"\n";
let tree = dummy_tree("");
assert!(RequirementsTxtPqCrypto.check("", &tree).is_empty());
}

#[test]
fn cargo_k256_seed_flagged() {
let src = "\
[[package]]\n\
name = \"wallet\"\n\
version = \"0.1.0\"\n\
dependencies = [\"k256\"]\n\
\n\
[[package]]\n\
name = \"k256\"\n\
version = \"0.13.0\"\n";
let tree = dummy_tree(src);
let findings = CargoLockPqCrypto.check(src, &tree);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].dep_name.as_deref(), Some("wallet"));
assert_eq!(findings[0].crypto_algorithm.as_deref(), Some("ECDSA"));
}

#[test]
fn pip_jwt_lib_flagged() {
let src = "pyjwt>=2.0\n";
let tree = dummy_tree(src);
let findings = RequirementsTxtPqCrypto.check(src, &tree);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].dep_name.as_deref(), Some("pyjwt"));
assert!(findings[0].crypto_algorithm.is_none());
assert_eq!(findings[0].confidence, 0.8);
}

#[test]
fn pip_fabric_no_algorithm() {
let src = "fabric>=3.0\n";
let tree = dummy_tree(src);
let findings = RequirementsTxtPqCrypto.check(src, &tree);
assert_eq!(findings.len(), 1);
assert!(
findings[0].crypto_algorithm.is_none(),
"fabric should not attribute RSA"
);
}
}
6 changes: 4 additions & 2 deletions src/rules/php.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::impl_rule;
use crate::rules::common::{is_secret_value_long_enough, make_finding, walk_tree};
use crate::rules::common::{
is_secret_value_long_enough, make_finding, walk_tree, HARDCODED_SECRET_PATTERN,
};
use crate::{Language, Severity};
use regex::Regex;

Expand Down Expand Up @@ -359,7 +361,7 @@ impl_rule! {
fn check(_self, source, tree) {

let mut findings = Vec::new();
let secret_pattern = Regex::new(r"(?i)(password|secret|api_?key|token)").unwrap();
let secret_pattern = Regex::new(HARDCODED_SECRET_PATTERN).unwrap();

walk_tree(tree.root_node(), source, &mut |node, src| {
// Detect: $password = "hardcoded";
Expand Down
7 changes: 5 additions & 2 deletions src/rules/python.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use crate::impl_rule;
use crate::rules::common::{get_source_line, is_secret_value_long_enough, make_finding, walk_tree};
use crate::rules::common::{
get_source_line, is_secret_value_long_enough, make_finding, walk_tree,
HARDCODED_SECRET_PATTERN,
};
use crate::rules::FileContext;
use crate::{Finding, Language, Severity};
use regex::Regex;
Expand Down Expand Up @@ -75,7 +78,7 @@ impl_rule! {

let mut findings = Vec::new();
let secret_pattern =
Regex::new(r"(?i)(password|secret|api_?key|token|auth|credential|private_?key)")
Regex::new(HARDCODED_SECRET_PATTERN)
.unwrap();

walk_tree(tree.root_node(), source, &mut |node, src| {
Expand Down
6 changes: 4 additions & 2 deletions src/rules/ruby.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::impl_rule;
use crate::rules::common::{is_secret_value_long_enough, make_finding, walk_tree};
use crate::rules::common::{
is_secret_value_long_enough, make_finding, walk_tree, HARDCODED_SECRET_PATTERN,
};
use crate::{Language, Severity};
use regex::Regex;

Expand Down Expand Up @@ -484,7 +486,7 @@ impl_rule! {

let mut findings = Vec::new();
let secret_pattern =
Regex::new(r"(?i)(password|secret|api_?key|token|auth|credential|private_?key)")
Regex::new(HARDCODED_SECRET_PATTERN)
.unwrap();

walk_tree(tree.root_node(), source, &mut |node, src| {
Expand Down
Loading
Loading