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/src/resolver.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { ImageRef, ImageResult } from "./types.js";
|
|
2
|
+
import { fetchOciLabels } from "./registry.js";
|
|
3
|
+
import {
|
|
4
|
+
resolveTagToCommit,
|
|
5
|
+
inferRepoFromDockerhub,
|
|
6
|
+
resolveGhcrDigestViaPackages,
|
|
7
|
+
resolveGhcrLatestViaPackages,
|
|
8
|
+
getLatestReleaseCommit,
|
|
9
|
+
getLatestCommit,
|
|
10
|
+
} from "./github.js";
|
|
11
|
+
|
|
12
|
+
const COMMIT_SHA_RE = /^[0-9a-f]{40,}$/;
|
|
13
|
+
const DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
|
|
14
|
+
|
|
15
|
+
function isResolvableTag(tag: string): boolean {
|
|
16
|
+
return !!tag && tag !== "latest" && !DIGEST_RE.test(tag);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function inferRepo(
|
|
20
|
+
ref: ImageRef
|
|
21
|
+
): Promise<[string | null, string | null]> {
|
|
22
|
+
if (ref.registry === "ghcr.io") {
|
|
23
|
+
return [ref.namespace, ref.name];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (ref.registry === "docker.io") {
|
|
27
|
+
const hubResult = await inferRepoFromDockerhub(ref.namespace, ref.name);
|
|
28
|
+
if (hubResult) return hubResult;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [null, null];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run the resolution chain for a single image.
|
|
36
|
+
*/
|
|
37
|
+
export async function resolveImage(
|
|
38
|
+
service: string,
|
|
39
|
+
ref: ImageRef
|
|
40
|
+
): Promise<ImageResult> {
|
|
41
|
+
const result: ImageResult = {
|
|
42
|
+
service,
|
|
43
|
+
image: ref.raw,
|
|
44
|
+
registry: ref.registry,
|
|
45
|
+
repo: null,
|
|
46
|
+
tag: ref.tag,
|
|
47
|
+
commit: null,
|
|
48
|
+
commit_url: null,
|
|
49
|
+
status: "repo_not_found",
|
|
50
|
+
resolution_method: null,
|
|
51
|
+
confidence: null,
|
|
52
|
+
steps: [],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Step 1: Check OCI labels
|
|
56
|
+
result.steps.push(`[1/5] Fetching OCI labels from ${ref.registry}/${ref.namespace}/${ref.name}:${ref.tag}`);
|
|
57
|
+
const labels = await fetchOciLabels(ref);
|
|
58
|
+
const source = labels["org.opencontainers.image.source"];
|
|
59
|
+
const revision = labels["org.opencontainers.image.revision"];
|
|
60
|
+
if (source && revision) {
|
|
61
|
+
result.steps.push(`[1/5] Found OCI labels: source=${source}, revision=${revision.slice(0, 12)}`);
|
|
62
|
+
result.repo = source;
|
|
63
|
+
result.commit = revision;
|
|
64
|
+
result.commit_url = `${source}/commit/${revision}`;
|
|
65
|
+
result.status = "resolved";
|
|
66
|
+
result.resolution_method = "oci_labels";
|
|
67
|
+
result.confidence = ref.tag === "latest" ? "approximate" : "exact";
|
|
68
|
+
return result;
|
|
69
|
+
} else {
|
|
70
|
+
result.steps.push("[1/5] No OCI labels found");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Step 2: Infer repo
|
|
74
|
+
result.steps.push(`[2/5] Inferring GitHub repo from ${ref.registry}/${ref.namespace}/${ref.name}`);
|
|
75
|
+
const [owner, repoName] = await inferRepo(ref);
|
|
76
|
+
if (owner && repoName) {
|
|
77
|
+
result.steps.push(`[2/5] Repo inferred: ${owner}/${repoName}`);
|
|
78
|
+
result.repo = `https://github.com/${owner}/${repoName}`;
|
|
79
|
+
} else {
|
|
80
|
+
result.steps.push("[2/5] Could not infer GitHub repo");
|
|
81
|
+
result.status = "repo_not_found";
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if tag is a commit SHA
|
|
86
|
+
if (COMMIT_SHA_RE.test(ref.tag)) {
|
|
87
|
+
result.steps.push("[2/5] Tag is a commit SHA, using directly");
|
|
88
|
+
result.commit = ref.tag;
|
|
89
|
+
result.commit_url = `${result.repo}/commit/${ref.tag}`;
|
|
90
|
+
result.status = "resolved";
|
|
91
|
+
result.resolution_method = "commit_sha_tag";
|
|
92
|
+
result.confidence = "exact";
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Step 3: Tag-to-commit resolution
|
|
97
|
+
if (isResolvableTag(ref.tag)) {
|
|
98
|
+
result.steps.push(`[3/5] Matching tag "${ref.tag}" against git tags in ${owner}/${repoName}`);
|
|
99
|
+
const tagResult = await resolveTagToCommit(owner, repoName, ref.tag);
|
|
100
|
+
if (tagResult) {
|
|
101
|
+
const [commitSha, isExact] = tagResult;
|
|
102
|
+
if (isExact) {
|
|
103
|
+
result.steps.push(`[3/5] Exact tag match: ${commitSha.slice(0, 12)}`);
|
|
104
|
+
} else {
|
|
105
|
+
result.steps.push(`[3/5] Prefix match (e.g. v2.10 -> v2.10.x): ${commitSha.slice(0, 12)}`);
|
|
106
|
+
}
|
|
107
|
+
result.commit = commitSha;
|
|
108
|
+
result.commit_url = `${result.repo}/commit/${commitSha}`;
|
|
109
|
+
result.status = "resolved";
|
|
110
|
+
result.resolution_method = "tag_match";
|
|
111
|
+
result.confidence = isExact ? "exact" : "approximate";
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
result.steps.push("[3/5] No matching git tag found");
|
|
115
|
+
result.status = "repo_found_tag_not_matched";
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Step 4: For GHCR images, try the packages API for digest or :latest
|
|
120
|
+
if (ref.registry === "ghcr.io") {
|
|
121
|
+
let pkgResult: { repo: string; commit: string | null; tags: string[] } | null = null;
|
|
122
|
+
let pkgConfidence: string | null = null;
|
|
123
|
+
|
|
124
|
+
if (DIGEST_RE.test(ref.tag)) {
|
|
125
|
+
result.steps.push(`[4/5] Trying GHCR packages API for digest ${ref.tag.slice(0, 20)}...`);
|
|
126
|
+
pkgResult = await resolveGhcrDigestViaPackages(
|
|
127
|
+
ref.namespace,
|
|
128
|
+
ref.name,
|
|
129
|
+
ref.tag
|
|
130
|
+
);
|
|
131
|
+
pkgConfidence = "exact";
|
|
132
|
+
} else if (ref.tag === "latest" || !ref.tag) {
|
|
133
|
+
result.steps.push("[4/5] Trying GHCR packages API for :latest tag");
|
|
134
|
+
pkgResult = await resolveGhcrLatestViaPackages(ref.namespace, ref.name);
|
|
135
|
+
pkgConfidence = "approximate";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (pkgResult) {
|
|
139
|
+
const repoFull = pkgResult.repo;
|
|
140
|
+
result.repo = `https://github.com/${repoFull}`;
|
|
141
|
+
if (pkgResult.commit) {
|
|
142
|
+
const commit = pkgResult.commit;
|
|
143
|
+
result.steps.push(`[4/5] Packages API: repo=${repoFull}, commit=${commit.slice(0, 12)}`);
|
|
144
|
+
result.commit = commit;
|
|
145
|
+
result.commit_url = `${result.repo}/commit/${result.commit}`;
|
|
146
|
+
result.status = "resolved";
|
|
147
|
+
result.resolution_method = "packages_api";
|
|
148
|
+
result.confidence = pkgConfidence;
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
result.steps.push("[4/5] Packages API: found package but no resolvable tags");
|
|
152
|
+
const tags = pkgResult.tags ?? [];
|
|
153
|
+
const resolvable = tags.filter((t) => t !== "latest");
|
|
154
|
+
result.status = resolvable.length > 0 ? "repo_found_tag_not_matched" : "no_tag";
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Step 5: For :latest on any registry, try the latest GitHub release,
|
|
160
|
+
// then fall back to the latest commit on the default branch
|
|
161
|
+
if ((ref.tag === "latest" || !ref.tag) && owner && repoName) {
|
|
162
|
+
result.steps.push(`[5/5] Trying latest GitHub release for ${owner}/${repoName}`);
|
|
163
|
+
const releaseResult = await getLatestReleaseCommit(owner, repoName);
|
|
164
|
+
if (releaseResult) {
|
|
165
|
+
const [commitSha, tagName] = releaseResult;
|
|
166
|
+
result.steps.push(`[5/5] Latest release: tag=${tagName}, commit=${commitSha.slice(0, 12)}`);
|
|
167
|
+
result.commit = commitSha;
|
|
168
|
+
result.commit_url = `${result.repo}/commit/${commitSha}`;
|
|
169
|
+
result.status = "resolved";
|
|
170
|
+
result.resolution_method = "latest_release";
|
|
171
|
+
result.confidence = "approximate";
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// No releases - fall back to latest commit on default branch
|
|
176
|
+
result.steps.push("[5/5] No release found, trying latest commit on default branch");
|
|
177
|
+
const latestSha = await getLatestCommit(owner, repoName);
|
|
178
|
+
if (latestSha) {
|
|
179
|
+
result.steps.push(`[5/5] Latest commit: ${latestSha.slice(0, 12)}`);
|
|
180
|
+
result.commit = latestSha;
|
|
181
|
+
result.commit_url = `${result.repo}/commit/${latestSha}`;
|
|
182
|
+
result.status = "resolved";
|
|
183
|
+
result.resolution_method = "latest_commit";
|
|
184
|
+
result.confidence = "approximate";
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
result.steps.push("[5/5] Could not resolve to a commit");
|
|
190
|
+
result.status = "no_tag";
|
|
191
|
+
return result;
|
|
192
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ImageRef {
|
|
2
|
+
registry: string;
|
|
3
|
+
namespace: string;
|
|
4
|
+
name: string;
|
|
5
|
+
tag: string;
|
|
6
|
+
raw: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ImageResult {
|
|
10
|
+
service: string;
|
|
11
|
+
image: string;
|
|
12
|
+
registry: string;
|
|
13
|
+
repo: string | null;
|
|
14
|
+
tag: string;
|
|
15
|
+
commit: string | null;
|
|
16
|
+
commit_url: string | null;
|
|
17
|
+
status: string;
|
|
18
|
+
resolution_method: string | null;
|
|
19
|
+
confidence: string | null;
|
|
20
|
+
steps: string[];
|
|
21
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"sourceMap": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*.ts"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|