Skip to content

Drop effective_cve from oauthlib GHSA labels added in #212 - #213

Merged
asomya merged 1 commit into
mainfrom
label-oauthlib-drop-effective-cve
Oct 1, 2026
Merged

asomya merged 1 commit into
mainfrom
label-oauthlib-drop-effective-cve

Conversation

@asomya

@asomya asomya commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #212. Removes effective_cve from the two oauthlib labels on the grype-quality-python-d89207b image.

Why: in yardstick 0.16.2 the max_year filter derives years asymmetrically:

  • Matches (tool/grype.py, store/scan_result.py): CVE comes from the match ID / relatedVulnerabilities / DB lookup. For these GHSAs in the schema v5 DB none resolve, so year is None and the match is kept.
  • Labels (artifact.py, store/labels.py): effective_cve: CVE-2026-* gives year 2026 > max_year 2021, so the labels are filtered out.

Result: the matches stay unlabeled and the enterprise-data-service v5 quality gate still fails (2/3 indeterminate). Without effective_cve, labels fall back to the same DB lookup as the matches, so they should get year=None and survive the filter too.

Verified: yardstick 0.16.2's gate passes on a real v5 DB with these labels (and fails exactly like production with #212 as merged). Details: #213 (comment)

🤖 Generated with Claude Code

… filtering

The matching grype results have no resolvable CVE in the schema v5 DB, so
yardstick keeps the matches (year=None) but filters out the labels
(year=2026 via effective_cve), leaving the matches unlabeled.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Arvind Somya <arvind.somya@anchore.com>
@asomya

asomya commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Verified: v5 gate passes with this change

I ran yardstick 0.16.2's real gate code locally (yardstick.validate.validate_image, the same entry point grype-db-manager db validate uses). This is the yardstick version enterprise-data-service pins in uv.lock.

