Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/workflows/admissibility-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Admissibility Gate

on:
pull_request:
workflow_dispatch:

permissions:
contents: read
id-token: write
attestations: write

jobs:
evidence-admissibility:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20

- name: Enable Corepack
run: corepack enable

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install Syft
uses: anchore/sbom-action/download-syft@v0.17.0

- name: Install Cosign
uses: sigstore/cosign-installer@v3.8.1

- name: Build deterministic artifact payload
run: |
mkdir -p dist
sha256sum package.json pnpm-lock.yaml | awk '{print $1}' | sort > dist/admissible-artifact.txt

- name: Generate SBOM (CycloneDX)
run: syft . -o cyclonedx-json=evidence/sbom.cdx.json

- name: Assert SBOM completeness
run: |
test -f evidence/sbom.cdx.json
jq -e '.components and (.components | length > 0)' evidence/sbom.cdx.json

- name: Generate provenance attestation (SLSA)
uses: actions/attest-build-provenance@v3
with:
subject-path: dist/admissible-artifact.txt

- name: Materialize deterministic provenance snapshot
run: |
DIGEST="$(sha256sum dist/admissible-artifact.txt | awk '{print $1}')"
jq -n \
--arg digest "sha256:${DIGEST}" \
--arg repo "${{ github.repository }}" \
'{
_type: "https://in-toto.io/Statement/v1",
predicateType: "https://slsa.dev/provenance/v1",
subject: [{name: "dist/admissible-artifact.txt", digest: {sha256: ($digest | sub("^sha256:"; ""))}}],
builder: {id: "https://github.com/actions/runner"},
invocation: {configSource: {uri: $repo}}
}' > evidence/provenance.json

- name: Sign and verify artifact signature
run: |
cosign generate-key-pair
cosign sign-blob --yes --key cosign.key --output-signature evidence/artifact.sig dist/admissible-artifact.txt
cosign verify-blob --key cosign.pub --signature evidence/artifact.sig dist/admissible-artifact.txt
Comment on lines +68 to +72
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ephemeral key pair compromises long-term signature verification.

Generating a new key pair on every workflow run means:

  1. Signatures cannot be verified against a stable, trusted key outside this workflow run.
  2. The private key (cosign.key) is created in the workspace and could be accidentally exposed.

For a supply-chain security gate, consider using Sigstore's keyless signing (cosign sign-blob --yes without --key) which leverages OIDC identity, or store a persistent signing key in GitHub Secrets.

🔑 Suggested approach using keyless signing
      - name: Sign and verify artifact signature
        run: |
-         cosign generate-key-pair
-         cosign sign-blob --yes --key cosign.key --output-signature evidence/artifact.sig dist/admissible-artifact.txt
-         cosign verify-blob --key cosign.pub --signature evidence/artifact.sig dist/admissible-artifact.txt
+         cosign sign-blob --yes --output-signature evidence/artifact.sig --output-certificate evidence/artifact.crt dist/admissible-artifact.txt
+         cosign verify-blob --signature evidence/artifact.sig --certificate evidence/artifact.crt \
+           --certificate-identity-regexp ".*" --certificate-oidc-issuer-regexp ".*" dist/admissible-artifact.txt

If keyless signing isn't suitable, ensure the private key is stored securely in secrets and never written to disk in the workflow directory.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Sign and verify artifact signature
run: |
cosign generate-key-pair
cosign sign-blob --yes --key cosign.key --output-signature evidence/artifact.sig dist/admissible-artifact.txt
cosign verify-blob --key cosign.pub --signature evidence/artifact.sig dist/admissible-artifact.txt
- name: Sign and verify artifact signature
run: |
cosign sign-blob --yes --output-signature evidence/artifact.sig --output-certificate evidence/artifact.crt dist/admissible-artifact.txt
cosign verify-blob --signature evidence/artifact.sig --certificate evidence/artifact.crt \
--certificate-identity-regexp ".*" --certificate-oidc-issuer-regexp ".*" dist/admissible-artifact.txt
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/admissibility-gate.yml around lines 68 - 72, The workflow
currently generates a transient keypair with cosign (cosign generate-key-pair)
and signs with cosign.key/cosign.pub, which prevents stable verification and
risks leaking the private key; replace this with keyless signing by removing the
generate-key-pair step and invoking cosign sign-blob without --key (using OIDC)
and verify with the corresponding cosign verify-blob call, or if keyless is not
acceptable, load a persistent private key from a GitHub Secret instead of
writing cosign.key to the workspace and use that secret-provided key for cosign
sign-blob and verify-blob (ensure the secret never gets persisted to disk).


- name: Build evidence report/metrics/stamp
env:
ARTIFACT_PATH: dist/admissible-artifact.txt
SBOM_PATH: evidence/sbom.cdx.json
PROVENANCE_PATH: evidence/provenance.json
SIGNATURE_VERIFIED: "true"
run: node scripts/ci/build_admissibility_evidence.mjs

- name: Evaluate admissibility gate
run: pnpm verify:admissibility --input evidence/report.json

- name: Upload evidence artifacts
uses: actions/upload-artifact@v4
with:
name: admissibility-evidence-${{ github.run_id }}
path: |
evidence/report.json
evidence/metrics.json
evidence/stamp.json
evidence/sbom.cdx.json
evidence/provenance.json
evidence/artifact.sig
cosign.pub
33 changes: 33 additions & 0 deletions k8s/argo/admissibility-presync-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: admissibility-status
namespace: argocd
data:
status: "PASS"
Comment on lines +1 to +7
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Including the admissibility-status ConfigMap with a hardcoded PASS value in the same manifest as the PreSync check defeats the purpose of the gate. Since ArgoCD applies resources during the sync process, this manifest will overwrite any existing status in the cluster with PASS, effectively bypassing the check. Consider removing this ConfigMap from the base manifest and managing it via an external process (e.g., a CI runner) that reflects actual build admissibility.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace hardcoded PASS in PreSync gate input

The PreSync job reads admissibility-status and fails only when it is not PASS, but this manifest creates that ConfigMap with status: "PASS" unconditionally. Since no CI output is wired into this value, the hook always succeeds and cannot block sync when admissibility evidence fails, making the runtime gate effectively non-enforcing.

Useful? React with 👍 / 👎.

Comment on lines +6 to +7
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded "PASS" in ConfigMap defeats the PreSync gate purpose.

Similar to the deployment label issue, the ConfigMap's status: "PASS" is statically defined. The CI pipeline should dynamically create/update this ConfigMap with the actual admissibility verdict, otherwise this check will always pass.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@k8s/argo/admissibility-presync-check.yaml` around lines 6 - 7, The ConfigMap
currently hardcodes data.status: "PASS", which defeats the PreSync gate; change
the manifest so it does not contain a static "PASS" (remove the fixed value or
replace it with a placeholder like an unset/templated value) and update the
CI/CD pipeline to create/update the ConfigMap's data.status at runtime with the
real admissibility verdict; specifically modify the ConfigMap's data.status
entry in admissibility-presync-check.yaml and ensure your pipeline task writes
the actual verdict into that key before the PreSync gate evaluates it.

---
apiVersion: batch/v1
kind: Job
metadata:
name: admissibility-presync-check
namespace: argocd
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
restartPolicy: Never
Comment on lines +19 to +20
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Job does not specify a serviceAccountName. By default, it uses the default ServiceAccount, which typically lacks permissions to get ConfigMaps in the argocd namespace. This will cause the kubectl command to fail with a Forbidden error, blocking the sync even if the status is correct.

    spec:
      serviceAccountName: admissibility-checker-sa
      restartPolicy: Never

containers:
- name: gate
image: bitnami/kubectl:1.30
command:
- /bin/sh
- -ec
- |
STATUS="$(kubectl -n argocd get configmap admissibility-status -o jsonpath='{.data.status}')"
if [ "$STATUS" != "PASS" ]; then
echo "ArgoCD sync blocked: admissibility status is $STATUS"
exit 1
fi
echo "ArgoCD sync permitted: admissibility status is PASS"
Comment on lines +19 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing ServiceAccount and RBAC for kubectl access.

