Skip to content
Open
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
54 changes: 33 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,28 +153,40 @@ zenvra/

### Setup

```bash
# Clone
git clone https://github.com/Cameroon-Developer-Network/zenvra.git
cd zenvra

# Start infrastructure
docker compose up -d

# Configure environment
cp .env.example .env
# Add your ANTHROPIC_API_KEY and DATABASE_URL

# Build Rust workspace
cargo build
1. **Clone the repository:**
```bash
git clone https://github.com/Cameroon-Developer-Network/zenvra.git
cd zenvra
```

2. **Start infrastructure (Postgres & Redis):**
```bash
# Starts only the necessary databases
docker compose up -d postgres redis
```

3. **Configure environment:**
```bash
cp .env.example .env
# Open .env and add your AI provider keys (Anthropic, OpenAI, or Google)
# The default DATABASE_URL in .env.example works with the docker setup
```

4. **Start the Backend API:**
```bash
cargo run -p zenvra-server
```

5. **Start the Dashboard (Frontend):**
```bash
cd apps/web
npm install # or pnpm install
npm run dev
```

### Quick Scan via CLI

# Run all tests
cargo test --all

# Frontend
cd apps/web && pnpm install && pnpm dev

# Try the CLI
```bash
cargo run -p zenvra-cli -- scan ./path/to/code
```

Expand Down
1 change: 1 addition & 0 deletions apps/web/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface ScanRequest {
language?: string;
engines?: string[];
ai_config?: AiConfig;
min_severity?: 'critical' | 'high' | 'medium' | 'low' | 'info';
}

export interface Finding {
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/lib/stores/usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { writable } from 'svelte/store';
import { getHistory } from '$lib/api';

export const scanCount = writable<number>(0);

export async function refreshScanCount() {
try {
const history = await getHistory();
scanCount.set(history.length);
Comment on lines +6 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This store is counting a capped history page, not actual usage.

refreshScanCount() uses /api/v1/history, but the server only returns the latest 50 scans. After that point scanCount stops reflecting real usage, so anything deriving quota/billing state from this value will drift. A dedicated usage/count endpoint would be safer here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/stores/usage.ts` around lines 6 - 9, refreshScanCount()
currently calls getHistory() which returns a capped page (latest 50) so
scanCount will max out and not reflect real usage; change refreshScanCount to
call a dedicated usage/count endpoint (e.g. create and use getUsageCount() or
fetch '/api/v1/usage' that returns the total scan count) and set scanCount from
that response instead of history.length; update any callers that rely on
refreshScanCount to expect a numeric total and add minimal error handling for
the usage fetch in the refreshScanCount function.

} catch (e) {
console.error('Failed to refresh scan count:', e);
}
}
12 changes: 10 additions & 2 deletions apps/web/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
<script lang="ts">
import "../app.css";
import { page } from "$app/state";
import { scanCount, refreshScanCount } from "$lib/stores/usage";
import { onMount } from "svelte";
let { children } = $props();

const maxScans = 10;

onMount(async () => {
await refreshScanCount();
});