Setup

  • Production gate settings: max_year: 2021, max_unlabeled_percent: 50, max_f1_regression: 0.15, max_new_false_negatives: 10
  • A real schema v5 grype DB (latest OSS v5 build, 2026-03-09)
  • The real label files at 0304f02 (Label oauthlib 3.2.0 TPs for GHSA-hj66-6f7g-4r5v and GHSA-xpv3-w29h-x7cv #212 merged) and a5cba24 (this PR)
  • Matches shaped like the failed run's: the custom DB finds the shared aiohttp match (GHSA-pjjw-qhg8-p2p9) plus the two oauthlib GHSAs; the OSS DB finds only the aiohttp one

Results

Labels oauthlib labels kept by year filter Candidate result Gate
#212 as merged 0 of 2 (year 2026, from effective_cve) TP=1, indeterminate=2 (66.67%) FAIL, same reason text as production
this PR 2 of 2 (year None) TP=3, indeterminate=0 (0%) PASS
this PR, if a future DB links the GHSAs to their CVEs 2 of 2 matches also filtered out, nothing to compare PASS

Labels for the image: 348 unfiltered, 346 after the year filter with #212 (only the two new oauthlib labels dropped), 348 with this PR.

The CI CVE-lookup caveat doesn't apply on v5. Yardstick's lookup (GrypeDBManager.get_upstream_vulnerability) queries the vulnerability_aliases table, which only exists in schema v6 DBs. The v5 DB's tables are id, vulnerability, vulnerability_metadata and vulnerability_match_exclusion. The query errors, yardstick swallows the error, and the year stays None. In the test, normalize_to_cve returned both GHSAs unchanged. This also explains why the v6 gate passed in the failing run: v6 can resolve the CVEs, so those matches were filtered out there.

Failing run: https://github.com/anchore/enterprise-data-service/actions/runs/36905166845

Test harness (run_gate.py)
"""Run yardstick 0.16.2's real v5 quality gate for the python test image against a given labels dir.

usage: run_gate.py <labels_dir> <db_dir> [--related-cve]
"""

import datetime
import json
import logging
import os
import shutil
import sys

import yardstick
from yardstick import artifact
from yardstick.store import config as store_config
from yardstick.store import labels as label_store
from yardstick.store import scan_result
from yardstick.utils import grype_db
from yardstick.validate import GateConfig, validate_image

labels_dir, db_dir = sys.argv[1], os.path.abspath(sys.argv[2])
related_cve = "--related-cve" in sys.argv

REPO = "docker.io/anchore/test_images"
DIGEST = "sha256:b2b58a55c0b03c1626d2aaae2add9832208b02124dda7b7b41811e14f0fb272c"
IMAGE = f"{REPO}@{DIGEST}"

store_root = os.path.abspath("store")
shutil.rmtree(store_root, ignore_errors=True)
os.makedirs(store_root)
os.symlink(os.path.abspath(labels_dir), os.path.join(store_root, "labels"))
store_config.set_values(store_root=store_root)

# same production settings as config/grype-db-manager/include.d/validate.yaml (first gate)
gate_config = GateConfig(
    max_f1_regression=0.15,
    max_unlabeled_percent=50,
    max_new_false_negatives=10,
    max_year=2021,
    candidate_tool_label="custom-db",
)


def match(pkg, ver, vuln_id, cve=None):
    return {
        "artifact": {"name": pkg, "version": ver, "type": "python", "purl": f"pkg:pypi/{pkg}@{ver}"},
        "vulnerability": {"id": vuln_id, "namespace": "github:language:python"},
        "relatedVulnerabilities": [{"id": cve, "namespace": "nvd:cpe"}] if cve else [],
        "matchDetails": [],
    }


common = match("aiohttp", "3.8.1", "GHSA-pjjw-qhg8-p2p9")
oauth = [
    match("oauthlib", "3.2.0", "GHSA-hj66-6f7g-4r5v", "CVE-2026-49264" if related_cve else None),
    match("oauthlib", "3.2.0", "GHSA-xpv3-w29h-x7cv", "CVE-2026-49265" if related_cve else None),
]

ids = []
for tool_name, label, matches in [
    ("grype[custom-db]", "custom-db", [common, *oauth]),
    ("grype", None, [common]),
]:
    cfg = artifact.ScanConfiguration(
        image_repo=REPO,
        image_digest=DIGEST,
        tool_name=tool_name,
        tool_version="v0.87.0",
        tool_label=label,
        image_tag="grype-quality-python-d89207b",
        timestamp=datetime.datetime.now(tz=datetime.timezone.utc),
    )
    raw = json.dumps({"matches": matches, "descriptor": {"db": {"location": db_dir}}})
    scan_result.save(raw, artifact.ScanResult(config=cfg, matches=[]))
    ids.append(cfg.ID)

# diagnostics: what the year filter sees, using the same DB the gate will use
grype_db.use(db_dir)
all_labels = label_store.load_for_image(IMAGE)
kept = label_store.load_for_image(IMAGE, year_max_limit=2021)
new = {"GHSA-hj66-6f7g-4r5v", "GHSA-xpv3-w29h-x7cv"}
for le in all_labels:
    if le.vulnerability_id in new:
        print(f"label {le.vulnerability_id}: effective_cve={le.effective_cve} year={le.effective_year()}"
              f" kept_by_filter={le.vulnerability_id in {k.vulnerability_id for k in kept}}")
print(f"labels for image: {len(all_labels)} unfiltered, {len(kept)} after max_year=2021")
for v in ("GHSA-hj66-6f7g-4r5v", "GHSA-xpv3-w29h-x7cv"):
    print(f"db normalize_to_cve({v}) -> {grype_db.normalize_to_cve(v)}")

logging.disable(logging.WARNING)

# capture the candidate summary the Gate computes its verdict from (it's an InitVar, not stored)
from yardstick.validate import gate as gate_mod

_captured = {}
_orig_post_init = gate_mod.Gate.__post_init__


def _post_init(self, reference_comparison, candidate_comparison):
    _captured["candidate"] = candidate_comparison
    return _orig_post_init(self, reference_comparison, candidate_comparison)


gate_mod.Gate.__post_init__ = _post_init
gate = validate_image(
    IMAGE,
    gate_config=gate_config,
    descriptions=ids,
    always_run_label_comparison=False,
    verbosity=0,
)
c = _captured.get("candidate")
if c is None:
    print("candidate: no label comparison needed (candidate and reference matches identical after year filter)")
else:
  print(f"candidate: TP={c.true_positives} FP={c.false_positives} indeterminate={c.indeterminate}"
        f" ({c.indeterminate_percent:.2f}%)")
print("GATE:", "PASS" if gate.passed() else "FAIL")
for r in gate.reasons:
    print("  reason:", r)

🤖 Generated with Claude Code

@asomya
asomya marked this pull request as ready for review October 1, 2026 20:41
@asomya
asomya enabled auto-merge (squash) October 1, 2026 20:41

@jspilman jspilman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Even better

@asomya
asomya disabled auto-merge October 1, 2026 20:54
@asomya
asomya merged commit d6b9bb2 into main Oct 1, 2026
2 checks passed
@asomya
asomya deleted the label-oauthlib-drop-effective-cve branch October 1, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants