ghcr-manager 0.9.7 → 0.9.8

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.

Potentially problematic release.


This version of ghcr-manager might be problematic. Click here for more details.

Files changed (56) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/LICENSE +1 -1
  3. package/README.md +37 -56
  4. package/dist/cleanup-summary/_cleanup-summary-markdown.js +7 -10
  5. package/dist/cleanup-summary/_cleanup-summary.d.ts +3 -4
  6. package/dist/cleanup-summary/_cleanup-summary.js +2 -3
  7. package/dist/cli/_cleanup-command.js +1 -1
  8. package/dist/cli/_tag-selector-resolver.js +29 -9
  9. package/dist/cli/index.js +0 -4
  10. package/dist/core/_types.d.ts +1 -6
  11. package/dist/core/_types.js +1 -1
  12. package/dist/db/_db-merge-scan-copy.js +3 -2
  13. package/dist/db/_manifest-reachability.js +47 -8
  14. package/dist/db/_scan-writer.js +1 -13
  15. package/dist/db/planner/_planner-direct-target-root-options.d.ts +11 -0
  16. package/dist/db/planner/_planner-direct-target-root-options.js +1 -0
  17. package/dist/db/planner/_planner-direct-target-root-tag-filters.d.ts +9 -0
  18. package/dist/db/planner/_planner-direct-target-root-tag-filters.js +42 -0
  19. package/dist/db/planner/_planner-direct-target-roots-combined-sql.d.ts +7 -0
  20. package/dist/db/planner/_planner-direct-target-roots-combined-sql.js +198 -0
  21. package/dist/db/planner/_planner-direct-target-roots-combined.d.ts +4 -0
  22. package/dist/db/planner/_planner-direct-target-roots-combined.js +10 -0
  23. package/dist/db/planner/_planner-direct-target-roots-tagged.d.ts +4 -0
  24. package/dist/db/planner/_planner-direct-target-roots-tagged.js +125 -0
  25. package/dist/db/planner/_planner-direct-target-roots.d.ts +2 -12
  26. package/dist/db/planner/_planner-direct-target-roots.js +8 -203
  27. package/dist/db/planner/_planner-direct-target-tags.js +3 -4
  28. package/dist/db/planner/_planner-output.js +28 -8
  29. package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.d.ts +1 -0
  30. package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.js +65 -0
  31. package/dist/db/planner/_planner-plan-artifacts-closure-sql.d.ts +1 -0
  32. package/dist/db/planner/_planner-plan-artifacts-closure-sql.js +195 -0
  33. package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.d.ts +1 -0
  34. package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.js +86 -0
  35. package/dist/db/planner/_planner-plan-artifacts.js +26 -128
  36. package/dist/db/planner/_planner-sql.js +13 -2
  37. package/dist/db/planner/_planner-types.d.ts +2 -0
  38. package/dist/db/planner/_planner-types.js +1 -0
  39. package/dist/execute/_plan-executor.js +7 -11
  40. package/dist/execute/_types.d.ts +2 -19
  41. package/dist/execute/_untag-client.d.ts +2 -2
  42. package/dist/execute/_untag-client.js +1 -42
  43. package/dist/execute/index.d.ts +1 -1
  44. package/dist/ingest/github/_manifest-kind.d.ts +6 -0
  45. package/dist/ingest/github/_manifest-kind.js +16 -2
  46. package/package.json +16 -10
  47. package/resources/sql/schema/001_schema.sql +14 -4
  48. package/dist/cli/_untag-command.d.ts +0 -1
  49. package/dist/cli/_untag-command.js +0 -58
  50. package/dist/db/_manifest-kind-refinement.d.ts +0 -2
  51. package/dist/db/_manifest-kind-refinement.js +0 -43
  52. package/resources/sql/views/002_v_scan_root_manifests.sql +0 -44
  53. package/resources/sql/views/003_v_digest_tag_relations.sql +0 -50
  54. package/resources/sql/views/004_v_cleanup_root_closure_members.sql +0 -101
  55. package/resources/sql/views/005_v_cleanup_blocking_overlaps.sql +0 -42
  56. package/resources/sql/views/006_v_cleanup_root_decision_readable.sql +0 -67
@@ -5,6 +5,7 @@ export const DeletePlanValidationStatuses = {
5
5
  };
