Add skill version timeline (fingerprint lineage)
Task #14: show a full version timeline for each skill family, not just the single most-similar prior scan. What changed: - OpenAPI spec (lib/api-spec/openapi.yaml): new GET /scans/{id}/lineage (operationId getScanLineage) returning an array of ScanLineageEntry (id, name, verdict, riskScore, relation, similarity, comparedScanId, fingerprint, createdAt). Regenerated api-zod + api-client-react via codegen. - API (artifacts/api-server/src/routes/scans.ts): new lineage endpoint. Builds an undirected graph over all scans linked by the comparedScanId chain AND identical (non-empty) fingerprints, then BFS-walks the connected component containing the requested scan and returns it newest-first. Works purely from existing data, no re-scanning. 404 for unknown ids. - UI (artifacts/skillguard/src/pages/scan-report.tsx): new VersionTimeline card rendering the family as a vertical timeline; each entry shows verdict, relation badge, similarity, risk score and date. The viewed scan is marked "Aktuell angezeigt"; every other entry links to the existing comparison view /vergleich/{viewedId}/{entryId}. Card hidden when the family has <=1 member. Notes: - Lineage = connected component, so any member returns the full family. - Verified end-to-end locally (created new/modified/identical chain, checked lineage ordering + 404, confirmed timeline + compare links in the UI), then deleted the test scans. Replit-Task-Id: c7f87ce6-59d8-4396-b16b-f20846f42f0b
This commit is contained in:
parent
ba9788a93c
commit
54323706b5
13 changed files with 477 additions and 2 deletions
|
|
@ -2,3 +2,4 @@
|
|||
- [OpenAI gpt-5 temperature](openai-temperature-gpt5.md) — gpt-5* reject `temperature != 1`; omit temperature in OpenAI-compatible clients or AI analysis silently fails.
|
||||
- [NDJSON streaming on Replit](ndjson-streaming-express-replit.md) — use `res.on("close")`+`writableFinished` (NOT `req.on("close")`); persist on disconnect; proxy doesn't buffer; gate fallback to avoid dup rows.
|
||||
- [Skill fingerprint & relation matching](skill-fingerprint-matching.md) — don't put display name in fingerprint path; match modified by file-path Jaccard (hash-Jaccard misses single-file edits), report content-aware similarity.
|
||||
- [Testing api-server from shell](api-server-local-curl.md) — external `$REPLIT_DEV_DOMAIN/api` curl returns HTTP 000; curl `http://localhost:<PORT>/api` instead (port from workflow log).
|
||||
|
|
|
|||
15
.agents/memory/api-server-local-curl.md
Normal file
15
.agents/memory/api-server-local-curl.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: Testing the SkillGuard api-server from the shell
|
||||
description: How to reach the api-server with curl during development (external proxy fails).
|
||||
---
|
||||
|
||||
# Testing the api-server from the shell
|
||||
|
||||
Curling the api-server through the external proxy (`$REPLIT_DEV_DOMAIN/api/...`)
|
||||
fails during development — it returns HTTP 000 / connection refused (the proxy
|
||||
routes by the selected artifact + mTLS, so a bare curl does not reach it).
|
||||
|
||||
**How to apply:** curl the api-server directly on its local port instead, e.g.
|
||||
`http://localhost:8080/api/scans`. Confirm the port from the api-server workflow
|
||||
log line `Server listening port: <PORT>` (it reads `PORT`, defaulting to 8080).
|
||||
The web artifact's preview, however, reaches the API fine through the proxy.
|
||||
|
|
@ -18,6 +18,7 @@ import {
|
|||
DeleteScanParams,
|
||||
CompareScansParams,
|
||||
CompareScansResponse,
|
||||
GetScanLineageResponse,
|
||||
} from "@workspace/api-zod";
|
||||
import {
|
||||
parseZip,
|
||||
|
|
@ -639,6 +640,88 @@ router.get("/scans/:id/compare/:otherId", async (req, res) => {
|
|||
);
|
||||
});
|
||||
|
||||
router.get("/scans/:id/lineage", 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" });
|
||||
|
||||
// Load only the columns needed to reconstruct the lineage graph for every
|
||||
// stored scan, then walk the connected component containing this scan.
|
||||
const all = await db
|
||||
.select({
|
||||
id: scansTable.id,
|
||||
name: scansTable.name,
|
||||
verdict: scansTable.verdict,
|
||||
riskScore: scansTable.riskScore,
|
||||
relation: scansTable.relation,
|
||||
similarity: scansTable.similarity,
|
||||
comparedScanId: scansTable.comparedScanId,
|
||||
fingerprint: scansTable.fingerprint,
|
||||
createdAt: scansTable.createdAt,
|
||||
})
|
||||
.from(scansTable);
|
||||
|
||||
const byId = new Map(all.map((s) => [s.id, s]));
|
||||
|
||||
// Build an undirected graph: scans are linked when one was compared against
|
||||
// the other (comparedScanId chain) or when they share an identical
|
||||
// fingerprint. The fingerprint family is the connected component.
|
||||
const adjacency = new Map<number, Set<number>>();
|
||||
const addEdge = (a: number, b: number) => {
|
||||
if (!byId.has(a) || !byId.has(b) || a === b) return;
|
||||
(adjacency.get(a) ?? adjacency.set(a, new Set()).get(a)!).add(b);
|
||||
(adjacency.get(b) ?? adjacency.set(b, new Set()).get(b)!).add(a);
|
||||
};
|
||||
|
||||
const byFingerprint = new Map<string, number[]>();
|
||||
for (const s of all) {
|
||||
if (s.comparedScanId != null) addEdge(s.id, s.comparedScanId);
|
||||
if (s.fingerprint) {
|
||||
const list = byFingerprint.get(s.fingerprint) ?? [];
|
||||
list.push(s.id);
|
||||
byFingerprint.set(s.fingerprint, list);
|
||||
}
|
||||
}
|
||||
for (const ids of byFingerprint.values()) {
|
||||
for (let i = 1; i < ids.length; i++) addEdge(ids[0], ids[i]);
|
||||
}
|
||||
|
||||
const family = new Set<number>([scan.id]);
|
||||
const queue: number[] = [scan.id];
|
||||
while (queue.length > 0) {
|
||||
const cur = queue.shift()!;
|
||||
for (const next of adjacency.get(cur) ?? []) {
|
||||
if (!family.has(next)) {
|
||||
family.add(next);
|
||||
queue.push(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const entries = Array.from(family)
|
||||
.map((fid) => byId.get(fid)!)
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||
.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
verdict: s.verdict,
|
||||
riskScore: s.riskScore,
|
||||
relation: s.relation,
|
||||
similarity: s.similarity,
|
||||
comparedScanId: s.comparedScanId,
|
||||
fingerprint: s.fingerprint,
|
||||
createdAt: s.createdAt.toISOString(),
|
||||
}));
|
||||
|
||||
return res.json(GetScanLineageResponse.parse(entries));
|
||||
});
|
||||
|
||||
router.delete("/scans/:id", async (req, res) => {
|
||||
const params = DeleteScanParams.safeParse(req.params);
|
||||
if (!params.success)
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 81 KiB |
|
|
@ -1,6 +1,11 @@
|
|||
import { useState, useMemo } from "react";
|
||||
import { useRoute, Link } from "wouter";
|
||||
import { useGetScan, getGetScanQueryKey } from "@workspace/api-client-react";
|
||||
import {
|
||||
useGetScan,
|
||||
getGetScanQueryKey,
|
||||
useGetScanLineage,
|
||||
getGetScanLineageQueryKey,
|
||||
} from "@workspace/api-client-react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
|
@ -11,7 +16,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { Progress } from "@/components/ui/progress";
|
||||
import { VerdictBadge, SeverityBadge, AxisBadge, CheckpointStatusBadge, CHECKPOINT_STATUS_LABELS, RelationBadge } from "@/components/ui-helpers";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import { ShieldQuestion, AlertTriangle, Download, FileCode, CheckCircle2, Code, Shield, FileDown, ListChecks, Fingerprint, GitCompare, History } from "lucide-react";
|
||||
import { ShieldQuestion, AlertTriangle, Download, FileCode, CheckCircle2, Code, Shield, FileDown, ListChecks, Fingerprint, GitCompare, History, GitCommitVertical } from "lucide-react";
|
||||
import type { ScanDetail } from "@workspace/api-client-react";
|
||||
|
||||
export default function ScanReport() {
|
||||
|
|
@ -265,6 +270,8 @@ export default function ScanReport() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<VersionTimeline scanId={data.id} />
|
||||
|
||||
<Tabs defaultValue="findings" className="w-full">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="findings" className="gap-2"><Shield className="w-4 h-4"/> Auffälligkeiten ({data.findings.length})</TabsTrigger>
|
||||
|
|
@ -469,6 +476,87 @@ export default function ScanReport() {
|
|||
);
|
||||
}
|
||||
|
||||
function VersionTimeline({ scanId }: { scanId: number }) {
|
||||
const { data, isLoading } = useGetScanLineage(scanId, {
|
||||
query: {
|
||||
enabled: Number.isFinite(scanId) && scanId > 0,
|
||||
queryKey: getGetScanLineageQueryKey(scanId),
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton className="h-48 w-full" />;
|
||||
}
|
||||
|
||||
// Nothing meaningful to show unless this skill has more than one known version.
|
||||
if (!data || data.length <= 1) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<History className="w-5 h-5" /> Versionsverlauf
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Alle bekannten Versionen dieses Skills (verknüpft über Fingerprint-Abstammung), neueste zuerst. Wählen Sie eine Version, um den Vergleich anzuzeigen.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ol className="relative border-l border-border ml-3 space-y-6">
|
||||
{data.map((entry) => {
|
||||
const isCurrent = entry.id === scanId;
|
||||
return (
|
||||
<li key={entry.id} className="ml-6">
|
||||
<span
|
||||
className={`absolute -left-[9px] flex h-4 w-4 items-center justify-center rounded-full ring-4 ring-background ${
|
||||
isCurrent ? "bg-primary" : "bg-muted-foreground/40"
|
||||
}`}
|
||||
>
|
||||
<GitCommitVertical className="w-3 h-3 text-background" />
|
||||
</span>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between rounded-lg border bg-muted/20 p-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{isCurrent ? (
|
||||
<span className="font-medium">{entry.name || `Scan #${entry.id}`}</span>
|
||||
) : (
|
||||
<Link href={`/berichte/${entry.id}`} className="font-medium hover:underline">
|
||||
{entry.name || `Scan #${entry.id}`}
|
||||
</Link>
|
||||
)}
|
||||
<VerdictBadge verdict={entry.verdict} />
|
||||
<RelationBadge relation={entry.relation} />
|
||||
{entry.relation === "modified" && entry.similarity != null && (
|
||||
<Badge variant="outline" className="font-mono text-xs">{entry.similarity}% ähnlich</Badge>
|
||||
)}
|
||||
{isCurrent && (
|
||||
<Badge variant="secondary" className="text-xs">Aktuell angezeigt</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>{formatDate(entry.createdAt)}</span>
|
||||
<span>·</span>
|
||||
<span>Risiko {entry.riskScore} / 100</span>
|
||||
</div>
|
||||
</div>
|
||||
{!isCurrent && (
|
||||
<Button asChild variant="outline" size="sm" className="gap-2 shrink-0">
|
||||
<Link href={`/vergleich/${scanId}/${entry.id}`}>
|
||||
<GitCompare className="w-4 h-4" />
|
||||
Vergleich
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const VERDICT_LABELS: Record<string, string> = {
|
||||
pass: "Freigabe",
|
||||
review: "Manuelle Prüfung",
|
||||
|
|
|
|||
|
|
@ -286,6 +286,52 @@ export type ScanDetail = Scan & ({
|
|||
comparedScan: ComparedScan | null;
|
||||
});
|
||||
|
||||
export type ScanLineageEntryVerdict = typeof ScanLineageEntryVerdict[keyof typeof ScanLineageEntryVerdict];
|
||||
|
||||
|
||||
export const ScanLineageEntryVerdict = {
|
||||
pass: 'pass',
|
||||
review: 'review',
|
||||
block: 'block',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Relation of this version to the one it was compared against
|
||||
* @nullable
|
||||
*/
|
||||
export type ScanLineageEntryRelation = typeof ScanLineageEntryRelation[keyof typeof ScanLineageEntryRelation] | null;
|
||||
|
||||
|
||||
export const ScanLineageEntryRelation = {
|
||||
new: 'new',
|
||||
identical: 'identical',
|
||||
modified: 'modified',
|
||||
} as const;
|
||||
|
||||
export interface ScanLineageEntry {
|
||||
id: number;
|
||||
name: string;
|
||||
verdict: ScanLineageEntryVerdict;
|
||||
riskScore: number;
|
||||
/**
|
||||
* Relation of this version to the one it was compared against
|
||||
* @nullable
|
||||
*/
|
||||
relation: ScanLineageEntryRelation;
|
||||
/**
|
||||
* Content-aware similarity (0-100) to its compared version
|
||||
* @nullable
|
||||
*/
|
||||
similarity: number | null;
|
||||
/**
|
||||
* The prior version this scan was compared against, if any
|
||||
* @nullable
|
||||
*/
|
||||
comparedScanId: number | null;
|
||||
fingerprint: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type ScanComparisonSideVerdict = typeof ScanComparisonSideVerdict[keyof typeof ScanComparisonSideVerdict];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import type {
|
|||
Scan,
|
||||
ScanComparison,
|
||||
ScanDetail,
|
||||
ScanLineageEntry,
|
||||
SkillScanInput
|
||||
} from './api.schemas';
|
||||
|
||||
|
|
@ -438,6 +439,84 @@ export function useCompareScans<TData = Awaited<ReturnType<typeof compareScans>>
|
|||
|
||||
|
||||
|
||||
export const getGetScanLineageUrl = (id: number,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `/api/scans/${id}/lineage`
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns every scan in the same fingerprint lineage as the given scan (linked by an identical fingerprint or by the comparedScanId chain), newest first, so the full version history of a skill can be shown on a timeline without re-scanning.
|
||||
* @summary Get the version timeline for a skill family
|
||||
*/
|
||||
export const getScanLineage = async (id: number, options?: RequestInit): Promise<ScanLineageEntry[]> => {
|
||||
|
||||
return customFetch<ScanLineageEntry[]>(getGetScanLineageUrl(id),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const getGetScanLineageQueryKey = (id: number,) => {
|
||||
return [
|
||||
`/api/scans/${id}/lineage`
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getGetScanLineageQueryOptions = <TData = Awaited<ReturnType<typeof getScanLineage>>, TError = ErrorType<ApiError>>(id: number, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getScanLineage>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
||||
) => {
|
||||
|
||||
const {query: queryOptions, request: requestOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetScanLineageQueryKey(id);
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getScanLineage>>> = ({ signal }) => getScanLineage(id, { signal, ...requestOptions });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getScanLineage>>, TError, TData> & { queryKey: QueryKey }
|
||||
}
|
||||
|
||||
export type GetScanLineageQueryResult = NonNullable<Awaited<ReturnType<typeof getScanLineage>>>
|
||||
export type GetScanLineageQueryError = ErrorType<ApiError>
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get the version timeline for a skill family
|
||||
*/
|
||||
|
||||
export function useGetScanLineage<TData = Awaited<ReturnType<typeof getScanLineage>>, TError = ErrorType<ApiError>>(
|
||||
id: number, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getScanLineage>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
||||
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
|
||||
const queryOptions = getGetScanLineageQueryOptions(id,options)
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const getGetScanUrl = (id: number,) => {
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -125,6 +125,38 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/ApiError"
|
||||
|
||||
/scans/{id}/lineage:
|
||||
get:
|
||||
operationId: getScanLineage
|
||||
tags: [scans]
|
||||
summary: Get the version timeline for a skill family
|
||||
description: >-
|
||||
Returns every scan in the same fingerprint lineage as the given scan
|
||||
(linked by an identical fingerprint or by the comparedScanId chain),
|
||||
newest first, so the full version history of a skill can be shown on a
|
||||
timeline without re-scanning.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
description: Version timeline (most recent first)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ScanLineageEntry"
|
||||
"404":
|
||||
description: Not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ApiError"
|
||||
|
||||
/scans/{id}:
|
||||
get:
|
||||
operationId: getScan
|
||||
|
|
@ -598,6 +630,43 @@ components:
|
|||
createdAt:
|
||||
type: string
|
||||
|
||||
ScanLineageEntry:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- verdict
|
||||
- riskScore
|
||||
- relation
|
||||
- similarity
|
||||
- comparedScanId
|
||||
- fingerprint
|
||||
- createdAt
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
verdict:
|
||||
type: string
|
||||
enum: [pass, review, block]
|
||||
riskScore:
|
||||
type: integer
|
||||
relation:
|
||||
type: ["string", "null"]
|
||||
enum: [new, identical, modified, null]
|
||||
description: Relation of this version to the one it was compared against
|
||||
similarity:
|
||||
type: ["integer", "null"]
|
||||
description: Content-aware similarity (0-100) to its compared version
|
||||
comparedScanId:
|
||||
type: ["integer", "null"]
|
||||
description: The prior version this scan was compared against, if any
|
||||
fingerprint:
|
||||
type: string
|
||||
createdAt:
|
||||
type: string
|
||||
|
||||
ScanComparisonSide:
|
||||
type: object
|
||||
required: [id, name, verdict, riskScore, fileCount, fingerprint, createdAt]
|
||||
|
|
|
|||
|
|
@ -168,6 +168,28 @@ export const CompareScansResponse = zod.object({
|
|||
})
|
||||
|
||||
|
||||
/**
|
||||
* Returns every scan in the same fingerprint lineage as the given scan (linked by an identical fingerprint or by the comparedScanId chain), newest first, so the full version history of a skill can be shown on a timeline without re-scanning.
|
||||
* @summary Get the version timeline for a skill family
|
||||
*/
|
||||
export const GetScanLineageParams = zod.object({
|
||||
"id": zod.coerce.number()
|
||||
})
|
||||
|
||||
export const GetScanLineageResponseItem = zod.object({
|
||||
"id": zod.number(),
|
||||
"name": zod.string(),
|
||||
"verdict": zod.enum(['pass', 'review', 'block']),
|
||||
"riskScore": zod.number(),
|
||||
"relation": zod.union([zod.literal('new'),zod.literal('identical'),zod.literal('modified'),zod.literal(null)]).nullable().describe('Relation of this version to the one it was compared against'),
|
||||
"similarity": zod.number().nullable().describe('Content-aware similarity (0-100) to its compared version'),
|
||||
"comparedScanId": zod.number().nullable().describe('The prior version this scan was compared against, if any'),
|
||||
"fingerprint": zod.string(),
|
||||
"createdAt": zod.string()
|
||||
})
|
||||
export const GetScanLineageResponse = zod.array(GetScanLineageResponseItem)
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get a scan report with findings
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ export * from './scanFile';
|
|||
export * from './scanFileDiff';
|
||||
export * from './scanFileDiffStatus';
|
||||
export * from './scanFileKind';
|
||||
export * from './scanLineageEntry';
|
||||
export * from './scanLineageEntryRelation';
|
||||
export * from './scanLineageEntryVerdict';
|
||||
export * from './scanRelation';
|
||||
export * from './scanSource';
|
||||
export * from './scanStatus';
|
||||
|
|
|
|||
33
lib/api-zod/src/generated/types/scanLineageEntry.ts
Normal file
33
lib/api-zod/src/generated/types/scanLineageEntry.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Generated by orval v8.9.1 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
import type { ScanLineageEntryRelation } from './scanLineageEntryRelation';
|
||||
import type { ScanLineageEntryVerdict } from './scanLineageEntryVerdict';
|
||||
|
||||
export interface ScanLineageEntry {
|
||||
id: number;
|
||||
name: string;
|
||||
verdict: ScanLineageEntryVerdict;
|
||||
riskScore: number;
|
||||
/**
|
||||
* Relation of this version to the one it was compared against
|
||||
* @nullable
|
||||
*/
|
||||
relation: ScanLineageEntryRelation;
|
||||
/**
|
||||
* Content-aware similarity (0-100) to its compared version
|
||||
* @nullable
|
||||
*/
|
||||
similarity: number | null;
|
||||
/**
|
||||
* The prior version this scan was compared against, if any
|
||||
* @nullable
|
||||
*/
|
||||
comparedScanId: number | null;
|
||||
fingerprint: string;
|
||||
createdAt: string;
|
||||
}
|
||||
20
lib/api-zod/src/generated/types/scanLineageEntryRelation.ts
Normal file
20
lib/api-zod/src/generated/types/scanLineageEntryRelation.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Generated by orval v8.9.1 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Relation of this version to the one it was compared against
|
||||
* @nullable
|
||||
*/
|
||||
export type ScanLineageEntryRelation = typeof ScanLineageEntryRelation[keyof typeof ScanLineageEntryRelation] | null;
|
||||
|
||||
|
||||
export const ScanLineageEntryRelation = {
|
||||
new: 'new',
|
||||
identical: 'identical',
|
||||
modified: 'modified',
|
||||
} as const;
|
||||
16
lib/api-zod/src/generated/types/scanLineageEntryVerdict.ts
Normal file
16
lib/api-zod/src/generated/types/scanLineageEntryVerdict.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* Generated by orval v8.9.1 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export type ScanLineageEntryVerdict = typeof ScanLineageEntryVerdict[keyof typeof ScanLineageEntryVerdict];
|
||||
|
||||
|
||||
export const ScanLineageEntryVerdict = {
|
||||
pass: 'pass',
|
||||
review: 'review',
|
||||
block: 'block',
|
||||
} as const;
|
||||
Loading…
Add table
Reference in a new issue