code-provenance 0.1.0

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.
@@ -0,0 +1,4 @@
1
+ export type { ImageRef, ImageResult } from "./types.js";
2
+ export { parseCompose, parseImageRef } from "./composeParser.js";
3
+ export { resolveImage } from "./resolver.js";
4
+ export { formatJson, formatTable } from "./output.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { parseCompose, parseImageRef } from "./composeParser.js";
2
+ export { resolveImage } from "./resolver.js";
3
+ export { formatJson, formatTable } from "./output.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { ImageResult } from "./types.js";
2
+ /**
3
+ * Format results as a JSON array.
4
+ */
5
+ export declare function formatJson(results: ImageResult[]): string;
6
+ /**
7
+ * Format results as a box-drawing table.
8
+ */
9
+ export declare function formatTable(results: ImageResult[]): string;
package/dist/output.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Format results as a JSON array.
3
+ */
4
+ export function formatJson(results) {
5
+ return JSON.stringify(results, null, 2);
6
+ }
7
+ /**
8
+ * Format results as a box-drawing table.
9
+ */
10
+ export function formatTable(results) {
11
+ const columns = ["SERVICE", "IMAGE", "REPO", "COMMIT", "STATUS", "CONFIDENCE"];
12
+ // Build rows
13
+ const rows = results.map((r) => [
14
+ r.service,
15
+ r.image,
16
+ r.repo ? r.repo.replace("https://", "") : "-",
17
+ r.commit ? r.commit.slice(0, 12) : "-",
18
+ r.status,
19
+ r.confidence || "-",
20
+ ]);
21
+ // Calculate column widths
22
+ const widths = columns.map((col, i) => {
23
+ const dataMax = rows.reduce((max, row) => Math.max(max, row[i].length), 0);
24
+ return Math.max(col.length, dataMax);
25
+ });
26
+ const pad = (s, w) => s + " ".repeat(w - s.length);
27
+ // Build lines
28
+ const topBorder = "┌" + widths.map((w) => "─".repeat(w + 2)).join("┬") + "┐";
29
+ const headerSep = "├" + widths.map((w) => "─".repeat(w + 2)).join("┼") + "┤";
30
+ const bottomBorder = "└" + widths.map((w) => "─".repeat(w + 2)).join("┴") + "┘";
31
+ const headerRow = "│" +
32
+ columns.map((col, i) => " " + pad(col, widths[i]) + " ").join("│") +
33
+ "│";
34
+ const dataRows = rows.map((row) => "│" +
35
+ row.map((cell, i) => " " + pad(cell, widths[i]) + " ").join("│") +
36
+ "│");
37
+ return [topBorder, headerRow, headerSep, ...dataRows, bottomBorder].join("\n");
38
+ }
39
+ //# sourceMappingURL=output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.js","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,OAAsB;IAC/C,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,OAAsB;IAChD,MAAM,OAAO,GAAG,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;IAE/E,aAAa;IACb,MAAM,IAAI,GAAe,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAC1C,CAAC,CAAC,OAAO;QACT,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG;QAC7C,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG;QACtC,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,UAAU,IAAI,GAAG;KACpB,CAAC,CAAC;IAEH,0BAA0B;IAC1B,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;QACpC,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;QAC3E,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;IAEnE,cAAc;IACd,MAAM,SAAS,GACb,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IAC7D,MAAM,SAAS,GACb,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IAC7D,MAAM,YAAY,GAChB,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IAE7D,MAAM,SAAS,GACb,GAAG;QACH,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAClE,GAAG,CAAC;IAEN,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CACvB,CAAC,GAAG,EAAE,EAAE,CACN,GAAG;QACH,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAChE,GAAG,CACN,CAAC;IAEF,OAAO,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,QAAQ,EAAE,YAAY,CAAC,CAAC,IAAI,CACtE,IAAI,CACL,CAAC;AACJ,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { ImageRef } from "./types.js";
2
+ /**
3
+ * Get an anonymous pull token from an OCI registry.
4
+ */
5
+ export declare function getRegistryToken(registry: string, repoPath: string): Promise<string | null>;
6
+ /**
7
+ * Fetch OCI labels from an image's config blob without pulling the image.
8
+ */
9
+ export declare function fetchOciLabels(ref: ImageRef): Promise<Record<string, string>>;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Get an anonymous pull token from an OCI registry.
3
+ */
4
+ export async function getRegistryToken(registry, repoPath) {
5
+ let url;
6
+ if (registry === "ghcr.io") {
7
+ url = `https://ghcr.io/token?scope=repository:${encodeURIComponent(repoPath)}:pull`;
8
+ }
9
+ else if (registry === "docker.io") {
10
+ url =
11
+ `https://auth.docker.io/token?service=registry.docker.io` +
12
+ `&scope=repository:${encodeURIComponent(repoPath)}:pull`;
13
+ }
14
+ else {
15
+ return null;
16
+ }
17
+ try {
18
+ const resp = await fetch(url, { signal: AbortSignal.timeout(10000) });
19
+ if (!resp.ok)
20
+ return null;
21
+ const data = (await resp.json());
22
+ return data.token ?? null;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function registryBaseUrl(registry) {
29
+ if (registry === "docker.io")
30
+ return "https://registry-1.docker.io";
31
+ return `https://${registry}`;
32
+ }
33
+ const MANIFEST_ACCEPT = [
34
+ "application/vnd.docker.distribution.manifest.v2+json",
35
+ "application/vnd.oci.image.manifest.v1+json",
36
+ "application/vnd.docker.distribution.manifest.list.v2+json",
37
+ "application/vnd.oci.image.index.v1+json",
38
+ ].join(", ");
39
+ const INDEX_MEDIA_TYPES = new Set([
40
+ "application/vnd.docker.distribution.manifest.list.v2+json",
41
+ "application/vnd.oci.image.index.v1+json",
42
+ ]);
43
+ /**
44
+ * Resolve a manifest reference to a config blob digest, handling multi-arch indexes.
45
+ */
46
+ async function resolveManifestToConfigDigest(baseUrl, repoPath, reference, token) {
47
+ try {
48
+ const resp = await fetch(`${baseUrl}/v2/${repoPath}/manifests/${reference}`, {
49
+ headers: {
50
+ Authorization: `Bearer ${token}`,
51
+ Accept: MANIFEST_ACCEPT,
52
+ },
53
+ signal: AbortSignal.timeout(10000),
54
+ });
55
+ if (!resp.ok)
56
+ return null;
57
+ const data = await resp.json();
58
+ const mediaType = data.mediaType ?? "";
59
+ // If it's an index/manifest list, pick amd64/linux
60
+ if (INDEX_MEDIA_TYPES.has(mediaType)) {
61
+ const manifests = data.manifests ?? [];
62
+ let platformDigest = null;
63
+ // Try amd64/linux first
64
+ for (const m of manifests) {
65
+ const platform = m.platform ?? {};
66
+ if (platform.os === "unknown")
67
+ continue;
68
+ if (platform.architecture === "amd64" && platform.os === "linux") {
69
+ platformDigest = m.digest;
70
+ break;
71
+ }
72
+ }
73
+ // Fall back to first non-attestation manifest
74
+ if (!platformDigest) {
75
+ for (const m of manifests) {
76
+ if (m.platform?.os !== "unknown") {
77
+ platformDigest = m.digest;
78
+ break;
79
+ }
80
+ }
81
+ }
82
+ if (!platformDigest)
83
+ return null;
84
+ return resolveManifestToConfigDigest(baseUrl, repoPath, platformDigest, token);
85
+ }
86
+ // Single manifest - extract config digest
87
+ return data.config?.digest ?? null;
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ /**
94
+ * Fetch OCI labels from an image's config blob without pulling the image.
95
+ */
96
+ export async function fetchOciLabels(ref) {
97
+ const repoPath = `${ref.namespace}/${ref.name}`;
98
+ const token = await getRegistryToken(ref.registry, repoPath);
99
+ if (!token)
100
+ return {};
101
+ const baseUrl = registryBaseUrl(ref.registry);
102
+ const configDigest = await resolveManifestToConfigDigest(baseUrl, repoPath, ref.tag, token);
103
+ if (!configDigest)
104
+ return {};
105
+ try {
106
+ const resp = await fetch(`${baseUrl}/v2/${repoPath}/blobs/${configDigest}`, {
107
+ headers: {
108
+ Authorization: `Bearer ${token}`,
109
+ Accept: "application/vnd.docker.container.image.v1+json",
110
+ },
111
+ redirect: "follow",
112
+ signal: AbortSignal.timeout(10000),
113
+ });
114
+ if (!resp.ok)
115
+ return {};
116
+ const data = await resp.json();
117
+ return data.config?.Labels ?? {};
118
+ }
119
+ catch {
120
+ return {};
121
+ }
122
+ }
123
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { ImageRef, ImageResult } from "./types.js";
2
+ /**
3
+ * Run the resolution chain for a single image.
4
+ */
5
+ export declare function resolveImage(service: string, ref: ImageRef): Promise<ImageResult>;
@@ -0,0 +1,165 @@
1
+ import { fetchOciLabels } from "./registry.js";
2
+ import { resolveTagToCommit, inferRepoFromDockerhub, resolveGhcrDigestViaPackages, resolveGhcrLatestViaPackages, getLatestReleaseCommit, getLatestCommit, } from "./github.js";
3
+ const COMMIT_SHA_RE = /^[0-9a-f]{40,}$/;
4
+ const DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
5
+ function isResolvableTag(tag) {
6
+ return !!tag && tag !== "latest" && !DIGEST_RE.test(tag);
7
+ }
8
+ async function inferRepo(ref) {
9
+ if (ref.registry === "ghcr.io") {
10
+ return [ref.namespace, ref.name];
11
+ }
12
+ if (ref.registry === "docker.io") {
13
+ const hubResult = await inferRepoFromDockerhub(ref.namespace, ref.name);
14
+ if (hubResult)
15
+ return hubResult;
16
+ }
17
+ return [null, null];
18
+ }
19
+ /**
20
+ * Run the resolution chain for a single image.
21
+ */
22
+ export async function resolveImage(service, ref) {
23
+ const result = {
24
+ service,
25
+ image: ref.raw,
26
+ registry: ref.registry,
27
+ repo: null,
28
+ tag: ref.tag,
29
+ commit: null,
30
+ commit_url: null,
31
+ status: "repo_not_found",
32
+ resolution_method: null,
33
+ confidence: null,
34
+ steps: [],
35
+ };
36
+ // Step 1: Check OCI labels
37
+ result.steps.push(`[1/5] Fetching OCI labels from ${ref.registry}/${ref.namespace}/${ref.name}:${ref.tag}`);
38
+ const labels = await fetchOciLabels(ref);
39
+ const source = labels["org.opencontainers.image.source"];
40
+ const revision = labels["org.opencontainers.image.revision"];
41
+ if (source && revision) {
42
+ result.steps.push(`[1/5] Found OCI labels: source=${source}, revision=${revision.slice(0, 12)}`);
43
+ result.repo = source;
44
+ result.commit = revision;
45
+ result.commit_url = `${source}/commit/${revision}`;
46
+ result.status = "resolved";
47
+ result.resolution_method = "oci_labels";
48
+ result.confidence = ref.tag === "latest" ? "approximate" : "exact";
49
+ return result;
50
+ }
51
+ else {
52
+ result.steps.push("[1/5] No OCI labels found");
53
+ }
54
+ // Step 2: Infer repo
55
+ result.steps.push(`[2/5] Inferring GitHub repo from ${ref.registry}/${ref.namespace}/${ref.name}`);
56
+ const [owner, repoName] = await inferRepo(ref);
57
+ if (owner && repoName) {
58
+ result.steps.push(`[2/5] Repo inferred: ${owner}/${repoName}`);
59
+ result.repo = `https://github.com/${owner}/${repoName}`;
60
+ }
61
+ else {
62
+ result.steps.push("[2/5] Could not infer GitHub repo");
63
+ result.status = "repo_not_found";
64
+ return result;
65
+ }
66
+ // Check if tag is a commit SHA
67
+ if (COMMIT_SHA_RE.test(ref.tag)) {
68
+ result.steps.push("[2/5] Tag is a commit SHA, using directly");
69
+ result.commit = ref.tag;
70
+ result.commit_url = `${result.repo}/commit/${ref.tag}`;
71
+ result.status = "resolved";
72
+ result.resolution_method = "commit_sha_tag";
73
+ result.confidence = "exact";
74
+ return result;
75
+ }
76
+ // Step 3: Tag-to-commit resolution
77
+ if (isResolvableTag(ref.tag)) {
78
+ result.steps.push(`[3/5] Matching tag "${ref.tag}" against git tags in ${owner}/${repoName}`);
79
+ const tagResult = await resolveTagToCommit(owner, repoName, ref.tag);
80
+ if (tagResult) {
81
+ const [commitSha, isExact] = tagResult;
82
+ if (isExact) {
83
+ result.steps.push(`[3/5] Exact tag match: ${commitSha.slice(0, 12)}`);
84
+ }
85
+ else {
86
+ result.steps.push(`[3/5] Prefix match (e.g. v2.10 -> v2.10.x): ${commitSha.slice(0, 12)}`);
87
+ }
88
+ result.commit = commitSha;
89
+ result.commit_url = `${result.repo}/commit/${commitSha}`;
90
+ result.status = "resolved";
91
+ result.resolution_method = "tag_match";
92
+ result.confidence = isExact ? "exact" : "approximate";
93
+ return result;
94
+ }
95
+ result.steps.push("[3/5] No matching git tag found");
96
+ result.status = "repo_found_tag_not_matched";
97
+ return result;
98
+ }
99
+ // Step 4: For GHCR images, try the packages API for digest or :latest
100
+ if (ref.registry === "ghcr.io") {
101
+ let pkgResult = null;
102
+ let pkgConfidence = null;
103
+ if (DIGEST_RE.test(ref.tag)) {
104
+ result.steps.push(`[4/5] Trying GHCR packages API for digest ${ref.tag.slice(0, 20)}...`);
105
+ pkgResult = await resolveGhcrDigestViaPackages(ref.namespace, ref.name, ref.tag);
106
+ pkgConfidence = "exact";
107
+ }
108
+ else if (ref.tag === "latest" || !ref.tag) {
109
+ result.steps.push("[4/5] Trying GHCR packages API for :latest tag");
110
+ pkgResult = await resolveGhcrLatestViaPackages(ref.namespace, ref.name);
111
+ pkgConfidence = "approximate";
112
+ }
113
+ if (pkgResult) {
114
+ const repoFull = pkgResult.repo;
115
+ result.repo = `https://github.com/${repoFull}`;
116
+ if (pkgResult.commit) {
117
+ const commit = pkgResult.commit;
118
+ result.steps.push(`[4/5] Packages API: repo=${repoFull}, commit=${commit.slice(0, 12)}`);
119
+ result.commit = commit;
120
+ result.commit_url = `${result.repo}/commit/${result.commit}`;
121
+ result.status = "resolved";
122
+ result.resolution_method = "packages_api";
123
+ result.confidence = pkgConfidence;
124
+ return result;
125
+ }
126
+ result.steps.push("[4/5] Packages API: found package but no resolvable tags");
127
+ const tags = pkgResult.tags ?? [];
128
+ const resolvable = tags.filter((t) => t !== "latest");
129
+ result.status = resolvable.length > 0 ? "repo_found_tag_not_matched" : "no_tag";
130
+ return result;
131
+ }
132
+ }
133
+ // Step 5: For :latest on any registry, try the latest GitHub release,
134
+ // then fall back to the latest commit on the default branch
135
+ if ((ref.tag === "latest" || !ref.tag) && owner && repoName) {
136
+ result.steps.push(`[5/5] Trying latest GitHub release for ${owner}/${repoName}`);
137
+ const releaseResult = await getLatestReleaseCommit(owner, repoName);
138
+ if (releaseResult) {
139
+ const [commitSha, tagName] = releaseResult;
140
+ result.steps.push(`[5/5] Latest release: tag=${tagName}, commit=${commitSha.slice(0, 12)}`);
141
+ result.commit = commitSha;
142
+ result.commit_url = `${result.repo}/commit/${commitSha}`;
143
+ result.status = "resolved";
144
+ result.resolution_method = "latest_release";
145
+ result.confidence = "approximate";
146
+ return result;
147
+ }
148
+ // No releases - fall back to latest commit on default branch
149
+ result.steps.push("[5/5] No release found, trying latest commit on default branch");
150
+ const latestSha = await getLatestCommit(owner, repoName);
151
+ if (latestSha) {
152
+ result.steps.push(`[5/5] Latest commit: ${latestSha.slice(0, 12)}`);
153
+ result.commit = latestSha;
154
+ result.commit_url = `${result.repo}/commit/${latestSha}`;
155
+ result.status = "resolved";
156
+ result.resolution_method = "latest_commit";
157
+ result.confidence = "approximate";
158
+ return result;
159
+ }
160
+ }
161
+ result.steps.push("[5/5] Could not resolve to a commit");
162
+ result.status = "no_tag";
163
+ return result;
164
+ }
165
+ //# sourceMappingURL=resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.js","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export interface ImageRef {
2
+ registry: string;
3
+ namespace: string;
4
+ name: string;
5
+ tag: string;
6
+ raw: string;
7
+ }
8
+ export interface ImageResult {
9
+ service: string;
10
+ image: string;
11
+ registry: string;
12
+ repo: string | null;
13
+ tag: string;
14
+ commit: string | null;
15
+ commit_url: string | null;
16
+ status: string;
17
+ resolution_method: string | null;
18
+ confidence: string | null;
19
+ steps: string[];
20
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "code-provenance",
3
+ "version": "0.1.0",
4
+ "description": "Resolve Docker images to their source code commits on GitHub",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "bin": {
15
+ "code-provenance": "dist/cli.js"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "test": "node --test dist/**/*.test.js",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "dependencies": {
23
+ "yaml": "^2.4.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^20.11.0",
27
+ "typescript": "^5.3.0"
28
+ },
29
+ "author": "SCRT Labs",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/scrtlabs/code-provenance.git",
34
+ "directory": "node"
35
+ },
36
+ "homepage": "https://github.com/scrtlabs/code-provenance/tree/main/node"
37
+ }
package/run.sh ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+
6
+ # Install deps if needed
7
+ if [ ! -d "$SCRIPT_DIR/node_modules" ]; then
8
+ echo "Installing dependencies..."
9
+ npm --prefix "$SCRIPT_DIR" install
10
+ fi
11
+
12
+ # Build if needed
13
+ if [ ! -d "$SCRIPT_DIR/dist" ]; then
14
+ echo "Building..."
15
+ npm --prefix "$SCRIPT_DIR" run build
16
+ fi
17
+
18
+ # Auto-detect GitHub token from gh CLI if not already set
19
+ if [ -z "${GITHUB_TOKEN:-}" ] && command -v gh &>/dev/null; then
20
+ GITHUB_TOKEN=$(gh auth token 2>/dev/null) || true
21
+ export GITHUB_TOKEN
22
+ fi
23
+
24
+ # Run the tool, passing all arguments through
25
+ node "$SCRIPT_DIR/dist/cli.js" "$@"
package/src/cli.ts ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { existsSync } from "node:fs";
5
+ import { parseCompose, parseImageRef } from "./composeParser.js";
6
+ import { resolveImage } from "./resolver.js";
7
+ import { formatJson, formatTable } from "./output.js";
8
+
9
+ function printHelp(): void {
10
+ console.log(`usage: code-provenance [-h] [--json] [compose_file]
11
+
12
+ Resolve Docker images to their source code commits on GitHub.
13
+
14
+ positional arguments:
15
+ compose_file Path to docker-compose file (default: docker-compose.yml)
16
+
17
+ options:
18
+ -h, --help show this help message and exit
19
+ --json Output results as JSON
20
+ -v, --verbose Show resolution steps`);
21
+ }
22
+
23
+ async function main(): Promise<number> {
24
+ const args = process.argv.slice(2);
25
+
26
+ if (args.includes("-h") || args.includes("--help")) {
27
+ printHelp();
28
+ return 0;
29
+ }
30
+
31
+ const jsonOutput = args.includes("--json");
32
+ const verbose = args.includes("--verbose") || args.includes("-v");
33
+ const positionalArgs = args.filter((a) => a !== "--json" && a !== "--verbose" && a !== "-v");
34
+ const composeFile = positionalArgs[0] || "docker-compose.yml";
35
+
36
+ if (!existsSync(composeFile)) {
37
+ console.error(`Error: ${composeFile} not found`);
38
+ return 1;
39
+ }
40
+
41
+ const yamlContent = readFileSync(composeFile, "utf-8");
42
+ const services = parseCompose(yamlContent);
43
+
44
+ if (services.length === 0) {
45
+ console.error("No services with images found.");
46
+ return 0;
47
+ }
48
+
49
+ // Resolve all images in parallel
50
+ const results = await Promise.all(
51
+ services.map(([serviceName, imageString]) => {
52
+ const ref = parseImageRef(imageString);
53
+ return resolveImage(serviceName, ref);
54
+ })
55
+ );
56
+
57
+ if (verbose) {
58
+ for (const r of results) {
59
+ console.error(`\nResolving ${r.image} ...`);
60
+ for (const step of r.steps) {
61
+ console.error(` ${step}`);
62
+ }
63
+ console.error(
64
+ ` → ${r.status}` +
65
+ (r.status === "resolved"
66
+ ? ` (${r.resolution_method}, ${r.confidence})`
67
+ : "")
68
+ );
69
+ }
70
+ console.error();
71
+ }
72
+
73
+ if (jsonOutput) {
74
+ console.log(formatJson(results));
75
+ } else {
76
+ console.log(formatTable(results));
77
+ }
78
+
79
+ return 0;
80
+ }
81
+
82
+ main()
83
+ .then((code) => process.exit(code))
84
+ .catch((err) => {
85
+ console.error(err);
86
+ process.exit(1);
87
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { parseImageRef, parseCompose } from "./composeParser.js";
4
+
5
+ describe("parseImageRef", () => {
6
+ it("parses ghcr with tag", () => {
7
+ const ref = parseImageRef("ghcr.io/excalidraw/excalidraw:v0.17.3");
8
+ assert.equal(ref.registry, "ghcr.io");
9
+ assert.equal(ref.namespace, "excalidraw");
10
+ assert.equal(ref.name, "excalidraw");
11
+ assert.equal(ref.tag, "v0.17.3");
12
+ });
13
+
14
+ it("parses docker hub official image", () => {
15
+ const ref = parseImageRef("postgres:16");
16
+ assert.equal(ref.registry, "docker.io");
17
+ assert.equal(ref.namespace, "library");
18
+ assert.equal(ref.name, "postgres");
19
+ assert.equal(ref.tag, "16");
20
+ });
21
+
22
+ it("parses docker hub namespaced image", () => {
23
+ const ref = parseImageRef("traefik/whoami:v1.10.3");
24
+ assert.equal(ref.registry, "docker.io");
25
+ assert.equal(ref.namespace, "traefik");
26
+ assert.equal(ref.name, "whoami");
27
+ assert.equal(ref.tag, "v1.10.3");
28
+ });
29
+
30
+ it("defaults to latest when no tag", () => {
31
+ const ref = parseImageRef("nginx");
32
+ assert.equal(ref.registry, "docker.io");
33
+ assert.equal(ref.namespace, "library");
34
+ assert.equal(ref.name, "nginx");
35
+ assert.equal(ref.tag, "latest");
36
+ });
37
+
38
+ it("handles digest reference", () => {
39
+ const ref = parseImageRef(
40
+ "ghcr.io/org/app@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
41
+ );
42
+ assert.equal(ref.registry, "ghcr.io");
43
+ assert.equal(ref.namespace, "org");
44
+ assert.equal(ref.name, "app");
45
+ assert.equal(
46
+ ref.tag,
47
+ "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
48
+ );
49
+ });
50
+ });
51
+
52
+ describe("parseCompose", () => {
53
+ it("extracts services with image field", () => {
54
+ const yaml = `
55
+ services:
56
+ web:
57
+ image: nginx:latest
58
+ api:
59
+ build: .
60
+ db:
61
+ image: postgres:16
62
+ `;
63
+ const result = parseCompose(yaml);
64
+ assert.equal(result.length, 2);
65
+ assert.deepEqual(result[0], ["web", "nginx:latest"]);
66
+ assert.deepEqual(result[1], ["db", "postgres:16"]);
67
+ });
68
+
69
+ it("handles empty services", () => {
70
+ const yaml = `
71
+ services: {}
72
+ `;
73
+ const result = parseCompose(yaml);
74
+ assert.equal(result.length, 0);
75
+ });
76
+ });