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
58 changes: 57 additions & 1 deletion app/analyze/[id]/AnalyzeClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { FilterBar } from '@/components/FilterBar';
import { FingerprintCard } from '@/components/FingerprintCard';
import { LoadingState } from '@/components/LoadingState';
import { ProgressBar } from '@/components/ProgressBar';
import { SecurityCollapseBanner } from '@/components/SecurityCollapseBanner';
import { SecurityOverview } from '@/components/SecurityOverview';
import type {
AnalysisRecord,
DebtNode,
Expand All @@ -33,6 +35,12 @@ function AnalyzeContent() {
maxScore: 100,
nodeTypes: ['function', 'class', 'module', 'variable'],
search: '',
criticalSecurityOnly: false,
owaspCategories: [],
cweCategories: [],
securityScoreThreshold: 0,
secretLeaksOnly: false,
injectionOnly: false,
});

const poll = useCallback(async () => {
Expand Down Expand Up @@ -94,8 +102,15 @@ function AnalyzeContent() {

const filteredNodes = useMemo(() => {
return nodes.filter((n) => {
if (n.debt_score > filter.maxScore) return false;
if (n.debt_score < filter.minScore) return false;
if (n.security_score < filter.securityScoreThreshold) return false;
if (!filter.nodeTypes.includes(n.node_type)) return false;
if (filter.criticalSecurityOnly && !n.has_critical_security) return false;
if (filter.owaspCategories.length > 0 && !n.owasp_categories.some((category) => filter.owaspCategories.includes(category))) return false;
if (filter.cweCategories.length > 0 && !n.cwe_categories.some((category) => filter.cweCategories.includes(category))) return false;
if (filter.secretLeaksOnly && !n.security_findings.some((finding) => finding.category === 'Secrets')) return false;
if (filter.injectionOnly && !n.security_findings.some((finding) => finding.category === 'Injection' || finding.owaspIds.includes('A03'))) return false;
if (filter.search) {
const q = filter.search.toLowerCase();
if (
Expand All @@ -116,6 +131,39 @@ function AnalyzeContent() {
);
}, [links, filteredNodes]);

const availableOwaspCategories = useMemo(
() => [...new Set(nodes.flatMap((node) => node.owasp_categories ?? []))].sort(),
[nodes]
);

const availableCweCategories = useMemo(
() => [...new Set(nodes.flatMap((node) => node.cwe_categories ?? []))].sort(),
[nodes]
);

const securityFindings = useMemo(
() => nodes.flatMap((node) => node.security_findings ?? []),
[nodes]
);

const collapseBanner = useMemo(() => {
if (!analysis?.security_collapse) return null;
const affectedCoreModules = [...new Set(nodes.filter((node) => node.has_critical_security).map((node) => node.file_path))].slice(0, 8);
const reasons = [
`Critical vulnerabilities: ${analysis.critical_vulnerabilities}`,
`Repository security score: ${analysis.repo_security_score.toFixed(1)}/100`,
`Affected modules: ${affectedCoreModules.length}`,
];
const severity: 'critical' | 'high' | 'moderate' = analysis.repo_security_score > 85 ? 'critical' : 'high';
return {
isCollapsed: true,
severity,
reasons,
affectedCoreModules,
propagationRisk: Math.min(100, Math.round(analysis.repo_security_score * 0.8 + analysis.critical_vulnerabilities * 6)),
};
}, [analysis, nodes]);

const isLoading =
!analysis ||
(analysis.status !== 'complete' && analysis.status !== 'failed');
Expand All @@ -129,7 +177,7 @@ function AnalyzeContent() {
{isRateLimit ? 'GitHub Quota Paused' : 'Analysis Failed'}
</h2>
<p className="text-slate-400 text-sm mb-6 leading-relaxed">{error}</p>

{isRateLimit ? (
<div className="flex flex-col gap-3 items-center">
<button
Expand Down Expand Up @@ -242,6 +290,12 @@ function AnalyzeContent() {
</div>
</div>

{collapseBanner && (
<SecurityCollapseBanner collapse={collapseBanner} criticalFindings={analysis.critical_vulnerabilities} />
)}

<SecurityOverview analysis={analysis} nodes={nodes} />

{/* Dynamic visual graph and filters block */}
<div className="flex-1 flex flex-col lg:flex-row gap-6 min-h-0">
<div className="lg:w-[70%] flex flex-col gap-6 min-h-0">
Expand Down Expand Up @@ -271,6 +325,8 @@ function AnalyzeContent() {
filter={filter}
onChange={setFilter}
nodeCount={filteredNodes.length}
availableOwaspCategories={availableOwaspCategories}
availableCweCategories={availableCweCategories}
/>
<div className="hidden lg:block flex-1 min-h-[200px]">
<NodeSidebar node={selected} analysisId={analysisId} />
Expand Down
104 changes: 101 additions & 3 deletions app/api/analyze/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { computeDuplicationScores } from '@/lib/duplication';
import { computeBlastRadius, buildGraphData } from '@/lib/blast-radius';
import { scoreAllSymbols, averageScore } from '@/lib/debt-scorer';
import { classifyAgentCode } from '@/lib/huggingface';
import { analyzeSecurityRepository, getSecurityNodeMetrics } from '@/lib/security/detector';
import { computeSecurityAwarePriority } from '@/lib/security/security-scorer';
import { isSecuritySensitivePath } from '@/lib/security/security-utils';

export const maxDuration = 300;

Expand Down Expand Up @@ -206,6 +209,24 @@ async function runPipeline(
console.error("[Pipeline Warning] Duplication analysis failed:", dupErr);
}

console.log("[Pipeline] Progress: 52");
await updateAnalysisProgress(analysisId, {
status: 'scoring',
progress: 52,
progress_message: 'security analysis',
});

let securityResult = null as Awaited<ReturnType<typeof analyzeSecurityRepository>> | null;
try {
securityResult = analyzeSecurityRepository({
files,
symbols,
blastRadiusMap: new Map<string, number>(),
});
} catch (securityErr) {
console.error('[Pipeline Warning] Security detection failed:', securityErr);
}

let blastRadiusMap = new Map<string, number>();
try {
blastRadiusMap = computeBlastRadius(symbols);
Expand All @@ -220,6 +241,55 @@ async function runPipeline(
console.error("[Pipeline Warning] Debt scoring formulas failed:", scoreErr);
}

if (!securityResult) {
try {
securityResult = analyzeSecurityRepository({
files,
symbols,
blastRadiusMap,
});
} catch (securityErr) {
console.error('[Pipeline Warning] Security analysis retry failed:', securityErr);
}
}

const securityByFile = new Map<string, ReturnType<typeof getSecurityNodeMetrics>>();
if (securityResult) {
for (const symbol of symbols) {
securityByFile.set(symbol.filePath, getSecurityNodeMetrics(securityResult, symbol.filePath));
}
}

const securityPriorityMap = new Map<string, number>();
for (const symbol of symbols) {
const metrics = securityByFile.get(symbol.filePath) ?? getSecurityNodeMetrics(securityResult ?? {
findings: [],
summary: { totalVulnerabilities: 0, critical: 0, high: 0, medium: 0, low: 0, score: 0, categoryCounts: {}, owaspCategories: [], cweCategories: [], topFindings: [] },
collapse: { isCollapsed: false, severity: 'moderate', reasons: [], affectedCoreModules: [], propagationRisk: 0 },
nodeMetrics: {},
repoSecurityScore: 0,
criticalVulnerabilities: 0,
}, symbol.filePath);
securityPriorityMap.set(
symbol.id,
computeSecurityAwarePriority({
debtScore: debtScores.get(symbol.id) ?? 0,
blastRadius: blastRadiusMap.get(symbol.id) ?? 0,
securityScore: metrics.securityScore,
})
);
}

const securitySummary = securityResult?.summary ?? null;
const repoSecurityScore = securityResult?.repoSecurityScore ?? 0;
const collapseResult = securityResult?.collapse ?? {
isCollapsed: false,
severity: 'moderate' as const,
reasons: [],
affectedCoreModules: [],
propagationRisk: 0,
};

const avgScore = averageScore(debtScores);

// 4. Isolated dependency graph generation (Requirement 10)
Expand All @@ -230,12 +300,16 @@ async function runPipeline(
progress_message: 'generating graph',
total_nodes: symbols.length,
avg_debt_score: avgScore,
security_summary: securitySummary,
security_collapse: collapseResult.isCollapsed,
critical_vulnerabilities: securityResult?.criticalVulnerabilities ?? 0,
repo_security_score: repoSecurityScore,
});

let topSymbols: any[] = [];
let links: any[] = [];
try {
const graphData = buildGraphData(symbols, debtScores, blastRadiusMap);
const graphData = buildGraphData(symbols, debtScores, blastRadiusMap, securityPriorityMap);
topSymbols = graphData.topSymbols;
links = graphData.links;
} catch (graphErr) {
Expand Down Expand Up @@ -280,9 +354,17 @@ async function runPipeline(
line_start: sym.lineStart,
line_end: sym.lineEnd,
debt_score: debtScores.get(sym.id) ?? 0,
security_score: securityByFile.get(sym.filePath)?.securityScore ?? 0,
security_weighted_score: securityByFile.get(sym.filePath)?.securityWeightedScore ?? 0,
has_critical_security: securityByFile.get(sym.filePath)?.hasCriticalSecurity ?? false,
vulnerability_count: securityByFile.get(sym.filePath)?.vulnerabilityCount ?? 0,
security_risk_level: securityByFile.get(sym.filePath)?.securityRiskLevel ?? 'none',
complexity: sym.complexity,
duplication_score: duplicationScores.get(sym.filePath) ?? 0,
blast_radius: blastRadiusMap.get(sym.id) ?? 0,
owasp_categories: securityByFile.get(sym.filePath)?.owaspCategories ?? [],
cwe_categories: securityByFile.get(sym.filePath)?.cweCategories ?? [],
security_findings: securityByFile.get(sym.filePath)?.securityFindings ?? [],
dependencies: sym.calls.filter((c: string) => topIds.has(c)),
dependents: sym.calledBy.filter((c: string) => topIds.has(c)),
explanation: null,
Expand All @@ -296,9 +378,21 @@ async function runPipeline(
for (let i = 0; i < nodesToInsert.length; i += batchSize) {
const batch = nodesToInsert.slice(i, i + batchSize);
const { error } = await supabase.from('debt_nodes').insert(batch);
if (error) {
if (!error) continue;

const errorMessage = error.message ?? '';
const isSchemaCacheError = /schema cache|Could not find the '.+' column/i.test(errorMessage);
if (!isSchemaCacheError) {
console.error(`[Pipeline Error] Nodes insertion batch failed:`, error);
throw new Error(`Failed to save nodes in database: ${error.message}`);
throw new Error(`Failed to save nodes in database: ${errorMessage}`);
}

console.warn('[Pipeline Warning] Retrying node insert without security columns because Supabase schema cache is stale:', errorMessage);
const fallbackBatch = batch.map(({ security_score, security_weighted_score, has_critical_security, vulnerability_count, security_risk_level, owasp_categories, cwe_categories, security_findings, ...rest }) => rest);
const fallbackInsert = await supabase.from('debt_nodes').insert(fallbackBatch);
if (fallbackInsert.error) {
console.error('[Pipeline Error] Fallback insert also failed:', fallbackInsert.error);
throw new Error(`Failed to save nodes in database: ${fallbackInsert.error.message}`);
}
}

Expand All @@ -312,6 +406,10 @@ async function runPipeline(
avg_debt_score: avgScore,
fingerprint_label: fingerprintLabel ?? undefined,
fingerprint_confidence: fingerprintConfidence ?? undefined,
security_summary: securitySummary,
security_collapse: collapseResult.isCollapsed,
critical_vulnerabilities: securityResult?.criticalVulnerabilities ?? 0,
repo_security_score: repoSecurityScore,
});

console.log(`[Pipeline] Job ${analysisId} completed successfully.`);
Expand Down
6 changes: 6 additions & 0 deletions app/api/explain/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ export async function POST(request: NextRequest) {
complexity: node.complexity || 0,
blastRadius: node.blast_radius || 0,
codeSnippet,
securityScore: node.security_score ?? 0,
vulnerabilityCount: node.vulnerability_count ?? 0,
securityRiskLevel: node.security_risk_level ?? 'none',
owaspCategories: node.owasp_categories ?? [],
cweCategories: node.cwe_categories ?? [],
securityFindings: node.security_findings ?? [],
});

// Save back to DB
Expand Down
Loading
Loading