Initial Commit
This commit is contained in:
6
packages/security-stack/security/slsa/src/otel/index.ts
Normal file
6
packages/security-stack/security/slsa/src/otel/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
createSlsaOtelAdapter,
|
||||
noopSlsaOtelAdapter,
|
||||
type SlsaEventAttributes,
|
||||
type SlsaOtelAdapter,
|
||||
} from "./slsa-otel.js";
|
||||
74
packages/security-stack/security/slsa/src/otel/slsa-otel.md
Normal file
74
packages/security-stack/security/slsa/src/otel/slsa-otel.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# slsa-otel
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
OpenTelemetry adapter for SLSA audit events. Every major capability in the
|
||||
package — provenance generation, verification, Rekor upload, inclusion-proof
|
||||
verification, policy evaluation — emits a structured span event through this
|
||||
adapter. By default the adapter attaches events to the currently-active span,
|
||||
so it composes naturally with the calling service's trace context without
|
||||
SLSA needing to manage its own span lifecycle.
|
||||
|
||||
## Features
|
||||
|
||||
- Five well-known event names: `slsa.provenance.generated`,
|
||||
`slsa.provenance.verified`, `slsa.rekor.upload`,
|
||||
`slsa.rekor.inclusion_verified`, `slsa.policy.violation`.
|
||||
- Typed attribute bag (`SlsaEventAttributes`) under the `slsa.*` namespace.
|
||||
- Zero-config no-op default when no exporter is registered.
|
||||
- Explicit `tracer` override for background / detached contexts.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createSlsaOtelAdapter, generateProvenance } from "@suaveplan/slsa";
|
||||
|
||||
const otel = createSlsaOtelAdapter();
|
||||
const stmt = generateProvenance({
|
||||
/* ...input... */,
|
||||
otel,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { trace } from "@opentelemetry/api";
|
||||
import { createSlsaOtelAdapter, RekorClient } from "@suaveplan/slsa";
|
||||
|
||||
const tracer = trace.getTracer("publish-pipeline");
|
||||
const otel = createSlsaOtelAdapter(tracer);
|
||||
|
||||
const client = new RekorClient({ otel });
|
||||
await tracer.startActiveSpan("publish", async (span) => {
|
||||
try {
|
||||
const entry = await client.uploadHashedrekord(rekord);
|
||||
await client.verifyInclusionProof(entry);
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `SlsaEventAttributes` — named attribute bag; every field is optional.
|
||||
- `SlsaOtelAdapter` — interface with five `on*` methods, all returning `void`.
|
||||
- `createSlsaOtelAdapter(tracer?)` — returns a live adapter. With no
|
||||
`tracer`, events attach to the active span; otherwise spans are started
|
||||
inline and ended.
|
||||
- `noopSlsaOtelAdapter` — default adapter that discards events.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Span events are preferred over metrics because SLSA events are inherently
|
||||
correlated: "generated", "verified", and "logged to Rekor" usually happen
|
||||
inside a single CI job's trace. Counters would lose that correlation.
|
||||
Attributes are typed as a closed shape so accidental typos surface as
|
||||
compile-time errors, but the runtime cast to `Record<string, never>` is used
|
||||
because `@opentelemetry/api`'s `AttributeValue` type is wider than our event
|
||||
schema and TypeScript's invariance on index signatures would otherwise
|
||||
require an unsafe upcast at every call site.
|
||||
110
packages/security-stack/security/slsa/src/otel/slsa-otel.test.ts
Normal file
110
packages/security-stack/security/slsa/src/otel/slsa-otel.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { type Span, SpanStatusCode, trace } from "@opentelemetry/api";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { createSlsaOtelAdapter, noopSlsaOtelAdapter } from "./slsa-otel.js";
|
||||
|
||||
type RecordedEvent = { name: string; attrs: Record<string, unknown> };
|
||||
|
||||
function makeFakeSpan(): Span & { events: RecordedEvent[] } {
|
||||
const events: RecordedEvent[] = [];
|
||||
const span = {
|
||||
events,
|
||||
addEvent: (name: string, attrs?: Record<string, unknown>) => {
|
||||
events.push({ name, attrs: attrs ?? {} });
|
||||
return span;
|
||||
},
|
||||
end: () => {},
|
||||
setAttribute: () => span,
|
||||
setAttributes: () => span,
|
||||
setStatus: () => span,
|
||||
updateName: () => span,
|
||||
recordException: () => {},
|
||||
isRecording: () => true,
|
||||
spanContext: () => ({
|
||||
traceId: "00000000000000000000000000000001",
|
||||
spanId: "0000000000000001",
|
||||
traceFlags: 1,
|
||||
}),
|
||||
status: { code: SpanStatusCode.UNSET },
|
||||
} as unknown as Span & { events: RecordedEvent[] };
|
||||
return span;
|
||||
}
|
||||
|
||||
describe("noopSlsaOtelAdapter", () => {
|
||||
it("accepts all events without throwing", { timeout: 5000 }, () => {
|
||||
expect(() => {
|
||||
noopSlsaOtelAdapter.onProvenanceGenerated({});
|
||||
noopSlsaOtelAdapter.onProvenanceVerified({});
|
||||
noopSlsaOtelAdapter.onRekorUpload({});
|
||||
noopSlsaOtelAdapter.onRekorInclusionVerified({});
|
||||
noopSlsaOtelAdapter.onPolicyViolation({});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createSlsaOtelAdapter", () => {
|
||||
it("is callable with zero args and does not throw", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const a = createSlsaOtelAdapter();
|
||||
expect(() => {
|
||||
a.onProvenanceGenerated({ "slsa.result.valid": true });
|
||||
a.onProvenanceVerified({});
|
||||
a.onRekorUpload({});
|
||||
a.onRekorInclusionVerified({});
|
||||
a.onPolicyViolation({});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("emits events on the active span", { timeout: 5000 }, () => {
|
||||
const span = makeFakeSpan();
|
||||
const originalGet = trace.getActiveSpan;
|
||||
(
|
||||
trace as unknown as { getActiveSpan: () => Span | undefined }
|
||||
).getActiveSpan = () => span;
|
||||
try {
|
||||
const adapter = createSlsaOtelAdapter();
|
||||
adapter.onProvenanceGenerated({ "slsa.builder.id": "gitlab-ci" });
|
||||
adapter.onRekorUpload({ "slsa.rekor.uuid": "u1" });
|
||||
} finally {
|
||||
(
|
||||
trace as unknown as {
|
||||
getActiveSpan: () => Span | undefined;
|
||||
}
|
||||
).getActiveSpan = originalGet;
|
||||
}
|
||||
expect(span.events.length).toBe(2);
|
||||
expect(span.events[0]?.name).toBe("slsa.provenance.generated");
|
||||
expect(span.events[1]?.name).toBe("slsa.rekor.upload");
|
||||
});
|
||||
|
||||
it("falls back to tracer when no active span", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const created: Array<Span & { events: RecordedEvent[] }> = [];
|
||||
const tracer = {
|
||||
startSpan: () => {
|
||||
const s = makeFakeSpan();
|
||||
created.push(s);
|
||||
return s;
|
||||
},
|
||||
startActiveSpan: () => {
|
||||
throw new Error("not used");
|
||||
},
|
||||
} as unknown as import("@opentelemetry/api").Tracer;
|
||||
|
||||
const adapter = createSlsaOtelAdapter(tracer);
|
||||
adapter.onPolicyViolation({ "slsa.policy.violations": 3 });
|
||||
|
||||
expect(created.length).toBe(1);
|
||||
expect(created[0]?.events[0]?.name).toBe("slsa.policy.violation");
|
||||
});
|
||||
|
||||
it("silently discards when no active span and no tracer", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const adapter = createSlsaOtelAdapter();
|
||||
expect(() =>
|
||||
adapter.onRekorInclusionVerified({ "slsa.rekor.uuid": "z" })
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
70
packages/security-stack/security/slsa/src/otel/slsa-otel.ts
Normal file
70
packages/security-stack/security/slsa/src/otel/slsa-otel.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { type Tracer, trace } from "@opentelemetry/api";
|
||||
|
||||
/**
|
||||
* Attributes emitted on SLSA audit events. Keyed under the `slsa.*` namespace
|
||||
* so they integrate cleanly with OTel collectors that filter on attribute
|
||||
* prefix.
|
||||
*/
|
||||
export interface SlsaEventAttributes {
|
||||
"slsa.statement.digest"?: string;
|
||||
"slsa.builder.id"?: string;
|
||||
"slsa.source.repository"?: string;
|
||||
"slsa.result.valid"?: boolean;
|
||||
"slsa.rekor.uuid"?: string;
|
||||
"slsa.rekor.log_index"?: number;
|
||||
"slsa.rekor.tree_size"?: number;
|
||||
"slsa.policy.violations"?: number;
|
||||
"slsa.policy.level"?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapter emitting structured span events on the currently-active span (or a
|
||||
* configured tracer). Default behaviour is no-op when no exporter is
|
||||
* registered, matching `@opentelemetry/api`'s expected shape.
|
||||
*/
|
||||
export interface SlsaOtelAdapter {
|
||||
onProvenanceGenerated(attrs: SlsaEventAttributes): void;
|
||||
onProvenanceVerified(attrs: SlsaEventAttributes): void;
|
||||
onRekorUpload(attrs: SlsaEventAttributes): void;
|
||||
onRekorInclusionVerified(attrs: SlsaEventAttributes): void;
|
||||
onPolicyViolation(attrs: SlsaEventAttributes): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a `SlsaOtelAdapter` that emits span events to the current active
|
||||
* span (via `trace.getActiveSpan()`). Providing an explicit `tracer` makes
|
||||
* the adapter create its own spans — useful for background processes that
|
||||
* do not run inside a parent context.
|
||||
*/
|
||||
export function createSlsaOtelAdapter(tracer?: Tracer): SlsaOtelAdapter {
|
||||
const emit = (name: string, attrs: SlsaEventAttributes): void => {
|
||||
const active = trace.getActiveSpan();
|
||||
if (active) {
|
||||
active.addEvent(name, attrs as Record<string, never>);
|
||||
return;
|
||||
}
|
||||
if (tracer) {
|
||||
const span = tracer.startSpan(name);
|
||||
span.addEvent(name, attrs as Record<string, never>);
|
||||
span.end();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
onProvenanceGenerated: (a) => emit("slsa.provenance.generated", a),
|
||||
onProvenanceVerified: (a) => emit("slsa.provenance.verified", a),
|
||||
onRekorUpload: (a) => emit("slsa.rekor.upload", a),
|
||||
onRekorInclusionVerified: (a) =>
|
||||
emit("slsa.rekor.inclusion_verified", a),
|
||||
onPolicyViolation: (a) => emit("slsa.policy.violation", a),
|
||||
};
|
||||
}
|
||||
|
||||
/** A no-op adapter used as the default when callers do not supply one. */
|
||||
export const noopSlsaOtelAdapter: SlsaOtelAdapter = {
|
||||
onProvenanceGenerated: () => {},
|
||||
onProvenanceVerified: () => {},
|
||||
onRekorUpload: () => {},
|
||||
onRekorInclusionVerified: () => {},
|
||||
onPolicyViolation: () => {},
|
||||
};
|
||||
Reference in New Issue
Block a user