const navItems = [
{ name: "Dashboard", href: "/", icon: "layout-grid", disabled: false },
{ name: "Scan Code", href: "/scan", icon: "search", disabled: false },
Expand Down Expand Up @@ -56,9 +64,9 @@
<p class="text-xs font-semibold text-zinc-500 uppercase tracking-wider mb-2">Usage Plan</p>
<p class="text-sm font-medium mb-1">Free Tier</p>
<div class="w-full bg-zinc-800 h-1.5 rounded-full overflow-hidden mt-2">
<div class="bg-brand-primary w-2/5 h-full rounded-full shadow-[0_0_8px_rgba(236,72,153,0.5)]"></div>
<div class="bg-brand-primary h-full rounded-full shadow-[0_0_8px_rgba(236,72,153,0.5)]" style="width: {($scanCount / maxScans) * 100}%"></div>
</div>
<p class="text-[10px] text-zinc-500 mt-2">4/10 scans remaining</p>
<p class="text-[10px] text-zinc-500 mt-2">{maxScans - $scanCount}/{maxScans} scans remaining</p>
Comment on lines +67 to +69
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clamp the usage math once the free tier is exhausted.

If $scanCount goes past maxScans, the bar width exceeds 100% and the remaining label becomes negative. Clamp both values so the UI stays sane after the limit is reached.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/`+layout.svelte around lines 67 - 69, Clamp the progress
percent and remaining scans before rendering: compute a safe percent (e.g. use
Math.min(100, ( $scanCount / maxScans )*100) with a guard for maxScans === 0)
and a non-negative remaining count (e.g. Math.max(0, maxScans - $scanCount)),
then use those values in the progress bar div inline style and in the paragraph
text instead of using ($scanCount / maxScans) * 100 and maxScans - $scanCount
directly; update references to $scanCount and maxScans in the progress bar div
and the "{maxScans - $scanCount}/{maxScans} scans remaining" paragraph to use
the clamped variables.

</div>
</div>
</aside>
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/routes/history/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@

onMount(async () => {
try {
findings = await getScanResults(scanId);
if (scanId) {
findings = await getScanResults(scanId);
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
error = msg || "Failed to load scan results.";
Expand Down
54 changes: 50 additions & 4 deletions apps/web/src/routes/scan/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { type Finding } from "$lib/api";
import { aiConfig } from "$lib/stores/aiConfig.svelte";
import { refreshScanCount } from "$lib/stores/usage";

const LANGUAGES = [
{ value: "python", label: "Python" },
Expand All @@ -20,6 +21,7 @@
];

let selectedLanguage = $state("python");
let selectedMinSeverity = $state("info");

Comment on lines 23 to 25
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. No zod for minseverity 📘 Rule violation ⛨ Security

A new user input (selectedMinSeverity / min_severity) is introduced and sent to the API without
Zod-based validation. This weakens input validation guarantees and violates the Zod-only validation
requirement for forms/API inputs.
Agent Prompt
## Issue description
A new UI/API input (`min_severity`) is added without Zod validation.

## Issue Context
Compliance requires using Zod schemas for validating all new/changed form inputs and API inputs.

## Fix Focus Areas
- apps/web/src/routes/scan/+page.svelte[23-25]
- apps/web/src/routes/scan/+page.svelte[71-76]
- apps/web/src/routes/scan/+page.svelte[339-352]
- apps/web/src/lib/api.ts[14-20]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

let code = $state(`// Paste your code here to scan for vulnerabilities
def get_user(user_id):
Expand Down Expand Up @@ -69,7 +71,8 @@ def get_user(user_id):
api_key: aiConfig.apiKey,
model: aiConfig.model,
endpoint: aiConfig.endpoint || undefined,
} : undefined
} : undefined,
min_severity: selectedMinSeverity !== "info" ? selectedMinSeverity : undefined
})
Comment on lines 71 to 76
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Direct fetch in +page.svelte 📘 Rule violation ⚙ Maintainability

The scan page modifies a direct fetch request instead of using the typed API client wrapper,
reducing consistency and type safety for frontend network calls. This violates the requirement to
route frontend network access through apps/web/src/lib/api.ts.
Agent Prompt
## Issue description
The scan page is constructing/modifying request payloads inside an ad-hoc `fetch` flow instead of calling the typed API wrapper.

## Issue Context
Compliance requires frontend network calls to be centralized in `apps/web/src/lib/api.ts` for consistent behavior and typing.

## Fix Focus Areas
- apps/web/src/routes/scan/+page.svelte[62-120]
- apps/web/src/routes/scan/+page.svelte[141-195]
- apps/web/src/lib/api.ts[48-137]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

});

Expand All @@ -95,6 +98,7 @@ def get_user(user_id):
scanStatus = "Scan complete!";
isScanning = false;
eventSource.close();
refreshScanCount();
break;
case 'error':
scanStatus = `Error: ${data.data}`;
Expand All @@ -106,8 +110,13 @@ def get_user(user_id):

