SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
import { Router, type IRouter } from "express";
|
|
|
|
|
import { db } from "@workspace/db";
|
|
|
|
|
import {
|
|
|
|
|
scansTable,
|
|
|
|
|
scanFilesTable,
|
|
|
|
|
findingsTable,
|
|
|
|
|
type Scan,
|
|
|
|
|
type ScanFile,
|
|
|
|
|
type Finding,
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
type ScanRelation,
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
} from "@workspace/db";
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
import { eq, desc, count } from "drizzle-orm";
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
import {
|
|
|
|
|
ListScansResponse,
|
|
|
|
|
CreateScanBody,
|
|
|
|
|
GetScanParams,
|
|
|
|
|
GetScanResponse,
|
|
|
|
|
DeleteScanParams,
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
CompareScansParams,
|
|
|
|
|
CompareScansResponse,
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
} from "@workspace/api-zod";
|
|
|
|
|
import {
|
|
|
|
|
parseZip,
|
|
|
|
|
parseSingleFile,
|
|
|
|
|
parseText,
|
|
|
|
|
deriveScanName,
|
|
|
|
|
} from "../lib/skillParser";
|
2026-06-10 18:53:17 +00:00
|
|
|
import { analyzeSkill, type EngineResult } from "../lib/scanEngine";
|
|
|
|
|
import { STATIC_RULES, AI_RULES, type ParsedFile } from "../lib/ruleCatalog";
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
import { computeFingerprint } from "../lib/skillFingerprint";
|
|
|
|
|
import { lineDiff, lineSimilarity } from "../lib/lineDiff";
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
import { logger } from "../lib/logger";
|
|
|
|
|
|
|
|
|
|
const router: IRouter = Router();
|
|
|
|
|
|
2026-06-10 18:53:17 +00:00
|
|
|
type CreateScanInput = ReturnType<typeof CreateScanBody.parse>;
|
|
|
|
|
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
export function serializeScan(scan: Scan) {
|
|
|
|
|
return {
|
|
|
|
|
id: scan.id,
|
|
|
|
|
name: scan.name,
|
|
|
|
|
source: scan.source,
|
|
|
|
|
status: scan.status,
|
|
|
|
|
verdict: scan.verdict,
|
|
|
|
|
riskScore: scan.riskScore,
|
|
|
|
|
fileCount: scan.fileCount,
|
|
|
|
|
aiUsed: scan.aiUsed,
|
|
|
|
|
aiError: scan.aiError,
|
|
|
|
|
findingCounts: scan.findingCounts,
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
fingerprint: scan.fingerprint,
|
|
|
|
|
relation: scan.relation,
|
|
|
|
|
similarity: scan.similarity,
|
|
|
|
|
comparedScanId: scan.comparedScanId,
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
createdAt: scan.createdAt.toISOString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function serializeFile(f: ScanFile) {
|
|
|
|
|
return {
|
|
|
|
|
path: f.path,
|
|
|
|
|
kind: f.kind,
|
|
|
|
|
language: f.language,
|
|
|
|
|
size: f.size,
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
hash: f.hash,
|
|
|
|
|
hasContent: f.content !== null,
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
type ComparedScan = {
|
|
|
|
|
id: number;
|
|
|
|
|
name: string;
|
|
|
|
|
verdict: string;
|
|
|
|
|
riskScore: number;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async function resolveComparedScan(
|
|
|
|
|
id: number | null,
|
|
|
|
|
): Promise<ComparedScan | null> {
|
|
|
|
|
if (id == null) return null;
|
|
|
|
|
const [s] = await db.select().from(scansTable).where(eq(scansTable.id, id));
|
|
|
|
|
if (!s) return null;
|
|
|
|
|
return {
|
|
|
|
|
id: s.id,
|
|
|
|
|
name: s.name,
|
|
|
|
|
verdict: s.verdict,
|
|
|
|
|
riskScore: s.riskScore,
|
|
|
|
|
createdAt: s.createdAt.toISOString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function countFingerprint(fingerprint: string): Promise<number> {
|
|
|
|
|
if (!fingerprint) return 1;
|
|
|
|
|
const [row] = await db
|
|
|
|
|
.select({ c: count() })
|
|
|
|
|
.from(scansTable)
|
|
|
|
|
.where(eq(scansTable.fingerprint, fingerprint));
|
|
|
|
|
return Number(row?.c ?? 1);
|
|
|
|
|
}
|
|
|
|
|
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
function serializeFinding(f: Finding) {
|
|
|
|
|
return {
|
|
|
|
|
id: f.id,
|
|
|
|
|
ruleId: f.ruleId,
|
|
|
|
|
axis: f.axis,
|
|
|
|
|
severity: f.severity,
|
|
|
|
|
title: f.title,
|
|
|
|
|
description: f.description,
|
|
|
|
|
remediation: f.remediation,
|
|
|
|
|
file: f.file,
|
|
|
|
|
line: f.line,
|
|
|
|
|
snippet: f.snippet,
|
|
|
|
|
detectedBy: f.detectedBy,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 18:53:17 +00:00
|
|
|
function serializeScanDetail(
|
|
|
|
|
scan: Scan,
|
|
|
|
|
files: ScanFile[],
|
|
|
|
|
findings: Finding[],
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
checkCount: number,
|
|
|
|
|
comparedScan: ComparedScan | null,
|
2026-06-10 18:53:17 +00:00
|
|
|
) {
|
|
|
|
|
return {
|
|
|
|
|
...serializeScan(scan),
|
|
|
|
|
checkpoints: scan.checkpoints ?? [],
|
|
|
|
|
files: files.map(serializeFile),
|
|
|
|
|
findings: [...findings].sort((a, b) => a.id - b.id).map(serializeFinding),
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
checkCount,
|
|
|
|
|
comparedScan,
|
2026-06-10 18:53:17 +00:00
|
|
|
};
|
|
|
|
|
}
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
async function buildScanDetail(
|
|
|
|
|
scan: Scan,
|
|
|
|
|
files: ScanFile[],
|
|
|
|
|
findings: Finding[],
|
|
|
|
|
) {
|
|
|
|
|
const [checkCount, comparedScan] = await Promise.all([
|
|
|
|
|
countFingerprint(scan.fingerprint),
|
|
|
|
|
resolveComparedScan(scan.comparedScanId),
|
|
|
|
|
]);
|
|
|
|
|
return serializeScanDetail(scan, files, findings, checkCount, comparedScan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type RelationInfo = {
|
|
|
|
|
relation: ScanRelation;
|
|
|
|
|
similarity: number | null;
|
|
|
|
|
comparedScanId: number | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determine how the freshly parsed skill relates to the scans already stored.
|
|
|
|
|
* Exact fingerprint match -> identical; otherwise the most content-similar prior
|
|
|
|
|
* skill (when it overlaps enough or shares a byte-identical file) -> modified;
|
|
|
|
|
* nothing meaningfully in common -> new.
|
|
|
|
|
*/
|
|
|
|
|
async function computeRelation(
|
|
|
|
|
fingerprint: string,
|
|
|
|
|
files: ParsedFile[],
|
|
|
|
|
): Promise<RelationInfo> {
|
|
|
|
|
if (fingerprint) {
|
|
|
|
|
const identical = await db
|
|
|
|
|
.select({ id: scansTable.id })
|
|
|
|
|
.from(scansTable)
|
|
|
|
|
.where(eq(scansTable.fingerprint, fingerprint))
|
|
|
|
|
.orderBy(desc(scansTable.createdAt))
|
|
|
|
|
.limit(1);
|
|
|
|
|
if (identical.length > 0) {
|
|
|
|
|
return { relation: "identical", similarity: 100, comparedScanId: identical[0].id };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Group every prior scan's files so we can measure how much of the file tree
|
|
|
|
|
// overlaps. We match on file *paths* (so single-file skills whose content
|
|
|
|
|
// changed are still recognised as a modified version of the same skill) and
|
|
|
|
|
// fall back to hash overlap to disambiguate equally-good path matches.
|
|
|
|
|
const priorFiles = await db
|
|
|
|
|
.select({
|
|
|
|
|
scanId: scanFilesTable.scanId,
|
|
|
|
|
path: scanFilesTable.path,
|
|
|
|
|
hash: scanFilesTable.hash,
|
|
|
|
|
content: scanFilesTable.content,
|
|
|
|
|
})
|
|
|
|
|
.from(scanFilesTable);
|
|
|
|
|
|
|
|
|
|
const byScan = new Map<number, Map<string, { hash: string; content: string | null }>>();
|
|
|
|
|
for (const row of priorFiles) {
|
|
|
|
|
if (!row.path) continue;
|
|
|
|
|
let map = byScan.get(row.scanId);
|
|
|
|
|
if (!map) {
|
|
|
|
|
map = new Map();
|
|
|
|
|
byScan.set(row.scanId, map);
|
|
|
|
|
}
|
|
|
|
|
map.set(row.path, { hash: row.hash, content: row.content });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newPaths = new Set(files.map((f) => f.path));
|
|
|
|
|
const newHashes = new Set(
|
|
|
|
|
files.map((f) => f.hash).filter((h): h is string => Boolean(h)),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Score every prior scan by content-aware similarity (not just path overlap).
|
|
|
|
|
// Path overlap alone is misleading: single-file text skills always share the
|
|
|
|
|
// path "SKILL.md", so unrelated pastes would otherwise look related. We pick
|
|
|
|
|
// the most similar prior scan and only call it a modified version when the
|
|
|
|
|
// content actually overlaps enough OR at least one file is byte-identical.
|
|
|
|
|
let bestId: number | null = null;
|
|
|
|
|
let bestSimilarity = -1;
|
|
|
|
|
let bestHasHashOverlap = false;
|
|
|
|
|
for (const [scanId, map] of byScan) {
|
|
|
|
|
const priorHashes = new Set(
|
|
|
|
|
Array.from(map.values())
|
|
|
|
|
.map((v) => v.hash)
|
|
|
|
|
.filter(Boolean),
|
|
|
|
|
);
|
|
|
|
|
const sharesPath = Array.from(map.keys()).some((p) => newPaths.has(p));
|
|
|
|
|
const hashOverlap = Array.from(priorHashes).some((h) => newHashes.has(h));
|
|
|
|
|
// Nothing in common at all -> cannot be a version of this skill.
|
|
|
|
|
if (!sharesPath && !hashOverlap) continue;
|
|
|
|
|
|
|
|
|
|
const similarity = computeContentSimilarity(files, map);
|
|
|
|
|
if (
|
|
|
|
|
similarity > bestSimilarity ||
|
|
|
|
|
(similarity === bestSimilarity && hashOverlap && !bestHasHashOverlap)
|
|
|
|
|
) {
|
|
|
|
|
bestSimilarity = similarity;
|
|
|
|
|
bestId = scanId;
|
|
|
|
|
bestHasHashOverlap = hashOverlap;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Treat as a modified version only with a meaningful content overlap or a
|
|
|
|
|
// shared byte-identical file; otherwise it is a genuinely new skill that just
|
|
|
|
|
// happens to reuse a common file path.
|
|
|
|
|
if (
|
|
|
|
|
bestId !== null &&
|
|
|
|
|
(bestHasHashOverlap || bestSimilarity >= MODIFIED_SIMILARITY_THRESHOLD)
|
|
|
|
|
) {
|
|
|
|
|
return {
|
|
|
|
|
relation: "modified",
|
|
|
|
|
similarity: bestSimilarity,
|
|
|
|
|
comparedScanId: bestId,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { relation: "new", similarity: null, comparedScanId: null };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Minimum content similarity (0-100) for a non-identical upload to count as a
|
|
|
|
|
* modified version of a prior scan rather than a brand-new skill. Keeps
|
|
|
|
|
* unrelated single-file pastes (which always share the "SKILL.md" path) from
|
|
|
|
|
* being falsely linked together.
|
|
|
|
|
*/
|
|
|
|
|
const MODIFIED_SIMILARITY_THRESHOLD = 40;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Content-aware similarity (0-100) between the new files and a matched prior
|
|
|
|
|
* scan. Identical files (same hash) count fully; changed text files use the
|
|
|
|
|
* line-level similarity; added/removed or changed binary files count as 0.
|
|
|
|
|
*/
|
|
|
|
|
function computeContentSimilarity(
|
|
|
|
|
newFiles: ParsedFile[],
|
|
|
|
|
prior: Map<string, { hash: string; content: string | null }>,
|
|
|
|
|
): number {
|
|
|
|
|
const newByPath = new Map(newFiles.map((f) => [f.path, f]));
|
|
|
|
|
const paths = new Set<string>([...newByPath.keys(), ...prior.keys()]);
|
|
|
|
|
if (paths.size === 0) return 0;
|
|
|
|
|
|
|
|
|
|
let total = 0;
|
|
|
|
|
for (const path of paths) {
|
|
|
|
|
const cur = newByPath.get(path);
|
|
|
|
|
const prev = prior.get(path);
|
|
|
|
|
if (!cur || !prev) continue; // added or removed -> 0
|
|
|
|
|
if (cur.hash && cur.hash === prev.hash) {
|
|
|
|
|
total += 1;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!cur.isBinary && prev.content !== null) {
|
|
|
|
|
total += lineSimilarity(prev.content, cur.content);
|
|
|
|
|
}
|
|
|
|
|
// changed binary -> 0
|
|
|
|
|
}
|
|
|
|
|
return Math.round((total / paths.size) * 100);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 18:53:17 +00:00
|
|
|
type ParseResult =
|
|
|
|
|
| { ok: true; files: ParsedFile[] }
|
|
|
|
|
| { ok: false; status: number; message: string };
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
|
2026-06-10 18:53:17 +00:00
|
|
|
function parseScanInput(input: CreateScanInput): ParseResult {
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
try {
|
2026-06-10 18:53:17 +00:00
|
|
|
let files: ParsedFile[];
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
if (input.source === "zip") {
|
|
|
|
|
if (!input.contentBase64)
|
2026-06-10 18:53:17 +00:00
|
|
|
return { ok: false, status: 400, message: "ZIP-Inhalt fehlt." };
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
files = parseZip(Buffer.from(input.contentBase64, "base64"));
|
|
|
|
|
} else if (input.source === "file") {
|
|
|
|
|
if (!input.contentBase64)
|
2026-06-10 18:53:17 +00:00
|
|
|
return { ok: false, status: 400, message: "Dateiinhalt fehlt." };
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
files = [
|
|
|
|
|
parseSingleFile(
|
|
|
|
|
input.filename ?? "datei",
|
|
|
|
|
Buffer.from(input.contentBase64, "base64"),
|
|
|
|
|
),
|
|
|
|
|
];
|
|
|
|
|
} else {
|
|
|
|
|
if (!input.text || !input.text.trim())
|
2026-06-10 18:53:17 +00:00
|
|
|
return { ok: false, status: 400, message: "Text fehlt." };
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
files = [parseText(input.text)];
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
}
|
2026-06-10 18:53:17 +00:00
|
|
|
if (files.length === 0)
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
status: 400,
|
|
|
|
|
message: "Keine analysierbaren Dateien gefunden.",
|
|
|
|
|
};
|
|
|
|
|
return { ok: true, files };
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
} catch (err) {
|
|
|
|
|
logger.error({ err }, "Skill-Parsing fehlgeschlagen");
|
2026-06-10 18:53:17 +00:00
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
status: 400,
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
message:
|
|
|
|
|
"Das Skill konnte nicht gelesen werden. Bitte prüfen Sie das Format (gültiges ZIP / Textdatei).",
|
2026-06-10 18:53:17 +00:00
|
|
|
};
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
}
|
2026-06-10 18:53:17 +00:00
|
|
|
}
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
|
2026-06-10 18:53:17 +00:00
|
|
|
async function persistScan(
|
|
|
|
|
input: CreateScanInput,
|
|
|
|
|
name: string,
|
|
|
|
|
files: ParsedFile[],
|
|
|
|
|
result: EngineResult,
|
|
|
|
|
): Promise<{ scan: Scan; files: ScanFile[]; findings: Finding[] }> {
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
const fingerprint = computeFingerprint(
|
|
|
|
|
files.map((f) => ({ path: f.path, hash: f.hash })),
|
|
|
|
|
);
|
|
|
|
|
// Determine relation against the existing database BEFORE inserting the new
|
|
|
|
|
// scan so the comparison excludes this scan itself. The skill is always
|
|
|
|
|
// re-scanned; identical uploads are stored as duplicates.
|
|
|
|
|
const relationInfo = await computeRelation(fingerprint, files);
|
|
|
|
|
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
const [scan] = await db
|
|
|
|
|
.insert(scansTable)
|
|
|
|
|
.values({
|
|
|
|
|
name,
|
|
|
|
|
source: input.source,
|
|
|
|
|
status: "completed",
|
|
|
|
|
verdict: result.verdict,
|
|
|
|
|
riskScore: result.riskScore,
|
|
|
|
|
fileCount: files.length,
|
|
|
|
|
aiUsed: result.aiUsed,
|
|
|
|
|
aiError: result.aiError,
|
|
|
|
|
findingCounts: result.counts,
|
2026-06-10 18:53:17 +00:00
|
|
|
checkpoints: result.checkpoints,
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
fingerprint,
|
|
|
|
|
relation: relationInfo.relation,
|
|
|
|
|
similarity: relationInfo.similarity,
|
|
|
|
|
comparedScanId: relationInfo.comparedScanId,
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
})
|
|
|
|
|
.returning();
|
|
|
|
|
|
|
|
|
|
let insertedFiles: ScanFile[] = [];
|
|
|
|
|
if (files.length > 0) {
|
|
|
|
|
insertedFiles = await db
|
|
|
|
|
.insert(scanFilesTable)
|
|
|
|
|
.values(
|
|
|
|
|
files.map((f) => ({
|
|
|
|
|
scanId: scan.id,
|
|
|
|
|
path: f.path,
|
|
|
|
|
kind: f.kind,
|
|
|
|
|
language: f.language,
|
|
|
|
|
size: f.size,
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
hash: f.hash,
|
|
|
|
|
content: f.isBinary ? null : f.content,
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.returning();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let insertedFindings: Finding[] = [];
|
|
|
|
|
if (result.findings.length > 0) {
|
|
|
|
|
insertedFindings = await db
|
|
|
|
|
.insert(findingsTable)
|
|
|
|
|
.values(
|
|
|
|
|
result.findings.map((f) => ({
|
|
|
|
|
scanId: scan.id,
|
|
|
|
|
ruleId: f.ruleId,
|
|
|
|
|
axis: f.axis,
|
|
|
|
|
severity: f.severity,
|
|
|
|
|
title: f.title,
|
|
|
|
|
description: f.description,
|
|
|
|
|
remediation: f.remediation,
|
|
|
|
|
file: f.file,
|
|
|
|
|
line: f.line,
|
|
|
|
|
snippet: f.snippet,
|
|
|
|
|
detectedBy: f.detectedBy,
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.returning();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 18:53:17 +00:00
|
|
|
return { scan, files: insertedFiles, findings: insertedFindings };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
router.get("/scans", async (_req, res) => {
|
|
|
|
|
const rows = await db
|
|
|
|
|
.select()
|
|
|
|
|
.from(scansTable)
|
|
|
|
|
.orderBy(desc(scansTable.createdAt));
|
|
|
|
|
res.json(ListScansResponse.parse(rows.map(serializeScan)));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.post("/scans", async (req, res) => {
|
|
|
|
|
const parsed = CreateScanBody.safeParse(req.body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return res
|
|
|
|
|
.status(400)
|
|
|
|
|
.json({ message: "Ungültige Eingabe", details: parsed.error.issues });
|
|
|
|
|
}
|
|
|
|
|
const input = parsed.data;
|
|
|
|
|
|
|
|
|
|
const parseResult = parseScanInput(input);
|
|
|
|
|
if (!parseResult.ok) {
|
|
|
|
|
return res.status(parseResult.status).json({ message: parseResult.message });
|
|
|
|
|
}
|
|
|
|
|
const files = parseResult.files;
|
|
|
|
|
|
|
|
|
|
const name = input.name?.trim() || deriveScanName(files, "Unbenanntes Skill");
|
|
|
|
|
const result = await analyzeSkill(files, input.useAi);
|
|
|
|
|
const { scan, files: insertedFiles, findings } = await persistScan(
|
|
|
|
|
input,
|
|
|
|
|
name,
|
|
|
|
|
files,
|
|
|
|
|
result,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return res
|
|
|
|
|
.status(201)
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
.json(GetScanResponse.parse(await buildScanDetail(scan, insertedFiles, findings)));
|
2026-06-10 18:53:17 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const STREAM_PACING_MS = 80;
|
|
|
|
|
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
|
|
|
|
|
|
|
|
router.post("/scans/stream", async (req, res) => {
|
|
|
|
|
const parsed = CreateScanBody.safeParse(req.body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
res
|
|
|
|
|
.status(400)
|
|
|
|
|
.json({ message: "Ungültige Eingabe", details: parsed.error.issues });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const input = parsed.data;
|
|
|
|
|
|
|
|
|
|
const parseResult = parseScanInput(input);
|
|
|
|
|
if (!parseResult.ok) {
|
|
|
|
|
res.status(parseResult.status).json({ message: parseResult.message });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const files = parseResult.files;
|
|
|
|
|
const name = input.name?.trim() || deriveScanName(files, "Unbenanntes Skill");
|
|
|
|
|
|
|
|
|
|
res.status(200);
|
|
|
|
|
res.setHeader("Content-Type", "application/x-ndjson; charset=utf-8");
|
|
|
|
|
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
|
|
|
res.setHeader("X-Accel-Buffering", "no");
|
|
|
|
|
res.setHeader("Connection", "keep-alive");
|
|
|
|
|
res.flushHeaders();
|
|
|
|
|
|
|
|
|
|
// Detect a genuine client disconnect. NOTE: do NOT use req.on("close") here —
|
|
|
|
|
// for a POST it fires as soon as the request body is consumed, not on abort.
|
|
|
|
|
// res "close" before writableFinished means the client went away.
|
|
|
|
|
let aborted = false;
|
|
|
|
|
res.on("close", () => {
|
|
|
|
|
if (!res.writableFinished) aborted = true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const write = (obj: unknown) => {
|
|
|
|
|
if (aborted || res.writableEnded) return;
|
|
|
|
|
res.write(JSON.stringify(obj) + "\n");
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
};
|
2026-06-10 18:53:17 +00:00
|
|
|
|
|
|
|
|
write({
|
|
|
|
|
type: "start",
|
|
|
|
|
name,
|
|
|
|
|
fileCount: files.length,
|
|
|
|
|
totalChecks: STATIC_RULES.length + (input.useAi ? AI_RULES.length : 0),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let cumulative = 0;
|
|
|
|
|
try {
|
|
|
|
|
const result = await analyzeSkill(files, input.useAi, async (event) => {
|
|
|
|
|
if (event.type === "ai-start") {
|
|
|
|
|
write({ type: "ai-start" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
cumulative += event.checkpoint.scoreDelta;
|
|
|
|
|
write({
|
|
|
|
|
type: "checkpoint",
|
|
|
|
|
checkpoint: event.checkpoint,
|
|
|
|
|
runningScore: Math.min(100, cumulative),
|
|
|
|
|
});
|
|
|
|
|
if (!aborted) await delay(STREAM_PACING_MS);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { scan } = await persistScan(input, name, files, result);
|
|
|
|
|
|
|
|
|
|
write({
|
|
|
|
|
type: "done",
|
|
|
|
|
scanId: scan.id,
|
|
|
|
|
riskScore: result.riskScore,
|
|
|
|
|
verdict: result.verdict,
|
|
|
|
|
findingCounts: result.counts,
|
|
|
|
|
aiUsed: result.aiUsed,
|
|
|
|
|
aiError: result.aiError,
|
|
|
|
|
});
|
|
|
|
|
if (!aborted && !res.writableEnded) res.end();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.error({ err }, "Streaming-Scan fehlgeschlagen");
|
|
|
|
|
write({ type: "error", message: "Die Analyse ist fehlgeschlagen." });
|
|
|
|
|
if (!aborted && !res.writableEnded) res.end();
|
|
|
|
|
}
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.get("/scans/:id", async (req, res) => {
|
|
|
|
|
const params = GetScanParams.safeParse(req.params);
|
|
|
|
|
if (!params.success)
|
|
|
|
|
return res.status(400).json({ message: "Ungültige ID" });
|
|
|
|
|
|
|
|
|
|
const [scan] = await db
|
|
|
|
|
.select()
|
|
|
|
|
.from(scansTable)
|
|
|
|
|
.where(eq(scansTable.id, params.data.id));
|
|
|
|
|
if (!scan) return res.status(404).json({ message: "Scan nicht gefunden" });
|
|
|
|
|
|
|
|
|
|
const files = await db
|
|
|
|
|
.select()
|
|
|
|
|
.from(scanFilesTable)
|
|
|
|
|
.where(eq(scanFilesTable.scanId, scan.id));
|
|
|
|
|
const findings = await db
|
|
|
|
|
.select()
|
|
|
|
|
.from(findingsTable)
|
|
|
|
|
.where(eq(findingsTable.scanId, scan.id))
|
|
|
|
|
.orderBy(findingsTable.id);
|
|
|
|
|
|
Add Skill-Fingerprint database & report comparison
Each scan gets a deterministic overall fingerprint (SHA-256 over sorted
path+fileHash pairs) plus per-file SHA-256 hashes and stored text content
(binary: hash+size only). On upload the skill is always re-scanned and
classified vs prior scans as new / identical / modified, with a per-fingerprint
check counter, a "most similar known skill" link, and a file-level diff view.
Deviations from the plan:
- Relation matching keys off shared file *paths* (Jaccard over paths, tie-break
on hashes), not hash-Jaccard alone, which is always 0 for single-file edits
(text paste = one SKILL.md) and would mis-class every edited single-file skill
as "new". Similarity is content-aware: identical files = 1.0, changed text
files use line-level LCS ratio, added/removed/changed-binary = 0.
- parseText no longer uses the display name as the file path (fixed "SKILL.md")
so identical pastes with different names are "identical", not "modified".
Backend: skillFingerprint.ts, lineDiff.ts (+lineSimilarity), skillParser.ts
(per-file hash+isBinary), routes/scans.ts (computeRelation, content similarity,
checkCount, comparedScan, GET /scans/:id/compare/:otherId). DB: scans
fingerprint/relation/similarity/comparedScanId (+index), scan_files hash/content.
API spec + orval codegen regenerated. UI: fingerprint card + compare link on
report, relation badges in history, new /vergleich/:id/:otherId page with
side-by-side summaries and expandable line diff. German UI, no emojis.
Verified end-to-end against the running API and screenshotted both UI pages;
test data cleaned up afterward.
Code-review fix: relation classification no longer relies on path-Jaccard
(every text paste shares path SKILL.md, so unrelated pastes were falsely
linked as "modified"). computeRelation now selects the candidate by
content-aware similarity and only returns "modified" when similarity >= 40
or a file is byte-identical; otherwise "new". Updated OpenAPI similarity
description; removed now-unused jaccard import.
Replit-Task-Id: 79a8e472-6635-493c-8995-3233ba7df75c
2026-06-10 19:34:46 +00:00
|
|
|
return res.json(GetScanResponse.parse(await buildScanDetail(scan, files, findings)));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.get("/scans/:id/compare/:otherId", async (req, res) => {
|
|
|
|
|
const params = CompareScansParams.safeParse(req.params);
|
|
|
|
|
if (!params.success)
|
|
|
|
|
return res.status(400).json({ message: "Ungültige ID" });
|
|
|
|
|
|
|
|
|
|
const { id, otherId } = params.data;
|
|
|
|
|
|
|
|
|
|
const [current] = await db
|
|
|
|
|
.select()
|
|
|
|
|
.from(scansTable)
|
|
|
|
|
.where(eq(scansTable.id, id));
|
|
|
|
|
const [previous] = await db
|
|
|
|
|
.select()
|
|
|
|
|
.from(scansTable)
|
|
|
|
|
.where(eq(scansTable.id, otherId));
|
|
|
|
|
|
|
|
|
|
if (!current || !previous)
|
|
|
|
|
return res.status(404).json({ message: "Scan nicht gefunden" });
|
|
|
|
|
|
|
|
|
|
const [currentFiles, previousFiles] = await Promise.all([
|
|
|
|
|
db.select().from(scanFilesTable).where(eq(scanFilesTable.scanId, id)),
|
|
|
|
|
db.select().from(scanFilesTable).where(eq(scanFilesTable.scanId, otherId)),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const currentByPath = new Map(currentFiles.map((f) => [f.path, f]));
|
|
|
|
|
const previousByPath = new Map(previousFiles.map((f) => [f.path, f]));
|
|
|
|
|
const paths = Array.from(
|
|
|
|
|
new Set([...currentByPath.keys(), ...previousByPath.keys()]),
|
|
|
|
|
).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
|
|
|
|
|
|
|
|
const fileDiffs = paths.map((path) => {
|
|
|
|
|
const cur = currentByPath.get(path) ?? null;
|
|
|
|
|
const prev = previousByPath.get(path) ?? null;
|
|
|
|
|
|
|
|
|
|
let status: "unchanged" | "modified" | "added" | "removed";
|
|
|
|
|
if (cur && !prev) status = "added";
|
|
|
|
|
else if (!cur && prev) status = "removed";
|
|
|
|
|
else if (cur && prev && cur.hash === prev.hash) status = "unchanged";
|
|
|
|
|
else status = "modified";
|
|
|
|
|
|
|
|
|
|
let diff:
|
|
|
|
|
| {
|
|
|
|
|
type: "context" | "add" | "remove";
|
|
|
|
|
text: string;
|
|
|
|
|
previousLine: number | null;
|
|
|
|
|
currentLine: number | null;
|
|
|
|
|
}[]
|
|
|
|
|
| null = null;
|
|
|
|
|
if (
|
|
|
|
|
status === "modified" &&
|
|
|
|
|
cur?.content !== null &&
|
|
|
|
|
cur?.content !== undefined &&
|
|
|
|
|
prev?.content !== null &&
|
|
|
|
|
prev?.content !== undefined
|
|
|
|
|
) {
|
|
|
|
|
diff = lineDiff(prev.content, cur.content);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
path,
|
|
|
|
|
status,
|
|
|
|
|
previousHash: prev?.hash ?? null,
|
|
|
|
|
currentHash: cur?.hash ?? null,
|
|
|
|
|
previousSize: prev?.size ?? null,
|
|
|
|
|
currentSize: cur?.size ?? null,
|
|
|
|
|
previousHasContent: prev ? prev.content !== null : null,
|
|
|
|
|
currentHasContent: cur ? cur.content !== null : null,
|
|
|
|
|
lineDiff: diff,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const side = (s: Scan) => ({
|
|
|
|
|
id: s.id,
|
|
|
|
|
name: s.name,
|
|
|
|
|
verdict: s.verdict,
|
|
|
|
|
riskScore: s.riskScore,
|
|
|
|
|
fileCount: s.fileCount,
|
|
|
|
|
fingerprint: s.fingerprint,
|
|
|
|
|
createdAt: s.createdAt.toISOString(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return res.json(
|
|
|
|
|
CompareScansResponse.parse({
|
|
|
|
|
current: side(current),
|
|
|
|
|
previous: side(previous),
|
|
|
|
|
files: fileDiffs,
|
|
|
|
|
}),
|
|
|
|
|
);
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.delete("/scans/:id", async (req, res) => {
|
|
|
|
|
const params = DeleteScanParams.safeParse(req.params);
|
|
|
|
|
if (!params.success)
|
|
|
|
|
return res.status(400).json({ message: "Ungültige ID" });
|
|
|
|
|
await db.delete(scansTable).where(eq(scansTable.id, params.data.id));
|
|
|
|
|
return res.status(204).send();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export default router;
|