Skip to content

Commit 67407d2

Browse files
committed
feat(bump): working on next release
1 parent 3ead639 commit 67407d2

11 files changed

Lines changed: 599 additions & 36 deletions

File tree

‎src/report/html/renderer.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,8 @@ pub fn generate_html_report(
355355
.finding.medium {{ border-left-color: var(--medium); background: #fffbf0; }}
356356
.finding.low {{ border-left-color: var(--low); background: #f0fff4; }}
357357
.finding.info {{ border-left-color: var(--info); background: #f0f9fb; }}
358+
.finding-meta-row ul, .finding-meta-row ol {{margin-left:20px}}
359+
.cwe-badge {{margin:2px}}
358360
359361
.finding-header {{ display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }}
360362
.finding-header h3 {{ margin: 0; flex: 1; }}

‎src/scanner/phases.rs‎

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,11 +1217,53 @@ Respond with ONLY JSON:
12171217

12181218
let searcher = VariantSearcher::new(target_path.to_string_lossy().to_string());
12191219

1220-
// For now, return empty variants (full implementation requires more work)
12211220
match searcher.search_variants() {
1222-
Ok(_hits) => {
1223-
// Future: convert hits to findings and merge
1224-
tracing::info!("Variant search completed (stub implementation)");
1221+
Ok(variant_hits) => {
1222+
let variant_findings: Vec<VulnerabilityFinding> = variant_hits
1223+
.into_iter()
1224+
.map(|hit| {
1225+
let finding_id = format!("variant-{}:{}", hit.file_path, hit.line_number);
1226+
VulnerabilityFinding {
1227+
id: finding_id,
1228+
title: "Code variant detected".to_string(),
1229+
description: format!(
1230+
"Potential vulnerability variant found with similarity score: {:.2}",
1231+
hit.similarity_score
1232+
),
1233+
severity: crate::findings::Severity::Medium,
1234+
confidence_score: hit.similarity_score,
1235+
cwe_id: None,
1236+
file_path: hit.file_path,
1237+
line_number: Some(hit.line_number),
1238+
code_snippet: Some(hit.snippet),
1239+
diff_hunk: None,
1240+
recommendation: Some("Review this code variant for potential vulnerabilities".to_string()),
1241+
code_location: None,
1242+
already_reported: false,
1243+
sources: vec!["variant_search".to_string()],
1244+
commit_reference: None,
1245+
ticket_reference: None,
1246+
priority_score: None,
1247+
cross_file_references: None,
1248+
verification_status: None,
1249+
verification_notes: None,
1250+
verification_error: None,
1251+
agent_evidence_path: None,
1252+
security_issue: None,
1253+
poc_code: None,
1254+
mitigation_code: None,
1255+
poc_format: None,
1256+
llm_model: None,
1257+
agent_mode: false,
1258+
statement_range: None,
1259+
triage_verdict: None,
1260+
}
1261+
})
1262+
.collect();
1263+
1264+
let count = variant_findings.len();
1265+
findings.extend(variant_findings);
1266+
tracing::info!("Variant search completed: {} variants found", count);
12251267
Ok((findings, analyzed_files.to_vec()))
12261268
}
12271269
Err(e) => {

‎src/scanner/pipeline/orchestrator.rs‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ impl PhaseGraph {
3434
let phases = vec![
3535
ScanPhase::Indexing,
3636
ScanPhase::Semgrep,
37-
ScanPhase::CweRouting,
3837
ScanPhase::LlmStaticAnalysis,
38+
ScanPhase::CweRouting,
3939
ScanPhase::LlmDiscovery,
4040
ScanPhase::LlmVerification,
4141
ScanPhase::SecurityAgentVerification,
@@ -78,16 +78,16 @@ impl PhaseGraph {
7878
"Run Semgrep static analysis",
7979
2
8080
);
81-
add_metadata!(
82-
ScanPhase::CweRouting,
83-
"CWE Routing",
84-
"Route findings to specialized models",
85-
3
86-
);
8781
add_metadata!(
8882
ScanPhase::LlmStaticAnalysis,
8983
"LLM Static Analysis",
9084
"Analyze files with LLM",
85+
3
86+
);
87+
add_metadata!(
88+
ScanPhase::CweRouting,
89+
"CWE Routing",
90+
"Route findings to specialized models",
9191
4
9292
);
9393
add_metadata!(

‎src/threat_model/generation.rs‎

Lines changed: 234 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,15 +154,235 @@ pub fn save_to_context(target_path: &Path, threat_model: &str) {
154154
.expect("Failed to save threat model to context");
155155
}
156156

157-
/// Load architecture summary from AnalysisContext or regenerate via CodebaseUnderstanding.
157+
/// Generate architecture summary by statically inspecting the codebase.
158158
#[cfg_attr(test, visibility::make(pub))]
159-
pub fn load_or_generate_architecture(_target_path: &Path, context: &AnalysisContext) -> String {
159+
pub fn generate_architecture_static(target_path: &Path) -> String {
160+
let mut summary = String::new();
161+
162+
// Get project type
163+
let project_type = detect_project_type(target_path);
164+
summary.push_str("=== ARCHITECTURAL SUMMARY ===\n");
165+
summary.push_str(&format!("Project type: {}\n", project_type));
166+
167+
// Index project files
168+
let file_index = crate::indexer::FileIndex::index_project(
169+
target_path.to_str().unwrap_or("."),
170+
&[
171+
"rust".to_string(),
172+
"typescript".to_string(),
173+
"javascript".to_string(),
174+
"python".to_string(),
175+
],
176+
1024 * 1024, // 1MB max file size
177+
&[
178+
"target/".to_string(),
179+
"node_modules/".to_string(),
180+
".git/".to_string(),
181+
],
182+
);
183+
184+
let file_count = file_index.as_ref().map(|i| i.files.len()).unwrap_or(0);
185+
summary.push_str(&format!("Total files: {}\n\n", file_count));
186+
187+
// Detect components by scanning file contents
188+
let (has_http, has_db, has_filesys, has_auth) = detect_components(target_path, file_index);
189+
190+
summary.push_str("Components detected:\n");
191+
if has_http {
192+
summary.push_str("- HTTP API: yes\n");
193+
} else {
194+
summary.push_str("- No web framework\n");
195+
}
196+
if has_db {
197+
summary.push_str("- database: yes\n");
198+
} else {
199+
summary.push_str("- No database\n");
200+
}
201+
if has_filesys {
202+
summary.push_str("- file system: yes\n");
203+
} else {
204+
summary.push_str("- No file system\n");
205+
}
206+
if has_auth {
207+
summary.push_str("- Authentication: yes\n");
208+
} else {
209+
summary.push_str("- No auth\n");
210+
}
211+
summary.push('\n');
212+
213+
// Entry points
214+
summary.push_str("Entry points:\n");
215+
if has_http {
216+
summary.push_str("- HTTP endpoints\n");
217+
}
218+
if file_count > 0 {
219+
summary.push_str("- Source code entry (main.rs / index.js / etc.)\n");
220+
}
221+
222+
// Data stores
223+
summary.push_str("\nData stores:\n");
224+
if !has_db {
225+
summary.push_str("- None detected\n");
226+
}
227+
228+
summary
229+
}
230+
231+
/// Detect components by scanning indexed files for keywords.
232+
fn detect_components(
233+
target_path: &Path,
234+
file_index: Result<crate::indexer::FileIndex, std::io::Error>,
235+
) -> (bool, bool, bool, bool) {
236+
let mut has_http = false;
237+
let mut has_db = false;
238+
let mut has_filesys = false;
239+
let mut has_auth = false;
240+
241+
let http_keywords = [
242+
"HTTP",
243+
"endpoint",
244+
"router",
245+
"axum",
246+
"actix",
247+
"warp",
248+
"rocket",
249+
"tower",
250+
"express",
251+
"flask",
252+
"django",
253+
"fastapi",
254+
"spring",
255+
"gin",
256+
"echo",
257+
"http::",
258+
"actix_web",
259+
"axum::",
260+
];
261+
let db_keywords = [
262+
"sqlite",
263+
"postgres",
264+
"mysql",
265+
"mongodb",
266+
"redis",
267+
"database",
268+
"data store",
269+
"Repository",
270+
"Entity",
271+
"migration",
272+
"sqlx",
273+
"diesel",
274+
"orm",
275+
"prisma",
276+
];
277+
let fs_keywords = [
278+
"fs::read",
279+
"fs::write",
280+
"File::open",
281+
"File::create",
282+
"tempfile",
283+
"file upload",
284+
"filesystem",
285+
"std::fs",
286+
"read_to_string",
287+
];
288+
let auth_keywords = [
289+
"auth",
290+
"session",
291+
"token",
292+
"jwt",
293+
"password",
294+
"credential",
295+
"oauth",
296+
"bearer",
297+
"authentication",
298+
"authorization",
299+
];
300+
301+
// Get files from index or fallback to scanning common source files
302+
let files_to_scan = match file_index {
303+
Ok(index) => index.files.into_iter().take(100).collect(),
304+
Err(_) => {
305+
// Fallback: scan common source files directly
306+
let mut files = Vec::new();
307+
let src_path = target_path.join("src");
308+
if src_path.exists() {
309+
if let Ok(entries) = std::fs::read_dir(&src_path) {
310+
for entry in entries.flatten().take(100) {
311+
if entry.path().extension().and_then(|e| e.to_str()) == Some("rs") {
312+
files.push(crate::indexer::FileInfo {
313+
path: entry.path(),
314+
size: 0,
315+
language: "rust".to_string(),
316+
hash: None,
317+
});
318+
}
319+
}
320+
}
321+
}
322+
files
323+
}
324+
};
325+
326+
for file_info in files_to_scan {
327+
if let Ok(content) = std::fs::read_to_string(&file_info.path) {
328+
let lower = content.to_lowercase();
329+
330+
if !has_http
331+
&& http_keywords
332+
.iter()
333+
.any(|k| lower.contains(&k.to_lowercase()))
334+
{
335+
has_http = true;
336+
}
337+
if !has_db
338+
&& db_keywords
339+
.iter()
340+
.any(|k| lower.contains(&k.to_lowercase()))
341+
{
342+
has_db = true;
343+
}
344+
if !has_filesys
345+
&& fs_keywords
346+
.iter()
347+
.any(|k| lower.contains(&k.to_lowercase()))
348+
{
349+
has_filesys = true;
350+
}
351+
if !has_auth
352+
&& auth_keywords
353+
.iter()
354+
.any(|k| lower.contains(&k.to_lowercase()))
355+
{
356+
has_auth = true;
357+
}
358+
359+
// Early exit if all detected
360+
if has_http && has_db && has_filesys && has_auth {
361+
break;
362+
}
363+
}
364+
}
365+
366+
(has_http, has_db, has_filesys, has_auth)
367+
}
368+
369+
/// Load architecture summary from AnalysisContext or regenerate via static codebase analysis.
370+
#[cfg_attr(test, visibility::make(pub))]
371+
pub fn load_or_generate_architecture(target_path: &Path, context: &AnalysisContext) -> String {
160372
if !context.architecture_summary.is_empty() {
161373
tracing::debug!("Using existing architecture summary from context");
162374
context.architecture_summary.clone()
163375
} else {
164-
tracing::warn!("No architecture summary in context, using empty architecture");
165-
"No architecture summary available".to_string()
376+
tracing::info!("Generating architecture summary via static analysis");
377+
let generated = generate_architecture_static(target_path);
378+
// Persist for reuse by later phases
379+
let mut ctx =
380+
AnalysisContext::load(target_path).unwrap_or_else(|_| AnalysisContext::default());
381+
ctx.architecture_summary = generated.clone();
382+
if let Err(e) = ctx.save(target_path) {
383+
tracing::warn!("Failed to persist architecture summary: {e}");
384+
}
385+
generated
166386
}
167387
}
168388

@@ -289,8 +509,17 @@ mod tests {
289509
use tempfile::tempdir;
290510
let tmp = tempdir().unwrap();
291511

512+
// Create a simple Rust file for detection
513+
let src_dir = tmp.path().join("src");
514+
std::fs::create_dir_all(&src_dir).unwrap();
515+
std::fs::write(src_dir.join("main.rs"), "fn main() {}").unwrap();
516+
292517
let ctx = AnalysisContext::default();
293518
let arch = load_or_generate_architecture(tmp.path(), &ctx);
294-
assert_eq!(arch, "No architecture summary available");
519+
520+
// Should generate architecture summary, not return placeholder
521+
assert!(arch.contains("ARCHITECTURAL SUMMARY"));
522+
assert!(arch.contains("Project type"));
523+
assert_ne!(arch, "No architecture summary available");
295524
}
296525
}

‎src/threat_model/mod.rs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Threat Modeling Phase
22
//!
33
//! Implements STRIDE-based threat modeling that:
4-
//! - Consumes CodebaseUnderstanding output from Phase 1
4+
//! - Consumes architecture summary from static analysis (generated on clean scan)
55
//! - Identifies trust boundaries, data flows, attack surfaces
66
//! - Generates comprehensive threat models
77
//! - Persists to AnalysisContext
@@ -27,15 +27,15 @@ pub struct ThreatModelingPhase;
2727
impl ThreatModelingPhase {
2828
/// Run threat modeling phase on the target codebase.
2929
///
30-
/// Uses architecture understanding from CodebaseUnderstanding phase to:
30+
/// Uses architecture summary from static analysis to:
3131
/// - Identify trust boundaries (external APIs, DB connections, file system access)
3232
/// - Map data flows (request/response cycles, persistence points)
3333
/// - Locate attack surfaces (entry points, deserialization, privilege escalation)
3434
/// - Generate STRIDE threats (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege)
3535
///
3636
/// # Arguments
3737
/// * `target_path` - Path to the codebase
38-
/// * `context` - AnalysisContext containing CodebaseUnderstanding output
38+
/// * `context` - AnalysisContext containing architecture summary
3939
/// * `llm_client` - Optional LLM client for deep analysis (fallback to static if unavailable)
4040
///
4141
/// # Returns
@@ -45,7 +45,7 @@ impl ThreatModelingPhase {
4545
context: &AnalysisContext,
4646
llm_client: Option<&LlmClient>,
4747
) -> Result<String, String> {
48-
// Load or rebuild architecture summary from CodebaseUnderstanding
48+
// Load or generate architecture summary via static analysis
4949
let architecture = load_or_generate_architecture(target_path, context);
5050

5151
let prompt = if let Some(client) = llm_client {

0 commit comments

Comments
 (0)