Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion crates/aegis-core/src/ast/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ pub struct Import {
pub module: String,
/// 1-indexed line where the import appears.
pub line: usize,
/// Local alias under which the module is referred to in code, if
/// the language supports renaming on import. Today only populated
/// for Go's `import myrand "math/rand"` form (the import_spec
/// node's `name` field). Other languages' aliasing — Python
/// `import X as Y`, TS `import * as Y from 'X'`, Rust `use X as Y`
/// — return `None` here for now; can be added when a rule
/// concretely needs them.
pub alias: Option<String>,
}

/// Run the language adapter's `import_query` over the parse tree and
Expand Down Expand Up @@ -57,7 +65,23 @@ pub fn extract_imports(parsed: &ParsedFile<'_>) -> Vec<Import> {
continue;
}
seen.push(key);
out.push(Import { module, line });
// Best-effort alias detection. Today only Go's
// `import myrand "math/rand"` populates a `name` field on
// the import_spec parent. Other languages parse to
// different shapes; guard with a node-kind check.
let alias = cap
.node
.parent()
.filter(|p| p.kind() == "import_spec")
.and_then(|spec| spec.child_by_field_name("name"))
.and_then(|n| n.utf8_text(src).ok())
.map(|s| s.to_string())
.filter(|s| s != "_" && s != ".");
out.push(Import {
module,
line,
alias,
});
}
}
out
Expand Down Expand Up @@ -119,4 +143,38 @@ mod tests {
let os_import = imports.iter().find(|i| i.module == "os").unwrap();
assert_eq!(os_import.line, 2);
}

#[test]
fn go_aliased_import_captures_alias() {
let src = "package main\n\nimport myrand \"math/rand\"\n";
let pf = parse("main.go", src).unwrap();
let imps = extract_imports(&pf);
let mr = imps.iter().find(|i| i.module == "math/rand").unwrap();
assert_eq!(mr.alias.as_deref(), Some("myrand"));
}

#[test]
fn go_unaliased_import_has_no_alias() {
let src = "package main\n\nimport \"math/rand\"\n";
let pf = parse("main.go", src).unwrap();
let imps = extract_imports(&pf);
let mr = imps.iter().find(|i| i.module == "math/rand").unwrap();
assert_eq!(mr.alias, None);
}

#[test]
fn go_blank_and_dot_imports_have_no_alias() {
// `import _ "side/effect"` and `import . "x"` — `_` and `.`
// are not legitimate receivers; alias detection filters them
// out so resolve_receiver doesn't try to match them.
let src = "package main\n\nimport (\n\
\t_ \"side/effect\"\n\
\t. \"x\"\n\
)\n";
let pf = parse("main.go", src).unwrap();
let imps = extract_imports(&pf);
for imp in &imps {
assert!(imp.alias.is_none(), "blank/dot import got alias: {imp:?}");
}
}
}
47 changes: 47 additions & 0 deletions crates/aegis-core/src/ast/parsed_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,23 @@ impl<'src> ParsedFile<'src> {
if receiver.is_empty() {
return None;
}
// Pass 1: explicit alias match (Go `import myrand "math/rand"`).
// Aliases override last-segment resolution — `myrand.Intn` must
// never get attributed to the wrong module just because some
// other import happens to end in "myrand".
for imp in self.imports() {
if imp.alias.as_deref() == Some(receiver) {
return Some(imp);
}
}
// Pass 2: receiver matches the module path or its last segment.
// Aliased imports skipped here — if the user wrote
// `import myrand "math/rand"`, they don't refer to it as
// `rand`, only as `myrand`.
for imp in self.imports() {
if imp.alias.is_some() {
continue;
}
if imp.module == receiver {
return Some(imp);
}
Expand Down Expand Up @@ -251,4 +267,35 @@ mod tests {
let pf = parse("foo.py", "import os\n").unwrap();
assert!(pf.resolve_receiver("").is_none());
}

#[test]
fn resolve_receiver_prefers_alias_over_last_segment() {
// Go renamed import: `myrand` should resolve to math/rand
// (alias match), not to nothing.
let src = "package main\n\nimport myrand \"math/rand\"\n\nfunc f() { myrand.Intn(10) }\n";
let pf = parse("main.go", src).unwrap();
let resolved = pf.resolve_receiver("myrand").expect("alias resolves");
assert_eq!(resolved.module, "math/rand");
assert_eq!(resolved.alias.as_deref(), Some("myrand"));
// The plain `rand` last-segment must NOT resolve, because the
// import was aliased — code under that file refers to it as
// `myrand`, not `rand`.
assert!(pf.resolve_receiver("rand").is_none());
}

#[test]
fn resolve_receiver_aliased_and_unaliased_coexist() {
// Mix in the same file: aliased crypto/rand + unaliased
// math/rand. Aliased name `crand` resolves only to crypto/rand;
// bare `rand` resolves to math/rand.
let src = "package main\n\nimport (\n\
\tcrand \"crypto/rand\"\n\
\t\"math/rand\"\n\
)\n";
let pf = parse("main.go", src).unwrap();
let crand = pf.resolve_receiver("crand").expect("alias resolves");
assert_eq!(crand.module, "crypto/rand");
let rand = pf.resolve_receiver("rand").expect("last-segment resolves");
assert_eq!(rand.module, "math/rand");
}
}
33 changes: 33 additions & 0 deletions crates/aegis-core/src/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2011,6 +2011,39 @@ mod tests {
assert!(v.iter().any(|v| v.rule_id == "SEC010"), "got {v:?}");
}

#[test]
fn sec010_go_aliased_math_rand_blocks() {
// `import myrand "math/rand"` renames math/rand → myrand.
// SEC010's Go matcher resolves the receiver via Layer 1
// imports, so myrand.Intn must still be flagged.
let v = check(
".go",
"package main\n\n\
import myrand \"math/rand\"\n\n\
func makeToken() int {\n \
token := myrand.Intn(1000000)\n \
return token\n\
}\n",
);
assert!(v.iter().any(|v| v.rule_id == "SEC010"), "got {v:?}");
}

#[test]
fn sec010_go_aliased_crypto_rand_does_not_block() {
// `import crand "crypto/rand"` — crand is the safe one.
let v = check(
".go",
"package main\n\n\
import crand \"crypto/rand\"\n\n\
func makeToken() []byte {\n \
token := make([]byte, 16)\n \
_, _ = crand.Read(token)\n \
return token\n\
}\n",
);
assert!(!v.iter().any(|v| v.rule_id == "SEC010"), "got {v:?}");
}

#[test]
fn sec010_go_crypto_rand_read_does_not_block() {
// Same `rand.Read` literal text, but `import "crypto/rand"`.
Expand Down
Loading