ghcr-manager 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/dist/cli/_args.d.ts +6 -0
- package/dist/cli/_args.js +38 -0
- package/dist/cli/_logger.d.ts +9 -0
- package/dist/cli/_logger.js +24 -0
- package/dist/cli/_scan-command.d.ts +1 -0
- package/dist/cli/_scan-command.js +32 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +30 -0
- package/dist/core/_types.d.ts +49 -0
- package/dist/core/_types.js +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/db/_manifest-reachability.d.ts +2 -0
- package/dist/db/_manifest-reachability.js +94 -0
- package/dist/db/_scan-writer.d.ts +18 -0
- package/dist/db/_scan-writer.js +176 -0
- package/dist/db/_schema.d.ts +2 -0
- package/dist/db/_schema.js +19 -0
- package/dist/db/_snapshot-repository.d.ts +24 -0
- package/dist/db/_snapshot-repository.js +98 -0
- package/dist/db/index.d.ts +4 -0
- package/dist/db/index.js +9 -0
- package/dist/ingest/github/_manifest-client.d.ts +8 -0
- package/dist/ingest/github/_manifest-client.js +100 -0
- package/dist/ingest/github/_manifest-ingest.d.ts +3 -0
- package/dist/ingest/github/_manifest-ingest.js +104 -0
- package/dist/ingest/github/_package-version-page-load.d.ts +13 -0
- package/dist/ingest/github/_package-version-page-load.js +52 -0
- package/dist/ingest/github/_packages-client.d.ts +10 -0
- package/dist/ingest/github/_packages-client.js +59 -0
- package/dist/ingest/github/_paginated-ingest.d.ts +11 -0
- package/dist/ingest/github/_paginated-ingest.js +28 -0
- package/dist/ingest/github/_parallel-paginated-ingest.d.ts +11 -0
- package/dist/ingest/github/_parallel-paginated-ingest.js +49 -0
- package/dist/ingest/github/_registry-token-client.d.ts +6 -0
- package/dist/ingest/github/_registry-token-client.js +67 -0
- package/dist/ingest/github/_shared.d.ts +28 -0
- package/dist/ingest/github/_shared.js +102 -0
- package/dist/ingest/github/index.d.ts +7 -0
- package/dist/ingest/github/index.js +26 -0
- package/dist/tuning/index.d.ts +6 -0
- package/dist/tuning/index.js +6 -0
- package/package.json +59 -0
- package/resources/sql/schema/001_schema.sql +109 -0
- package/resources/sql/views/001_v_latest_scan_per_package.sql +27 -0
- package/resources/sql/views/002_v_missing_digests.sql +32 -0
- package/resources/sql/views/003_v_missing_digests_related_manifests.sql +78 -0
- package/resources/sql/views/004_v_manifests_related_manifests.sql +142 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { buildFetchTransportErrorMessage, buildHttpErrorMessage, withFetchRetry } from "./_shared.js";
|
|
2
|
+
export async function loadRegistryPullToken(fetchImpl, registryBaseUrl, options) {
|
|
3
|
+
const startTime = Date.now();
|
|
4
|
+
const url = _buildRegistryTokenUrl(registryBaseUrl, options);
|
|
5
|
+
let response;
|
|
6
|
+
try {
|
|
7
|
+
response = await withFetchRetry(async () => {
|
|
8
|
+
const tokenResponse = await fetchImpl(url, {
|
|
9
|
+
headers: _buildTokenHeaders(options)
|
|
10
|
+
});
|
|
11
|
+
if (!tokenResponse.ok && _shouldRetryStatus(tokenResponse.status)) {
|
|
12
|
+
throw new Error(await buildHttpErrorMessage(tokenResponse, "GHCR token request failed"));
|
|
13
|
+
}
|
|
14
|
+
return tokenResponse;
|
|
15
|
+
}, {
|
|
16
|
+
logger: options.logger,
|
|
17
|
+
label: "GHCR token request",
|
|
18
|
+
shouldRetry: (error) => _shouldRetryError(error)
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw new Error(buildFetchTransportErrorMessage(error, "GHCR token request failed"), {
|
|
23
|
+
cause: error
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(await buildHttpErrorMessage(response, "GHCR token request failed"));
|
|
28
|
+
}
|
|
29
|
+
const body = (await response.json());
|
|
30
|
+
if (!body.token) {
|
|
31
|
+
throw new Error("GHCR token response did not include a token");
|
|
32
|
+
}
|
|
33
|
+
const registryPullToken = {
|
|
34
|
+
token: body.token,
|
|
35
|
+
expiresAt: _getExpiresAt(body.expires_in, body.issued_at)
|
|
36
|
+
};
|
|
37
|
+
options.logger.debug(`Loaded GHCR pull token in ${Date.now() - startTime}ms (expires in ${Math.max(0, Math.round((registryPullToken.expiresAt - Date.now()) / 1000))}s)`);
|
|
38
|
+
return registryPullToken;
|
|
39
|
+
}
|
|
40
|
+
function _buildRegistryTokenUrl(registryBaseUrl, options) {
|
|
41
|
+
const registryUrl = new URL(registryBaseUrl);
|
|
42
|
+
const tokenUrl = new URL("/token", registryUrl);
|
|
43
|
+
tokenUrl.searchParams.set("service", registryUrl.host);
|
|
44
|
+
tokenUrl.searchParams.set("scope", `repository:${options.owner}/${options.packageName}:pull`);
|
|
45
|
+
return tokenUrl.toString();
|
|
46
|
+
}
|
|
47
|
+
function _buildTokenHeaders(options) {
|
|
48
|
+
const basicAuth = Buffer.from(`${options.owner}:${options.token}`).toString("base64");
|
|
49
|
+
return {
|
|
50
|
+
"User-Agent": "ghcr-manager",
|
|
51
|
+
Authorization: `Basic ${basicAuth}`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function _getExpiresAt(expiresIn, issuedAt) {
|
|
55
|
+
const expiresInSeconds = typeof expiresIn === "number" && expiresIn > 0 ? expiresIn : 60;
|
|
56
|
+
const issuedAtMilliseconds = typeof issuedAt === "string" && !Number.isNaN(Date.parse(issuedAt)) ? Date.parse(issuedAt) : Date.now();
|
|
57
|
+
return issuedAtMilliseconds + expiresInSeconds * 1000;
|
|
58
|
+
}
|
|
59
|
+
function _shouldRetryStatus(status) {
|
|
60
|
+
return status === 429 || status === 502 || status === 503 || status === 504;
|
|
61
|
+
}
|
|
62
|
+
function _shouldRetryError(error) {
|
|
63
|
+
if (!(error instanceof Error)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return /fetch failed|status 429|status 502|status 503|status 504/.test(error.message);
|
|
67
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface GitHubScanOptions {
|
|
2
|
+
owner: string;
|
|
3
|
+
packageName: string;
|
|
4
|
+
token: string;
|
|
5
|
+
logger: GitHubScanLogger;
|
|
6
|
+
}
|
|
7
|
+
export interface GitHubScanLogger {
|
|
8
|
+
debug(message: string): void;
|
|
9
|
+
info(message: string): void;
|
|
10
|
+
warn(message: string): void;
|
|
11
|
+
error(message: string): void;
|
|
12
|
+
}
|
|
13
|
+
export interface FetchLikeResponse {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
status: number;
|
|
16
|
+
headers: Headers;
|
|
17
|
+
json(): Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<FetchLikeResponse>;
|
|
20
|
+
export declare const acceptedManifestMediaTypes: string;
|
|
21
|
+
export declare function defaultFetch(input: string, init?: RequestInit): Promise<FetchLikeResponse>;
|
|
22
|
+
export declare function buildFetchTransportErrorMessage(error: unknown, fallback: string): string;
|
|
23
|
+
export declare function buildHttpErrorMessage(response: FetchLikeResponse, fallback: string): Promise<string>;
|
|
24
|
+
export declare function withFetchRetry<T>(run: () => Promise<T>, options: {
|
|
25
|
+
logger: GitHubScanLogger;
|
|
26
|
+
label: string;
|
|
27
|
+
shouldRetry?: (error: unknown) => boolean;
|
|
28
|
+
}): Promise<T>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ingestRequestRetryCount, ingestRequestRetryDelayMs } from "../../tuning/index.js";
|
|
2
|
+
export const acceptedManifestMediaTypes = [
|
|
3
|
+
"application/vnd.oci.image.index.v1+json",
|
|
4
|
+
"application/vnd.oci.image.manifest.v1+json",
|
|
5
|
+
"application/vnd.docker.distribution.manifest.list.v2+json",
|
|
6
|
+
"application/vnd.docker.distribution.manifest.v2+json",
|
|
7
|
+
"application/vnd.oci.artifact.manifest.v1+json"
|
|
8
|
+
].join(", ");
|
|
9
|
+
export async function defaultFetch(input, init) {
|
|
10
|
+
return fetch(input, init);
|
|
11
|
+
}
|
|
12
|
+
export function buildFetchTransportErrorMessage(error, fallback) {
|
|
13
|
+
const details = [fallback];
|
|
14
|
+
details.push(..._collectErrorDetails(error));
|
|
15
|
+
return details.join(" - ");
|
|
16
|
+
}
|
|
17
|
+
export async function buildHttpErrorMessage(response, fallback) {
|
|
18
|
+
const details = [fallback, `status ${response.status}`];
|
|
19
|
+
const body = await _readJsonErrorBody(response);
|
|
20
|
+
const message = typeof body?.message === "string" ? body.message : undefined;
|
|
21
|
+
const documentationUrl = typeof body?.documentation_url === "string" ? body.documentation_url : undefined;
|
|
22
|
+
const authenticateHeader = response.headers.get("www-authenticate") ?? undefined;
|
|
23
|
+
if (message) {
|
|
24
|
+
details.push(message);
|
|
25
|
+
}
|
|
26
|
+
if (documentationUrl) {
|
|
27
|
+
details.push(documentationUrl);
|
|
28
|
+
}
|
|
29
|
+
if (authenticateHeader) {
|
|
30
|
+
details.push(`www-authenticate: ${authenticateHeader}`);
|
|
31
|
+
}
|
|
32
|
+
return details.join(" - ");
|
|
33
|
+
}
|
|
34
|
+
export async function withFetchRetry(run, options) {
|
|
35
|
+
let attempt = 0;
|
|
36
|
+
for (;;) {
|
|
37
|
+
try {
|
|
38
|
+
return await run();
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
attempt += 1;
|
|
42
|
+
const shouldRetry = options.shouldRetry ? options.shouldRetry(error) : true;
|
|
43
|
+
if (!shouldRetry || attempt > ingestRequestRetryCount) {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
47
|
+
options.logger.warn(`${options.label} failed on attempt ${attempt}/${ingestRequestRetryCount + 1}; retrying in ${ingestRequestRetryDelayMs}ms - ${errorMessage}`);
|
|
48
|
+
await _sleep(ingestRequestRetryDelayMs);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function _readJsonErrorBody(response) {
|
|
53
|
+
const contentType = response.headers.get("content-type")?.split(";")[0];
|
|
54
|
+
if (contentType && contentType !== "application/json" && !contentType.endsWith("+json")) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const body = await response.json();
|
|
59
|
+
if (body && typeof body === "object") {
|
|
60
|
+
return body;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
function _sleep(delayMs) {
|
|
69
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
70
|
+
}
|
|
71
|
+
function _collectErrorDetails(error) {
|
|
72
|
+
if (!(error instanceof Error)) {
|
|
73
|
+
return [String(error)];
|
|
74
|
+
}
|
|
75
|
+
const details = [];
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
let current = error;
|
|
78
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
79
|
+
seen.add(current);
|
|
80
|
+
const message = _formatErrorMessage(current);
|
|
81
|
+
if (message && !details.includes(message)) {
|
|
82
|
+
details.push(message);
|
|
83
|
+
}
|
|
84
|
+
current = current.cause;
|
|
85
|
+
}
|
|
86
|
+
return details.length > 0 ? details : [String(error)];
|
|
87
|
+
}
|
|
88
|
+
function _formatErrorMessage(error) {
|
|
89
|
+
const code = "code" in error && typeof error.code === "string"
|
|
90
|
+
? error.code
|
|
91
|
+
: undefined;
|
|
92
|
+
if (error.message && code) {
|
|
93
|
+
return `${error.message} (${code})`;
|
|
94
|
+
}
|
|
95
|
+
if (error.message) {
|
|
96
|
+
return error.message;
|
|
97
|
+
}
|
|
98
|
+
if (code) {
|
|
99
|
+
return code;
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ScanWriter, SnapshotRepository } from "../../db/index.js";
|
|
2
|
+
import { type FetchLike, type GitHubScanOptions } from "./_shared.js";
|
|
3
|
+
export { type GitHubScanOptions } from "./_shared.js";
|
|
4
|
+
interface _GitHubScanRuntime {
|
|
5
|
+
fetchImpl?: FetchLike;
|
|
6
|
+
}
|
|
7
|
+
export declare function importGitHubScan(options: GitHubScanOptions, writer: ScanWriter, repository: SnapshotRepository, runtime?: _GitHubScanRuntime): Promise<void>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ingestManifests } from "./_manifest-ingest.js";
|
|
2
|
+
import { ingestPackageVersions } from "./_packages-client.js";
|
|
3
|
+
import { defaultFetch } from "./_shared.js";
|
|
4
|
+
const _GITHUB_API_BASE_URL = "https://api.github.com";
|
|
5
|
+
const _REGISTRY_BASE_URL = "https://ghcr.io";
|
|
6
|
+
export async function importGitHubScan(options, writer, repository, runtime) {
|
|
7
|
+
const fetchImpl = runtime?.fetchImpl ?? defaultFetch;
|
|
8
|
+
const scanStartedAt = new Date().toISOString();
|
|
9
|
+
const fullPackageName = `${options.owner}/${options.packageName}`;
|
|
10
|
+
writer.resetScan(options.owner, options.packageName, scanStartedAt);
|
|
11
|
+
const scanId = writer.getActiveScanId();
|
|
12
|
+
options.logger.info(`Starting GitHub package scan for ${fullPackageName}`);
|
|
13
|
+
try {
|
|
14
|
+
options.logger.info(`Starting remote data pull for ${fullPackageName}`);
|
|
15
|
+
const counts = await ingestPackageVersions(fetchImpl, _GITHUB_API_BASE_URL, options, writer);
|
|
16
|
+
options.logger.info(`Loaded ${counts.packageVersions} package versions and ${counts.tags} tags`);
|
|
17
|
+
await ingestManifests(fetchImpl, _REGISTRY_BASE_URL, options, writer, repository, scanId);
|
|
18
|
+
options.logger.info(`Completed remote data pull for ${fullPackageName}`);
|
|
19
|
+
writer.markScanCompleted(new Date().toISOString());
|
|
20
|
+
options.logger.info(`Completed GitHub package scan for ${fullPackageName}`);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
writer.markScanFailed(new Date().toISOString());
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const packageVersionPageFetchConcurrency = 4;
|
|
2
|
+
export declare const manifestFetchConcurrency = 16;
|
|
3
|
+
export declare const ingestRequestRetryCount = 3;
|
|
4
|
+
export declare const ingestRequestRetryDelayMs = 1000;
|
|
5
|
+
export declare const paginatedIngestProgressIntervalPages = 10;
|
|
6
|
+
export declare const manifestIngestProgressStepRatio = 0.05;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const packageVersionPageFetchConcurrency = 4;
|
|
2
|
+
export const manifestFetchConcurrency = 16;
|
|
3
|
+
export const ingestRequestRetryCount = 3;
|
|
4
|
+
export const ingestRequestRetryDelayMs = 1000;
|
|
5
|
+
export const paginatedIngestProgressIntervalPages = 10;
|
|
6
|
+
export const manifestIngestProgressStepRatio = 0.05;
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ghcr-manager",
|
|
3
|
+
"description": "Inspect, analyze, and manage GitHub Container Registry packages.",
|
|
4
|
+
"homepage": "https://github.com/gh-workflow/ghcr-manager#readme",
|
|
5
|
+
"bugs": {
|
|
6
|
+
"url": "https://github.com/gh-workflow/ghcr-manager/issues"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/gh-workflow/ghcr-manager"
|
|
11
|
+
},
|
|
12
|
+
"version": "0.0.4",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20.0.0"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"resources/sql",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"README.md",
|
|
22
|
+
"CHANGELOG.md"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"ghcr-manager": "./dist/cli/index.js"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc --project tsconfig.json",
|
|
29
|
+
"coverage": "c8 --src src --reporter=text --reporter=text-summary node --import tsx --test",
|
|
30
|
+
"ghcr-manager": "tsx src/cli/index.ts",
|
|
31
|
+
"format": "prettier --write .",
|
|
32
|
+
"format:check": "prettier --check .",
|
|
33
|
+
"lint": "npm run check:file-names && npm run check:test-mapping && npm run typecheck && npm run lint:ts && npm run lint:yaml && npm run lint:markdown && npm run format:check",
|
|
34
|
+
"check:file-names": "node tools/check-src-file-names.mjs",
|
|
35
|
+
"check:test-mapping": "node tools/check-test-mapping.mjs",
|
|
36
|
+
"lint:ts": "eslint \"src/**/*.ts\" \"tests/**/*.ts\"",
|
|
37
|
+
"lint:yaml": "eslint \".github/**/*.yml\" \"*.yml\"",
|
|
38
|
+
"lint:markdown": "markdownlint-cli2",
|
|
39
|
+
"typecheck": "tsc --noEmit --project tsconfig.json",
|
|
40
|
+
"test": "node --import tsx --test"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"better-sqlite3": "^12.9.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@eslint/js": "^10.0.1",
|
|
47
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
48
|
+
"@types/node": "^25.6.0",
|
|
49
|
+
"c8": "^11.0.0",
|
|
50
|
+
"eslint": "^10.2.1",
|
|
51
|
+
"eslint-plugin-yml": "^3.3.2",
|
|
52
|
+
"globals": "^17.5.0",
|
|
53
|
+
"markdownlint-cli2": "^0.22.1",
|
|
54
|
+
"prettier": "^3.6.2",
|
|
55
|
+
"tsx": "^4.20.6",
|
|
56
|
+
"typescript-eslint": "^8.46.2",
|
|
57
|
+
"typescript": "^6.0.3"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
PRAGMA foreign_keys = ON;
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS package_scans (
|
|
4
|
+
scan_id INTEGER PRIMARY KEY,
|
|
5
|
+
scan_uuid TEXT NOT NULL UNIQUE,
|
|
6
|
+
owner TEXT NOT NULL,
|
|
7
|
+
package_name TEXT NOT NULL,
|
|
8
|
+
scan_started_at TEXT NOT NULL,
|
|
9
|
+
scan_completed_at TEXT,
|
|
10
|
+
status TEXT NOT NULL,
|
|
11
|
+
CHECK(status IN ('running', 'completed', 'failed'))
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS package_versions (
|
|
15
|
+
scan_id INTEGER NOT NULL,
|
|
16
|
+
version_id INTEGER NOT NULL,
|
|
17
|
+
digest TEXT NOT NULL,
|
|
18
|
+
created_at TEXT NOT NULL,
|
|
19
|
+
updated_at TEXT NOT NULL,
|
|
20
|
+
PRIMARY KEY(scan_id, version_id),
|
|
21
|
+
UNIQUE(scan_id, version_id, digest),
|
|
22
|
+
FOREIGN KEY(scan_id) REFERENCES package_scans(scan_id)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE TABLE IF NOT EXISTS package_version_payloads (
|
|
26
|
+
scan_id INTEGER NOT NULL,
|
|
27
|
+
version_id INTEGER NOT NULL,
|
|
28
|
+
raw_json TEXT NOT NULL,
|
|
29
|
+
PRIMARY KEY(scan_id, version_id),
|
|
30
|
+
FOREIGN KEY(scan_id, version_id) REFERENCES package_versions(scan_id, version_id)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
34
|
+
scan_id INTEGER NOT NULL,
|
|
35
|
+
tag TEXT NOT NULL,
|
|
36
|
+
digest TEXT NOT NULL,
|
|
37
|
+
version_id INTEGER NOT NULL,
|
|
38
|
+
PRIMARY KEY(scan_id, tag),
|
|
39
|
+
FOREIGN KEY(scan_id, version_id, digest) REFERENCES package_versions(scan_id, version_id, digest)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS manifests (
|
|
43
|
+
scan_id INTEGER NOT NULL,
|
|
44
|
+
digest TEXT NOT NULL,
|
|
45
|
+
media_type TEXT NOT NULL,
|
|
46
|
+
artifact_type TEXT,
|
|
47
|
+
config_media_type TEXT,
|
|
48
|
+
subject_digest TEXT,
|
|
49
|
+
annotations_json TEXT,
|
|
50
|
+
platform_os TEXT,
|
|
51
|
+
platform_architecture TEXT,
|
|
52
|
+
platform_variant TEXT,
|
|
53
|
+
PRIMARY KEY(scan_id, digest),
|
|
54
|
+
FOREIGN KEY(scan_id) REFERENCES package_scans(scan_id)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE TABLE IF NOT EXISTS manifest_descriptors (
|
|
58
|
+
scan_id INTEGER NOT NULL,
|
|
59
|
+
parent_digest TEXT NOT NULL,
|
|
60
|
+
child_digest TEXT NOT NULL,
|
|
61
|
+
media_type TEXT NOT NULL,
|
|
62
|
+
artifact_type TEXT,
|
|
63
|
+
platform_os TEXT,
|
|
64
|
+
platform_architecture TEXT,
|
|
65
|
+
platform_variant TEXT,
|
|
66
|
+
PRIMARY KEY(scan_id, parent_digest, child_digest),
|
|
67
|
+
FOREIGN KEY(scan_id, parent_digest) REFERENCES manifests(scan_id, digest)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS manifest_payloads (
|
|
71
|
+
scan_id INTEGER NOT NULL,
|
|
72
|
+
digest TEXT NOT NULL,
|
|
73
|
+
raw_json TEXT NOT NULL,
|
|
74
|
+
PRIMARY KEY(scan_id, digest),
|
|
75
|
+
FOREIGN KEY(scan_id, digest) REFERENCES manifests(scan_id, digest)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS manifest_edges (
|
|
79
|
+
scan_id INTEGER NOT NULL,
|
|
80
|
+
parent_digest TEXT NOT NULL,
|
|
81
|
+
child_digest TEXT NOT NULL,
|
|
82
|
+
edge_kind TEXT NOT NULL,
|
|
83
|
+
PRIMARY KEY(scan_id, parent_digest, child_digest, edge_kind),
|
|
84
|
+
FOREIGN KEY(scan_id, parent_digest) REFERENCES manifests(scan_id, digest),
|
|
85
|
+
FOREIGN KEY(scan_id, child_digest) REFERENCES manifests(scan_id, digest)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
CREATE TABLE IF NOT EXISTS manifest_reachability (
|
|
89
|
+
scan_id INTEGER NOT NULL,
|
|
90
|
+
ancestor_digest TEXT NOT NULL,
|
|
91
|
+
descendant_digest TEXT NOT NULL,
|
|
92
|
+
min_distance INTEGER NOT NULL,
|
|
93
|
+
PRIMARY KEY(scan_id, ancestor_digest, descendant_digest),
|
|
94
|
+
FOREIGN KEY(scan_id, ancestor_digest) REFERENCES manifests(scan_id, digest),
|
|
95
|
+
FOREIGN KEY(scan_id, descendant_digest) REFERENCES manifests(scan_id, digest),
|
|
96
|
+
CHECK(min_distance >= 0)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_package_versions_scan_created_at ON package_versions(scan_id, created_at);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_package_versions_scan_digest ON package_versions(scan_id, digest);
|
|
101
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_package_scans_scan_uuid ON package_scans(scan_uuid);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_package_scans_owner_name_started_at
|
|
103
|
+
ON package_scans(owner, package_name, scan_started_at DESC);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_tags_scan_digest ON tags(scan_id, digest);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_manifest_descriptors_scan_child ON manifest_descriptors(scan_id, child_digest);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_parent ON manifest_edges(scan_id, parent_digest);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_child ON manifest_edges(scan_id, child_digest);
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_manifest_reachability_scan_descendant
|
|
109
|
+
ON manifest_reachability(scan_id, descendant_digest);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
DROP VIEW IF EXISTS v_latest_scan_per_package;
|
|
2
|
+
|
|
3
|
+
CREATE VIEW v_latest_scan_per_package AS
|
|
4
|
+
SELECT scan_id,
|
|
5
|
+
scan_uuid,
|
|
6
|
+
owner,
|
|
7
|
+
package_name,
|
|
8
|
+
scan_started_at,
|
|
9
|
+
scan_completed_at
|
|
10
|
+
FROM (
|
|
11
|
+
SELECT
|
|
12
|
+
ps.scan_id,
|
|
13
|
+
ps.scan_uuid,
|
|
14
|
+
ps.owner,
|
|
15
|
+
ps.package_name,
|
|
16
|
+
ps.scan_started_at,
|
|
17
|
+
ps.scan_completed_at,
|
|
18
|
+
ROW_NUMBER() OVER (
|
|
19
|
+
PARTITION BY ps.owner, ps.package_name
|
|
20
|
+
ORDER BY ps.scan_completed_at DESC
|
|
21
|
+
) AS rn
|
|
22
|
+
FROM package_scans ps
|
|
23
|
+
WHERE ps.scan_completed_at IS NOT NULL
|
|
24
|
+
AND ps.status = 'completed'
|
|
25
|
+
)
|
|
26
|
+
WHERE rn = 1
|
|
27
|
+
;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
DROP VIEW IF EXISTS v_missing_digests;
|
|
2
|
+
|
|
3
|
+
CREATE VIEW v_missing_digests AS
|
|
4
|
+
SELECT DISTINCT
|
|
5
|
+
lsp.scan_id,
|
|
6
|
+
lsp.owner,
|
|
7
|
+
lsp.package_name,
|
|
8
|
+
d.child_digest AS missing_digest,
|
|
9
|
+
d.parent_digest AS anchor_digest
|
|
10
|
+
FROM manifest_descriptors d
|
|
11
|
+
JOIN v_latest_scan_per_package lsp ON lsp.scan_id = d.scan_id
|
|
12
|
+
LEFT JOIN manifests m
|
|
13
|
+
ON m.scan_id = d.scan_id
|
|
14
|
+
AND m.digest = d.child_digest
|
|
15
|
+
WHERE m.digest IS NULL
|
|
16
|
+
|
|
17
|
+
UNION
|
|
18
|
+
|
|
19
|
+
SELECT DISTINCT
|
|
20
|
+
lsp.scan_id,
|
|
21
|
+
lsp.owner,
|
|
22
|
+
lsp.package_name,
|
|
23
|
+
mf.subject_digest AS missing_digest,
|
|
24
|
+
mf.digest AS anchor_digest
|
|
25
|
+
FROM manifests mf
|
|
26
|
+
JOIN v_latest_scan_per_package lsp ON lsp.scan_id = mf.scan_id
|
|
27
|
+
LEFT JOIN manifests m
|
|
28
|
+
ON m.scan_id = mf.scan_id
|
|
29
|
+
AND m.digest = mf.subject_digest
|
|
30
|
+
WHERE mf.subject_digest IS NOT NULL
|
|
31
|
+
AND m.digest IS NULL
|
|
32
|
+
;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
DROP VIEW IF EXISTS v_missing_digests_related_manifests;
|
|
2
|
+
|
|
3
|
+
CREATE VIEW v_missing_digests_related_manifests AS
|
|
4
|
+
WITH
|
|
5
|
+
related_manifests AS (
|
|
6
|
+
SELECT DISTINCT
|
|
7
|
+
m.scan_id,
|
|
8
|
+
md.missing_digest,
|
|
9
|
+
m.digest AS related_manifest_digest,
|
|
10
|
+
m.media_type,
|
|
11
|
+
1 AS hops_missing_to_related_manifest
|
|
12
|
+
FROM v_missing_digests md
|
|
13
|
+
JOIN manifests m
|
|
14
|
+
ON m.scan_id = md.scan_id
|
|
15
|
+
AND m.digest = md.anchor_digest
|
|
16
|
+
|
|
17
|
+
UNION
|
|
18
|
+
|
|
19
|
+
SELECT DISTINCT
|
|
20
|
+
m.scan_id,
|
|
21
|
+
md.missing_digest,
|
|
22
|
+
m.digest AS related_manifest_digest,
|
|
23
|
+
m.media_type,
|
|
24
|
+
r.min_distance + 1 AS hops_missing_to_related_manifest
|
|
25
|
+
FROM v_missing_digests md
|
|
26
|
+
JOIN manifests m
|
|
27
|
+
ON m.scan_id = md.scan_id
|
|
28
|
+
JOIN manifest_reachability r
|
|
29
|
+
ON r.scan_id = m.scan_id
|
|
30
|
+
AND r.ancestor_digest = m.digest
|
|
31
|
+
AND r.descendant_digest = md.anchor_digest
|
|
32
|
+
|
|
33
|
+
UNION
|
|
34
|
+
|
|
35
|
+
SELECT DISTINCT
|
|
36
|
+
m.scan_id,
|
|
37
|
+
md.missing_digest,
|
|
38
|
+
m.digest AS related_manifest_digest,
|
|
39
|
+
m.media_type,
|
|
40
|
+
r.min_distance + 1 AS hops_missing_to_related_manifest
|
|
41
|
+
FROM v_missing_digests md
|
|
42
|
+
JOIN manifests m
|
|
43
|
+
ON m.scan_id = md.scan_id
|
|
44
|
+
JOIN manifest_reachability r
|
|
45
|
+
ON r.scan_id = m.scan_id
|
|
46
|
+
AND r.ancestor_digest = md.anchor_digest
|
|
47
|
+
AND r.descendant_digest = m.digest
|
|
48
|
+
),
|
|
49
|
+
closest_related_manifests AS (
|
|
50
|
+
SELECT
|
|
51
|
+
scan_id,
|
|
52
|
+
missing_digest,
|
|
53
|
+
related_manifest_digest,
|
|
54
|
+
media_type,
|
|
55
|
+
MIN(hops_missing_to_related_manifest) AS hops_missing_to_related_manifest
|
|
56
|
+
FROM related_manifests
|
|
57
|
+
GROUP BY
|
|
58
|
+
missing_digest,
|
|
59
|
+
scan_id,
|
|
60
|
+
related_manifest_digest,
|
|
61
|
+
media_type
|
|
62
|
+
)
|
|
63
|
+
SELECT
|
|
64
|
+
ps.scan_id,
|
|
65
|
+
ps.owner,
|
|
66
|
+
ps.package_name,
|
|
67
|
+
crm.missing_digest,
|
|
68
|
+
crm.related_manifest_digest,
|
|
69
|
+
crm.media_type,
|
|
70
|
+
crm.hops_missing_to_related_manifest,
|
|
71
|
+
t.tag,
|
|
72
|
+
t.version_id
|
|
73
|
+
FROM closest_related_manifests crm
|
|
74
|
+
JOIN package_scans ps
|
|
75
|
+
ON ps.scan_id = crm.scan_id
|
|
76
|
+
LEFT JOIN tags t
|
|
77
|
+
ON t.scan_id = crm.scan_id
|
|
78
|
+
AND t.digest = crm.related_manifest_digest;
|