pepr 0.46.2-nightly.0 → 0.46.2-nightly.2
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/dist/cli.js +79 -23
- package/dist/controller.js +1 -1
- package/dist/lib.js +124 -38
- package/dist/lib.js.map +2 -2
- package/dist/src/cli/init/templates.d.ts +2 -2
- package/dist/src/cli/init/templates.d.ts.map +1 -1
- package/dist/src/lib/assets/assets.d.ts.map +1 -1
- package/dist/src/lib/assets/defaultTestObjects.d.ts.map +1 -1
- package/dist/src/lib/assets/deploy.d.ts.map +1 -1
- package/dist/src/lib/assets/index.d.ts.map +1 -1
- package/dist/src/lib/assets/pods.d.ts.map +1 -1
- package/dist/src/lib/assets/webhooks.d.ts.map +1 -1
- package/dist/src/lib/assets/yaml/generateAllYaml.d.ts.map +1 -1
- package/dist/src/lib/controller/index.d.ts.map +1 -1
- package/dist/src/lib/core/capability.d.ts.map +1 -1
- package/dist/src/lib/core/module.d.ts.map +1 -1
- package/dist/src/lib/core/storage.d.ts.map +1 -1
- package/dist/src/lib/deploymentChecks.d.ts.map +1 -1
- package/dist/src/lib/filter/adjudicators/admissionRequest.d.ts.map +1 -1
- package/dist/src/lib/filter/adjudicators/binding.d.ts.map +1 -1
- package/dist/src/lib/filter/adjudicators/kubernetesObject.d.ts.map +1 -1
- package/dist/src/lib/filter/adjudicators/mismatch.d.ts.map +1 -1
- package/dist/src/lib/filter/adjudicators/postCollection.d.ts.map +1 -1
- package/dist/src/lib/filter/filter.d.ts.map +1 -1
- package/dist/src/lib/helpers.d.ts.map +1 -1
- package/dist/src/lib/included-files.d.ts.map +1 -1
- package/dist/src/lib/processors/decode-utils.d.ts.map +1 -1
- package/dist/src/lib/processors/mutate-processor.d.ts.map +1 -1
- package/dist/src/lib/processors/validate-processor.d.ts.map +1 -1
- package/dist/src/lib/processors/watch-processor.d.ts.map +1 -1
- package/dist/src/lib/telemetry/metrics.d.ts.map +1 -1
- package/dist/src/lib/telemetry/webhookTimeouts.d.ts.map +1 -1
- package/package.json +4 -5
- package/src/lib/assets/assets.ts +46 -11
- package/src/lib/assets/defaultTestObjects.ts +13 -2
- package/src/lib/assets/deploy.ts +25 -5
- package/src/lib/assets/index.ts +8 -2
- package/src/lib/assets/pods.ts +5 -1
- package/src/lib/assets/webhooks.ts +12 -3
- package/src/lib/assets/yaml/generateAllYaml.ts +12 -2
- package/src/lib/controller/index.ts +9 -3
- package/src/lib/core/capability.ts +32 -8
- package/src/lib/core/module.ts +5 -1
- package/src/lib/core/storage.ts +3 -1
- package/src/lib/deploymentChecks.ts +3 -1
- package/src/lib/filter/adjudicators/admissionRequest.ts +4 -1
- package/src/lib/filter/adjudicators/binding.ts +17 -4
- package/src/lib/filter/adjudicators/kubernetesObject.ts +4 -2
- package/src/lib/filter/adjudicators/mismatch.ts +25 -6
- package/src/lib/filter/adjudicators/postCollection.ts +15 -3
- package/src/lib/filter/filter.ts +63 -15
- package/src/lib/helpers.ts +36 -10
- package/src/lib/included-files.ts +5 -1
- package/src/lib/processors/decode-utils.ts +4 -1
- package/src/lib/processors/mutate-processor.ts +4 -1
- package/src/lib/processors/validate-processor.ts +4 -1
- package/src/lib/processors/watch-processor.ts +49 -19
- package/src/lib/telemetry/metrics.ts +6 -2
- package/src/lib/telemetry/webhookTimeouts.ts +4 -1
- package/src/templates/.prettierrc.json +3 -2
- package/src/templates/capabilities/hello-pepr.ts +2 -8
- package/dist/src/sdk/cosign.d.ts +0 -18
- package/dist/src/sdk/cosign.d.ts.map +0 -1
- package/src/lib/.prettierrc +0 -14
- package/src/sdk/cosign.ts +0 -327
|
@@ -4,7 +4,13 @@ import Log from "../telemetry/logger";
|
|
|
4
4
|
import { Binding } from "../types";
|
|
5
5
|
import { Capability } from "../core/capability";
|
|
6
6
|
import { Event } from "../enums";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
K8s,
|
|
9
|
+
KubernetesObject,
|
|
10
|
+
WatchCfg,
|
|
11
|
+
WatchEvent,
|
|
12
|
+
GenericClass,
|
|
13
|
+
} from "kubernetes-fluent-client";
|
|
8
14
|
import { Queue } from "../core/queue";
|
|
9
15
|
import { WatchPhase, WatcherType } from "kubernetes-fluent-client/dist/fluent/types";
|
|
10
16
|
import { KubernetesListObject } from "kubernetes-fluent-client/dist/types";
|
|
@@ -51,8 +57,12 @@ export function getOrCreateQueue(obj: KubernetesObject): Queue<KubernetesObject>
|
|
|
51
57
|
|
|
52
58
|
// Watch configuration
|
|
53
59
|
const watchCfg: WatchCfg = {
|
|
54
|
-
resyncFailureMax: process.env.PEPR_RESYNC_FAILURE_MAX
|
|
55
|
-
|
|
60
|
+
resyncFailureMax: process.env.PEPR_RESYNC_FAILURE_MAX
|
|
61
|
+
? parseInt(process.env.PEPR_RESYNC_FAILURE_MAX, 10)
|
|
62
|
+
: 5,
|
|
63
|
+
resyncDelaySec: process.env.PEPR_RESYNC_DELAY_SECONDS
|
|
64
|
+
? parseInt(process.env.PEPR_RESYNC_DELAY_SECONDS, 10)
|
|
65
|
+
: 5,
|
|
56
66
|
lastSeenLimitSeconds: process.env.PEPR_LAST_SEEN_LIMIT_SECONDS
|
|
57
67
|
? parseInt(process.env.PEPR_LAST_SEEN_LIMIT_SECONDS, 10)
|
|
58
68
|
: 300,
|
|
@@ -79,7 +89,9 @@ export function setupWatch(capabilities: Capability[], ignoredNamespaces?: strin
|
|
|
79
89
|
capabilities.map(capability =>
|
|
80
90
|
capability.bindings
|
|
81
91
|
.filter(binding => binding.isWatch)
|
|
82
|
-
.forEach(bindingElement =>
|
|
92
|
+
.forEach(bindingElement =>
|
|
93
|
+
runBinding(bindingElement, capability.namespaces, ignoredNamespaces),
|
|
94
|
+
),
|
|
83
95
|
);
|
|
84
96
|
}
|
|
85
97
|
|
|
@@ -100,12 +112,20 @@ async function runBinding(
|
|
|
100
112
|
// The watch callback is run when an object is received or dequeued
|
|
101
113
|
Log.debug({ watchCfg }, "Effective WatchConfig");
|
|
102
114
|
|
|
103
|
-
const watchCallback = async (
|
|
115
|
+
const watchCallback = async (
|
|
116
|
+
kubernetesObject: KubernetesObject,
|
|
117
|
+
phase: WatchPhase,
|
|
118
|
+
): Promise<void> => {
|
|
104
119
|
// First, filter the object based on the phase
|
|
105
120
|
if (phaseMatch.includes(phase)) {
|
|
106
121
|
try {
|
|
107
122
|
// Then, check if the object matches the filter
|
|
108
|
-
const filterMatch = filterNoMatchReason(
|
|
123
|
+
const filterMatch = filterNoMatchReason(
|
|
124
|
+
binding,
|
|
125
|
+
kubernetesObject,
|
|
126
|
+
capabilityNamespaces,
|
|
127
|
+
ignoredNamespaces,
|
|
128
|
+
);
|
|
109
129
|
if (filterMatch !== "") {
|
|
110
130
|
Log.debug(filterMatch);
|
|
111
131
|
return;
|
|
@@ -139,7 +159,10 @@ async function runBinding(
|
|
|
139
159
|
// [ true, void, undefined ] SHOULD remove finalizer
|
|
140
160
|
// [ false ] should NOT remove finalizer
|
|
141
161
|
if (shouldRemoveFinalizer === false) {
|
|
142
|
-
Log.debug(
|
|
162
|
+
Log.debug(
|
|
163
|
+
{ obj: kubernetesObject },
|
|
164
|
+
`Skipping removal of finalizer '${peprFinal}' from '${resource}'`,
|
|
165
|
+
);
|
|
143
166
|
} else {
|
|
144
167
|
await removeFinalizer(binding, kubernetesObject);
|
|
145
168
|
}
|
|
@@ -147,16 +170,19 @@ async function runBinding(
|
|
|
147
170
|
};
|
|
148
171
|
|
|
149
172
|
// Setup the resource watch
|
|
150
|
-
const watcher = K8s(binding.model, { ...binding.filters, kindOverride: binding.kind }).Watch(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
173
|
+
const watcher = K8s(binding.model, { ...binding.filters, kindOverride: binding.kind }).Watch(
|
|
174
|
+
async (obj, phase) => {
|
|
175
|
+
Log.debug(obj, `Watch event ${phase} received`);
|
|
176
|
+
|
|
177
|
+
if (binding.isQueue) {
|
|
178
|
+
const queue = getOrCreateQueue(obj);
|
|
179
|
+
await queue.enqueue(obj, phase, watchCallback);
|
|
180
|
+
} else {
|
|
181
|
+
await watchCallback(obj, phase);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
watchCfg,
|
|
185
|
+
);
|
|
160
186
|
|
|
161
187
|
// Register event handlers
|
|
162
188
|
registerWatchEventHandlers(watcher, logEvent, metricsCollector);
|
|
@@ -214,10 +240,14 @@ export function registerWatchEventHandlers(
|
|
|
214
240
|
[WatchEvent.CONNECT]: url => logEvent(WatchEvent.CONNECT, url),
|
|
215
241
|
[WatchEvent.DATA_ERROR]: err => logEvent(WatchEvent.DATA_ERROR, err.message),
|
|
216
242
|
[WatchEvent.RECONNECT]: retryCount =>
|
|
217
|
-
logEvent(
|
|
243
|
+
logEvent(
|
|
244
|
+
WatchEvent.RECONNECT,
|
|
245
|
+
`Reconnecting after ${retryCount} attempt${retryCount === 1 ? "" : "s"}`,
|
|
246
|
+
),
|
|
218
247
|
[WatchEvent.RECONNECT_PENDING]: () => logEvent(WatchEvent.RECONNECT_PENDING),
|
|
219
248
|
[WatchEvent.ABORT]: err => logEvent(WatchEvent.ABORT, err.message),
|
|
220
|
-
[WatchEvent.OLD_RESOURCE_VERSION]: errMessage =>
|
|
249
|
+
[WatchEvent.OLD_RESOURCE_VERSION]: errMessage =>
|
|
250
|
+
logEvent(WatchEvent.OLD_RESOURCE_VERSION, errMessage),
|
|
221
251
|
[WatchEvent.NETWORK_ERROR]: err => logEvent(WatchEvent.NETWORK_ERROR, err.message),
|
|
222
252
|
[WatchEvent.LIST_ERROR]: err => logEvent(WatchEvent.LIST_ERROR, err.message),
|
|
223
253
|
[WatchEvent.LIST]: list => logEvent(WatchEvent.LIST, JSON.stringify(list, undefined, 2)),
|
|
@@ -56,7 +56,9 @@ export class MetricsCollector {
|
|
|
56
56
|
this.addSummary(this.#metricNames.mutate, "Mutation operation summary");
|
|
57
57
|
this.addSummary(this.#metricNames.validate, "Validation operation summary");
|
|
58
58
|
this.addGauge(this.#metricNames.cacheMiss, "Number of cache misses per window", ["window"]);
|
|
59
|
-
this.addGauge(this.#metricNames.resyncFailureCount, "Number of failures per resync operation", [
|
|
59
|
+
this.addGauge(this.#metricNames.resyncFailureCount, "Number of failures per resync operation", [
|
|
60
|
+
"count",
|
|
61
|
+
]);
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
#getMetricName = (name: string): string => `${this.#prefix}_${name}`;
|
|
@@ -173,7 +175,9 @@ export class MetricsCollector {
|
|
|
173
175
|
if (firstKey !== undefined) {
|
|
174
176
|
this.#cacheMissWindows.delete(firstKey);
|
|
175
177
|
}
|
|
176
|
-
this.#gauges
|
|
178
|
+
this.#gauges
|
|
179
|
+
.get(this.#getMetricName(this.#metricNames.cacheMiss))
|
|
180
|
+
?.remove({ window: firstKey });
|
|
177
181
|
}
|
|
178
182
|
};
|
|
179
183
|
}
|
|
@@ -9,7 +9,10 @@ export class MeasureWebhookTimeout {
|
|
|
9
9
|
|
|
10
10
|
constructor(webhookType: WebhookType) {
|
|
11
11
|
this.#webhookType = webhookType;
|
|
12
|
-
metricsCollector.addCounter(
|
|
12
|
+
metricsCollector.addCounter(
|
|
13
|
+
`${webhookType}_timeouts`,
|
|
14
|
+
`Number of ${webhookType} webhook timeouts`,
|
|
15
|
+
);
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
start(timeout: number = 10): void {
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
"bracketSpacing": true,
|
|
5
5
|
"embeddedLanguageFormatting": "auto",
|
|
6
6
|
"insertPragma": false,
|
|
7
|
-
"printWidth":
|
|
7
|
+
"printWidth": 100,
|
|
8
8
|
"quoteProps": "as-needed",
|
|
9
9
|
"requirePragma": false,
|
|
10
10
|
"semi": true,
|
|
11
11
|
"tabWidth": 2,
|
|
12
|
-
"useTabs": false
|
|
12
|
+
"useTabs": false,
|
|
13
|
+
"vueIndentScriptAndStyle": false
|
|
13
14
|
}
|
|
@@ -89,9 +89,7 @@ When(a.ConfigMap)
|
|
|
89
89
|
.IsCreated()
|
|
90
90
|
.WithName("example-1")
|
|
91
91
|
.Mutate(request => {
|
|
92
|
-
request
|
|
93
|
-
.SetLabel("pepr", "was-here")
|
|
94
|
-
.SetAnnotation("pepr.dev", "annotations-work-too");
|
|
92
|
+
request.SetLabel("pepr", "was-here").SetAnnotation("pepr.dev", "annotations-work-too");
|
|
95
93
|
|
|
96
94
|
// Use the Store to persist data between requests and Pepr controller pods
|
|
97
95
|
Store.setItem("example-1", "was-here");
|
|
@@ -228,11 +226,7 @@ function example4Cb(cm: PeprMutateRequest<a.ConfigMap>) {
|
|
|
228
226
|
* Note because the Capability defines namespaces, the namespace specified here must be one of those.
|
|
229
227
|
* Alternatively, you can remove the namespace from the Capability definition and specify it here.
|
|
230
228
|
*/
|
|
231
|
-
When(a.ConfigMap)
|
|
232
|
-
.IsCreated()
|
|
233
|
-
.InNamespace("pepr-demo-2")
|
|
234
|
-
.WithName("example-4a")
|
|
235
|
-
.Mutate(example4Cb);
|
|
229
|
+
When(a.ConfigMap).IsCreated().InNamespace("pepr-demo-2").WithName("example-4a").Mutate(example4Cb);
|
|
236
230
|
|
|
237
231
|
/**
|
|
238
232
|
* ---------------------------------------------------------------------------------------------------
|
package/dist/src/sdk/cosign.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export declare enum MediaTypeDockerV2 {
|
|
2
|
-
Manifest = "application/vnd.docker.distribution.manifest.v2+json"
|
|
3
|
-
}
|
|
4
|
-
export declare enum MediaTypeOciV1 {
|
|
5
|
-
Manifest = "application/vnd.oci.image.manifest.v1+json",
|
|
6
|
-
Index = "application/vnd.oci.image.index.v1+json"
|
|
7
|
-
}
|
|
8
|
-
export declare function head(rawUrl: string, mediaType: string, optsParam?: Record<string, any>): Promise<any>;
|
|
9
|
-
export declare function get(rawUrl: string, mediaType: string, optsParam?: Record<string, any>): Promise<any>;
|
|
10
|
-
export declare function download(rawUrl: string, localPath: string, optsParam?: Record<string, any>): Promise<void>;
|
|
11
|
-
/**
|
|
12
|
-
* Returns all containers in a pod
|
|
13
|
-
* @param {string} iref image reference
|
|
14
|
-
* @param {array} pubkeys list of paths to node crypto code signing pubkeys
|
|
15
|
-
* @returns {boolean} whether the iref was signed by a key in the pubkeys
|
|
16
|
-
*/
|
|
17
|
-
export declare function verifyImage(iref: string, pubkeys: string[], tlsCrts?: string[]): Promise<boolean>;
|
|
18
|
-
//# sourceMappingURL=cosign.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cosign.d.ts","sourceRoot":"","sources":["../../../src/sdk/cosign.ts"],"names":[],"mappings":"AAWA,oBAAY,iBAAiB;IAC3B,QAAQ,yDAAyD;CAClE;AAED,oBAAY,cAAc;IACxB,QAAQ,+CAA+C;IACvD,KAAK,4CAA4C;CAClD;AAGD,wBAAsB,IAAI,CACxB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAClC,OAAO,CAAC,GAAG,CAAC,CAwCd;AAGD,wBAAsB,GAAG,CACvB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAClC,OAAO,CAAC,GAAG,CAAC,CAmDd;AAGD,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAClC,OAAO,CAAC,IAAI,CAAC,CA8Cf;AAMD;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,CAAC,EAAE,MAAM,EAAE,GACjB,OAAO,CAAC,OAAO,CAAC,CAsIlB"}
|
package/src/lib/.prettierrc
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"arrowParens": "avoid",
|
|
3
|
-
"bracketSameLine": false,
|
|
4
|
-
"bracketSpacing": true,
|
|
5
|
-
"embeddedLanguageFormatting": "auto",
|
|
6
|
-
"insertPragma": false,
|
|
7
|
-
"printWidth": 120,
|
|
8
|
-
"quoteProps": "as-needed",
|
|
9
|
-
"requirePragma": false,
|
|
10
|
-
"semi": true,
|
|
11
|
-
"useTabs": false,
|
|
12
|
-
"vueIndentScriptAndStyle": false,
|
|
13
|
-
"tabWidth": 2
|
|
14
|
-
}
|
package/src/sdk/cosign.ts
DELETED
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
-
|
|
4
|
-
import { https } from "follow-redirects";
|
|
5
|
-
import { readFile, unlink } from "node:fs/promises";
|
|
6
|
-
import { createWriteStream } from "node:fs";
|
|
7
|
-
import * as crypto from "node:crypto";
|
|
8
|
-
import { PublicKeyDetails, TrustedRoot } from "@sigstore/protobuf-specs";
|
|
9
|
-
import { bundleFromJSON } from "@sigstore/bundle";
|
|
10
|
-
import { toSignedEntity, toTrustMaterial, Verifier } from "@sigstore/verify";
|
|
11
|
-
|
|
12
|
-
export enum MediaTypeDockerV2 {
|
|
13
|
-
Manifest = "application/vnd.docker.distribution.manifest.v2+json",
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export enum MediaTypeOciV1 {
|
|
17
|
-
Manifest = "application/vnd.oci.image.manifest.v1+json",
|
|
18
|
-
Index = "application/vnd.oci.image.index.v1+json",
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
22
|
-
export async function head(
|
|
23
|
-
rawUrl: string,
|
|
24
|
-
mediaType: string,
|
|
25
|
-
optsParam: Record<string, any> = {},
|
|
26
|
-
): Promise<any> {
|
|
27
|
-
const url = new URL(rawUrl);
|
|
28
|
-
|
|
29
|
-
return new Promise((resolve, reject) => {
|
|
30
|
-
const opts = {
|
|
31
|
-
protocol: url.protocol,
|
|
32
|
-
hostname: url.hostname,
|
|
33
|
-
port: url.port,
|
|
34
|
-
path: url.pathname,
|
|
35
|
-
method: "HEAD",
|
|
36
|
-
headers: { Accept: mediaType },
|
|
37
|
-
...optsParam,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
https
|
|
41
|
-
.request(opts, resp => {
|
|
42
|
-
const { statusCode } = resp;
|
|
43
|
-
|
|
44
|
-
let error;
|
|
45
|
-
if (!statusCode?.toString().startsWith("2") && !statusCode?.toString().startsWith("3")) {
|
|
46
|
-
reject(new Error(`err: status code: ${statusCode}: expected 2xx|3xx`));
|
|
47
|
-
error = true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (error) {
|
|
51
|
-
resp.resume();
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
resp.setEncoding("utf8");
|
|
56
|
-
|
|
57
|
-
resp.on("data", () => {});
|
|
58
|
-
|
|
59
|
-
resp.on("end", () => {
|
|
60
|
-
resolve(resp.headers);
|
|
61
|
-
});
|
|
62
|
-
})
|
|
63
|
-
.on("error", e => reject(e))
|
|
64
|
-
.end();
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
69
|
-
export async function get(
|
|
70
|
-
rawUrl: string,
|
|
71
|
-
mediaType: string,
|
|
72
|
-
optsParam: Record<string, any> = {},
|
|
73
|
-
): Promise<any> {
|
|
74
|
-
const url = new URL(rawUrl);
|
|
75
|
-
|
|
76
|
-
return new Promise((resolve, reject) => {
|
|
77
|
-
const opts = {
|
|
78
|
-
protocol: url.protocol,
|
|
79
|
-
hostname: url.hostname,
|
|
80
|
-
port: url.port,
|
|
81
|
-
path: url.pathname,
|
|
82
|
-
method: "GET",
|
|
83
|
-
headers: {
|
|
84
|
-
"User-Agent": "node",
|
|
85
|
-
Accept: mediaType,
|
|
86
|
-
},
|
|
87
|
-
...optsParam,
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
https
|
|
91
|
-
.request(opts, resp => {
|
|
92
|
-
const { statusCode } = resp;
|
|
93
|
-
|
|
94
|
-
let error;
|
|
95
|
-
|
|
96
|
-
if (!statusCode?.toString().startsWith("2") && !statusCode?.toString().startsWith("3")) {
|
|
97
|
-
console.log(resp.headers);
|
|
98
|
-
reject(new Error(`err: status code: ${statusCode}: expected 2xx`));
|
|
99
|
-
error = true;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (error) {
|
|
103
|
-
resp.resume();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
resp.setEncoding("utf8");
|
|
108
|
-
|
|
109
|
-
let raw = "";
|
|
110
|
-
resp.on("data", chunk => {
|
|
111
|
-
raw += chunk;
|
|
112
|
-
});
|
|
113
|
-
resp.on("end", () => {
|
|
114
|
-
try {
|
|
115
|
-
resolve({ head: resp.headers, body: raw });
|
|
116
|
-
} catch (e) {
|
|
117
|
-
reject(e);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
})
|
|
121
|
-
.on("error", e => reject(e))
|
|
122
|
-
.end();
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
127
|
-
export async function download(
|
|
128
|
-
rawUrl: string,
|
|
129
|
-
localPath: string,
|
|
130
|
-
optsParam: Record<string, any> = {},
|
|
131
|
-
): Promise<void> {
|
|
132
|
-
const url = new URL(rawUrl);
|
|
133
|
-
|
|
134
|
-
return new Promise((resolve, reject) => {
|
|
135
|
-
const opts = {
|
|
136
|
-
protocol: url.protocol,
|
|
137
|
-
hostname: url.hostname,
|
|
138
|
-
port: url.port,
|
|
139
|
-
path: url.pathname,
|
|
140
|
-
method: "GET",
|
|
141
|
-
headers: {
|
|
142
|
-
"User-Agent": "node",
|
|
143
|
-
Accept: "application/octet-stream",
|
|
144
|
-
},
|
|
145
|
-
...optsParam,
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
https
|
|
149
|
-
.request(opts, resp => {
|
|
150
|
-
const { statusCode } = resp;
|
|
151
|
-
|
|
152
|
-
let error;
|
|
153
|
-
|
|
154
|
-
if (!statusCode?.toString().startsWith("2") && !statusCode?.toString().startsWith("3")) {
|
|
155
|
-
console.log(resp.headers);
|
|
156
|
-
reject(new Error(`err: status code: ${statusCode}: expected 2xx`));
|
|
157
|
-
error = true;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (error) {
|
|
161
|
-
resp.resume();
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const ws = createWriteStream(localPath).on("finish", () => {
|
|
166
|
-
ws.close(() => resolve());
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
resp.pipe(ws);
|
|
170
|
-
})
|
|
171
|
-
.on("error", async err => {
|
|
172
|
-
await unlink(localPath);
|
|
173
|
-
reject(err);
|
|
174
|
-
})
|
|
175
|
-
.end();
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
//
|
|
180
|
-
// TODO: should support using certs too
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Returns all containers in a pod
|
|
185
|
-
* @param {string} iref image reference
|
|
186
|
-
* @param {array} pubkeys list of paths to node crypto code signing pubkeys
|
|
187
|
-
* @returns {boolean} whether the iref was signed by a key in the pubkeys
|
|
188
|
-
*/
|
|
189
|
-
export async function verifyImage(
|
|
190
|
-
iref: string,
|
|
191
|
-
pubkeys: string[],
|
|
192
|
-
tlsCrts?: string[],
|
|
193
|
-
): Promise<boolean> {
|
|
194
|
-
const X: Record<string, any> = {};
|
|
195
|
-
|
|
196
|
-
// <host---> / <image----------------------->
|
|
197
|
-
// / <name---------------> : <tag->
|
|
198
|
-
// docker.io / library / hello-world : latest
|
|
199
|
-
//
|
|
200
|
-
// <host> / <image------------------------------------->
|
|
201
|
-
// / <name------------------------------> : <tag>
|
|
202
|
-
// ttl.sh / 5dad3c9b-7ccc-4115-be27-c9244e7c0e06 : 2000m
|
|
203
|
-
|
|
204
|
-
X.iref = {};
|
|
205
|
-
X.iref.raw = iref;
|
|
206
|
-
X.iref.host = iref.split("/")[0];
|
|
207
|
-
X.iref.image = iref.replace(`${X.iref.host}/`, "");
|
|
208
|
-
X.iref.tag = X.iref.image.split(":").at(-1);
|
|
209
|
-
X.iref.name = X.iref.image.replace(`:${X.iref.tag}`, "");
|
|
210
|
-
|
|
211
|
-
X.manifest = {
|
|
212
|
-
url: `https://${X.iref.host}/v2/${X.iref.name}/manifests/${X.iref.tag}`,
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const supportsMediaType = async (url: string, mediaType: string): Promise<boolean> => {
|
|
216
|
-
return (await head(url, mediaType, { ca: tlsCrts }))["content-type"] === mediaType;
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
const canOciV1Manifest = async (manifestUrl: string): Promise<boolean> => {
|
|
220
|
-
return supportsMediaType(manifestUrl, MediaTypeOciV1.Manifest);
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const canDockerV2Manifest = async (manifestUrl: string): Promise<boolean> => {
|
|
224
|
-
return supportsMediaType(manifestUrl, MediaTypeDockerV2.Manifest);
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
// prettier-ignore
|
|
228
|
-
const manifestResp =
|
|
229
|
-
await canOciV1Manifest(X.manifest.url) ? await get(X.manifest.url, MediaTypeOciV1.Manifest, {ca: tlsCrts}) :
|
|
230
|
-
await canDockerV2Manifest(X.manifest.url) ? await get(X.manifest.url, MediaTypeDockerV2.Manifest, {ca: tlsCrts}) :
|
|
231
|
-
(():never => { throw "Can't pull image manifest with supported MediaType." })();
|
|
232
|
-
X.manifest.content = manifestResp.body;
|
|
233
|
-
|
|
234
|
-
X.manifest.digest = `sha256:${crypto
|
|
235
|
-
.createHash("sha256")
|
|
236
|
-
.update(X.manifest.content)
|
|
237
|
-
.digest("hex")
|
|
238
|
-
.toString()}`;
|
|
239
|
-
|
|
240
|
-
X.sig = {};
|
|
241
|
-
X.sig.tag = `${X.manifest.digest.replace(":", "-")}.sig`;
|
|
242
|
-
X.sig.triangulated = `${X.iref.host}/${X.iref.name}:${X.sig.tag}`;
|
|
243
|
-
X.sig.url = `https://${X.iref.host}/v2/${X.iref.name}/manifests/${X.sig.tag}`;
|
|
244
|
-
|
|
245
|
-
const sigManifestResp = await get(X.sig.url, MediaTypeOciV1.Manifest, { ca: tlsCrts });
|
|
246
|
-
X.sig.manifest = sigManifestResp.body;
|
|
247
|
-
|
|
248
|
-
const cosignSigLayer = JSON.parse(X.sig.manifest).layers.filter((f: any) =>
|
|
249
|
-
Object.hasOwn(f?.annotations, "dev.cosignproject.cosign/signature"),
|
|
250
|
-
)[0];
|
|
251
|
-
|
|
252
|
-
X.sig.blob = {};
|
|
253
|
-
X.sig.blob.digest = cosignSigLayer.digest;
|
|
254
|
-
X.sig.blob.signature = cosignSigLayer.annotations["dev.cosignproject.cosign/signature"];
|
|
255
|
-
X.sig.blob.url = `https://${X.iref.host}/v2/${X.iref.name}/blobs/${X.sig.blob.digest}`;
|
|
256
|
-
|
|
257
|
-
const sigBlobResp = await get(X.sig.blob.url, "application/octet-stream", { ca: tlsCrts });
|
|
258
|
-
X.sig.blob.content = sigBlobResp.body;
|
|
259
|
-
|
|
260
|
-
let verified = false;
|
|
261
|
-
|
|
262
|
-
for (const pubkey of pubkeys) {
|
|
263
|
-
// https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/__tests__/verifier.test.ts
|
|
264
|
-
const pubKeyRaw = await readFile(`${pubkey}`, { encoding: "utf8" });
|
|
265
|
-
const pubKey = crypto.createPublicKey({
|
|
266
|
-
key: pubKeyRaw,
|
|
267
|
-
format: "pem",
|
|
268
|
-
encoding: "utf-8",
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
const trustedRoot = {
|
|
272
|
-
tlogs: [],
|
|
273
|
-
ctlogs: [],
|
|
274
|
-
timestampAuthorities: [],
|
|
275
|
-
certificateAuthorities: [],
|
|
276
|
-
} as unknown as TrustedRoot;
|
|
277
|
-
|
|
278
|
-
const keys = {
|
|
279
|
-
hint: {
|
|
280
|
-
rawBytes: pubKey.export({ type: "spki", format: "der" }),
|
|
281
|
-
keyDetails: PublicKeyDetails.PKIX_ECDSA_P256_SHA_256,
|
|
282
|
-
},
|
|
283
|
-
};
|
|
284
|
-
const trustMaterial = toTrustMaterial(trustedRoot, keys);
|
|
285
|
-
|
|
286
|
-
const subject = new Verifier(trustMaterial, {
|
|
287
|
-
ctlogThreshold: 0,
|
|
288
|
-
tlogThreshold: 0,
|
|
289
|
-
tsaThreshold: 0,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
const bundle = bundleFromJSON({
|
|
293
|
-
mediaType: "application/vnd.dev.sigstore.bundle+json;version=0.1",
|
|
294
|
-
verificationMaterial: {
|
|
295
|
-
publicKey: {
|
|
296
|
-
hint: "hint",
|
|
297
|
-
},
|
|
298
|
-
tlogEntries: [],
|
|
299
|
-
timestampVerificationData: {
|
|
300
|
-
rfc3161Timestamps: [],
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
messageSignature: {
|
|
304
|
-
messageDigest: {
|
|
305
|
-
algorithm: "SHA2_256",
|
|
306
|
-
digest: crypto.createHash("sha256").update(X.sig.blob.content).digest().toString(),
|
|
307
|
-
},
|
|
308
|
-
signature: X.sig.blob.signature,
|
|
309
|
-
},
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
const signedEntity = toSignedEntity(bundle, Buffer.from(X.sig.blob.content));
|
|
313
|
-
|
|
314
|
-
try {
|
|
315
|
-
subject.verify(signedEntity);
|
|
316
|
-
verified = true;
|
|
317
|
-
break;
|
|
318
|
-
} catch (e) {
|
|
319
|
-
if (e.message.includes("signature verification failed")) {
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
throw e;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return verified;
|
|
327
|
-
}
|