The Job uses kubectl to read the ConfigMap but doesn't specify a serviceAccountName. The default service account in the argocd namespace may not have permission to read ConfigMaps, causing the Job to fail with a permissions error.

🛠️ Proposed fix
   template:
     spec:
       restartPolicy: Never
+      serviceAccountName: argocd-application-controller  # or create a dedicated SA with ConfigMap read permissions
       containers:
         - name: gate

Alternatively, create a dedicated ServiceAccount and RoleBinding:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: admissibility-checker
  namespace: argocd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: admissibility-configmap-reader
  namespace: argocd
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["admissibility-status"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: admissibility-checker-binding
  namespace: argocd
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: admissibility-configmap-reader
subjects:
  - kind: ServiceAccount
    name: admissibility-checker
    namespace: argocd
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
spec:
restartPolicy: Never
containers:
- name: gate
image: bitnami/kubectl:1.30
command:
- /bin/sh
- -ec
- |
STATUS="$(kubectl -n argocd get configmap admissibility-status -o jsonpath='{.data.status}')"
if [ "$STATUS" != "PASS" ]; then
echo "ArgoCD sync blocked: admissibility status is $STATUS"
exit 1
fi
echo "ArgoCD sync permitted: admissibility status is PASS"
spec:
restartPolicy: Never
serviceAccountName: argocd-application-controller # or create a dedicated SA with ConfigMap read permissions
containers:
- name: gate
image: bitnami/kubectl:1.30
command:
- /bin/sh
- -ec
- |
STATUS="$(kubectl -n argocd get configmap admissibility-status -o jsonpath='{.data.status}')"
if [ "$STATUS" != "PASS" ]; then
echo "ArgoCD sync blocked: admissibility status is $STATUS"
exit 1
fi
echo "ArgoCD sync permitted: admissibility status is PASS"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@k8s/argo/admissibility-presync-check.yaml` around lines 19 - 33, The Job
currently runs kubectl without a specific serviceAccountName so it may lack RBAC
to read the admissibility-status ConfigMap; add serviceAccountName:
admissibility-checker to the Job spec and create a ServiceAccount named
admissibility-checker plus a Role named admissibility-configmap-reader
(namespace: argocd) that grants get on the configmap resourceNames:
["admissibility-status"], then bind them with a RoleBinding named
admissibility-checker-binding that references the Role and the
admissibility-checker ServiceAccount in the argocd namespace so the kubectl
command in the container can read the ConfigMap.

2 changes: 2 additions & 0 deletions k8s/base/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ resources:
- ../crd/vex-override-verification.yaml
- ../crd/vex-override-ledger.yaml
- tsa-cert-chain-config.yaml
- ../policies/kyverno-admissibility-enforcement.yaml
- ../argo/admissibility-presync-check.yaml
59 changes: 59 additions & 0 deletions k8s/policies/kyverno-admissibility-enforcement.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: enforce-admissibility-supply-chain
annotations:
policies.kyverno.io/title: Enforce Admissibility for Deployments
policies.kyverno.io/category: Supply Chain Security
policies.kyverno.io/severity: high
spec:
validationFailureAction: Enforce
background: true
rules:
- name: require-admissibility-pass-label
match:
any:
- resources:
kinds:
- Pod
Comment on lines +17 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Restrict admissibility policy from matching every Pod

This rule currently matches all Pod objects cluster-wide, so the new enforcement applies to system namespaces and ArgoCD hook Pods as well as Summit workloads. Because most existing Pods (including k8s/argo/admissibility-presync-check.yaml's hook Pod) do not carry security.summit.io/admissibility: "PASS" or matching GitHub keyless signatures/attestations, admissions will be denied broadly and can halt routine sync/deploy activity. Scope this policy to the intended namespaces/images/workloads (or add explicit excludes) before enforcing.

Useful? React with 👍 / 👎.

validate:
message: "security.summit.io/admissibility label must be PASS"
pattern:
metadata:
labels:
security.summit.io/admissibility: "PASS"

- name: require-signed-images
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "*"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using * for imageReferences in a ClusterPolicy will subject every container in the cluster to these verification rules, including critical system components (e.g., kube-proxy, coredns, or Kyverno itself). Unless all images in the cluster are signed by the specified issuer, this will block system pods from starting, potentially breaking the cluster.

            - "ghcr.io/your-org/*"

Comment on lines +33 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there are other third-party images in k8s manifests that would be affected
rg -n 'image:' k8s/ --type yaml | grep -v 'summit/' | head -20

Repository: BrianCLong/summit

Length of output: 1551


🏁 Script executed:

cat -n k8s/policies/kyverno-admissibility-enforcement.yaml | head -60

Repository: BrianCLong/summit

Length of output: 2071


imageReferences: "*" will reject all third-party images lacking GitHub Actions signatures and SLSA attestations.

The wildcard in both rules (lines 33-34 and 51-52) applies to every container image in the cluster. With validationFailureAction: Enforce, the policy will block any Pod using images without:

  • GitHub Actions OIDC signatures (from https://token.actions.githubusercontent.com with subject https://github.com/*/*), OR
  • SLSA provenance attestations

Third-party images in your manifests—alpine, node:18-alpine, bitnami/kubectl, neo4j:5, amazon/aws-cli:2, trufflesecurity/trufflehog, aquasec/kube-bench, and others—do not carry these attestations and will be rejected.

To fix:

  1. Restrict imageReferences to your own registry patterns (e.g., ghcr.io/brianlong/*, intelgraph/*)
  2. Add exclude rules for third-party images your workloads require (e.g., alpine, bitnami/kubectl, neo4j:5)
  3. Or add namespace exclusions for system workloads that use third-party images
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@k8s/policies/kyverno-admissibility-enforcement.yaml` around lines 33 - 34,
The policy currently uses imageReferences: "*" which will reject all images
without GitHub Actions signatures or SLSA attestations; update the Kyverno rules
(look for imageReferences and validationFailureAction in the policy) to a scoped
allowlist and/or exclusions: replace "*" with your registry patterns (e.g.,
ghcr.io/brianlong/* or intelgraph/*) in imageReferences, add exclude blocks
listing required third‑party images (e.g., alpine, bitnami/kubectl, neo4j:5) or
add exclude/namespace rules for system namespaces, and ensure
validationFailureAction: Enforce remains only for workloads that must be gated
by attestations.

mutateDigest: true
required: true
attestors:
- count: 1
entries:
- keyless:
issuer: https://token.actions.githubusercontent.com
subject: https://github.com/*/*
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The keyless attestation subject is overly permissive. Setting it to https://github.com/*/* allows any GitHub repository to produce valid signatures that satisfy this policy. This should be restricted to your specific organization or repository to ensure only trusted builds are admitted.

                    subject: https://github.com/your-org/*

Comment on lines +40 to +42
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Overly permissive keyless subject pattern.

The subject https://github.com/*/* accepts signatures from any GitHub repository's Actions workflow. Consider restricting to your organization or specific repository to prevent accepting signatures from untrusted sources:

                 - keyless:
                     issuer: https://token.actions.githubusercontent.com
-                    subject: https://github.com/*/*
+                    subject: https://github.com/BrianCLong/summit/*
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- keyless:
issuer: https://token.actions.githubusercontent.com
subject: https://github.com/*/*
- keyless:
issuer: https://token.actions.githubusercontent.com
subject: https://github.com/BrianCLong/summit/*
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@k8s/policies/kyverno-admissibility-enforcement.yaml` around lines 40 - 42,
The keyless subject pattern is too broad—replace the subject value
`https://github.com/*/*` under the `keyless` block with a more restrictive
pattern that limits signatures to your org or specific repo (for example
`https://github.com/your-org/*` or `https://github.com/your-org/your-repo`) so
only workflows from trusted GitHub repositories are accepted; update the
`subject` field next to `issuer: https://token.actions.githubusercontent.com`
accordingly.


- name: require-slsa-attestation
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "*"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the signature check, using * for SLSA attestation verification will block all images in the cluster that lack these specific attestations. This should be scoped to your specific container registry to avoid breaking third-party or system components.

            - "ghcr.io/your-org/*"

attestations:
- type: slsaprovenance
conditions:
all:
- key: "{{ payload.predicateType }}"
operator: Equals
value: https://slsa.dev/provenance/v1
9 changes: 5 additions & 4 deletions k8s/server-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ spec:
metadata:
labels:
app: summit-server
security.summit.io/admissibility: "PASS"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Hardcoding the admissibility label to PASS in the deployment manifest allows any deployment to satisfy the Kyverno label requirement regardless of whether the CI pipeline actually verified the build. To make this an effective gate, this label should be injected dynamically by the CI process or a mutating webhook only after verification is complete.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded "PASS" label undermines the admissibility enforcement model.

The security.summit.io/admissibility label is statically set to "PASS" in the manifest. This means the Kyverno policy's label check will always pass regardless of whether the CI admissibility verification actually succeeded.

For the enforcement to be meaningful, this label should be injected dynamically by CI/CD based on the actual evaluateAdmissibility verdict—not baked into the source manifest. Otherwise, an attacker or misconfiguration can deploy without passing the admissibility checks.

Consider using a Kustomize patch or Helm values injection in the deployment pipeline that sets this label only when CI verification passes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@k8s/server-deployment.yaml` at line 13, Remove the hardcoded label key
"security.summit.io/admissibility: \"PASS\"" from the Deployment manifest and
instead expose a templated label placeholder (e.g., a Helm value like
.Values.admissibility or a kustomize patch variable) so the CI/CD pipeline can
inject the actual evaluateAdmissibility verdict; update the pipeline to set that
label to "PASS" only when the CI check succeeds (or omit/set "FAIL" otherwise)
via a kustomize patch or Helm values override applied at deploy time.

spec:
containers:
- name: summit-server
image: summit/server
ports:
- containerPort: 4000
- name: summit-server
image: summit/server
ports:
- containerPort: 4000
96 changes: 96 additions & 0 deletions lib/admissibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
export type AdmissibilityStatus = "PASS" | "FAIL";

export interface SignatureEvidence {
valid: boolean;
keyId?: string;
}

export interface SbomEvidence {
present: boolean;
complete: boolean;
format?: string;
components?: string[];
}

export interface ProvenanceEvidence {
present: boolean;
chainIntact: boolean;
builderId?: string;
subjects?: string[];
}

export interface DependencyEvidence {
prohibitedFound: string[];
}

export interface AdmissibilityEvidenceBundle {
artifact: {
digest: string;
name?: string;
};
signature: SignatureEvidence;
sbom: SbomEvidence;
provenance: ProvenanceEvidence;
dependencies: DependencyEvidence;
}

export interface AdmissibilityVerdict {
status: AdmissibilityStatus;
reasons: string[];
checks: {
signatureValid: boolean;
sbomComplete: boolean;
provenanceIntact: boolean;
prohibitedDependencies: boolean;
};
artifact: {
digest: string;
name?: string;
};
}

const sortUnique = (values: string[]): string[] =>
Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));

export function evaluateAdmissibility(
evidenceBundle: AdmissibilityEvidenceBundle
): AdmissibilityVerdict {
const reasons: string[] = [];

const signatureValid = evidenceBundle.signature.valid;
if (!signatureValid) {
reasons.push("INVALID_SIGNATURE");
}

const sbomComplete = evidenceBundle.sbom.present && evidenceBundle.sbom.complete;
if (!sbomComplete) {
reasons.push("MISSING_OR_INCOMPLETE_SBOM");
}

const provenanceIntact =
evidenceBundle.provenance.present && evidenceBundle.provenance.chainIntact;
if (!provenanceIntact) {
reasons.push("BROKEN_PROVENANCE_CHAIN");
}

const prohibitedDependencies = evidenceBundle.dependencies.prohibitedFound.length === 0;
if (!prohibitedDependencies) {
const blocked = sortUnique(evidenceBundle.dependencies.prohibitedFound).join(",");
reasons.push(`PROHIBITED_DEPENDENCIES:${blocked}`);
}

return {
status: reasons.length === 0 ? "PASS" : "FAIL",
reasons,
checks: {
signatureValid,
sbomComplete,
provenanceIntact,
prohibitedDependencies,
},
artifact: {
digest: evidenceBundle.artifact.digest,
name: evidenceBundle.artifact.name,
},
};
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@
"ga:certify": "npx tsx scripts/benchmarks/run_perf.ts && npx tsx scripts/ga-validator.ts",
"ga:sentinel": "node scripts/drift-sentinel.mjs",
"ga:report": "npx tsx scripts/generate-compliance-report.ts",
"ga:finish": "rm -rf scripts/ga-final-walkthrough.sh scripts/failure-injector.mjs signals/ *.bak && echo '🏁 RELEASE SEALTED. GA PILOT LIVE.' "
"ga:finish": "rm -rf scripts/ga-final-walkthrough.sh scripts/failure-injector.mjs signals/ *.bak && echo '🏁 RELEASE SEALTED. GA PILOT LIVE.' ",
"verify:admissibility": "npx tsx scripts/ci/verify-admissibility.ts",
"test:admissibility": "node --test --import tsx tests/ci/admissibility.test.mts"
},
"keywords": [
"intelligence-analysis",
Expand Down
75 changes: 75 additions & 0 deletions scripts/ci/build_admissibility_evidence.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { createHash } from 'node:crypto';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';

const artifactPath = process.env.ARTIFACT_PATH ?? 'dist/admissible-artifact.txt';
const sbomPath = process.env.SBOM_PATH ?? 'evidence/sbom.cdx.json';
const provenancePath = process.env.PROVENANCE_PATH ?? 'evidence/provenance.json';
const signatureVerified = process.env.SIGNATURE_VERIFIED === 'true';
const prohibitedDeps = (process.env.PROHIBITED_DEPS ?? '')
.split(',')
.map((item) => item.trim())
.filter(Boolean)
.sort((a, b) => a.localeCompare(b));

const artifactDigest = createHash('sha256')
.update(readFileSync(artifactPath))
.digest('hex');

const sbom = JSON.parse(readFileSync(sbomPath, 'utf8'));
const provenance = JSON.parse(readFileSync(provenancePath, 'utf8'));

const components = (sbom.components ?? [])
.map((c) => c?.name)
.filter(Boolean)
.sort((a, b) => a.localeCompare(b));

const report = {
artifact: {
name: artifactPath,
digest: `sha256:${artifactDigest}`,
},
signature: {
valid: signatureVerified,
keyId: 'cosign.pub',
},
sbom: {
present: Boolean(sbom.bomFormat),
complete: components.length > 0,
format: sbom.bomFormat ?? 'unknown',
components,
},
provenance: {
present: Boolean(provenance.predicateType),
chainIntact: Boolean(provenance.subject?.length && provenance.builder?.id),
builderId: provenance.builder?.id,
subjects: (provenance.subject ?? [])
.map((subject) => subject?.name)
.filter(Boolean)
.sort((a, b) => a.localeCompare(b)),
},
dependencies: {
prohibitedFound: prohibitedDeps,
},
};

const metrics = {
checks_total: 4,
checks_passed:
Number(report.signature.valid) +
Number(report.sbom.present && report.sbom.complete) +
Number(report.provenance.present && report.provenance.chainIntact) +
Number(report.dependencies.prohibitedFound.length === 0),
prohibited_dependency_count: report.dependencies.prohibitedFound.length,
sbom_component_count: report.sbom.components.length,
};

const stamp = {
run_id: process.env.GITHUB_RUN_ID ?? 'local',
git_sha: process.env.GITHUB_SHA ?? 'local',
generated_at: new Date().toISOString(),
};

mkdirSync('evidence', { recursive: true });
writeFileSync('evidence/report.json', `${JSON.stringify(report, null, 2)}\n`);
writeFileSync('evidence/metrics.json', `${JSON.stringify(metrics, null, 2)}\n`);
writeFileSync('evidence/stamp.json', `${JSON.stringify(stamp, null, 2)}\n`);
Loading
Loading