{
  "$schema": "https://ce.rodeo/schema/rubric.v1.json",
  "version": "cerodeo-v1",
  "effective_from": "2026-10-06T00:00:00Z",
  "deprecated_at": null,
  "summary": "Initial published rubric. Each image starts at 100, loses points per the penalties table (per occurrence), earns points per the bonuses table (binary unless noted). Final score clamped to [0, 100]. Grade is the band the score falls into.",
  "formula": "score = max(0, min(100, 100 + sum(penalties) + sum(bonuses)))",
  "grade_bands": [
    {"grade": "A", "min_score": 85},
    {"grade": "B", "min_score": 70},
    {"grade": "C", "min_score": 55},
    {"grade": "D", "min_score": 40},
    {"grade": "F", "min_score": 0}
  ],
  "penalties": [
    {
      "id": "critical_fixable",
      "per_occurrence": -25,
      "input": "Count of grype_matches where severity='Critical' AND vulnerability.fix.state='fixed'",
      "data_source": "grype sbom:<sbom.json>"
    },
    {
      "id": "high_fixable",
      "per_occurrence": -10,
      "input": "Count of grype_matches where severity='High' AND vulnerability.fix.state='fixed'",
      "data_source": "grype sbom:<sbom.json>"
    },
    {
      "id": "medium_fixable",
      "per_occurrence": -3,
      "input": "Count of grype_matches where severity='Medium' AND vulnerability.fix.state='fixed'",
      "data_source": "grype sbom:<sbom.json>"
    },
    {
      "id": "critical_unfixable",
      "per_occurrence": -8,
      "input": "Count of grype_matches where severity='Critical' AND vulnerability.fix.state!='fixed'",
      "data_source": "grype sbom:<sbom.json>"
    },
    {
      "id": "high_unfixable",
      "per_occurrence": -3,
      "input": "Count of grype_matches where severity='High' AND vulnerability.fix.state!='fixed'",
      "data_source": "grype sbom:<sbom.json>"
    }
  ],
  "bonuses": [
    {
      "id": "non_root_default",
      "value": 10,
      "input": "manifest.config.User is set and not one of ['', 'root', '0', '0:*']",
      "data_source": "skopeo inspect --config"
    },
    {
      "id": "rebuilt_lt_90d",
      "value": 10,
      "input": "image manifest Created timestamp is within the last 90 days",
      "data_source": "skopeo inspect"
    },
    {
      "id": "cosign_signed",
      "value": 5,
      "input": "cosign triangulate succeeds AND skopeo can resolve the resulting signature tag",
      "data_source": "cosign triangulate; skopeo inspect"
    },
    {
      "id": "slsa_provenance_attached",
      "value": 5,
      "input": "cosign download attestation --predicate-type=https://slsa.dev/provenance/v1 returns a DSSE envelope",
      "data_source": "cosign download attestation"
    },
    {
      "id": "sha256_subject_matches_manifest",
      "value": 5,
      "input": "The SLSA provenance's subject[].digest.sha256 equals the image's manifest digest",
      "data_source": "parsed from the SLSA attestation above"
    },
    {
      "id": "healthcheck_defined",
      "value": 3,
      "input": "manifest.config.Healthcheck is non-empty",
      "data_source": "skopeo inspect --config"
    },
    {
      "id": "oci_standard_labels",
      "value": 3,
      "input": "manifest.config.Labels contains >= 4 of {org.opencontainers.image.title, .description, .source, .version, .revision}",
      "data_source": "skopeo inspect --config"
    },
    {
      "id": "multi_arch_3plus",
      "value": 3,
      "input": "manifest list contains >= 3 distinct architectures. (If 2, award +2 under multi_arch_2 instead.)",
      "data_source": "skopeo inspect --raw"
    },
    {
      "id": "multi_arch_2",
      "value": 2,
      "input": "manifest list contains exactly 2 distinct architectures. (Excludes multi_arch_3plus — these are mutually exclusive.)",
      "data_source": "skopeo inspect --raw"
    },
    {
      "id": "upstream_github_release",
      "value": 3,
      "input": "If projects.upstream_url is set: GET api.github.com/repos/<owner>/<repo>/releases/tags/<tag> returns 200",
      "data_source": "GitHub API"
    },
    {
      "id": "sbom_attested",
      "value": 3,
      "input": "cosign download attestation --predicate-type=https://spdx.dev/Document returns a DSSE envelope",
      "data_source": "cosign download attestation"
    },
    {
      "id": "readme_has_example",
      "value": 2,
      "input": "Docker Hub /v2/repositories/<ns>/<repo> full_description matches /docker (run|pull|compose)/ (case-insensitive) OR contains a fenced code block mentioning the image ref",
      "data_source": "hub.docker.com/v2/repositories API"
    }
  ],
  "inputs": {
    "sbom_tool": "syft",
    "sbom_command": "syft <image_ref> -o syft-json",
    "vulnerability_tool": "grype",
    "vulnerability_command": "grype sbom:<sbom.json> -o json",
    "vulnerability_db_sources": [
      "GitHub Security Advisories (GHSA)",
      "NIST National Vulnerability Database (NVD)",
      "Red Hat Security Data",
      "Debian Security Tracker",
      "Ubuntu CVE Tracker",
      "Alpine secdb",
      "PyPA advisory DB",
      "npm advisory DB"
    ],
    "manifest_tool": "skopeo",
    "manifest_commands": [
      "skopeo inspect docker://<image_ref>",
      "skopeo inspect --raw docker://<image_ref>",
      "skopeo inspect --config docker://<image_ref>"
    ],
    "signature_tool": "cosign",
    "signature_commands": [
      "cosign triangulate --type signature <image_ref>",
      "cosign download attestation --predicate-type=https://slsa.dev/provenance/v1 <image_ref>",
      "cosign download attestation --predicate-type=https://spdx.dev/Document <image_ref>"
    ]
  },
  "reproducibility": {
    "required_pins_for_bit_identical": [
      "image manifest digest (ensures SBOM is identical)",
      "grype DB version (ensures CVE matching produces identical results)"
    ],
    "verify_script": "https://ce.rodeo/verify-score.sh",
    "notes": "Running the verify-score.sh script with the same (image_digest, grype_db_version) pair should produce a bit-identical score. If grype_db_version has moved on, CVE counts can drift; use the --grype-db flag to pin to the version recorded on the snapshot you want to reproduce."
  },
  "changelog": [
    {
      "version": "cerodeo-v1",
      "shipped": "2026-10-06",
      "notes": "Initial published rubric. 5 penalty tiers, 11 bonus signals, grade bands at 85/70/55/40/0."
    }
  ]
}
