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.
- package/README.md +99 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +72 -0
- package/dist/cli.js.map +1 -0
- package/dist/composeParser.d.ts +9 -0
- package/dist/composeParser.js +72 -0
- package/dist/composeParser.js.map +1 -0
- package/dist/composeParser.test.d.ts +1 -0
- package/dist/composeParser.test.js +65 -0
- package/dist/composeParser.test.js.map +1 -0
- package/dist/github.d.ts +37 -0
- package/dist/github.js +270 -0
- package/dist/github.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +9 -0
- package/dist/output.js +39 -0
- package/dist/output.js.map +1 -0
- package/dist/registry.d.ts +9 -0
- package/dist/registry.js +123 -0
- package/dist/registry.js.map +1 -0
- package/dist/resolver.d.ts +5 -0
- package/dist/resolver.js +165 -0
- package/dist/resolver.js.map +1 -0
- package/dist/types.d.ts +20 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +37 -0
- package/run.sh +25 -0
- package/src/cli.ts +87 -0
- package/src/composeParser.test.ts +76 -0
- package/src/composeParser.ts +79 -0
- package/src/github.ts +339 -0
- package/src/index.ts +4 -0
- package/src/output.ts +57 -0
- package/src/registry.ts +153 -0
- package/src/resolver.ts +192 -0
- package/src/types.ts +21 -0
- package/tsconfig.json +16 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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"}
|
package/dist/output.d.ts
ADDED
|
@@ -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":"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"}
|
|
@@ -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>>;
|
package/dist/registry.js
ADDED
|
@@ -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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"}
|
package/dist/resolver.js
ADDED
|
@@ -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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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
});
|