From 9b5a7f9f0c180ebb601bd250c57c8fe72d58c8dd Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:27:55 +0300 Subject: [PATCH 01/20] feat: add dependency intelligence model --- dashboard/lib/dependency-intelligence.ts | 87 ++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 dashboard/lib/dependency-intelligence.ts diff --git a/dashboard/lib/dependency-intelligence.ts b/dashboard/lib/dependency-intelligence.ts new file mode 100644 index 0000000..ba22bd9 --- /dev/null +++ b/dashboard/lib/dependency-intelligence.ts @@ -0,0 +1,87 @@ +import type { AdvisoryFinding, AdvisoryReport, ParsedPackage } from './advisory' + +export type UpdateType = 'up_to_date' | 'patch' | 'minor' | 'major' | 'unknown' + +export interface DependencyRecord { + name: string + ecosystem: string + installedVersion: string + latestVersion: string | null + updateType: UpdateType + vulnerabilityCount: number + highestSeverity: string | null + patchedVersion: string | null + sources: string[] +} + +function versionParts(value: string): [number, number, number] | null { + const match = String(value ?? '').match(/^(?:v|[<>=~^*\s]*)?(\d+)(?:\.(\d+))?(?:\.(\d+))?/) + if (!match) return null + return [Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0)] +} + +export function compareVersions(a: string, b: string): number { + const av = versionParts(a) + const bv = versionParts(b) + if (!av || !bv) return 0 + for (let i = 0; i < 3; i++) if (av[i] !== bv[i]) return av[i] > bv[i] ? 1 : -1 + return 0 +} + +export function classifyUpdate(installed: string, latest: string | null): UpdateType { + const a = versionParts(installed) + const b = versionParts(latest ?? '') + if (!a || !b) return 'unknown' + if (a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) return 'up_to_date' + if (a[0] !== b[0]) return 'major' + if (a[1] !== b[1]) return 'minor' + return 'patch' +} + +async function npmLatest(name: string): Promise { + try { + const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, { + signal: AbortSignal.timeout(5000), + headers: { Accept: 'application/json' }, + }) + if (!response.ok) return null + const data = await response.json() as { version?: string } + return data.version ?? null + } catch { + return null + } +} + +export async function buildDependencyInventory(report: AdvisoryReport): Promise { + const findings = report.findings ?? [] + const findingMap = new Map() + for (const finding of findings) { + const key = `${finding.ecosystem}:${finding.package}` + const list = findingMap.get(key) ?? [] + list.push(finding) + findingMap.set(key, list) + } + + const packages = report.packages.slice(0, 100) + const npmPackages = packages.filter(p => p.ecosystem === 'npm').slice(0, 30) + const latest = new Map() + const results = await Promise.all(npmPackages.map(async p => [p.name, await npmLatest(p.name)] as const)) + for (const [name, version] of results) latest.set(name, version) + + return packages.map((pkg: ParsedPackage): DependencyRecord => { + const vulns = findingMap.get(`${pkg.ecosystem}:${pkg.name}`) ?? [] + const ranked = [...vulns].sort((a, b) => ({ CRITICAL: 5, HIGH: 4, MODERATE: 3, LOW: 2, UNKNOWN: 1 }[b.severity] ?? 0) - ({ CRITICAL: 5, HIGH: 4, MODERATE: 3, LOW: 2, UNKNOWN: 1 }[a.severity] ?? 0)) + const latestVersion = pkg.ecosystem === 'npm' ? latest.get(pkg.name) ?? null : null + return { + name: pkg.name, + ecosystem: pkg.ecosystem, + installedVersion: pkg.version, + latestVersion, + updateType: classifyUpdate(pkg.version, latestVersion), + vulnerabilityCount: vulns.length, + highestSeverity: ranked[0]?.severity ?? null, + patchedVersion: vulns.find(v => v.patchedVer)?.patchedVer ?? null, + sources: [...new Set(vulns.flatMap(v => v.sources ?? [v.source]))], + } + }) +} From 3f5fd8c25393c0d3334b8fc2a6e60d8a744b422a Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:28:01 +0300 Subject: [PATCH 02/20] test: cover dependency update classification --- .../scripts/dependency-intelligence.test.mjs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 dashboard/scripts/dependency-intelligence.test.mjs diff --git a/dashboard/scripts/dependency-intelligence.test.mjs b/dashboard/scripts/dependency-intelligence.test.mjs new file mode 100644 index 0000000..5ff5b50 --- /dev/null +++ b/dashboard/scripts/dependency-intelligence.test.mjs @@ -0,0 +1,17 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { classifyUpdate, compareVersions } from '../lib/dependency-intelligence' + +test('compares semantic versions', () => { + assert.equal(compareVersions('1.2.3', '1.2.3'), 0) + assert.equal(compareVersions('1.2.3', '1.3.0'), -1) + assert.equal(compareVersions('2.0.0', '1.9.9'), 1) +}) + +test('classifies update levels', () => { + assert.equal(classifyUpdate('1.2.3', '1.2.4'), 'patch') + assert.equal(classifyUpdate('1.2.3', '1.4.0'), 'minor') + assert.equal(classifyUpdate('1.2.3', '2.0.0'), 'major') + assert.equal(classifyUpdate('1.2.3', '1.2.3'), 'up_to_date') + assert.equal(classifyUpdate('latest', null), 'unknown') +}) From 1e06df94371544c3fa6c2a1a0cca72a33f723bc9 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:28:18 +0300 Subject: [PATCH 03/20] refactor: use runtime dependency intelligence module --- dashboard/lib/dependency-intelligence.ts | 87 ------------------------ 1 file changed, 87 deletions(-) delete mode 100644 dashboard/lib/dependency-intelligence.ts diff --git a/dashboard/lib/dependency-intelligence.ts b/dashboard/lib/dependency-intelligence.ts deleted file mode 100644 index ba22bd9..0000000 --- a/dashboard/lib/dependency-intelligence.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { AdvisoryFinding, AdvisoryReport, ParsedPackage } from './advisory' - -export type UpdateType = 'up_to_date' | 'patch' | 'minor' | 'major' | 'unknown' - -export interface DependencyRecord { - name: string - ecosystem: string - installedVersion: string - latestVersion: string | null - updateType: UpdateType - vulnerabilityCount: number - highestSeverity: string | null - patchedVersion: string | null - sources: string[] -} - -function versionParts(value: string): [number, number, number] | null { - const match = String(value ?? '').match(/^(?:v|[<>=~^*\s]*)?(\d+)(?:\.(\d+))?(?:\.(\d+))?/) - if (!match) return null - return [Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0)] -} - -export function compareVersions(a: string, b: string): number { - const av = versionParts(a) - const bv = versionParts(b) - if (!av || !bv) return 0 - for (let i = 0; i < 3; i++) if (av[i] !== bv[i]) return av[i] > bv[i] ? 1 : -1 - return 0 -} - -export function classifyUpdate(installed: string, latest: string | null): UpdateType { - const a = versionParts(installed) - const b = versionParts(latest ?? '') - if (!a || !b) return 'unknown' - if (a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) return 'up_to_date' - if (a[0] !== b[0]) return 'major' - if (a[1] !== b[1]) return 'minor' - return 'patch' -} - -async function npmLatest(name: string): Promise { - try { - const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, { - signal: AbortSignal.timeout(5000), - headers: { Accept: 'application/json' }, - }) - if (!response.ok) return null - const data = await response.json() as { version?: string } - return data.version ?? null - } catch { - return null - } -} - -export async function buildDependencyInventory(report: AdvisoryReport): Promise { - const findings = report.findings ?? [] - const findingMap = new Map() - for (const finding of findings) { - const key = `${finding.ecosystem}:${finding.package}` - const list = findingMap.get(key) ?? [] - list.push(finding) - findingMap.set(key, list) - } - - const packages = report.packages.slice(0, 100) - const npmPackages = packages.filter(p => p.ecosystem === 'npm').slice(0, 30) - const latest = new Map() - const results = await Promise.all(npmPackages.map(async p => [p.name, await npmLatest(p.name)] as const)) - for (const [name, version] of results) latest.set(name, version) - - return packages.map((pkg: ParsedPackage): DependencyRecord => { - const vulns = findingMap.get(`${pkg.ecosystem}:${pkg.name}`) ?? [] - const ranked = [...vulns].sort((a, b) => ({ CRITICAL: 5, HIGH: 4, MODERATE: 3, LOW: 2, UNKNOWN: 1 }[b.severity] ?? 0) - ({ CRITICAL: 5, HIGH: 4, MODERATE: 3, LOW: 2, UNKNOWN: 1 }[a.severity] ?? 0)) - const latestVersion = pkg.ecosystem === 'npm' ? latest.get(pkg.name) ?? null : null - return { - name: pkg.name, - ecosystem: pkg.ecosystem, - installedVersion: pkg.version, - latestVersion, - updateType: classifyUpdate(pkg.version, latestVersion), - vulnerabilityCount: vulns.length, - highestSeverity: ranked[0]?.severity ?? null, - patchedVersion: vulns.find(v => v.patchedVer)?.patchedVer ?? null, - sources: [...new Set(vulns.flatMap(v => v.sources ?? [v.source]))], - } - }) -} From 2bc5a365a23e795219c323d556812d55282be778 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:28:19 +0300 Subject: [PATCH 04/20] feat: add dependency intelligence runtime module --- dashboard/lib/dependency-intelligence.mjs | 85 +++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 dashboard/lib/dependency-intelligence.mjs diff --git a/dashboard/lib/dependency-intelligence.mjs b/dashboard/lib/dependency-intelligence.mjs new file mode 100644 index 0000000..2fa3ae2 --- /dev/null +++ b/dashboard/lib/dependency-intelligence.mjs @@ -0,0 +1,85 @@ +export type UpdateType = 'up_to_date' | 'patch' | 'minor' | 'major' | 'unknown' + +export interface DependencyRecord { + name: string + ecosystem: string + installedVersion: string + latestVersion: string | null + updateType: UpdateType + vulnerabilityCount: number + highestSeverity: string | null + patchedVersion: string | null + sources: string[] +} + +function versionParts(value: string): [number, number, number] | null { + const match = String(value ?? '').match(/^(?:v|[<>=~^*\s]*)?(\d+)(?:\.(\d+))?(?:\.(\d+))?/) + if (!match) return null + return [Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0)] +} + +export function compareVersions(a: string, b: string): number { + const av = versionParts(a) + const bv = versionParts(b) + if (!av || !bv) return 0 + for (let i = 0; i < 3; i++) if (av[i] !== bv[i]) return av[i] > bv[i] ? 1 : -1 + return 0 +} + +export function classifyUpdate(installed: string, latest: string | null): UpdateType { + const a = versionParts(installed) + const b = versionParts(latest ?? '') + if (!a || !b) return 'unknown' + if (a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) return 'up_to_date' + if (a[0] !== b[0]) return 'major' + if (a[1] !== b[1]) return 'minor' + return 'patch' +} + +async function npmLatest(name: string): Promise { + try { + const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, { + signal: AbortSignal.timeout(5000), + headers: { Accept: 'application/json' }, + }) + if (!response.ok) return null + const data = await response.json() as { version?: string } + return data.version ?? null + } catch { + return null + } +} + +export async function buildDependencyInventory(report: AdvisoryReport): Promise { + const findings = report.findings ?? [] + const findingMap = new Map() + for (const finding of findings) { + const key = `${finding.ecosystem}:${finding.package}` + const list = findingMap.get(key) ?? [] + list.push(finding) + findingMap.set(key, list) + } + + const packages = report.packages.slice(0, 100) + const npmPackages = packages.filter(p => p.ecosystem === 'npm').slice(0, 30) + const latest = new Map() + const results = await Promise.all(npmPackages.map(async p => [p.name, await npmLatest(p.name)] as const)) + for (const [name, version] of results) latest.set(name, version) + + return packages.map((pkg: ParsedPackage): DependencyRecord => { + const vulns = findingMap.get(`${pkg.ecosystem}:${pkg.name}`) ?? [] + const ranked = [...vulns].sort((a, b) => ({ CRITICAL: 5, HIGH: 4, MODERATE: 3, LOW: 2, UNKNOWN: 1 }[b.severity] ?? 0) - ({ CRITICAL: 5, HIGH: 4, MODERATE: 3, LOW: 2, UNKNOWN: 1 }[a.severity] ?? 0)) + const latestVersion = pkg.ecosystem === 'npm' ? latest.get(pkg.name) ?? null : null + return { + name: pkg.name, + ecosystem: pkg.ecosystem, + installedVersion: pkg.version, + latestVersion, + updateType: classifyUpdate(pkg.version, latestVersion), + vulnerabilityCount: vulns.length, + highestSeverity: ranked[0]?.severity ?? null, + patchedVersion: vulns.find(v => v.patchedVer)?.patchedVer ?? null, + sources: [...new Set(vulns.flatMap(v => v.sources ?? [v.source]))], + } + }) +} From 9cd9d171d623e238e50c6b6d826aa4bb89bde4b9 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:28:22 +0300 Subject: [PATCH 05/20] fix: test runtime dependency module --- dashboard/scripts/dependency-intelligence.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/scripts/dependency-intelligence.test.mjs b/dashboard/scripts/dependency-intelligence.test.mjs index 5ff5b50..114203c 100644 --- a/dashboard/scripts/dependency-intelligence.test.mjs +++ b/dashboard/scripts/dependency-intelligence.test.mjs @@ -1,6 +1,6 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { classifyUpdate, compareVersions } from '../lib/dependency-intelligence' +import { classifyUpdate, compareVersions } from '../lib/dependency-intelligence.mjs' test('compares semantic versions', () => { assert.equal(compareVersions('1.2.3', '1.2.3'), 0) From 94959c55e3ff787c61bb1255a6eb5d6d156d3f72 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:28:28 +0300 Subject: [PATCH 06/20] fix: remove TypeScript syntax from runtime module --- dashboard/lib/dependency-intelligence.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard/lib/dependency-intelligence.mjs b/dashboard/lib/dependency-intelligence.mjs index 2fa3ae2..4d51945 100644 --- a/dashboard/lib/dependency-intelligence.mjs +++ b/dashboard/lib/dependency-intelligence.mjs @@ -50,7 +50,7 @@ async function npmLatest(name: string): Promise { } } -export async function buildDependencyInventory(report: AdvisoryReport): Promise { +export async function buildDependencyInventory(report) { const findings = report.findings ?? [] const findingMap = new Map() for (const finding of findings) { @@ -66,7 +66,7 @@ export async function buildDependencyInventory(report: AdvisoryReport): Promise< const results = await Promise.all(npmPackages.map(async p => [p.name, await npmLatest(p.name)] as const)) for (const [name, version] of results) latest.set(name, version) - return packages.map((pkg: ParsedPackage): DependencyRecord => { + return packages.map(pkg => { const vulns = findingMap.get(`${pkg.ecosystem}:${pkg.name}`) ?? [] const ranked = [...vulns].sort((a, b) => ({ CRITICAL: 5, HIGH: 4, MODERATE: 3, LOW: 2, UNKNOWN: 1 }[b.severity] ?? 0) - ({ CRITICAL: 5, HIGH: 4, MODERATE: 3, LOW: 2, UNKNOWN: 1 }[a.severity] ?? 0)) const latestVersion = pkg.ecosystem === 'npm' ? latest.get(pkg.name) ?? null : null From cbb5b8b94a63adf2e6c8961dc60e332eb7c8fb62 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:28:33 +0300 Subject: [PATCH 07/20] feat: add dependency intelligence API --- dashboard/app/api/dependencies/route.ts | 54 +++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 dashboard/app/api/dependencies/route.ts diff --git a/dashboard/app/api/dependencies/route.ts b/dashboard/app/api/dependencies/route.ts new file mode 100644 index 0000000..5304202 --- /dev/null +++ b/dashboard/app/api/dependencies/route.ts @@ -0,0 +1,54 @@ +import { NextRequest, NextResponse } from 'next/server' +import { runAdvisoryCheck } from '@/lib/advisory' +import { buildDependencyInventory } from '@/lib/dependency-intelligence.mjs' +import { auth } from '@/lib/auth' +import { parseRepoSlug } from '@/lib/repo-validation.mjs' +import { getJson, getRedis, setJson } from '@/lib/redis' +import { consumeRateLimit, requestIdentity } from '@/lib/rate-limit.mjs' + +export const dynamic = 'force-dynamic' + +const CACHE_TTL = 1800 + +export async function GET(req: NextRequest) { + const repo = new URL(req.url).searchParams.get('repo') + const parsedRepo = parseRepoSlug(repo) + if (!parsedRepo) { + return NextResponse.json({ error: 'Invalid repo format. Use owner/name or a GitHub URL' }, { status: 400 }) + } + + const { owner, name } = parsedRepo + const cacheKey = `dependencies:${owner}:${name}` + const session = await auth() + const identity = requestIdentity(req, (session as any)?.user?.email) + const redis = getRedis() + const limit = await consumeRateLimit(redis, identity, 'advisory') + if (!limit.allowed) { + return NextResponse.json({ error: 'rate_limited', message: 'Dependency scan rate limit exceeded. Try again shortly.' }, { status: 429, headers: limit.headers }) + } + + const cached = await getJson(cacheKey) + if (cached) return NextResponse.json({ ...cached, cached: true }) + + try { + const token = (session as any)?.accessToken ?? process.env.GITHUB_TOKEN + const advisory = await runAdvisoryCheck(owner, name, token) + const dependencies = await buildDependencyInventory(advisory) + const report = { + repo: `${owner}/${name}`, + scannedAt: new Date().toISOString(), + packages: dependencies, + summary: { + total: dependencies.length, + npm: dependencies.filter(d => d.ecosystem === 'npm').length, + vulnerable: dependencies.filter(d => d.vulnerabilityCount > 0).length, + outdated: dependencies.filter(d => d.updateType !== 'up_to_date' && d.updateType !== 'unknown').length, + majorUpdates: dependencies.filter(d => d.updateType === 'major').length, + }, + } + await setJson(cacheKey, report, CACHE_TTL) + return NextResponse.json(report) + } catch (error) { + return NextResponse.json({ error: error instanceof Error ? error.message : 'Dependency scan failed' }, { status: 500 }) + } +} From eef93314e93f3e9dc574a2e2a2a39c36d94401c5 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:28:43 +0300 Subject: [PATCH 08/20] feat: add dependency intelligence dashboard --- dashboard/app/dependencies/page.tsx | 97 +++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 dashboard/app/dependencies/page.tsx diff --git a/dashboard/app/dependencies/page.tsx b/dashboard/app/dependencies/page.tsx new file mode 100644 index 0000000..bd5996f --- /dev/null +++ b/dashboard/app/dependencies/page.tsx @@ -0,0 +1,97 @@ +'use client' + +import { useState } from 'react' +import Link from 'next/link' + +const UPDATE_META = { + up_to_date: { label: 'Current', color: 'var(--success)' }, + patch: { label: 'Patch', color: 'var(--warning)' }, + minor: { label: 'Minor', color: 'var(--warning)' }, + major: { label: 'Major', color: 'var(--danger)' }, + unknown: { label: 'Unknown', color: 'var(--text-faint)' }, +} + +export default function DependenciesPage() { + const [repo, setRepo] = useState('') + const [data, setData] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + async function scan() { + setLoading(true); setError('') + try { + const response = await fetch(`/api/dependencies?repo=${encodeURIComponent(repo.trim())}`) + const json = await response.json() + if (!response.ok) throw new Error(json.message ?? json.error ?? 'Dependency scan failed') + setData(json) + } catch (e) { + setError(e instanceof Error ? e.message : 'Dependency scan failed') + } finally { + setLoading(false) + } + } + + return ( +
+ ← Home +

Dependency Intelligence

+

+ Inventory declared dependencies, correlate known vulnerabilities, and check npm packages for available releases. + Registry freshness is currently available for npm; other ecosystems remain vulnerability-focused. +

+ +
+ setRepo(e.target.value)} onKeyDown={e => e.key === 'Enter' && scan()} + placeholder="owner/name" style={{ flex: 1, padding: '12px 14px', border: '1px solid var(--border)', borderRadius: 8, background: 'var(--surface)', color: 'var(--text)' }} /> + +
+ + {error &&
{error}
} + + {data && ( + <> +
+ {[ + ['Packages', data.summary.total], + ['npm', data.summary.npm], + ['Vulnerable', data.summary.vulnerable], + ['Outdated', data.summary.outdated], + ['Major', data.summary.majorUpdates], + ].map(([label, value]) => ( +
+
{value}
+
{label}
+
+ ))} +
+ +
+ + + {['Package','Ecosystem','Installed','Latest','Update','Vulnerabilities','Fix','Sources'].map(h => )} + + + {data.packages.map((d: any) => { + const meta = UPDATE_META[d.updateType as keyof typeof UPDATE_META] ?? UPDATE_META.unknown + return + + + + + + + + + + })} + +
{h}
{d.name}{d.ecosystem}{d.installedVersion}{d.latestVersion ?? '—'}{meta.label}{d.vulnerabilityCount}{d.patchedVersion ?? '—'}{d.sources.length ? d.sources.join(', ') : '—'}
+
+ + )} +
+ ) +} From 7df7f8c2b47ecaf317a4e70a8da11cb3739fc6ff Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:28:50 +0300 Subject: [PATCH 09/20] ui: add dependency intelligence navigation --- dashboard/components/Nav.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/dashboard/components/Nav.tsx b/dashboard/components/Nav.tsx index 5cd502e..880df2a 100644 --- a/dashboard/components/Nav.tsx +++ b/dashboard/components/Nav.tsx @@ -17,6 +17,7 @@ export default function Nav() { { href: "/badge", label: "Badge" }, { href: "/stats", label: "Stats" }, { href: "/security", label: "Security" }, + { href: "/dependencies", label: "Dependencies" }, { href: "/docs", label: "Docs" }, { href: "/changelog", label: "Changelog" }, { href: "/sponsor", label: "Sponsor" }, From 6e7d60299ca8bbe9e04a67b8109538ae7988fe5e Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:29:02 +0300 Subject: [PATCH 10/20] docs: document scoring v3 and dependency intelligence --- dashboard/app/docs/page.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/dashboard/app/docs/page.tsx b/dashboard/app/docs/page.tsx index e1c069e..54fc9d0 100644 --- a/dashboard/app/docs/page.tsx +++ b/dashboard/app/docs/page.tsx @@ -48,14 +48,14 @@ function Row({ label, children }: { label: string; children: React.ReactNode }) const DIMS = [ { key: "readme", weight: "20%", title: "README Quality", desc: "Scores length (10 + 5 + 5 pts for 500 / 1500 / 3000 chars), presence of keywords install, usage, license, contributing, feature, example (6 pts each), code blocks (8), images (6), ## headings (4), list items (4), setup / roadmap / sponsor / discord mentions (4 each). Max 100." }, - { key: "activity", weight: "20%", title: "Commit Activity", desc: "Counts commits to the default branch in the last 90 days via the GitHub Commits API. ≥30 = 100 · ≥15 = 75 · ≥5 = 50 · ≥1 = 25 · 0 = 0." }, - { key: "freshness", weight: "15%", title: "Repo Freshness", desc: "Days since last push to the default branch (pushed_at field). ≤7 days = 100 · ≤30 = 80 · ≤90 = 55 · ≤180 = 30 · older = 10." }, - { key: "docs", weight: "15%", title: "Documentation", desc: "Walks the full repo tree (git/trees/HEAD?recursive=1) looking for: LICENSE, CONTRIBUTING.md, CHANGELOG.md, CODE_OF_CONDUCT.md, SECURITY.md, docs/ folder — 16 pts each, max 100." }, - { key: "ci", weight: "10%", title: "CI/CD Setup", desc: "Counts GitHub Actions workflow files via the Actions Workflows API. ≥3 workflows = 100 · ≥1 = 60 · 0 = 0." }, - { key: "issues", weight: "10%", title: "Issue Response", desc: "Fetches up to 50 closed issues and compares against open_issues_count. Score = round(closed / total × 100). No issues at all = 100." }, - { key: "community", weight: "5%", title: "Community Signal", desc: "Math.min(Math.floor(log1p(stars) × 15) + Math.floor(log1p(forks) × 10), 100). Rewards repos with organic momentum." }, - { key: "pr_velocity", weight: "3%", title: "PR Velocity", desc: "Fetches last 20 closed PRs, filters to merged ones, averages (merged_at − created_at). <1 day = 100 · <3 = 85 · <7 = 65 · <14 = 45 · <30 = 25 · else = 10. No merged PRs = 50." }, - { key: "security", weight: "2%", title: "Security", desc: "Walks the repo tree for SECURITY.md (+30), .github/dependabot.yml (+35), and any workflow containing codeql / trivy / snyk (+35). Max 100." }, + { key: "activity", weight: "20%", title: "Commit Activity", desc: "Uses commit count, active-week cadence, and recent 30-day activity over the last 90 days so bursty commit dumps do not score like sustained maintenance." }, + { key: "freshness", weight: "15%", title: "Repo Freshness", desc: "Days since last push to the default branch. ≤7 days = 100 · ≤30 = 80 · ≤90 = 55 · ≤180 = 30 · older = 10." }, + { key: "docs", weight: "15%", title: "Documentation", desc: "Checks LICENSE, CONTRIBUTING.md, CHANGELOG.md, CODE_OF_CONDUCT.md, SECURITY.md, and docs/." }, + { key: "ci", weight: "10%", title: "CI/CD Setup", desc: "Counts GitHub Actions workflow files." }, + { key: "issues", weight: "10%", title: "Issue Maintenance", desc: "Combines stale open-issue ratio with median closed-issue resolution time instead of relying on a closed/open ratio." }, + { key: "community", weight: "5%", title: "Community Signal", desc: "Logarithmic signal from stars and forks." }, + { key: "pr_velocity", weight: "3%", title: "PR Maintenance", desc: "Uses median and 90th-percentile merge time plus stale open PRs instead of a simple average." }, + { key: "security", weight: "2%", title: "Security", desc: "Uses the real advisory/security evidence engine. Scanner coverage is reported separately from the security score." },, weight: "2%", title: "Security", desc: "Walks the repo tree for SECURITY.md (+30), .github/dependabot.yml (+35), and any workflow containing codeql / trivy / snyk (+35). Max 100." }, ]; export default function DocsPage() { @@ -98,6 +98,9 @@ export default function DocsPage() { {/* ── API Reference ── */}
+ + Returns a dependency inventory with installed versions, npm latest-release checks, vulnerability counts, remediation versions, and source provenance. Results are cached for 30 minutes. + Returns a RepoReport JSON object. Cached in Redis for 15 minutes unless{" "} weights param is present. From c5276d8de22cbcdfd35e33bed28cc6f1c9ffbd4b Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:30:23 +0300 Subject: [PATCH 11/20] fix: remove remaining TypeScript syntax --- dashboard/lib/dependency-intelligence.mjs | 28 ++++++----------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/dashboard/lib/dependency-intelligence.mjs b/dashboard/lib/dependency-intelligence.mjs index 4d51945..a3f5bd0 100644 --- a/dashboard/lib/dependency-intelligence.mjs +++ b/dashboard/lib/dependency-intelligence.mjs @@ -1,24 +1,10 @@ -export type UpdateType = 'up_to_date' | 'patch' | 'minor' | 'major' | 'unknown' - -export interface DependencyRecord { - name: string - ecosystem: string - installedVersion: string - latestVersion: string | null - updateType: UpdateType - vulnerabilityCount: number - highestSeverity: string | null - patchedVersion: string | null - sources: string[] -} - -function versionParts(value: string): [number, number, number] | null { +function versionParts(value) { const match = String(value ?? '').match(/^(?:v|[<>=~^*\s]*)?(\d+)(?:\.(\d+))?(?:\.(\d+))?/) if (!match) return null return [Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0)] } -export function compareVersions(a: string, b: string): number { +export function compareVersions(a, b) { const av = versionParts(a) const bv = versionParts(b) if (!av || !bv) return 0 @@ -26,7 +12,7 @@ export function compareVersions(a: string, b: string): number { return 0 } -export function classifyUpdate(installed: string, latest: string | null): UpdateType { +export function classifyUpdate(installed, latest) { const a = versionParts(installed) const b = versionParts(latest ?? '') if (!a || !b) return 'unknown' @@ -36,14 +22,14 @@ export function classifyUpdate(installed: string, latest: string | null): Update return 'patch' } -async function npmLatest(name: string): Promise { +async function npmLatest(name) { try { const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, { signal: AbortSignal.timeout(5000), headers: { Accept: 'application/json' }, }) if (!response.ok) return null - const data = await response.json() as { version?: string } + const data = await response.json() return data.version ?? null } catch { return null @@ -52,7 +38,7 @@ async function npmLatest(name: string): Promise { export async function buildDependencyInventory(report) { const findings = report.findings ?? [] - const findingMap = new Map() + const findingMap = new Map() for (const finding of findings) { const key = `${finding.ecosystem}:${finding.package}` const list = findingMap.get(key) ?? [] @@ -63,7 +49,7 @@ export async function buildDependencyInventory(report) { const packages = report.packages.slice(0, 100) const npmPackages = packages.filter(p => p.ecosystem === 'npm').slice(0, 30) const latest = new Map() - const results = await Promise.all(npmPackages.map(async p => [p.name, await npmLatest(p.name)] as const)) + const results = await Promise.all(npmPackages.map(async p => [p.name, await npmLatest(p.name)] )) for (const [name, version] of results) latest.set(name, version) return packages.map(pkg => { From 5ec6dd85313e9623c08c2a815e891afe1c68d75a Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:30:29 +0300 Subject: [PATCH 12/20] fix: simplify dependency summary JSX --- dashboard/app/dependencies/page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dashboard/app/dependencies/page.tsx b/dashboard/app/dependencies/page.tsx index bd5996f..9dc25b1 100644 --- a/dashboard/app/dependencies/page.tsx +++ b/dashboard/app/dependencies/page.tsx @@ -60,10 +60,10 @@ export default function DependenciesPage() { ['Vulnerable', data.summary.vulnerable], ['Outdated', data.summary.outdated], ['Major', data.summary.majorUpdates], - ].map(([label, value]) => ( -
-
{value}
-
{label}
+ ].map(item => ( +
+
{item[1]}
+
{item[0]}
))}
From 2b5af559cc46b5c927f5ef71e16729a0670e25ba Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:30:35 +0300 Subject: [PATCH 13/20] fix: remove remaining generic type syntax --- dashboard/lib/dependency-intelligence.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/lib/dependency-intelligence.mjs b/dashboard/lib/dependency-intelligence.mjs index a3f5bd0..ffab2e0 100644 --- a/dashboard/lib/dependency-intelligence.mjs +++ b/dashboard/lib/dependency-intelligence.mjs @@ -48,7 +48,7 @@ export async function buildDependencyInventory(report) { const packages = report.packages.slice(0, 100) const npmPackages = packages.filter(p => p.ecosystem === 'npm').slice(0, 30) - const latest = new Map() + const latest = new Map() const results = await Promise.all(npmPackages.map(async p => [p.name, await npmLatest(p.name)] )) for (const [name, version] of results) latest.set(name, version) From 27648f31bbec54198d2d14dddc2685055fda2ad8 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:31:40 +0300 Subject: [PATCH 14/20] fix: avoid ambiguous JSX array expression --- dashboard/app/dependencies/page.tsx | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/dashboard/app/dependencies/page.tsx b/dashboard/app/dependencies/page.tsx index 9dc25b1..e27b907 100644 --- a/dashboard/app/dependencies/page.tsx +++ b/dashboard/app/dependencies/page.tsx @@ -54,18 +54,20 @@ export default function DependenciesPage() { {data && ( <>
- {[ - ['Packages', data.summary.total], - ['npm', data.summary.npm], - ['Vulnerable', data.summary.vulnerable], - ['Outdated', data.summary.outdated], - ['Major', data.summary.majorUpdates], - ].map(item => ( -
-
{item[1]}
-
{item[0]}
-
- ))} +
+ {[ + { label: 'Packages', value: data.summary.total }, + { label: 'npm', value: data.summary.npm }, + { label: 'Vulnerable', value: data.summary.vulnerable }, + { label: 'Outdated', value: data.summary.outdated }, + { label: 'Major', value: data.summary.majorUpdates }, + ].map(item => ( +
+
{item.value}
+
{item.label}
+
+ ))} +
From b443be5227133476a4c05a343371823d89579b95 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:32:56 +0300 Subject: [PATCH 15/20] fix: repair dependency summary section --- dashboard/app/dependencies/page.tsx | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/dashboard/app/dependencies/page.tsx b/dashboard/app/dependencies/page.tsx index e27b907..c1906ae 100644 --- a/dashboard/app/dependencies/page.tsx +++ b/dashboard/app/dependencies/page.tsx @@ -52,22 +52,19 @@ export default function DependenciesPage() { {error &&
{error}
} {data && ( - <> -
-
- {[ - { label: 'Packages', value: data.summary.total }, - { label: 'npm', value: data.summary.npm }, - { label: 'Vulnerable', value: data.summary.vulnerable }, - { label: 'Outdated', value: data.summary.outdated }, - { label: 'Major', value: data.summary.majorUpdates }, - ].map(item => ( -
-
{item.value}
-
{item.label}
-
- ))} -
+ <>
+ {[ + ['Packages', data.summary.total], + ['npm', data.summary.npm], + ['Vulnerable', data.summary.vulnerable], + ['Outdated', data.summary.outdated], + ['Major', data.summary.majorUpdates], + ].map(item => ( +
+
{item[1]}
+
{item[0]}
+
+ ))}
From 3869541ffc1e0591ba9228bb71ab6667162d9595 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:33:09 +0300 Subject: [PATCH 16/20] fix: use explicit dependency summary cards --- dashboard/app/dependencies/page.tsx | 73 +++-------------------------- 1 file changed, 7 insertions(+), 66 deletions(-) diff --git a/dashboard/app/dependencies/page.tsx b/dashboard/app/dependencies/page.tsx index c1906ae..2b53956 100644 --- a/dashboard/app/dependencies/page.tsx +++ b/dashboard/app/dependencies/page.tsx @@ -1,70 +1,11 @@ -'use client' - -import { useState } from 'react' -import Link from 'next/link' - -const UPDATE_META = { - up_to_date: { label: 'Current', color: 'var(--success)' }, - patch: { label: 'Patch', color: 'var(--warning)' }, - minor: { label: 'Minor', color: 'var(--warning)' }, - major: { label: 'Major', color: 'var(--danger)' }, - unknown: { label: 'Unknown', color: 'var(--text-faint)' }, -} - -export default function DependenciesPage() { - const [repo, setRepo] = useState('') - const [data, setData] = useState(null) - const [loading, setLoading] = useState(false) - const [error, setError] = useState('') - - async function scan() { - setLoading(true); setError('') - try { - const response = await fetch(`/api/dependencies?repo=${encodeURIComponent(repo.trim())}`) - const json = await response.json() - if (!response.ok) throw new Error(json.message ?? json.error ?? 'Dependency scan failed') - setData(json) - } catch (e) { - setError(e instanceof Error ? e.message : 'Dependency scan failed') - } finally { - setLoading(false) - } - } - - return ( -
- ← Home -

Dependency Intelligence

-

- Inventory declared dependencies, correlate known vulnerabilities, and check npm packages for available releases. - Registry freshness is currently available for npm; other ecosystems remain vulnerability-focused. -

- -
- setRepo(e.target.value)} onKeyDown={e => e.key === 'Enter' && scan()} - placeholder="owner/name" style={{ flex: 1, padding: '12px 14px', border: '1px solid var(--border)', borderRadius: 8, background: 'var(--surface)', color: 'var(--text)' }} /> - -
- - {error &&
{error}
} - {data && ( - <>
- {[ - ['Packages', data.summary.total], - ['npm', data.summary.npm], - ['Vulnerable', data.summary.vulnerable], - ['Outdated', data.summary.outdated], - ['Major', data.summary.majorUpdates], - ].map(item => ( -
-
{item[1]}
-
{item[0]}
-
- ))} + <> +
+
{data.summary.total}
Packages
+
{data.summary.npm}
npm
+
{data.summary.vulnerable}
Vulnerable
+
{data.summary.outdated}
Outdated
+
{data.summary.majorUpdates}
Major
From 8e284b43fd84470183ce563b369b664525f2f230 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:33:28 +0300 Subject: [PATCH 17/20] fix: rebuild dependency dashboard JSX --- dashboard/app/dependencies/page.tsx | 108 +++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 18 deletions(-) diff --git a/dashboard/app/dependencies/page.tsx b/dashboard/app/dependencies/page.tsx index 2b53956..ac5cc74 100644 --- a/dashboard/app/dependencies/page.tsx +++ b/dashboard/app/dependencies/page.tsx @@ -1,31 +1,94 @@ +'use client' + +import { useState } from 'react' +import Link from 'next/link' + +const UPDATE_META = { + up_to_date: { label: 'Current', color: 'var(--success)' }, + patch: { label: 'Patch', color: 'var(--warning)' }, + minor: { label: 'Minor', color: 'var(--warning)' }, + major: { label: 'Major', color: 'var(--danger)' }, + unknown: { label: 'Unknown', color: 'var(--text-faint)' }, +} + +export default function DependenciesPage() { + const [repo, setRepo] = useState('') + const [data, setData] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + async function scan() { + setLoading(true) + setError('') + try { + const response = await fetch('/api/dependencies?repo=' + encodeURIComponent(repo.trim())) + const json = await response.json() + if (!response.ok) throw new Error(json.message ?? json.error ?? 'Dependency scan failed') + setData(json) + } catch (e) { + setError(e instanceof Error ? e.message : 'Dependency scan failed') + } finally { + setLoading(false) + } + } + + return ( +
+ ← Home +

Dependency Intelligence

+

+ Inventory declared dependencies, correlate known vulnerabilities, and check npm packages for available releases. + Registry freshness is currently available for npm; other ecosystems remain vulnerability-focused. +

+ +
+ setRepo(e.target.value)} onKeyDown={e => e.key === 'Enter' && scan()} + placeholder="owner/name" style={{ flex: 1, padding: '12px 14px', border: '1px solid var(--border)', borderRadius: 8, background: 'var(--surface)', color: 'var(--text)' }} /> + +
+ + {error &&
{error}
} + {data && ( <>
-
{data.summary.total}
Packages
-
{data.summary.npm}
npm
-
{data.summary.vulnerable}
Vulnerable
-
{data.summary.outdated}
Outdated
-
{data.summary.majorUpdates}
Major
+ + + + +
- {['Package','Ecosystem','Installed','Latest','Update','Vulnerabilities','Fix','Sources'].map(h => )} + + + + + + + + - {data.packages.map((d: any) => { - const meta = UPDATE_META[d.updateType as keyof typeof UPDATE_META] ?? UPDATE_META.unknown - return - - - - - - - - - + {data.packages.map(d => { + const meta = UPDATE_META[d.updateType] ?? UPDATE_META.unknown + return ( + + + + + + + + + + + ) })}
{h}PackageEcosystemInstalledLatestUpdateVulnerabilitiesFixSources
{d.name}{d.ecosystem}{d.installedVersion}{d.latestVersion ?? '—'}{meta.label}{d.vulnerabilityCount}{d.patchedVersion ?? '—'}{d.sources.length ? d.sources.join(', ') : '—'}
{d.name}{d.ecosystem}{d.installedVersion}{d.latestVersion ?? '—'}{meta.label}{d.vulnerabilityCount}{d.patchedVersion ?? '—'}{d.sources.length ? d.sources.join(', ') : '—'}
@@ -35,3 +98,12 @@
) } + +function SummaryCard({ label, value }) { + return ( +
+
{value}
+
{label}
+
+ ) +} From c7eb9d36d2d204568b895eb2b7ff2356c43418e1 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:34:44 +0300 Subject: [PATCH 18/20] fix: repair scoring documentation syntax --- dashboard/app/docs/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/app/docs/page.tsx b/dashboard/app/docs/page.tsx index 54fc9d0..0153007 100644 --- a/dashboard/app/docs/page.tsx +++ b/dashboard/app/docs/page.tsx @@ -55,7 +55,7 @@ const DIMS = [ { key: "issues", weight: "10%", title: "Issue Maintenance", desc: "Combines stale open-issue ratio with median closed-issue resolution time instead of relying on a closed/open ratio." }, { key: "community", weight: "5%", title: "Community Signal", desc: "Logarithmic signal from stars and forks." }, { key: "pr_velocity", weight: "3%", title: "PR Maintenance", desc: "Uses median and 90th-percentile merge time plus stale open PRs instead of a simple average." }, - { key: "security", weight: "2%", title: "Security", desc: "Uses the real advisory/security evidence engine. Scanner coverage is reported separately from the security score." },, weight: "2%", title: "Security", desc: "Walks the repo tree for SECURITY.md (+30), .github/dependabot.yml (+35), and any workflow containing codeql / trivy / snyk (+35). Max 100." }, + { key: "security", weight: "2%", title: "Security", desc: "Uses the real advisory/security evidence engine. Scanner coverage is reported separately from the security score." }, ]; export default function DocsPage() { From 63bcffb7e04ac6001dbac022197452453393fb02 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:35:49 +0300 Subject: [PATCH 19/20] fix: type dependency API response records --- dashboard/app/api/dependencies/route.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/dashboard/app/api/dependencies/route.ts b/dashboard/app/api/dependencies/route.ts index 5304202..7a82abf 100644 --- a/dashboard/app/api/dependencies/route.ts +++ b/dashboard/app/api/dependencies/route.ts @@ -6,6 +6,18 @@ import { parseRepoSlug } from '@/lib/repo-validation.mjs' import { getJson, getRedis, setJson } from '@/lib/redis' import { consumeRateLimit, requestIdentity } from '@/lib/rate-limit.mjs' +type DependencyRecord = { + name: string + ecosystem: string + installedVersion: string + latestVersion: string | null + updateType: 'up_to_date' | 'patch' | 'minor' | 'major' | 'unknown' + vulnerabilityCount: number + highestSeverity: string | null + patchedVersion: string | null + sources: string[] +} + export const dynamic = 'force-dynamic' const CACHE_TTL = 1800 @@ -33,7 +45,7 @@ export async function GET(req: NextRequest) { try { const token = (session as any)?.accessToken ?? process.env.GITHUB_TOKEN const advisory = await runAdvisoryCheck(owner, name, token) - const dependencies = await buildDependencyInventory(advisory) + const dependencies = await buildDependencyInventory(advisory) as DependencyRecord[] const report = { repo: `${owner}/${name}`, scannedAt: new Date().toISOString(), From 98fa25f71b79f3997f908b8225802bae890adab1 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:35:52 +0300 Subject: [PATCH 20/20] fix: type dependency dashboard data --- dashboard/app/dependencies/page.tsx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/dashboard/app/dependencies/page.tsx b/dashboard/app/dependencies/page.tsx index ac5cc74..75c2166 100644 --- a/dashboard/app/dependencies/page.tsx +++ b/dashboard/app/dependencies/page.tsx @@ -11,9 +11,26 @@ const UPDATE_META = { unknown: { label: 'Unknown', color: 'var(--text-faint)' }, } +type DependencyRecord = { + name: string + ecosystem: string + installedVersion: string + latestVersion: string | null + updateType: keyof typeof UPDATE_META + vulnerabilityCount: number + patchedVersion: string | null + sources: string[] +} +type DependencyResponse = { + repo: string + scannedAt: string + packages: DependencyRecord[] + summary: { total: number; npm: number; vulnerable: number; outdated: number; majorUpdates: number } +} + export default function DependenciesPage() { const [repo, setRepo] = useState('') - const [data, setData] = useState(null) + const [data, setData] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState('') @@ -99,7 +116,7 @@ export default function DependenciesPage() { ) } -function SummaryCard({ label, value }) { +function SummaryCard({ label, value }: { label: string; value: number }) { return (
{value}