fix(extensions): restore pre-bundle AST security scan [DEVEX-710] - #208
fix(extensions): restore pre-bundle AST security scan [DEVEX-710]#208qcai-godaddy wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR restores and wires a pre-bundle AST-based security scan for extensions (SEC001–SEC012 plus SEC011 package scripts) so deploy can block unsafe extensions before bundling, while retaining the existing post-bundle regex scan (SEC101–SEC115).
Changes:
- Adds a pre-bundle scan pipeline (
scan_extension) with oxc-based AST rules and a deploy-time block gate before esbuild. - Introduces supporting modules for AST scanning (alias mapping, DOM-escape rule SEC012, file discovery, config/util helpers) and expands findings to include column info.
- Updates deploy output/error formatting for blocked findings and updates docs/dependencies to support the new scanner.
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| rust/src/extension/types.rs | Extends Finding with message: String and col for AST-location reporting. |
| rust/src/extension/security/util.rs | Adds shared helpers (URL detection, first string arg, line/col + snippet extraction). |
| rust/src/extension/security/types.rs | Adds shared scan types (AliasMaps, ScanReport, summaries). |
| rust/src/extension/security/source.rs | Implements scan_extension orchestration + unit tests for key rules/flows. |
| rust/src/extension/security/scripts_scanner.rs | Adds SEC011 package.json lifecycle script scanning (warn-only). |
| rust/src/extension/security/mod.rs | Wires new modules and re-exports scan_extension; updates bundle findings to use String + col. |
| rust/src/extension/security/file_discovery.rs | Adds recursive source discovery with exclude globs. |
| rust/src/extension/security/engine.rs | Implements AST rule engine for SEC001–SEC010 and hooks SEC012 DOM-escape scan. |
| rust/src/extension/security/dom_escape.rs | Implements SEC012 DOM escape detection with scope-aware shadowing logic. |
| rust/src/extension/security/config.rs | Adds immutable security config (trusted domains + exclude patterns). |
| rust/src/extension/security/alias_builder.rs | Builds module alias maps from imports/requires to detect aliased sensitive APIs. |
| rust/src/extension/mod.rs | Exposes scan_extension from the extension module surface. |
| rust/src/application/commands/deploy/mod.rs | Updates deploy command help text to describe new pre-bundle scanning. |
| rust/src/application/commands/deploy/extensions.rs | Runs scan.prebundle before bundling; standardizes blocked finding formatting. |
| rust/Cargo.toml | Adds dependencies required for AST scanning and file discovery (oxc_*, globset). |
| rust/Cargo.lock | Locks new dependency graph for the added crates. |
| AGENTS.md | Documents the restored pre-bundle scanner and updated deploy scan order. |
Suppressed comments (1)
rust/src/extension/security/file_discovery.rs:39
- Iterating with
entries.flatten()(andfile_type’sErr(_) => continue) silently drops per-entry IO errors. That can skip files during the scan without surfacing an error, undermining the “fail on discovery/read errors” behavior.
for entry in entries.flatten() {
let path = entry.path();
let path_str = path.to_string_lossy();
if should_exclude(&path_str, excludes) {
continue;
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.
Suppressed comments (2)
rust/src/extension/security/source.rs:39
Findingnow includes a column, but the final output ordering inscan_extensiononly sorts by file+line. When multiple findings share the same line, the order can be unstable/non-deterministic; includecolin the sort key for consistent output.
findings.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
rust/src/application/commands/deploy/extensions.rs:96
format_security_findingsalways formats locations asfile:linewhencol == 0. SEC011 findings useline = 0/col = 0, so the user-facing message will includepackage.json:0, which is confusing. Consider omitting the line/col entirely whenline == 0.
let loc = if f.col > 0 {
format!("{}:{}:{}", f.file, f.line, f.col)
} else {
format!("{}:{}", f.file, f.line)
};
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated no new comments.
Suppressed comments (3)
rust/src/extension/security/util.rs:39
snippet_atslicessource[start..end]directly. BecauseSpanoffsets are byte indices, this can panic if the span is not on UTF-8 char boundaries (or if a span is malformed with start > end). A panic here would crash the security scan instead of failing closed / returning an error.
pub fn snippet_at(source: &str, span: Span) -> String {
let start = (span.start as usize).min(source.len());
let end = (span.end as usize).min(source.len());
let slice = &source[start..end];
let line = slice.lines().next().unwrap_or(slice);
rust/src/extension/security/source.rs:42
- Findings are sorted only by file + line. With the new
colfield (and multiple matches possible on the same line),Vec::sort_bycan produce nondeterministic ordering for equal keys, which makes output and tests/flaky debugging harder. Includecol(and optionallyrule_id) in the sort key for deterministic ordering.
findings.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
rust/src/application/commands/deploy/extensions.rs:96
Finding.line(andcol) are used as sentinels for some scanners (line: 0in SEC011,col: 0for regex matches). Formatting these asfile:0reads like a real source location and is confusing. Consider omitting the line/col portion whenline == 0.
let loc = if f.col > 0 {
format!("{}:{}:{}", f.file, f.line, f.col)
} else {
format!("{}:{}", f.file, f.line)
};
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
AGENTS.md:83
- The docs claim
scan_extension(dir) -> Result<ScanReport>, but the implementation returnsResult<ScanReport, String>(and callers handle aStringerror). This mismatch makes the API contract unclear for contributors.
- `scan_extension(dir) -> Result<ScanReport>`, `scan_bundle(content, path) -> Vec<Finding>`,
`is_blocked(findings) -> bool`.
rust/src/extension/security/source.rs:40
scan_extensionrecords each finding’sfileusing the full filesystem path (path.display()), while the post-bundle scanner uses the manifest-relativesourcestring. This makes error output noisier and less stable across machines; consider storing paths relative topackage_dirfor pre-bundle findings.
let source = std::fs::read_to_string(path)
.map_err(|e| format!("failed to read '{}': {e}", path.display()))?;
let path_str = path.display().to_string();
findings.extend(scan_source_file(&path_str, &source)?);
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (2)
rust/src/extension/security/util.rs:8
matches_http_urlis case-sensitive, so strings likeHTTP://example.comwon't trigger SEC008 even though they are valid URLs. Since this is part of a security scanner, it should be robust against scheme casing.
pub fn matches_http_url(s: &str) -> bool {
s.contains("http://") || s.contains("https://")
}
rust/src/extension/security/util.rs:27
offset_to_line_colreturns a byte-based column (computed from byte offsets), but theFinding.coldocs say it’s a 1-based column. For UTF-8 source this can misreport columns after multibyte characters; compute the column as a character count from the start of the line instead.
pub fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
let offset = source.floor_char_boundary(offset.min(source.len()));
let before = &source[..offset];
let line = before.bytes().filter(|&b| b == b'\n').count() + 1;
let col = match before.rfind('\n') {
|
@wcole1-godaddy is going to test this. |
Summary
extension/security/, withscan_extensionreturningResult<ScanReport, ScanError>so discovery/read/parse/symlink failures fail closed.scan.prebundleinto deploy before esbuild; block on severity Block, then keep the existing post-bundle regex scan (SEC101–SEC115).mount({ container })still catching container escapes (closest, ownerDocument, etc.) without treating container as a free-variable shadow.Test plan
cargo build/cargo clippy -- -D warnings/cargo fmt --check/cargo test(incl. 132extension::securitytests)clean/shadow-ok→ pass (0 findings)eval-bad→ blocked (SEC001)dom-bad→ blocked (SEC012, 3 findings)gddy platform app deployon a real app (needs experimental stage + auth + esbuild): confirm pre-bundle blocks bad packages and clean packages proceed past scan to upload