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
38 changes: 38 additions & 0 deletions .github/workflows/phase1-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Phase 1 Validation

on:
pull_request:
branches: [main]
paths:
- 'dashboard/**'
- '.github/workflows/phase1-validation.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
dashboard:
runs-on: ubuntu-latest
defaults:
run:
working-directory: dashboard
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: dashboard/package-lock.json
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Typecheck
run: npx tsc --noEmit
- name: Tests
run: npm test
- name: Production build
run: npm run build
57 changes: 21 additions & 36 deletions dashboard/app/api/security/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,16 @@ import { NextRequest, NextResponse } from 'next/server';
import { parseRepoSlug } from '@/lib/repo-validation.mjs';
import { classifyScannerStatus, summarizeScannerStatuses } from '@/lib/scanner-status.mjs';
import { consumeRateLimit, requestIdentity } from '@/lib/rate-limit.mjs';
import { getRedis } from '@/lib/redis';
import { getJson, getRedis, setJson } from '@/lib/redis';
import type {
MegaScanReport, DependabotModule, SecretsModule, CodeScanModule,
OsvModule, LicenseModule, TotalCounts, ScoringResult, ScoreDeduction,
SeverityCounts, Severity, CodeQualityModule, CiQualityModule,
CiCheckRun, CheckRunConclusion, SonarModule, DeepSourceModule, CodecovModule,
CiCheckRun, CheckRunConclusion, SonarModule, DeepSourceModule, CodecovModule, ScannerStatusResult,
} from '@/lib/security-types';

const GH_TOKEN = process.env.GITHUB_TOKEN ?? '';
const NVD_API_KEY = process.env.NVD_API_KEY ?? ''; // optional — raises rate limit from 5/30s → 50/30s
const REDIS_URL = process.env.UPSTASH_REDIS_REST_URL ?? '';
const REDIS_TOKEN = process.env.UPSTASH_REDIS_REST_TOKEN ?? '';
const CACHE_TTL = 900; // 15 minutes