eventSource.onerror = () => {
console.error("SSE connection failed");
isScanning = false;
eventSource.close();
scanStatus = "Connection lost. Reconnecting or finished.";
// We don't necessarily want to set isScanning to false here because SSE often reconnects automatically.
// But for Zenvra's simple model, we'll close it to avoid zombie states if the scan is actually done.
if (eventSource.readyState === EventSource.CLOSED || eventSource.readyState === EventSource.CONNECTING) {
isScanning = false;
eventSource.close();
}
Comment on lines 111 to +119
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

On the EventSource Web API, what does readyState === EventSource.CONNECTINGmean duringonerror, and should the client close the connection or allow the browser to retry automatically?

💡 Result:

readyState === EventSource.CONNECTING during the onerror event means the EventSource connection has encountered an error (e.g., network issue, server error) and the browser has set the state to CONNECTING to automatically attempt reconnection, as per the WHATWG HTML spec. This is the normal reconnection process: the browser will retry after a delay (default ~3s, adjustable via retry: field), keeping readyState as CONNECTING until success (OPEN) or permanent failure (CLOSED). The client should NOT close the connection in onerror when readyState is CONNECTING. Closing it (es.close) sets readyState to CLOSED, permanently stopping retries. Official docs (MDN, WHATWG) do not recommend closing; they advise logging/handling the error and letting the browser retry automatically for transient failures. Only close for fatal/permanent errors (when readyState === CLOSED) or per app logic. Many implementations add manual reconnection logic only if readyState === CLOSED to handle cases where browser stops retrying (e.g., certain HTTP errors like 500 in some browsers).

Citations:


Remove EventSource.CONNECTING from the close condition.

EventSource.CONNECTING indicates the browser is actively retrying after a transient failure. Closing the source while in this state cancels automatic reconnection, causing scans to abort on brief network hiccups. Per the WHATWG spec, clients should only close when readyState === EventSource.CLOSED (permanent failure) or allow the browser to retry automatically. Update the condition to exclude CONNECTING:

Current problematic code
if (eventSource.readyState === EventSource.CLOSED || eventSource.readyState === EventSource.CONNECTING) {
   isScanning = false;
   eventSource.close();
}

Only close if readyState === EventSource.CLOSED or handle retries explicitly for fatal errors, not transient ones.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/scan/`+page.svelte around lines 111 - 119, The onerror
handler is closing the EventSource during EventSource.CONNECTING (transient
retry) which aborts automatic reconnection; update the condition in the onerror
handler so it only closes when eventSource.readyState === EventSource.CLOSED
(remove EventSource.CONNECTING from the OR), and keep isScanning false +
eventSource.close() only in that CLOSED branch; reference the eventSource
variable and isScanning flag in your change.

};

} catch (error) {
Expand Down Expand Up @@ -144,7 +153,8 @@ def get_user(user_id):
api_key: aiConfig.apiKey,
model: aiConfig.model,
endpoint: aiConfig.endpoint || undefined,
} : undefined
} : undefined,
min_severity: selectedMinSeverity !== "info" ? selectedMinSeverity : undefined
})
});

Expand All @@ -168,6 +178,7 @@ def get_user(user_id):
scanStatus = "Scan complete!";
isScanning = false;
eventSource.close();
refreshScanCount();
break;
case 'error':
scanStatus = `Error: ${data.data}`;
Expand Down Expand Up @@ -325,6 +336,21 @@ def get_user(user_id):
</select>
</div>

<!-- Severity Filter -->
<div class="flex items-center gap-3">
<label class="text-xs font-bold text-zinc-500 uppercase tracking-widest whitespace-nowrap">Min Severity</label>
<select
bind:value={selectedMinSeverity}
class="glass bg-zinc-900/80 px-3 py-2 rounded-xl border border-zinc-800 text-xs font-medium text-zinc-300 focus:ring-2 ring-brand-primary outline-none transition-all flex-1"
>
<option value="info">All (Info+)</option>
<option value="low">Low+</option>
<option value="medium">Medium+</option>
<option value="high">High+</option>
<option value="critical">Critical Only</option>
</select>
</div>

<div class="flex-1 glass rounded-2xl overflow-hidden border-zinc-800 flex flex-col relative group">
<div class="px-6 py-3 border-b border-border bg-zinc-900/50 flex items-center justify-between">
<span class="text-xs font-bold text-zinc-500 uppercase tracking-widest">Input Code</span>
Expand Down Expand Up @@ -352,6 +378,20 @@ def get_user(user_id):
</label>
</div>

<div class="px-6 py-3 border-b border-border bg-zinc-900/50 flex items-center justify-between">
<span class="text-xs font-bold text-zinc-500 uppercase tracking-widest">Min Severity Filter</span>
<select
bind:value={selectedMinSeverity}
class="bg-transparent text-xs font-bold text-zinc-400 outline-none cursor-pointer"
>
<option value="info" class="bg-zinc-900">All Levels</option>
<option value="low" class="bg-zinc-900">Low+</option>
<option value="medium" class="bg-zinc-900">Medium+</option>
<option value="high" class="bg-zinc-900">High+</option>
<option value="critical" class="bg-zinc-900">Critical</option>
</select>
</div>

<div class="flex-1 overflow-y-auto p-4 space-y-2 custom-scrollbar">
{#if workspaceFiles.length === 0}
<div class="flex flex-col items-center justify-center h-full text-center space-y-3 opacity-50 py-12">
Expand Down Expand Up @@ -391,6 +431,12 @@ def get_user(user_id):
{#if findings.length > 0}
<button onclick={() => { findings = []; scanProgress = 0; scanStatus = "Ready to scan"; }} class="text-xs text-zinc-500 hover:text-zinc-300 transition-colors">Clear</button>
{/if}
{#if isScanning}
<div class="flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
<span class="text-[10px] font-black text-emerald-500 uppercase tracking-tighter">Live Stream</span>
</div>
{/if}
</div>

<div class="flex-1 overflow-y-auto p-6 space-y-4 custom-scrollbar">
Expand Down
26 changes: 22 additions & 4 deletions crates/scanner/src/engines/ai_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ fn build_rules() -> Vec<AiCodeRule> {
AiCodeRule {
name: "Unauthenticated Route Handler",
regex: NO_AUTH_ROUTE_REGEX.get_or_init(|| {
Regex::new(r"(?i)@(app|router)\.(delete|put|patch)\s*\([^)]+\)\s*\nasync\s+def\s+[a-z_]+\((?!.*\bauth\b|.*\bdepends\b|.*\btoken\b|.*\buser\b)")
.unwrap()
// Simplified: detect the decorator. Manual filtering below.
Regex::new(r"(?i)@(app|router)\.(delete|put|patch)\s*\(").unwrap()
})
Comment on lines +115 to 117
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Avoid introducing new .unwrap() in scanner library code.

Line 116 and Line 155 add new panic paths in library code. Please propagate regex construction errors instead of unwrapping.

Proposed direction
-fn build_rules() -> Vec<AiCodeRule> {
+fn build_rules() -> Result<Vec<AiCodeRule>> {
   vec![
     AiCodeRule {
       name: "Unauthenticated Route Handler",
-      regex: NO_AUTH_ROUTE_REGEX.get_or_init(|| {
-          Regex::new(r"(?i)@(app|router)\.(delete|put|patch)\s*\(").unwrap()
-      }).clone(),
+      regex: Regex::new(r"(?i)@(app|router)\.(delete|put|patch)\s*\(")?,
       ...
     },
     AiCodeRule {
       name: "Plain HTTP Endpoint (No TLS)",
-      regex: PLAIN_HTTP_REGEX.get_or_init(|| {
-          Regex::new(r#"(?i)(url\s*=\s*['"]http://|fetch\s*\(\s*['"]http://)"#).unwrap()
-      }).clone(),
+      regex: Regex::new(r#"(?i)(url\s*=\s*['"]http://|fetch\s*\(\s*['"]http://)"#)?,
       ...
     },
   ]
}
...
-    let rules = build_rules();
+    let rules = build_rules()?;

As per coding guidelines: crates/scanner/**/*.rs: “never use .unwrap() in library code” and crates/{scanner,server}/**/*.rs: “Do not use .unwrap() or .expect() in library and API code”.

Also applies to: 154-156

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/scanner/src/engines/ai_code.rs` around lines 115 - 117, The two uses
of Regex::new(...).unwrap() (the decorator-detection Regex instances created in
ai_code.rs) introduce panics in library code; replace the .unwrap() calls by
propagating the compile error instead—change the surrounding code to return
Result<..., regex::Error> (or propagate via ?), remove the unwraps, and
map/return the Regex::new(...) error to the caller so regex construction
failures are handled instead of panicking; update the function/closure
signatures that construct these Regexes accordingly (and adjust callers) so
errors flow through.

.clone(),
severity: Severity::Medium,
Expand Down Expand Up @@ -151,8 +151,8 @@ fn build_rules() -> Vec<AiCodeRule> {
AiCodeRule {
name: "Plain HTTP Endpoint (No TLS)",
regex: PLAIN_HTTP_REGEX.get_or_init(|| {
Regex::new(r#"(?i)(url\s*=\s*['"]http://(?!localhost|127\.0\.0\.1)|fetch\s*\(\s*['"]http://(?!localhost))"#)
.unwrap()
// Simplified: detect http:// without localhost/127.0.0.1
Regex::new(r#"(?i)(url\s*=\s*['"]http://|fetch\s*\(\s*['"]http://)"#).unwrap()
Comment on lines +154 to +155
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Plain HTTP regex became too narrow and likely regresses detection coverage.

The new pattern only catches url = "http://..." and fetch("http://..."). It misses common forms like requests.get("http://..."), axios.get("http://..."), and other quoted URLs.

Broader pattern while keeping local-host suppression in run
-                Regex::new(r#"(?i)(url\s*=\s*['"]http://|fetch\s*\(\s*['"]http://)"#).unwrap()
+                Regex::new(r#"(?i)['"]http://[^'"]+['"]"#).unwrap()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Simplified: detect http:// without localhost/127.0.0.1
Regex::new(r#"(?i)(url\s*=\s*['"]http://|fetch\s*\(\s*['"]http://)"#).unwrap()
// Simplified: detect http:// without localhost/127.0.0.1
Regex::new(r#"(?i)['"]http://[^'"]+['"]"#).unwrap()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/scanner/src/engines/ai_code.rs` around lines 154 - 155, The current
Regex::new(...) in crates/scanner/src/engines/ai_code.rs is too narrow (only
matches `url = "http://..."` and `fetch("http://..."`); broaden the pattern used
when constructing the Regex in the code (the Regex::new call) to match other
common HTTP usages like function calls and attribute accesses (e.g.,
`requests.get("http://...")`, `axios.get("http://...")`, plain quoted
`"http://..."`, and similar call/assignment patterns) while keeping the existing
localhost/127.0.0.1 suppression logic inside the run function intact; update the
Regex::new invocation to a more general case-insensitive pattern that captures
quoted http:// URLs and typical call forms and leave the filtering in run
unchanged.

})
.clone(),
severity: Severity::Medium,
Expand Down Expand Up @@ -201,6 +201,24 @@ pub async fn run(config: &ScanConfig) -> Result<Vec<RawFinding>> {

for rule in &rules {
if rule.regex.is_match(line) {
// Manual filtering for rules that were simplified to avoid look-ahead panics
if rule.name == "Unauthenticated Route Handler" {
let l = line.to_lowercase();
if l.contains("auth")
|| l.contains("depends")
|| l.contains("token")
|| l.contains("user")
{
continue;
}
Comment on lines +205 to +213
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

user keyword suppression is over-broad and drops real findings.

At Line 210, l.contains("user") suppresses valid unauthenticated mutation routes like @router.delete("/users/{id}"), creating false negatives.

Targeted fix
-                    if l.contains("auth")
-                        || l.contains("depends")
-                        || l.contains("token")
-                        || l.contains("user")
+                    if l.contains("auth")
+                        || l.contains("depends(")
+                        || l.contains("token")
                     {
                         continue;
                     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if rule.name == "Unauthenticated Route Handler" {
let l = line.to_lowercase();
if l.contains("auth")
|| l.contains("depends")
|| l.contains("token")
|| l.contains("user")
{
continue;
}
if rule.name == "Unauthenticated Route Handler" {
let l = line.to_lowercase();
if l.contains("auth")
|| l.contains("depends(")
|| l.contains("token")
{
continue;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/scanner/src/engines/ai_code.rs` around lines 205 - 213, The
suppression check is too broad: l.contains("user") hides valid routes like
"/users/{id}"; update the condition inside the rule.name == "Unauthenticated
Route Handler" block to only suppress when "user" appears as an auth-related
token or standalone identifier (not as part of "users"). Replace
l.contains("user") with a stricter match (e.g., a regex word-boundary check or
explicit tokens) such as testing
r"\b(user|current_user|user_id|get_current_user|authenticated_user)\b" or
equivalent so "/users" no longer matches while "{user}" or "current_user" still
suppress. Reference: the if block checking rule.name == "Unauthenticated Route
Handler" and the variable l.

}
if rule.name == "Plain HTTP Endpoint (No TLS)" {
let l = line.to_lowercase();
if l.contains("localhost") || l.contains("127.0.0.1") {
continue;
}
}

findings.push(RawFinding {
engine: Engine::AiCode,
cve_id: None,
Expand Down
42 changes: 30 additions & 12 deletions crates/scanner/src/engines/sca.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
//! for known CVEs. Returns `RawFinding`s with `Engine::Sca` and real CVE IDs
//! wherever available.

use crate::{finding::{RawFinding, Severity}, Engine, ScanConfig};
use crate::{
finding::{RawFinding, Severity},
Engine, ScanConfig,
};
use anyhow::Result;
use regex::Regex;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -81,7 +84,8 @@ fn parse_dependencies(config: &ScanConfig) -> Vec<Dependency> {
parse_requirements_txt(code)
} else if lower.ends_with("go.sum") {
parse_go_sum(code)
} else if lower.ends_with("pom.xml") || (lower.ends_with(".xml") && code.contains("<groupId>")) {
} else if lower.ends_with("pom.xml") || (lower.ends_with(".xml") && code.contains("<groupId>"))
{
parse_pom_xml(code)
} else {
// Try heuristic detection
Expand Down Expand Up @@ -109,7 +113,11 @@ fn parse_cargo_lock(content: &str) -> Vec<Dependency> {
if line == "[[package]]" {
// Flush previous
if let (Some(name), Some(version)) = (current_name.take(), current_version.take()) {
deps.push(Dependency { name, version, ecosystem: "crates.io".to_string() });
deps.push(Dependency {
name,
version,
ecosystem: "crates.io".to_string(),
});
}
} else if let Some(rest) = line.strip_prefix("name = ") {
current_name = Some(rest.trim_matches('"').to_string());
Expand All @@ -119,7 +127,11 @@ fn parse_cargo_lock(content: &str) -> Vec<Dependency> {
}
// Flush last
if let (Some(name), Some(version)) = (current_name, current_version) {
deps.push(Dependency { name, version, ecosystem: "crates.io".to_string() });
deps.push(Dependency {
name,
version,
ecosystem: "crates.io".to_string(),
});
}
deps
}
Expand Down Expand Up @@ -195,8 +207,15 @@ fn parse_pom_xml(content: &str) -> Vec<Dependency> {
};
for cap in dep_re.captures_iter(content) {
let name = cap[1].trim().to_string();
let version = cap.get(2).map(|m| m.as_str().trim().to_string()).unwrap_or_else(|| "unknown".to_string());
deps.push(Dependency { name, version, ecosystem: "Maven".to_string() });
let version = cap
.get(2)
.map(|m| m.as_str().trim().to_string())
.unwrap_or_else(|| "unknown".to_string());
deps.push(Dependency {
name,
version,
ecosystem: "Maven".to_string(),
});
}
deps
}
Expand Down Expand Up @@ -263,11 +282,7 @@ async fn query_osv(deps: &[Dependency]) -> Result<Vec<(Dependency, Vec<OsvVuln>)

let body = OsvBatchRequest { queries };

let response = client
.post(OSV_BATCH_URL)
.json(&body)
.send()
.await;
let response = client.post(OSV_BATCH_URL).json(&body).send().await;

let response = match response {
Ok(r) => r,
Expand Down Expand Up @@ -348,7 +363,10 @@ pub async fn run(config: &ScanConfig) -> Result<Vec<RawFinding>> {
cve_id,
cwe_id: None,
severity,
title: format!("Vulnerable dependency: {} v{} — {}", dep.name, dep.version, title),
title: format!(
"Vulnerable dependency: {} v{} — {}",
dep.name, dep.version, title
),
vulnerable_code: format!("{}@{} ({})", dep.name, dep.version, dep.ecosystem),
description: Some(format!(
"{} (ID: {})",
Expand Down
Loading
Loading