6
6
  export const DeletePlanValidationReasonCodes = {
7
7
  untagOnlyPartialTagMatch: "untag-only-partial-tag-match",
8
+ untagOnlyRetainedManifest: "untag-only-retained-manifest",
8
9
  fullyDeletableNoRetainedOverlap: "fully-deletable-no-retained-overlap",
9
10
  blockedOverlapWithRetainedRoot: "blocked-overlap-with-retained-root"
10
11
  };
@@ -2,8 +2,8 @@ import { DeletePlanValidationStatuses } from "../db/index.js";
2
2
  import { deletePackageVersion } from "./_package-version-delete-client.js";
3
3
  import { untagRootTags } from "./_untag-client.js";
4
4
  export async function executeDeletePlan(plan, options) {
5
- const deletedPackageVersions = [];
6
- const untaggedTags = [];
5
+ let deletedPackageVersionCount = 0;
6
+ let detachedTagCount = 0;
7
7
  const directTargetTagSet = new Set(plan.directTargetTags);
8
8
  const deletedVersionIds = new Set();
9
9
  for (const decision of plan.rootDecisions) {
@@ -24,7 +24,7 @@ export async function executeDeletePlan(plan, options) {
24
24
  if (selectedTags.length === 0) {
25
25
  throw new Error(`no selected tags resolved for untag-only root ${decision.digest}`);
26
26
  }
27
- untaggedTags.push(...(await untagRootTags(plan.owner, plan.packageName, decision.versionId, decision.digest, selectedTags, options)));
27
+ detachedTagCount += await untagRootTags(plan.owner, plan.packageName, decision.versionId, decision.digest, selectedTags, options);
28
28
  }
29
29
  const closureMembersByRootDigest = new Map(plan.fullyDeletableRoots.map((root) => [
30
30
  root.digest,
@@ -54,10 +54,7 @@ export async function executeDeletePlan(plan, options) {
54
54
  fetchImpl: options.fetchImpl
55
55
  });
56
56
  deletedVersionIds.add(target.versionId);
57
- deletedPackageVersions.push({
58
- versionId: target.versionId,
59
- digest: target.digest
60
- });
57
+ deletedPackageVersionCount += 1;
61
58
  }
62
59
  }
63
60
  return {
@@ -65,9 +62,8 @@ export async function executeDeletePlan(plan, options) {
65
62
  packageName: plan.packageName,
66
63
  scanCompletedAt: plan.scanCompletedAt,
67
64
  plannerInputs: plan.plannerInputs,
68
- deletedPackageVersions,
69
- untaggedTags,
70
- blockedRoots: plan.blockedRoots,
71
- unsupportedUntagRoots: []
65
+ deletedPackageVersionCount,
66
+ detachedTagCount,
67
+ blockedRoots: plan.blockedRoots
72
68
  };
73
69
  }
@@ -1,29 +1,12 @@
1
1
  import type { DeletePlan } from "../db/index.js";
2
- export interface DeletePackageVersionOperation {
3
- versionId: number;
4
- digest: string;
5
- }
6
- export interface UnsupportedUntagRoot {
7
- versionId: number;
8
- digest: string;
9
- reason: string;
10
- }
11
- export interface UntagTagOperation {
12
- tag: string;
13
- sourceVersionId: number;
14
- sourceDigest: string;
15
- detachedVersionId: number;
16
- detachedDigest: string;
17
- }
18
2
  export interface DeleteExecutionSummary {
19
3
  owner: string;
20
4
  packageName: string;
21
5
  scanCompletedAt: string;
22
6
  plannerInputs: DeletePlan["plannerInputs"];
23
- deletedPackageVersions: DeletePackageVersionOperation[];
24
- untaggedTags: UntagTagOperation[];
7
+ deletedPackageVersionCount: number;
8
+ detachedTagCount: number;
25
9
  blockedRoots: DeletePlan["blockedRoots"];
26
- unsupportedUntagRoots: UnsupportedUntagRoot[];
27
10
  }
28
11
  export interface DeleteExecutionOptions {
29
12
  token: string;
@@ -1,2 +1,2 @@
1
- import type { DeleteExecutionOptions, UntagTagOperation } from "./_types.js";
2
- export declare function untagRootTags(owner: string, packageName: string, sourceVersionId: number, sourceDigest: string, tags: string[], options: DeleteExecutionOptions): Promise<UntagTagOperation[]>;
1
+ import type { DeleteExecutionOptions } from "./_types.js";
2
+ export declare function untagRootTags(owner: string, packageName: string, sourceVersionId: number, sourceDigest: string, tags: string[], options: DeleteExecutionOptions): Promise<number>;
@@ -1,6 +1,5 @@
1
1
  import { buildDetachedManifestClone } from "./_manifest-detach.js";
2
2
  import { findPackageVersionByDigestAndTag } from "./_package-version-page-client.js";
3
- import { listPackageVersionTagSources, listPresentPackageVersionIds } from "./_package-version-tag-source-client.js";
4
3
  import { deletePackageVersion } from "./_package-version-delete-client.js";
5
4
  import { loadRegistryManifestByDigest, putRegistryManifestForTag } from "./_registry-manifest-client.js";
6
5
  import { loadRegistryPushToken } from "./_registry-token-client.js";
@@ -11,7 +10,6 @@ export async function untagRootTags(owner, packageName, sourceVersionId, sourceD
11
10
  const sourceManifest = await loadRegistryManifestByDigest(owner, packageName, sourceDigest, registryToken, options.logger, {
12
11
  fetchImpl: options.fetchImpl
13
12
  });
14
- const operations = [];
15
13
  for (const tag of tags) {
16
14
  options.logger.info(`Detaching tag ${owner}/${packageName}:${tag} from ${sourceDigest}`);
17
15
  const detachedManifestJson = buildDetachedManifestClone(sourceManifest.rawJson, sourceManifest.mediaType, {
@@ -27,45 +25,6 @@ export async function untagRootTags(owner, packageName, sourceVersionId, sourceD
27
25
  await deletePackageVersion(owner, packageName, detachedVersionId, options.token, options.logger, {
28
26
  fetchImpl: options.fetchImpl
29
27
  });
30
- await _assertTagRemoved(owner, packageName, tag, options);
31
- await _assertVersionRemoved(owner, packageName, detachedVersionId, options);
32
- operations.push({
33
- tag,
34
- sourceVersionId,
35
- sourceDigest,
36
- detachedVersionId,
37
- detachedDigest
38
- });
39
- }
40
- return operations;
41
- }
42
- async function _assertTagRemoved(owner, packageName, tag, options) {
43
- for (let attempt = 1; attempt <= 5; attempt += 1) {
44
- const remaining = await listPackageVersionTagSources(owner, packageName, [tag], options.token, options.logger, {
45
- fetchImpl: options.fetchImpl
46
- });
47
- if (remaining.length === 0) {
48
- return;
49
- }
50
- if (attempt < 5) {
51
- options.logger.warn(`Tag ${owner}/${packageName}:${tag} is still visible after untag; retrying check ${attempt}/5`);
52
- await new Promise((resolve) => setTimeout(resolve, 1000));
53
- }
54
- }
55
- throw new Error(`tag ${owner}/${packageName}:${tag} is still visible after untag`);
56
- }
57
- async function _assertVersionRemoved(owner, packageName, versionId, options) {
58
- for (let attempt = 1; attempt <= 5; attempt += 1) {
59
- const presentVersionIds = await listPresentPackageVersionIds(owner, packageName, [versionId], options.token, options.logger, {
60
- fetchImpl: options.fetchImpl
61
- });
62
- if (presentVersionIds.length === 0) {
63
- return;
64
- }
65
- if (attempt < 5) {
66
- options.logger.warn(`Temporary package version ${owner}/${packageName}#${versionId} is still visible after untag; retrying check ${attempt}/5`);
67
- await new Promise((resolve) => setTimeout(resolve, 1000));
68
- }
69
28
  }
70
- throw new Error(`temporary package version ${owner}/${packageName}#${versionId} is still visible after untag`);
29
+ return tags.length;
71
30
  }
@@ -2,4 +2,4 @@ export { executeDeletePlan } from "./_plan-executor.js";
2
2
  export { listPackageVersionTagSources, listPresentPackageVersionIds } from "./_package-version-tag-source-client.js";
3
3
  export { untagRootTags } from "./_untag-client.js";
4
4
  export type { GitHubPackageVersionTagSource } from "./_package-version-tag-source-client.js";
5
- export type { DeleteExecutionSummary, UntagTagOperation } from "./_types.js";
5
+ export type { DeleteExecutionSummary } from "./_types.js";
@@ -7,6 +7,12 @@ interface _RegistryManifestDocument {
7
7
  mediaType?: string;
8
8
  artifactType?: string;
9
9
  annotations?: Record<string, unknown>;
10
+ manifests?: Array<{
11
+ platform?: {
12
+ architecture?: string;
13
+ os?: string;
14
+ };
15
+ }>;
10
16
  config?: {
11
17
  mediaType?: string;
12
18
  artifactType?: string;
@@ -2,7 +2,7 @@ import { ManifestKinds } from "../../core/index.js";
2
2
  export function classifyManifestKind(document) {
3
3
  if (document.mediaType === "application/vnd.oci.image.index.v1+json" ||
4
4
  document.mediaType === "application/vnd.docker.distribution.manifest.list.v2+json") {
5
- return ManifestKinds.indexManifest;
5
+ return _isMultiArchManifest(document) ? ManifestKinds.multiArchManifest : ManifestKinds.indexManifest;
6
6
  }
7
7
  if (_isSignatureManifest(document)) {
8
8
  return ManifestKinds.signatureManifest;
@@ -10,7 +10,8 @@ export function classifyManifestKind(document) {
10
10
  if (_isAttestationManifest(document)) {
11
11
  return ManifestKinds.attestationManifest;
12
12
  }
13
- if (document.mediaType === "application/vnd.oci.image.manifest.v1+json") {
13
+ if (document.mediaType === "application/vnd.oci.image.manifest.v1+json" ||
14
+ document.mediaType === "application/vnd.docker.distribution.manifest.v2+json") {
14
15
  return ManifestKinds.imageManifest;
15
16
  }
16
17
  if (document.mediaType === "application/vnd.oci.artifact.manifest.v1+json") {
@@ -18,6 +19,19 @@ export function classifyManifestKind(document) {
18
19
  }
19
20
  return undefined;
20
21
  }
22
+ function _isMultiArchManifest(document) {
23
+ const realPlatformDescriptorCount = document.manifests?.filter((descriptor) => {
24
+ const architecture = descriptor.platform?.architecture;
25
+ const os = descriptor.platform?.os;
26
+ return (typeof architecture === "string" &&
27
+ typeof os === "string" &&
28
+ architecture.length > 0 &&
29
+ os.length > 0 &&
30
+ architecture !== "unknown" &&
31
+ os !== "unknown");
32
+ }).length ?? 0;
33
+ return realPlatformDescriptorCount > 1;
34
+ }
21
35
  function _isSignatureManifest(document) {
22
36
  const candidates = [
23
37
  document.artifactType,
package/package.json CHANGED
@@ -1,18 +1,21 @@
1
1
  {
2
2
  "name": "ghcr-manager",
3
3
  "description": "Inspect, analyze, and manage GitHub Container Registry packages.",
4
- "homepage": "https://github.com/gh-workflow/ghcr-manager#readme",
4
+ "homepage": "https://github.com/ghcr-manager/ghcr-manager#readme",
5
5
  "bugs": {
6
- "url": "https://github.com/gh-workflow/ghcr-manager/issues"
6
+ "url": "https://github.com/ghcr-manager/ghcr-manager/issues"
7
7
  },
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/gh-workflow/ghcr-manager"
10
+ "url": "https://github.com/ghcr-manager/ghcr-manager"
11
11
  },
12
- "version": "0.9.7",
12
+ "version": "0.9.8",
13
13
  "type": "module",
14
+ "workspaces": [
15
+ "visualizer"
16
+ ],
14
17
  "engines": {
15
- "node": ">=20.0.0"
18
+ "node": ">=24.0.0"
16
19
  },
17
20
  "files": [
18
21
  "dist",
@@ -26,19 +29,22 @@
26
29
  },
27
30
  "scripts": {
28
31
  "build": "tsc --project tsconfig.json",
29
- "coverage": "c8 --src src --reporter=text --reporter=text-summary node --import tsx --test",
32
+ "build:visualizer": "npm run build --workspace visualizer",
30
33
  "ghcr-manager": "tsx src/cli/index.ts",
31
34
  "ghcr-manager:dist": "node dist/cli/index.js",
35
+ "visualize": "npm run dev --workspace visualizer --",
36
+ "visualizer:start": "npm run start --workspace visualizer --",
32
37
  "format": "prettier --write .",
33
38
  "format:check": "prettier --check .",
34
- "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",
39
+ "lint": "npm run check:file-names && npm run check:test-mapping && npm run typecheck && npm run typecheck --workspace visualizer && npm run lint:ts && npm run lint:yaml && npm run lint:markdown && npm run lint --workspace visualizer && npm run format:check",
35
40
  "check:file-names": "node tools/tests/check-src-file-names.mjs",
36
41
  "check:test-mapping": "node tools/tests/check-test-mapping.mjs",
37
42
  "lint:ts": "eslint \"src/**/*.ts\" \"tests/**/*.ts\"",
38
43
  "lint:yaml": "eslint \".github/**/*.yml\" \"*.yml\"",
39
44
  "lint:markdown": "markdownlint-cli2",
40
45
  "typecheck": "tsc --noEmit --project tsconfig.json",
41
- "test": "node --import tsx --test"
46
+ "test": "bash -lc 'shopt -s globstar nullglob; node --import tsx --test tests/**/*.test.ts' && npm run test --workspace visualizer",
47
+ "coverage": "bash -lc 'shopt -s globstar nullglob; c8 --src src --reporter=text --reporter=text-summary node --import tsx --test tests/**/*.test.ts'"
42
48
  },
43
49
  "dependencies": {
44
50
  "better-sqlite3": "^12.9.0"
@@ -46,10 +52,10 @@
46
52
  "devDependencies": {
47
53
  "@eslint/js": "^10.0.1",
48
54
  "@types/better-sqlite3": "^7.6.13",
49
- "@types/node": "^25.6.0",
55
+ "@types/node": "^24.12.4",
50
56
  "c8": "^11.0.0",
51
57
  "eslint": "^10.2.1",
52
- "eslint-plugin-yml": "^3.3.2",
58
+ "eslint-plugin-yml": "^3.4.0",
53
59
  "globals": "^17.5.0",
54
60
  "markdownlint-cli2": "^0.22.1",
55
61
  "prettier": "^3.8.3",
@@ -49,13 +49,10 @@ CREATE TABLE IF NOT EXISTS manifests (
49
49
  config_media_type TEXT,
50
50
  subject_digest TEXT,
51
51
  annotations_json TEXT,
52
- platform_os TEXT,
53
- platform_architecture TEXT,
54
- platform_variant TEXT,
55
52
  manifest_kind TEXT,
56
53
  CHECK(manifest_kind IN (
57
54
  'index_manifest',
58
- 'cross_arch_manifest',
55
+ 'multi_arch_manifest',
59
56
  'image_manifest',
60
57
  'artifact_manifest',
61
58
  'attestation_manifest',
@@ -109,6 +106,15 @@ CREATE TABLE IF NOT EXISTS manifest_reachability (
109
106
  CHECK(min_distance >= 0)
110
107
  );
111
108
 
109
+ CREATE TABLE IF NOT EXISTS manifest_graphs (
110
+ scan_id INTEGER NOT NULL,
111
+ digest TEXT NOT NULL,
112
+ graph_id INTEGER NOT NULL,
113
+ PRIMARY KEY(scan_id, digest),
114
+ FOREIGN KEY(scan_id, digest) REFERENCES manifests(scan_id, digest),
115
+ CHECK(graph_id > 0)
116
+ );
117
+
112
118
  CREATE TABLE IF NOT EXISTS cleanup_runs (
113
119
  cleanup_run_id INTEGER PRIMARY KEY,
114
120
  scan_id INTEGER NOT NULL,
@@ -152,6 +158,7 @@ CREATE TABLE IF NOT EXISTS cleanup_root_decisions (
152
158
  CHECK(validation_status IN ('fully-deletable', 'blocked', 'untag-only')),
153
159
  CHECK(validation_reason_code IN (
154
160
  'untag-only-partial-tag-match',
161
+ 'untag-only-retained-manifest',
155
162
  'fully-deletable-no-retained-overlap',
156
163
  'blocked-overlap-with-retained-root'
157
164
  )),
@@ -200,7 +207,10 @@ CREATE INDEX IF NOT EXISTS idx_tags_scan_version ON tags(scan_id, version_id);
200
207
  CREATE INDEX IF NOT EXISTS idx_manifest_descriptors_scan_child ON manifest_descriptors(scan_id, child_digest);
201
208
  CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_parent ON manifest_edges(scan_id, parent_digest);
202
209
  CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_child ON manifest_edges(scan_id, child_digest);
210
+ CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_child_kind ON manifest_edges(scan_id, child_digest, edge_kind);
203
211
  CREATE INDEX IF NOT EXISTS idx_manifest_reachability_scan_descendant
204
212
  ON manifest_reachability(scan_id, descendant_digest);
205
213
  CREATE INDEX IF NOT EXISTS idx_manifest_reachability_scan_descendant_distance
206
214
  ON manifest_reachability(scan_id, descendant_digest, min_distance);
215
+ CREATE INDEX IF NOT EXISTS idx_manifest_graphs_scan_graph_id
216
+ ON manifest_graphs(scan_id, graph_id);
@@ -1 +0,0 @@
1
- export declare function handleUntag(args: string[]): Promise<number>;
@@ -1,58 +0,0 @@
1
- import { listPackageVersionTagSources, untagRootTags } from "../execute/index.js";
2
- import { collectRepeatedOption, hasFlag, requireOption, resolveLogLevel, resolveToken } from "./_args.js";
3
- import { writeJsonOutput } from "./_json-output.js";
4
- import { createLogger } from "./_logger.js";
5
- export async function handleUntag(args) {
6
- const owner = requireOption(args, "--owner");
7
- const packageName = requireOption(args, "--package");
8
- const requestedTags = [...new Set(collectRepeatedOption(args, "--tag"))];
9
- if (requestedTags.length === 0) {
10
- throw new Error("missing required option: --tag");
11
- }
12
- const token = resolveToken(args);
13
- const dryRun = hasFlag(args, "--dry-run");
14
- const logger = createLogger(resolveLogLevel(args));
15
- const tagSources = await listPackageVersionTagSources(owner, packageName, requestedTags, token, logger);
16
- const matchedTags = new Set(tagSources.map((tagSource) => tagSource.tag));
17
- const missingTags = requestedTags.filter((tag) => !matchedTags.has(tag));
18
- if (missingTags.length > 0) {
19
- throw new Error(`could not resolve tag(s): ${missingTags.join(", ")}`);
20
- }
21
- const roots = _groupTagSources(tagSources);
22
- const untaggedTags = [];
23
- if (!dryRun) {
24
- for (const root of roots) {
25
- untaggedTags.push(...(await untagRootTags(owner, packageName, root.versionId, root.digest, root.tags, {
26
- token,
27
- logger
28
- })));
29
- }
30
- }
31
- const summary = {
32
- owner,
33
- packageName,
34
- requestedTags,
35
- dryRun,
36
- roots,
37
- untaggedTags
38
- };
39
- writeJsonOutput(args, "--summary-json-path", summary);
40
- return 0;
41
- }
42
- function _groupTagSources(tagSources) {
43
- const groups = new Map();
44
- for (const tagSource of tagSources) {
45
- const key = `${tagSource.sourceVersionId}:${tagSource.sourceDigest}`;
46
- const existing = groups.get(key);
47
- if (existing) {
48
- existing.tags.push(tagSource.tag);
49
- continue;
50
- }
51
- groups.set(key, {
52
- versionId: tagSource.sourceVersionId,
53
- digest: tagSource.sourceDigest,
54
- tags: [tagSource.tag]
55
- });
56
- }
57
- return [...groups.values()];
58
- }
@@ -1,2 +0,0 @@
1
- import type Database from "better-sqlite3";
2
- export declare function refineManifestKinds(database: Database.Database, scanId: number): void;
@@ -1,43 +0,0 @@
1
- import { ManifestKinds } from "../core/index.js";
2
- const _refineManifestKindsStatementByDatabase = new WeakMap();
3
- export function refineManifestKinds(database, scanId) {
4
- _refineManifestKindsStatement(database).run(ManifestKinds.crossArchManifest, scanId, ManifestKinds.indexManifest);
5
- }
6
- function _refineManifestKindsStatement(database) {
7
- const cached = _refineManifestKindsStatementByDatabase.get(database);
8
- if (cached) {
9
- return cached;
10
- }
11
- const statement = database.prepare(`
12
- UPDATE manifests AS parent
13
- SET manifest_kind = ?
14
- WHERE parent.scan_id = ?
15
- AND parent.manifest_kind = ?
16
- AND parent.media_type IN (
17
- 'application/vnd.oci.image.index.v1+json',
18
- 'application/vnd.docker.distribution.manifest.list.v2+json'
19
- )
20
- AND NOT EXISTS (
21
- SELECT 1
22
- FROM tags helper_tag
23
- WHERE helper_tag.scan_id = parent.scan_id
24
- AND helper_tag.version_id = parent.version_id
25
- AND helper_tag.is_digest_tag = 1
26
- )
27
- AND EXISTS (
28
- SELECT 1
29
- FROM manifest_descriptors descriptor
30
- JOIN manifests child
31
- ON child.scan_id = descriptor.scan_id
32
- AND child.digest = descriptor.child_digest
33
- WHERE descriptor.scan_id = parent.scan_id
34
- AND descriptor.parent_digest = parent.digest
35
- AND child.media_type IN (
36
- 'application/vnd.oci.image.manifest.v1+json',
37
- 'application/vnd.docker.distribution.manifest.v2+json'
38
- )
39
- )
40
- `);
41
- _refineManifestKindsStatementByDatabase.set(database, statement);
42
- return statement;
43
- }
@@ -1,44 +0,0 @@
1
- DROP VIEW IF EXISTS v_scan_root_manifests;
2
-
3
- CREATE VIEW v_scan_root_manifests AS
4
- SELECT
5
- ps.scan_id,
6
- ps.owner,
7
- ps.package_name,
8
- m.version_id AS root_version_id,
9
- m.digest AS root_digest,
10
- m.manifest_kind AS root_manifest_kind,
11
- pv.created_at,
12
- pv.updated_at,
13
- COUNT(t.tag) AS tag_count,
14
- CASE WHEN COUNT(t.tag) > 0 THEN 1 ELSE 0 END AS is_tagged,
15
- CASE
16
- WHEN EXISTS (
17
- SELECT 1
18
- FROM manifest_reachability mr
19
- WHERE mr.scan_id = m.scan_id
20
- AND mr.descendant_digest = m.digest
21
- AND mr.min_distance > 0
22
- ) THEN 1
23
- ELSE 0
24
- END AS has_ancestor
25
- FROM manifests m
26
- JOIN package_versions pv
27
- ON pv.scan_id = m.scan_id
28
- AND pv.version_id = m.version_id
29
- JOIN package_scans ps
30
- ON ps.scan_id = m.scan_id
31
- LEFT JOIN tags t
32
- ON t.scan_id = m.scan_id
33
- AND t.version_id = m.version_id
34
- AND t.is_digest_tag = 0
35
- GROUP BY
36
- ps.scan_id,
37
- ps.owner,
38
- ps.package_name,
39
- m.version_id,
40
- m.digest,
41
- m.manifest_kind,
42
- pv.created_at,
43
- pv.updated_at
44
- ;
@@ -1,50 +0,0 @@
1
- DROP VIEW IF EXISTS v_digest_tag_relations;
2
-
3
- CREATE VIEW v_digest_tag_relations AS
4
- WITH digest_like_tags AS (
5
- SELECT
6
- lsp.scan_id,
7
- lsp.owner,
8
- lsp.package_name,
9
- t.tag,
10
- t.version_id AS artifact_version_id,
11
- m.digest AS artifact_digest,
12
- m.manifest_kind AS artifact_manifest_kind,
13
- m.subject_digest AS artifact_subject_digest,
14
- 'sha256:' || SUBSTR(t.tag, 8, 64) AS parent_digest,
15
- SUBSTR(t.tag, 72) AS tag_suffix
16
- FROM tags t
17
- JOIN v_latest_scan_per_package lsp
18
- ON lsp.scan_id = t.scan_id
19
- JOIN manifests m
20
- ON m.scan_id = t.scan_id
21
- AND m.version_id = t.version_id
22
- WHERE t.tag LIKE 'sha256-%'
23
- AND LENGTH(t.tag) >= 71
24
- AND SUBSTR(t.tag, 8, 64) NOT GLOB '*[^0-9A-Fa-f]*'
25
- )
26
- SELECT
27
- dlt.scan_id,
28
- dlt.owner,
29
- dlt.package_name,
30
- dlt.tag,
31
- dlt.artifact_version_id,
32
- dlt.artifact_digest,
33
- dlt.artifact_manifest_kind,
34
- dlt.artifact_subject_digest,
35
- dlt.parent_digest,
36
- dlt.tag_suffix,
37
- pm.version_id AS parent_version_id,
38
- pm.manifest_kind AS parent_manifest_kind,
39
- CASE
40
- WHEN dlt.artifact_subject_digest = dlt.parent_digest THEN 1
41
- ELSE 0
42
- END AS subject_matches_parent,
43
- CASE
44
- WHEN pm.digest IS NULL THEN 0
45
- ELSE 1
46
- END AS parent_exists
47
- FROM digest_like_tags dlt
48
- LEFT JOIN manifests pm
49
- ON pm.scan_id = dlt.scan_id
50
- AND pm.digest = dlt.parent_digest;
@@ -1,101 +0,0 @@
1
- DROP VIEW IF EXISTS v_cleanup_root_closure_members;
2
-
3
- CREATE VIEW v_cleanup_root_closure_members AS
4
- WITH selected_roots AS (
5
- SELECT
6
- run.cleanup_run_id,
7
- run.scan_id,
8
- ps.owner,
9
- ps.package_name,
10
- decision.digest AS root_digest,
11
- root_manifest.version_id AS root_version_id,
12
- root_manifest.manifest_kind AS root_manifest_kind,
13
- decision.selection_mode,
14
- decision.selection_reason,
15
- decision.validation_status,
16
- decision.validation_reason_code,
17
- decision.validation_reason
18
- FROM cleanup_root_decisions decision
19
- JOIN cleanup_runs run
20
- ON run.cleanup_run_id = decision.cleanup_run_id
21
- AND run.scan_id = decision.scan_id
22
- JOIN package_scans ps
23
- ON ps.scan_id = run.scan_id
24
- JOIN manifests root_manifest
25
- ON root_manifest.scan_id = decision.scan_id
26
- AND root_manifest.digest = decision.digest
27
- ),
28
- closure_members AS (
29
- SELECT
30
- sr.cleanup_run_id,
31
- sr.scan_id,
32
- sr.owner,
33
- sr.package_name,
34
- sr.root_digest,
35
- sr.root_version_id,
36
- sr.root_manifest_kind,
37
- sr.selection_mode,
38
- sr.selection_reason,
39
- sr.validation_status,
40
- sr.validation_reason_code,
41
- sr.validation_reason,
42
- sr.root_digest AS member_digest,
43
- sr.root_version_id AS member_version_id,
44
- sr.root_manifest_kind AS member_manifest_kind,
45
- 0 AS hops_from_root,
46
- 'root' AS member_role
47
- FROM selected_roots sr
48
-
49
- UNION ALL
50
-
51
- SELECT
52
- sr.cleanup_run_id,
53
- sr.scan_id,
54
- sr.owner,
55
- sr.package_name,
56
- sr.root_digest,
57
- sr.root_version_id,
58
- sr.root_manifest_kind,
59
- sr.selection_mode,
60
- sr.selection_reason,
61
- sr.validation_status,
62
- sr.validation_reason_code,
63
- sr.validation_reason,
64
- member_manifest.digest AS member_digest,
65
- member_manifest.version_id AS member_version_id,
66
- member_manifest.manifest_kind AS member_manifest_kind,
67
- reachability.min_distance AS hops_from_root,
68
- 'descendant' AS member_role
69
- FROM selected_roots sr
70
- JOIN manifest_reachability reachability
71
- ON reachability.scan_id = sr.scan_id
72
- AND reachability.ancestor_digest = sr.root_digest
73
- AND reachability.min_distance > 0
74
- JOIN manifests member_manifest
75
- ON member_manifest.scan_id = sr.scan_id
76
- AND member_manifest.digest = reachability.descendant_digest
77
- )
78
- SELECT
79
- cm.cleanup_run_id,
80
- cm.scan_id,
81
- cm.owner,
82
- cm.package_name,
83
- cm.root_digest,
84
- cm.root_version_id,
85
- cm.root_manifest_kind,
86
- cm.selection_mode,
87
- cm.selection_reason,
88
- cm.validation_status,
89
- cm.validation_reason_code,
90
- cm.validation_reason,
91
- cm.member_digest,
92
- cm.member_version_id,
93
- cm.member_manifest_kind,
94
- cm.hops_from_root,
95
- cm.member_role,
96
- tag.tag AS member_tag
97
- FROM closure_members cm
98
- LEFT JOIN tags tag
99
- ON tag.scan_id = cm.scan_id
100
- AND tag.version_id = cm.member_version_id
101
- AND tag.is_digest_tag = 0;