const GH_HEADERS = {
Expand Down Expand Up @@ -954,25 +952,11 @@ function aggregateTotals(report: Partial<MegaScanReport> & {
// ── Redis Cache ───────────────────────────────────────────────────────────────

async function cacheGet(key: string): Promise<MegaScanReport | null> {
if (!REDIS_URL) return null;
try {
const res = await fetch(`${REDIS_URL}/get/${encodeURIComponent(key)}`, {
headers: { Authorization: `Bearer ${REDIS_TOKEN}` },
});
const data = await res.json() as { result?: string };
return data.result ? JSON.parse(data.result) as MegaScanReport : null;
} catch { return null; }
return getJson<MegaScanReport>(key);
}

async function cacheSet(key: string, value: unknown): Promise<void> {
if (!REDIS_URL) return;
try {
await fetch(`${REDIS_URL}/set/${encodeURIComponent(key)}`, {
method: 'POST',
headers: { Authorization: `Bearer ${REDIS_TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ value: JSON.stringify(value), ex: CACHE_TTL }),
});
} catch { /* non-fatal */ }
async function cacheSet(key: string, value: MegaScanReport): Promise<void> {
await setJson(key, value, CACHE_TTL);
}

// ── Route Handler ─────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -1053,21 +1037,22 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
trivy: { available: false, message: 'CLI tool — run scripts/mega_scanner.py locally', findings: [] },
};

const scanner_statuses = {
dependabot: classifyScannerStatus('dependabot', dependabot),
secrets: classifyScannerStatus('secrets', secrets_github),
code_scanning: classifyScannerStatus('code_scanning', code_scanning),
osv: classifyScannerStatus('osv', osv),
nvd: classifyScannerStatus('nvd', nvd),
gh_advisory: classifyScannerStatus('gh_advisory', gh_advisory),
pypi_safety: classifyScannerStatus('pypi_safety', pypi_safety),
retirejs: classifyScannerStatus('retirejs', retirejs),
license: classifyScannerStatus('license', license),
ci_checks: classifyScannerStatus('ci_checks', code_quality.ci),
sonarcloud: classifyScannerStatus('sonarcloud', code_quality.sonar),
deepsource: classifyScannerStatus('deepsource', code_quality.deepsource),
codecov: classifyScannerStatus('codecov', code_quality.codecov),
trufflehog: { source: 'trufflehog', status: 'unavailable', error: 'Scanner not configured for this hosted endpoint' },
const unavailable = (source: string, error: string): ScannerStatusResult => ({ source, status: 'unavailable', error });
const scanner_statuses: Record<string, ScannerStatusResult> = {
dependabot: classifyScannerStatus('dependabot', dependabot) as ScannerStatusResult,
secrets: classifyScannerStatus('secrets', secrets_github) as ScannerStatusResult,
code_scanning: classifyScannerStatus('code_scanning', code_scanning) as ScannerStatusResult,
osv: classifyScannerStatus('osv', osv) as ScannerStatusResult,
nvd: classifyScannerStatus('nvd', nvd) as ScannerStatusResult,
gh_advisory: classifyScannerStatus('gh_advisory', gh_advisory) as ScannerStatusResult,
pypi_safety: classifyScannerStatus('pypi_safety', pypi_safety) as ScannerStatusResult,
retirejs: classifyScannerStatus('retirejs', retirejs) as ScannerStatusResult,
license: classifyScannerStatus('license', license) as ScannerStatusResult,
ci_checks: classifyScannerStatus('ci_checks', code_quality.ci) as ScannerStatusResult,
sonarcloud: classifyScannerStatus('sonarcloud', code_quality.sonar) as ScannerStatusResult,
deepsource: classifyScannerStatus('deepsource', code_quality.deepsource) as ScannerStatusResult,
codecov: classifyScannerStatus('codecov', code_quality.codecov) as ScannerStatusResult,
trufflehog: unavailable('trufflehog', 'Scanner not configured for this hosted endpoint'),
semgrep: { source: 'semgrep', status: 'unavailable', error: 'Scanner not configured for this hosted endpoint' },
nuclei: { source: 'nuclei', status: 'unavailable', error: 'Scanner not configured for this hosted endpoint' },
trivy: { source: 'trivy', status: 'unavailable', error: 'Scanner not configured for this hosted endpoint' },
Expand Down
22 changes: 12 additions & 10 deletions dashboard/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ const BASE = "https://devlens-io.vercel.app";
export const metadata: Metadata = {
metadataBase: new URL(BASE),
title: {
default: "DevLens — GitHub Repo Health Scorer",
default: "DevLens — GitHub Repository Intelligence",
template: "%s | DevLens",
},
description:
"Free GitHub repo health scorer. Analyse any public repository across 7 dimensions — README quality, commit activity, CI/CD, documentation, issue response, and community signal. Get a score out of 100 instantly.",
"Free GitHub repository intelligence for health, security, code quality, activity, documentation, and community signals. Analyze public repositories with transparent evidence and confidence.",
keywords: [
"GitHub repo health",
"repository score",
Expand Down Expand Up @@ -44,23 +44,23 @@ export const metadata: Metadata = {
locale: "en_US",
url: BASE,
siteName: "DevLens",
title: "DevLens — GitHub Repo Health Scorer",
title: "DevLens — GitHub Repository Intelligence",
description:
"Free tool to analyse any public GitHub repo across 7 health dimensions. Get an instant score out of 100 — no login, no data stored.",
"Analyze any public GitHub repository across 9 health dimensions, plus security and code-quality intelligence. Free and evidence-driven.",
images: [
{
url: `${BASE}/og.png`,
width: 1200,
height: 630,
alt: "DevLens — GitHub Repo Health Scorer",
alt: "DevLens — GitHub Repository Intelligence",
},
],
},
twitter: {
card: "summary_large_image",
title: "DevLens — GitHub Repo Health Scorer",
title: "DevLens — GitHub Repository Intelligence",
description:
"Analyse any public GitHub repo across 7 health dimensions. Free, instant, no login.",
"Analyze any public GitHub repository across 9 health dimensions, plus security and code-quality intelligence. Free and instant.",
images: [`${BASE}/og.png`],
creator: "@SamoTech",
},
Expand All @@ -80,7 +80,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
name: "DevLens",
url: BASE,
description:
"Free GitHub repo health scorer. Analyse any public repository across 7 weighted dimensions and get an instant score out of 100.",
"Free GitHub repository intelligence across 9 weighted health dimensions, with security and code-quality analysis.",
applicationCategory: "DeveloperApplication",
operatingSystem: "Any",
offers: {
Expand All @@ -98,9 +98,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
"Commit activity analysis",
"CI/CD setup detection",
"Documentation completeness check",
"Issue response rate",
"Community signal scoring",
"Issue response and maintenance analysis",
"Community signal analysis",
"Repo freshness rating",
"PR velocity analysis",
"Security and code-quality intelligence",
],
};

Expand Down
28 changes: 28 additions & 0 deletions dashboard/lib/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,31 @@ export function getRedis(): Redis | null {
}
return redis
}

/** Read a JSON-serializable value from the shared Redis abstraction. */
export async function getJson<T>(key: string): Promise<T | null> {
const client = getRedis()
if (!client) return null
try {
return await client.get<T>(key)
} catch {
return null
}
}

/** Write a JSON-serializable value with an optional TTL. */
export async function setJson(key: string, value: unknown, ttlSeconds?: number): Promise<boolean> {
const client = getRedis()
if (!client) return false
try {
const serialized = JSON.stringify(value)
if (ttlSeconds && ttlSeconds > 0) {
await client.set(key, serialized, { ex: ttlSeconds })
} else {
await client.set(key, serialized)
}
return true
} catch {
return false
}
}
Loading
Loading