From b044842325737db2112d9a921efec70b38c6c229 Mon Sep 17 00:00:00 2001 From: jaydeep869 Date: Sat, 11 Jul 2026 20:05:50 +0530 Subject: [PATCH 1/5] feat(baseline): add Best Practices Badge automation-proposal URL output format Closes #283 Adds a new 'badge' output format to darnit that generates an OpenSSF Best Practices Badge automation-proposal URL from OSPS Baseline audit results. Maintainers can follow the URL to pre-fill their badge entry and earn a badge for meeting the OSPS Baseline criteria. ## What changed - New module: darnit_baseline/formatters/badge.py - control_id_to_key(): OSPS-AC-01.01 -> osps_ac_01_01 - status_to_badge_status(): PASS->Met | FAIL->Unmet | WARN->? | NA->N/A - generate_badge_url(): builds full automation-proposal URL with _status and _justification params per control; truncates long justifications at 500 chars; warns when project URL is missing - formatters/__init__.py: export the three new public symbols - tools.py (audit_openssf_baseline MCP tool): - Add output_format='badge' branch - Add optional project_url param (auto-detected from owner/repo if absent) - cli.py (darnit audit): - Add 'badge' to -o/--output choices - Add --project-url flag ## Tests 30 new unit tests covering: - All control ID transform cases - All 6 status-mapping cases (PASS/FAIL/WARN/NA/N/A/unknown) - URL structure (as=edit, url=, _status, _justification params) - Edge cases: empty results, no project URL, long justifications, missing id/status fields, empty/whitespace-only details - Package-level re-export sanity check Co-authored-by: david-a-wheeler Signed-off-by: jaydeep869 --- .../darnit_baseline/formatters/__init__.py | 12 + .../src/darnit_baseline/formatters/badge.py | 161 +++++++++++++ .../src/darnit_baseline/tools.py | 19 +- packages/darnit/src/darnit/cli.py | 21 +- .../formatters/test_badge_formatter.py | 217 ++++++++++++++++++ 5 files changed, 426 insertions(+), 4 deletions(-) create mode 100644 packages/darnit-baseline/src/darnit_baseline/formatters/badge.py create mode 100644 tests/darnit_baseline/formatters/test_badge_formatter.py diff --git a/packages/darnit-baseline/src/darnit_baseline/formatters/__init__.py b/packages/darnit-baseline/src/darnit_baseline/formatters/__init__.py index 76d2ad58..a9bb7283 100644 --- a/packages/darnit-baseline/src/darnit_baseline/formatters/__init__.py +++ b/packages/darnit-baseline/src/darnit_baseline/formatters/__init__.py @@ -1,5 +1,11 @@ """OSPS-specific output formatters.""" +from .badge import ( + BADGE_BASE_URL, + control_id_to_key, + generate_badge_url, + status_to_badge_status, +) from .sarif import ( build_sarif_rules, generate_sarif_audit, @@ -8,6 +14,12 @@ ) __all__ = [ + # Badge formatter + "generate_badge_url", + "control_id_to_key", + "status_to_badge_status", + "BADGE_BASE_URL", + # SARIF formatter "generate_sarif_audit", "build_sarif_rules", "result_to_sarif_result", diff --git a/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py b/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py new file mode 100644 index 00000000..72627b53 --- /dev/null +++ b/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py @@ -0,0 +1,161 @@ +"""OpenSSF Best Practices Badge automation-proposal URL generator. + +Converts an OSPS Baseline audit result set into a ready-to-follow URL that +pre-fills the Best Practices Badge entry form for the audited project. + +Format reference: + https://www.bestpractices.dev/projects?as=edit&url=ENCODED_URL& + KEY=VALUE&KEY=VALUE&... + +Example: + https://www.bestpractices.dev/projects?as=edit& + url=https%3A%2F%2Fgithub.com%2Fcurl%2Fcurl& + osps_ac_01_01_status=Met& + osps_ac_01_01_justification=GitHub+org+enforces+2FA + +Key transform: OSPS-AC-01.01 → osps_ac_01_01 +Status mapping: PASS → Met | FAIL → Unmet | WARN → ? | NA/N/A → N/A +""" + +import re +from urllib.parse import quote, urlencode + +BADGE_BASE_URL = "https://www.bestpractices.dev/projects" + +# Maximum justification length (characters) before truncation. +# Keeps URLs within practical browser/server limits. +_MAX_JUSTIFICATION_LEN = 500 + +# Status mapping from darnit audit statuses to badge site values. +_STATUS_MAP: dict[str, str] = { + "PASS": "Met", + "FAIL": "Unmet", + "WARN": "?", # inconclusive — cannot assert Met or Unmet + "NA": "N/A", + "N/A": "N/A", +} + + +def control_id_to_key(control_id: str) -> str: + """Transform an OSPS control ID into a Best Practices Badge parameter key. + + Args: + control_id: OSPS control ID, e.g. ``"OSPS-AC-01.01"`` + + Returns: + Badge parameter key, e.g. ``"osps_ac_01_01"`` + + Examples: + >>> control_id_to_key("OSPS-AC-01.01") + 'osps_ac_01_01' + >>> control_id_to_key("OSPS-VM-03.02") + 'osps_vm_03_02' + """ + # Replace hyphens and dots with underscores, then lowercase + key = re.sub(r"[-.]", "_", control_id).lower() + return key + + +def status_to_badge_status(status: str) -> str: + """Map a darnit audit status to a Best Practices Badge status string. + + Args: + status: One of ``"PASS"``, ``"FAIL"``, ``"WARN"``, ``"NA"``, ``"N/A"`` + + Returns: + Badge status: ``"Met"``, ``"Unmet"``, ``"?"``, or ``"N/A"`` + + Examples: + >>> status_to_badge_status("PASS") + 'Met' + >>> status_to_badge_status("FAIL") + 'Unmet' + >>> status_to_badge_status("WARN") + '?' + >>> status_to_badge_status("NA") + 'N/A' + """ + return _STATUS_MAP.get(status, "?") + + +def generate_badge_url( + results: list[dict], + project_url: str = "", +) -> str: + """Generate an OpenSSF Best Practices Badge automation-proposal URL. + + Builds a long URL that pre-fills the badge entry form at + https://www.bestpractices.dev with the status and justification for each + OSPS control in the audit results. The maintainer can follow the link, + review the pre-filled fields, and submit to earn their badge. + + Args: + results: List of audit result dicts, each containing at minimum + ``"id"`` (str), ``"status"`` (str), and optionally + ``"details"`` (str). + project_url: The project's canonical repository URL + (e.g. ``"https://github.com/curl/curl"``). + Required for a valid badge submission but the URL is + still generated without it (with a warning comment). + + Returns: + A fully-formed URL string ready to open in a browser. + """ + # Collect query parameters in insertion order + # Start with the fixed preamble + params: list[tuple[str, str]] = [("as", "edit")] + + if project_url: + params.append(("url", project_url)) + + for result in results: + control_id: str = result.get("id", "") + status: str = result.get("status", "") + details: str = result.get("details", "") or "" + + if not control_id or not status: + continue + + key_prefix = control_id_to_key(control_id) + badge_status = status_to_badge_status(status) + + params.append((f"{key_prefix}_status", badge_status)) + + # Include justification when available; truncate to keep URL practical + justification = details.strip() + if justification: + if len(justification) > _MAX_JUSTIFICATION_LEN: + justification = justification[:_MAX_JUSTIFICATION_LEN] + "…" + params.append((f"{key_prefix}_justification", justification)) + + # urlencode uses + for spaces (application/x-www-form-urlencoded), + # which the badge site accepts and which keeps URLs shorter than %20. + query = urlencode(params, quote_via=quote) + url = f"{BADGE_BASE_URL}?{query}" + + header_lines = [ + "## OpenSSF Best Practices Badge — Automation Proposal", + "", + "Follow this link to pre-fill your badge entry with the audit results.", + "Review each field, then submit to claim your badge.", + "", + ] + + if not project_url: + header_lines += [ + "> ⚠️ No project URL detected. The link above is missing the `url=` parameter.", + "> Pass `--project-url https://github.com/ORG/REPO` (CLI) or `project_url=` (MCP)", + "> to include it, which is required for a valid badge submission.", + "", + ] + + header_lines.append(url) + return "\n".join(header_lines) + + +__all__ = [ + "control_id_to_key", + "status_to_badge_status", + "generate_badge_url", + "BADGE_BASE_URL", +] diff --git a/packages/darnit-baseline/src/darnit_baseline/tools.py b/packages/darnit-baseline/src/darnit_baseline/tools.py index 1d2317d8..a46715b9 100644 --- a/packages/darnit-baseline/src/darnit_baseline/tools.py +++ b/packages/darnit-baseline/src/darnit_baseline/tools.py @@ -48,6 +48,7 @@ def audit_openssf_baseline( staging: bool = False, prefer_upstream: bool = True, profile: str | None = None, + project_url: str | None = None, ) -> str: """ Run a comprehensive OpenSSF Baseline audit on a repository. @@ -64,8 +65,10 @@ def audit_openssf_baseline( - Different fields use AND logic: domain=AC AND level=1 - Same field repeated uses OR logic: priority=low OR priority=high - Bare values match against control tags dict keys - output_format: Output format - "markdown", "json", "summary", or "sarif". Default: "markdown". + output_format: Output format - "markdown", "json", "summary", "sarif", or "badge". + Default: "markdown". Use "summary" for compact JSON (id/status/level/details only, no evidence). + Use "badge" to generate an OpenSSF Best Practices Badge automation-proposal URL. auto_init_config: Create .project.yaml if missing. Default: True attest: Generate in-toto attestation after audit. Default: False sign_attestation: Sign attestation with Sigstore. Default: True @@ -74,6 +77,9 @@ def audit_openssf_baseline( Useful for auditing forks against their upstream repository. Default: True profile: Optional audit profile name to filter controls. Short name (e.g., "level1_quick") or qualified name (e.g., "openssf-baseline:level1_quick"). Default: None (all controls) + project_url: Canonical repository URL for the Best Practices Badge submission + (e.g. "https://github.com/curl/curl"). Auto-detected from git when omitted. + Only used when output_format="badge". Returns: Formatted audit report with compliance status and remediation instructions @@ -160,7 +166,16 @@ def audit_openssf_baseline( ) # Format output - if output_format == "summary": + if output_format == "badge": + from .formatters.badge import generate_badge_url + + # Resolve project URL: explicit arg > auto-detected from git remote + resolved_project_url = project_url or "" + if not resolved_project_url and owner and repo: + resolved_project_url = f"https://github.com/{owner}/{repo}" + + return generate_badge_url(results, resolved_project_url) + elif output_format == "summary": # Compact JSON: only id/status/level/details — no evidence or pass_history. # ~5-8K vs ~164K for full JSON with 62 controls. compact_results = [ diff --git a/packages/darnit/src/darnit/cli.py b/packages/darnit/src/darnit/cli.py index 2ae52287..9c492d9c 100644 --- a/packages/darnit/src/darnit/cli.py +++ b/packages/darnit/src/darnit/cli.py @@ -203,6 +203,15 @@ def cmd_audit(args: argparse.Namespace) -> int: # Output results if args.output == "json": sys.stdout.write(format_results_json(results, config.framework_name) + "\n") + elif args.output == "badge": + from darnit_baseline.formatters.badge import generate_badge_url + + # Resolve project URL from --project-url flag or auto-detect from owner/repo + project_url = getattr(args, "project_url", None) or "" + if not project_url and owner and repo: + project_url = f"https://github.com/{owner}/{repo}" + + sys.stdout.write(generate_badge_url(results, project_url) + "\n") else: sys.stdout.write(format_results_text(results, config.framework_name) + "\n") @@ -744,9 +753,17 @@ def create_parser() -> argparse.ArgumentParser: ) audit_parser.add_argument( "-o", "--output", - choices=["text", "json"], + choices=["text", "json", "badge"], default="text", - help="Output format (default: text)", + help="Output format (default: text). Use 'badge' to generate a Best Practices Badge URL.", + ) + audit_parser.add_argument( + "--project-url", + dest="project_url", + default=None, + help="Canonical repository URL for Best Practices Badge submission " + "(e.g. https://github.com/ORG/REPO). Auto-detected from git when omitted. " + "Only used with -o badge.", ) audit_parser.add_argument( "--no-fail", diff --git a/tests/darnit_baseline/formatters/test_badge_formatter.py b/tests/darnit_baseline/formatters/test_badge_formatter.py new file mode 100644 index 00000000..f2528ece --- /dev/null +++ b/tests/darnit_baseline/formatters/test_badge_formatter.py @@ -0,0 +1,217 @@ +"""Unit tests for the Best Practices Badge output formatter. + +Tests cover: +- Control ID key transform +- Status mapping for all known statuses +- URL generation structure and params +- Auto-detection fallback (no project URL) +- Long justification truncation +""" + +import pytest + +from darnit_baseline.formatters.badge import ( + BADGE_BASE_URL, + control_id_to_key, + generate_badge_url, + status_to_badge_status, +) + + +# --------------------------------------------------------------------------- +# control_id_to_key +# --------------------------------------------------------------------------- + + +class TestControlIdToKey: + """Tests for the OSPS control ID → badge key transform.""" + + def test_basic_transform(self): + assert control_id_to_key("OSPS-AC-01.01") == "osps_ac_01_01" + + def test_vm_domain(self): + assert control_id_to_key("OSPS-VM-03.02") == "osps_vm_03_02" + + def test_br_domain(self): + assert control_id_to_key("OSPS-BR-02.01") == "osps_br_02_01" + + def test_do_domain(self): + assert control_id_to_key("OSPS-DO-01.01") == "osps_do_01_01" + + def test_qa_domain(self): + assert control_id_to_key("OSPS-QA-07.01") == "osps_qa_07_01" + + def test_all_hyphens_and_dots_replaced(self): + key = control_id_to_key("OSPS-GV-02.01") + assert "-" not in key + assert "." not in key + + def test_result_is_lowercase(self): + key = control_id_to_key("OSPS-AC-01.01") + assert key == key.lower() + + +# --------------------------------------------------------------------------- +# status_to_badge_status +# --------------------------------------------------------------------------- + + +class TestStatusToBadgeStatus: + """Tests for the darnit status → badge status mapping.""" + + def test_pass_maps_to_met(self): + assert status_to_badge_status("PASS") == "Met" + + def test_fail_maps_to_unmet(self): + assert status_to_badge_status("FAIL") == "Unmet" + + def test_warn_maps_to_question_mark(self): + # WARN = inconclusive; cannot assert Met or Unmet + assert status_to_badge_status("WARN") == "?" + + def test_na_maps_to_na(self): + assert status_to_badge_status("NA") == "N/A" + + def test_na_with_slash_maps_to_na(self): + assert status_to_badge_status("N/A") == "N/A" + + def test_unknown_status_maps_to_question_mark(self): + # Any unrecognised status is treated as inconclusive + assert status_to_badge_status("WHATEVER") == "?" + + +# --------------------------------------------------------------------------- +# generate_badge_url +# --------------------------------------------------------------------------- + + +class TestGenerateBadgeUrl: + """Tests for the full URL generation.""" + + _SIMPLE_RESULTS = [ + { + "id": "OSPS-AC-01.01", + "status": "PASS", + "details": "GitHub org enforces 2FA", + }, + { + "id": "OSPS-AC-03.01", + "status": "FAIL", + "details": "Branch protection not enabled on main", + }, + ] + + def test_url_starts_with_badge_base(self): + output = generate_badge_url(self._SIMPLE_RESULTS, "https://github.com/example/repo") + assert BADGE_BASE_URL in output + + def test_url_contains_as_edit(self): + output = generate_badge_url(self._SIMPLE_RESULTS, "https://github.com/example/repo") + assert "as=edit" in output + + def test_url_contains_encoded_project_url(self): + output = generate_badge_url(self._SIMPLE_RESULTS, "https://github.com/example/repo") + # URL-encoded form of the project URL must appear + assert "github.com" in output + assert "example" in output + + def test_url_contains_status_params(self): + output = generate_badge_url(self._SIMPLE_RESULTS, "https://github.com/example/repo") + assert "osps_ac_01_01_status=Met" in output + assert "osps_ac_03_01_status=Unmet" in output + + def test_url_contains_justification_params(self): + output = generate_badge_url(self._SIMPLE_RESULTS, "https://github.com/example/repo") + assert "osps_ac_01_01_justification=" in output + assert "osps_ac_03_01_justification=" in output + + def test_url_with_warn_result(self): + results = [{"id": "OSPS-VM-01.01", "status": "WARN", "details": "Could not verify"}] + output = generate_badge_url(results, "https://github.com/example/repo") + assert "osps_vm_01_01_status=%3F" in output or "osps_vm_01_01_status=?" in output + + def test_url_with_na_result(self): + results = [{"id": "OSPS-BR-04.01", "status": "NA", "details": "Not applicable"}] + output = generate_badge_url(results, "https://github.com/example/repo") + assert "osps_br_04_01_status=N%2FA" in output or "N/A" in output + + def test_empty_results_still_returns_url(self): + output = generate_badge_url([], "https://github.com/example/repo") + assert BADGE_BASE_URL in output + assert "as=edit" in output + + def test_no_project_url_shows_warning(self): + output = generate_badge_url(self._SIMPLE_RESULTS, "") + assert "⚠️" in output + assert "url=" not in output.split("\n")[-1].split("?")[1].split("&")[0] # no url= param + + def test_no_project_url_no_url_param(self): + """When project_url is empty, the url= query param should be absent.""" + output = generate_badge_url([], "") + # The URL line is the last non-empty line + url_line = [line for line in output.splitlines() if line.startswith("https://")][-1] + # Should have as=edit but no url= since none was provided + assert "as=edit" in url_line + assert "url=" not in url_line + + def test_justification_truncated_at_500_chars(self): + long_details = "A" * 600 + results = [{"id": "OSPS-AC-01.01", "status": "PASS", "details": long_details}] + output = generate_badge_url(results, "https://github.com/example/repo") + # After URL-encoding "A" stays as "A" so we can check the truncation char appears + assert "…" in output or "%E2%80%A6" in output # ellipsis or its URL-encoded form + + def test_result_missing_details_no_justification_param(self): + results = [{"id": "OSPS-AC-01.01", "status": "PASS"}] + output = generate_badge_url(results, "https://github.com/example/repo") + assert "osps_ac_01_01_status=Met" in output + assert "osps_ac_01_01_justification" not in output + + def test_result_with_empty_details_no_justification_param(self): + results = [{"id": "OSPS-AC-01.01", "status": "PASS", "details": " "}] + output = generate_badge_url(results, "https://github.com/example/repo") + assert "osps_ac_01_01_justification" not in output + + def test_markdown_header_present(self): + output = generate_badge_url(self._SIMPLE_RESULTS, "https://github.com/example/repo") + assert "Best Practices Badge" in output + + def test_result_with_missing_id_skipped(self): + results = [ + {"status": "PASS", "details": "No ID"}, # skipped + {"id": "OSPS-AC-01.01", "status": "PASS", "details": "Has ID"}, # included + ] + output = generate_badge_url(results, "https://github.com/example/repo") + assert "osps_ac_01_01_status=Met" in output + # Only one status param expected + assert output.count("_status=") == 1 + + def test_result_with_missing_status_skipped(self): + results = [ + {"id": "OSPS-AC-01.01", "details": "No status"}, # skipped + {"id": "OSPS-AC-03.01", "status": "FAIL"}, # included + ] + output = generate_badge_url(results, "https://github.com/example/repo") + assert "osps_ac_03_01_status=Unmet" in output + assert output.count("_status=") == 1 + + +# --------------------------------------------------------------------------- +# Integration: import from package __init__ +# --------------------------------------------------------------------------- + + +def test_badge_exports_available_from_formatters_package(): + """Ensure the badge symbols are re-exported from the formatters package.""" + from darnit_baseline.formatters import ( + BADGE_BASE_URL as _BADGE_BASE_URL, + control_id_to_key as _ctk, + generate_badge_url as _gbu, + status_to_badge_status as _stbs, + ) + + assert callable(_gbu) + assert callable(_ctk) + assert callable(_stbs) + assert isinstance(_BADGE_BASE_URL, str) + assert "bestpractices.dev" in _BADGE_BASE_URL From dded67549193332267b276882f393ce1386c390c Mon Sep 17 00:00:00 2001 From: jaydeep869 Date: Sun, 12 Jul 2026 01:50:10 +0530 Subject: [PATCH 2/5] fix(lint): sort imports and remove unused pytest import in badge tests Remove unused pytest import and move inline import block to module level to satisfy ruff I001 import sort order rules flagged in CI. Signed-off-by: jaydeep869 --- .../formatters/test_badge_formatter.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/darnit_baseline/formatters/test_badge_formatter.py b/tests/darnit_baseline/formatters/test_badge_formatter.py index f2528ece..f472d50c 100644 --- a/tests/darnit_baseline/formatters/test_badge_formatter.py +++ b/tests/darnit_baseline/formatters/test_badge_formatter.py @@ -8,8 +8,18 @@ - Long justification truncation """ -import pytest - +from darnit_baseline.formatters import ( + BADGE_BASE_URL as _BADGE_BASE_URL, +) +from darnit_baseline.formatters import ( + control_id_to_key as _ctk, +) +from darnit_baseline.formatters import ( + generate_badge_url as _gbu, +) +from darnit_baseline.formatters import ( + status_to_badge_status as _stbs, +) from darnit_baseline.formatters.badge import ( BADGE_BASE_URL, control_id_to_key, @@ -17,7 +27,6 @@ status_to_badge_status, ) - # --------------------------------------------------------------------------- # control_id_to_key # --------------------------------------------------------------------------- @@ -203,13 +212,6 @@ def test_result_with_missing_status_skipped(self): def test_badge_exports_available_from_formatters_package(): """Ensure the badge symbols are re-exported from the formatters package.""" - from darnit_baseline.formatters import ( - BADGE_BASE_URL as _BADGE_BASE_URL, - control_id_to_key as _ctk, - generate_badge_url as _gbu, - status_to_badge_status as _stbs, - ) - assert callable(_gbu) assert callable(_ctk) assert callable(_stbs) From e99b235c1ba02a124b1e84560ea1fa3b59aa9b0d Mon Sep 17 00:00:00 2001 From: jaydeep869 Date: Mon, 13 Jul 2026 01:46:58 +0530 Subject: [PATCH 3/5] fix(badge): address reviewer feedback from Marc Three changes based on review comments on PR #319: Remove badge output from darnit core CLI. The darnit package must not depend on darnit-baseline, and badge output only makes sense for the openssf-baseline framework. The feature is available via the MCP tool audit_openssf_baseline(output_format="badge") which lives in the right package. Removes the -o badge choice, --project-url flag, and the badge branch from cmd_audit in cli.py. Drop justifications for inconclusive and N/A results. Only Met (PASS) and Unmet (FAIL) results include a justification in the URL. WARN and NA statuses map to ? / N/A with no justification, preventing error messages or command-not-found text from surfacing in a real badge submission. Cap total URL at 6 KB. All _status params are always included. Justifications are appended in result order until the running byte total exceeds the budget. This keeps the URL within common server and proxy limits even on large repos. Tests updated and extended to cover all three cases (34 tests, all passing). Signed-off-by: jaydeep869 --- .../src/darnit_baseline/formatters/badge.py | 109 +++++++++++++----- packages/darnit/src/darnit/cli.py | 21 +--- .../formatters/test_badge_formatter.py | 71 +++++++++--- 3 files changed, 140 insertions(+), 61 deletions(-) diff --git a/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py b/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py index 72627b53..d034277b 100644 --- a/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py +++ b/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py @@ -15,6 +15,16 @@ Key transform: OSPS-AC-01.01 → osps_ac_01_01 Status mapping: PASS → Met | FAIL → Unmet | WARN → ? | NA/N/A → N/A + +Justification policy: + Only Met (PASS) and Unmet (FAIL) results include a justification. + Inconclusive (WARN → ?) and N/A results never include one, to avoid + surfacing error messages or command-not-found noise in badge submissions. + +URL budget: + Total URL is capped at _URL_BUDGET_BYTES (6 KB). All _status params are + always included. Justifications are appended only while the running total + stays within budget. """ import re @@ -22,15 +32,21 @@ BADGE_BASE_URL = "https://www.bestpractices.dev/projects" -# Maximum justification length (characters) before truncation. -# Keeps URLs within practical browser/server limits. +# Maximum per-justification length (characters) before truncation. _MAX_JUSTIFICATION_LEN = 500 +# Total URL budget in bytes. Common server/proxy limits are 8 KB; we leave +# headroom so the URL reliably works everywhere. +_URL_BUDGET_BYTES = 6 * 1024 # 6 KB + +# Only these badge statuses warrant a justification in the URL. +_JUSTIFICATION_STATUSES = {"Met", "Unmet"} + # Status mapping from darnit audit statuses to badge site values. _STATUS_MAP: dict[str, str] = { "PASS": "Met", "FAIL": "Unmet", - "WARN": "?", # inconclusive — cannot assert Met or Unmet + "WARN": "?", # inconclusive — cannot assert Met or Unmet "NA": "N/A", "N/A": "N/A", } @@ -84,10 +100,22 @@ def generate_badge_url( ) -> str: """Generate an OpenSSF Best Practices Badge automation-proposal URL. - Builds a long URL that pre-fills the badge entry form at - https://www.bestpractices.dev with the status and justification for each - OSPS control in the audit results. The maintainer can follow the link, - review the pre-filled fields, and submit to earn their badge. + Builds a URL that pre-fills the badge entry form at + https://www.bestpractices.dev with the status and (where appropriate) the + justification for each OSPS control in the audit results. The maintainer + can follow the link, review the pre-filled fields, and submit to earn + their badge. + + Rules applied to keep the URL safe and clean: + + * Every control gets a ``_status`` parameter (always included). + * Justifications are only added for **Met** (PASS) and **Unmet** (FAIL) + results. Inconclusive (``?``) and N/A results never get a justification, + preventing error messages or command-not-found text from reaching a real + badge submission. + * The total URL is capped at 6 KB. Justifications are added in result + order until the budget is exhausted; the remaining status params are + still included without justification. Args: results: List of audit result dicts, each containing at minimum @@ -96,18 +124,17 @@ def generate_badge_url( project_url: The project's canonical repository URL (e.g. ``"https://github.com/curl/curl"``). Required for a valid badge submission but the URL is - still generated without it (with a warning comment). + still generated without it (with a warning). Returns: - A fully-formed URL string ready to open in a browser. + A formatted string containing a header and the automation-proposal URL. """ - # Collect query parameters in insertion order - # Start with the fixed preamble - params: list[tuple[str, str]] = [("as", "edit")] - + # --- Pass 1: build all _status params (always included) ----------------- + status_params: list[tuple[str, str]] = [("as", "edit")] if project_url: - params.append(("url", project_url)) + status_params.append(("url", project_url)) + valid_results: list[tuple[str, str, str]] = [] # (key_prefix, badge_status, details) for result in results: control_id: str = result.get("id", "") status: str = result.get("status", "") @@ -118,19 +145,47 @@ def generate_badge_url( key_prefix = control_id_to_key(control_id) badge_status = status_to_badge_status(status) + status_params.append((f"{key_prefix}_status", badge_status)) + valid_results.append((key_prefix, badge_status, details)) + + # --- Pass 2: add justifications within the URL budget ------------------- + # Calculate the base URL length with all status params but no justifications. + base_query = urlencode(status_params, quote_via=quote) + base_url = f"{BADGE_BASE_URL}?{base_query}" + remaining_budget = _URL_BUDGET_BYTES - len(base_url.encode()) + + # Build final params: status params first, then interleave justifications. + final_params: list[tuple[str, str]] = list(status_params) + justification_pairs: list[tuple[str, str]] = [] + + for key_prefix, badge_status, details in valid_results: + # Only Met/Unmet warrant justification text + if badge_status not in _JUSTIFICATION_STATUSES: + continue - params.append((f"{key_prefix}_status", badge_status)) - - # Include justification when available; truncate to keep URL practical justification = details.strip() - if justification: - if len(justification) > _MAX_JUSTIFICATION_LEN: - justification = justification[:_MAX_JUSTIFICATION_LEN] + "…" - params.append((f"{key_prefix}_justification", justification)) - - # urlencode uses + for spaces (application/x-www-form-urlencoded), - # which the badge site accepts and which keeps URLs shorter than %20. - query = urlencode(params, quote_via=quote) + if not justification: + continue + + if len(justification) > _MAX_JUSTIFICATION_LEN: + justification = justification[:_MAX_JUSTIFICATION_LEN] + "…" + + # Estimate the encoded cost of adding this param + encoded_pair = urlencode( + [(f"{key_prefix}_justification", justification)], quote_via=quote + ) + cost = len(f"&{encoded_pair}".encode()) + + if remaining_budget < cost: + # Budget exhausted — skip remaining justifications + break + + remaining_budget -= cost + justification_pairs.append((f"{key_prefix}_justification", justification)) + + final_params.extend(justification_pairs) + + query = urlencode(final_params, quote_via=quote) url = f"{BADGE_BASE_URL}?{query}" header_lines = [ @@ -144,8 +199,8 @@ def generate_badge_url( if not project_url: header_lines += [ "> ⚠️ No project URL detected. The link above is missing the `url=` parameter.", - "> Pass `--project-url https://github.com/ORG/REPO` (CLI) or `project_url=` (MCP)", - "> to include it, which is required for a valid badge submission.", + "> Pass `project_url=` to `audit_openssf_baseline` to include it,", + "> which is required for a valid badge submission.", "", ] diff --git a/packages/darnit/src/darnit/cli.py b/packages/darnit/src/darnit/cli.py index 9c492d9c..2ae52287 100644 --- a/packages/darnit/src/darnit/cli.py +++ b/packages/darnit/src/darnit/cli.py @@ -203,15 +203,6 @@ def cmd_audit(args: argparse.Namespace) -> int: # Output results if args.output == "json": sys.stdout.write(format_results_json(results, config.framework_name) + "\n") - elif args.output == "badge": - from darnit_baseline.formatters.badge import generate_badge_url - - # Resolve project URL from --project-url flag or auto-detect from owner/repo - project_url = getattr(args, "project_url", None) or "" - if not project_url and owner and repo: - project_url = f"https://github.com/{owner}/{repo}" - - sys.stdout.write(generate_badge_url(results, project_url) + "\n") else: sys.stdout.write(format_results_text(results, config.framework_name) + "\n") @@ -753,17 +744,9 @@ def create_parser() -> argparse.ArgumentParser: ) audit_parser.add_argument( "-o", "--output", - choices=["text", "json", "badge"], + choices=["text", "json"], default="text", - help="Output format (default: text). Use 'badge' to generate a Best Practices Badge URL.", - ) - audit_parser.add_argument( - "--project-url", - dest="project_url", - default=None, - help="Canonical repository URL for Best Practices Badge submission " - "(e.g. https://github.com/ORG/REPO). Auto-detected from git when omitted. " - "Only used with -o badge.", + help="Output format (default: text)", ) audit_parser.add_argument( "--no-fail", diff --git a/tests/darnit_baseline/formatters/test_badge_formatter.py b/tests/darnit_baseline/formatters/test_badge_formatter.py index f472d50c..84b04a24 100644 --- a/tests/darnit_baseline/formatters/test_badge_formatter.py +++ b/tests/darnit_baseline/formatters/test_badge_formatter.py @@ -5,7 +5,8 @@ - Status mapping for all known statuses - URL generation structure and params - Auto-detection fallback (no project URL) -- Long justification truncation +- No justification for inconclusive / N/A results +- Total URL budget cap """ from darnit_baseline.formatters import ( @@ -120,7 +121,6 @@ def test_url_contains_as_edit(self): def test_url_contains_encoded_project_url(self): output = generate_badge_url(self._SIMPLE_RESULTS, "https://github.com/example/repo") - # URL-encoded form of the project URL must appear assert "github.com" in output assert "example" in output @@ -129,20 +129,66 @@ def test_url_contains_status_params(self): assert "osps_ac_01_01_status=Met" in output assert "osps_ac_03_01_status=Unmet" in output - def test_url_contains_justification_params(self): + def test_url_contains_justification_for_pass(self): output = generate_badge_url(self._SIMPLE_RESULTS, "https://github.com/example/repo") assert "osps_ac_01_01_justification=" in output + + def test_url_contains_justification_for_fail(self): + output = generate_badge_url(self._SIMPLE_RESULTS, "https://github.com/example/repo") assert "osps_ac_03_01_justification=" in output - def test_url_with_warn_result(self): + # --- Justification policy: only Met/Unmet get justification ------------- + + def test_warn_result_has_no_justification(self): + """WARN maps to ? and must never include a justification param.""" results = [{"id": "OSPS-VM-01.01", "status": "WARN", "details": "Could not verify"}] output = generate_badge_url(results, "https://github.com/example/repo") assert "osps_vm_01_01_status=%3F" in output or "osps_vm_01_01_status=?" in output + assert "osps_vm_01_01_justification" not in output - def test_url_with_na_result(self): + def test_na_result_has_no_justification(self): + """NA maps to N/A and must never include a justification param.""" results = [{"id": "OSPS-BR-04.01", "status": "NA", "details": "Not applicable"}] output = generate_badge_url(results, "https://github.com/example/repo") - assert "osps_br_04_01_status=N%2FA" in output or "N/A" in output + assert "osps_br_04_01_justification" not in output + + def test_warn_status_string_na_has_no_justification(self): + """N/A status also suppresses justification.""" + results = [{"id": "OSPS-BR-04.01", "status": "N/A", "details": "Not applicable"}] + output = generate_badge_url(results, "https://github.com/example/repo") + assert "osps_br_04_01_justification" not in output + + # --- URL budget cap ------------------------------------------------------ + + def test_url_within_6kb_budget(self): + """Total URL must never exceed the 6 KB budget.""" + many_results = [ + { + "id": f"OSPS-AC-{i:02d}.01", + "status": "FAIL", + "details": "X" * 600, # each detail is 600 chars before truncation + } + for i in range(1, 30) + ] + output = generate_badge_url(many_results, "https://github.com/example/repo") + url_line = [line for line in output.splitlines() if line.startswith("https://")][-1] + assert len(url_line.encode()) <= 6 * 1024 + + def test_all_status_params_present_even_when_budget_exhausted(self): + """Even when the budget is exhausted, every _status param must appear.""" + many_results = [ + { + "id": f"OSPS-AC-{i:02d}.01", + "status": "FAIL", + "details": "X" * 600, + } + for i in range(1, 30) + ] + output = generate_badge_url(many_results, "https://github.com/example/repo") + for i in range(1, 30): + assert f"osps_ac_{i:02d}_01_status=Unmet" in output + + # --- Other edge cases ---------------------------------------------------- def test_empty_results_still_returns_url(self): output = generate_badge_url([], "https://github.com/example/repo") @@ -152,14 +198,11 @@ def test_empty_results_still_returns_url(self): def test_no_project_url_shows_warning(self): output = generate_badge_url(self._SIMPLE_RESULTS, "") assert "⚠️" in output - assert "url=" not in output.split("\n")[-1].split("?")[1].split("&")[0] # no url= param def test_no_project_url_no_url_param(self): """When project_url is empty, the url= query param should be absent.""" output = generate_badge_url([], "") - # The URL line is the last non-empty line url_line = [line for line in output.splitlines() if line.startswith("https://")][-1] - # Should have as=edit but no url= since none was provided assert "as=edit" in url_line assert "url=" not in url_line @@ -167,7 +210,6 @@ def test_justification_truncated_at_500_chars(self): long_details = "A" * 600 results = [{"id": "OSPS-AC-01.01", "status": "PASS", "details": long_details}] output = generate_badge_url(results, "https://github.com/example/repo") - # After URL-encoding "A" stays as "A" so we can check the truncation char appears assert "…" in output or "%E2%80%A6" in output # ellipsis or its URL-encoded form def test_result_missing_details_no_justification_param(self): @@ -187,18 +229,17 @@ def test_markdown_header_present(self): def test_result_with_missing_id_skipped(self): results = [ - {"status": "PASS", "details": "No ID"}, # skipped - {"id": "OSPS-AC-01.01", "status": "PASS", "details": "Has ID"}, # included + {"status": "PASS", "details": "No ID"}, + {"id": "OSPS-AC-01.01", "status": "PASS", "details": "Has ID"}, ] output = generate_badge_url(results, "https://github.com/example/repo") assert "osps_ac_01_01_status=Met" in output - # Only one status param expected assert output.count("_status=") == 1 def test_result_with_missing_status_skipped(self): results = [ - {"id": "OSPS-AC-01.01", "details": "No status"}, # skipped - {"id": "OSPS-AC-03.01", "status": "FAIL"}, # included + {"id": "OSPS-AC-01.01", "details": "No status"}, + {"id": "OSPS-AC-03.01", "status": "FAIL"}, ] output = generate_badge_url(results, "https://github.com/example/repo") assert "osps_ac_03_01_status=Unmet" in output From 017ed91da12b6c90bac6e0dd7e11076bc46ff6a6 Mon Sep 17 00:00:00 2001 From: jaydeep869 Date: Mon, 13 Jul 2026 02:23:14 +0530 Subject: [PATCH 4/5] fix(badge): address reviewer feedback from mlieberman85 ASCII-only content. Replace the ellipsis character used for truncation with '...' and the warning emoji with 'WARNING:' in badge.py. Keeps terminal rendering and diffs clean across all environments. Reduce justification cap to 200 chars. Down from 500. The badge form expects short human-readable notes, not evidence dumps. Combined with the 6 KB URL budget this keeps URLs well within practical limits. Replace re.sub with str.replace. Two str.replace calls are equivalent and clearer for two literal characters; the re import is no longer needed. Test import consolidation. Tests now import once from darnit_baseline.formatters with real names. A re-export breakage will fail every test in the file, which is the correct signal. Specific percent-encoded assertions. test_warn_result_has_no_justification and test_na_result_has_no_justification now assert %3F and N%2FA respectively, since urlencode(quote_via=quote) always encodes reserved characters. A regression shipping raw '?' or '/' would no longer slip past. 62-control URL ceiling test. test_url_under_8000_chars_for_62_controls builds a realistic 62-control audit and asserts the URL stays under 8000 characters, turning 'URL might be too long' into a hard signal. Signed-off-by: jaydeep869 --- .../src/darnit_baseline/formatters/badge.py | 19 ++-- .../formatters/test_badge_formatter.py | 100 ++++++++++-------- 2 files changed, 65 insertions(+), 54 deletions(-) diff --git a/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py b/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py index d034277b..8096682d 100644 --- a/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py +++ b/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py @@ -13,12 +13,12 @@ osps_ac_01_01_status=Met& osps_ac_01_01_justification=GitHub+org+enforces+2FA -Key transform: OSPS-AC-01.01 → osps_ac_01_01 -Status mapping: PASS → Met | FAIL → Unmet | WARN → ? | NA/N/A → N/A +Key transform: OSPS-AC-01.01 -> osps_ac_01_01 +Status mapping: PASS -> Met | FAIL -> Unmet | WARN -> ? | NA/N/A -> N/A Justification policy: Only Met (PASS) and Unmet (FAIL) results include a justification. - Inconclusive (WARN → ?) and N/A results never include one, to avoid + Inconclusive (WARN -> ?) and N/A results never include one, to avoid surfacing error messages or command-not-found noise in badge submissions. URL budget: @@ -27,13 +27,14 @@ stays within budget. """ -import re from urllib.parse import quote, urlencode BADGE_BASE_URL = "https://www.bestpractices.dev/projects" # Maximum per-justification length (characters) before truncation. -_MAX_JUSTIFICATION_LEN = 500 +# Short justifications keep URLs well within browser and server limits +# and match the badge form's expectation of human-readable notes. +_MAX_JUSTIFICATION_LEN = 200 # Total URL budget in bytes. Common server/proxy limits are 8 KB; we leave # headroom so the URL reliably works everywhere. @@ -67,8 +68,8 @@ def control_id_to_key(control_id: str) -> str: >>> control_id_to_key("OSPS-VM-03.02") 'osps_vm_03_02' """ - # Replace hyphens and dots with underscores, then lowercase - key = re.sub(r"[-.]", "_", control_id).lower() + # Replace hyphens and dots with underscores, then lowercase. + key = control_id.replace("-", "_").replace(".", "_").lower() return key @@ -168,7 +169,7 @@ def generate_badge_url( continue if len(justification) > _MAX_JUSTIFICATION_LEN: - justification = justification[:_MAX_JUSTIFICATION_LEN] + "…" + justification = justification[:_MAX_JUSTIFICATION_LEN] + "..." # Estimate the encoded cost of adding this param encoded_pair = urlencode( @@ -198,7 +199,7 @@ def generate_badge_url( if not project_url: header_lines += [ - "> ⚠️ No project URL detected. The link above is missing the `url=` parameter.", + "> WARNING: No project URL detected. The link above is missing the `url=` parameter.", "> Pass `project_url=` to `audit_openssf_baseline` to include it,", "> which is required for a valid badge submission.", "", diff --git a/tests/darnit_baseline/formatters/test_badge_formatter.py b/tests/darnit_baseline/formatters/test_badge_formatter.py index 84b04a24..06cc9e1e 100644 --- a/tests/darnit_baseline/formatters/test_badge_formatter.py +++ b/tests/darnit_baseline/formatters/test_badge_formatter.py @@ -6,22 +6,10 @@ - URL generation structure and params - Auto-detection fallback (no project URL) - No justification for inconclusive / N/A results -- Total URL budget cap +- Total URL budget cap (8000 chars for 62-control audit) """ from darnit_baseline.formatters import ( - BADGE_BASE_URL as _BADGE_BASE_URL, -) -from darnit_baseline.formatters import ( - control_id_to_key as _ctk, -) -from darnit_baseline.formatters import ( - generate_badge_url as _gbu, -) -from darnit_baseline.formatters import ( - status_to_badge_status as _stbs, -) -from darnit_baseline.formatters.badge import ( BADGE_BASE_URL, control_id_to_key, generate_badge_url, @@ -34,7 +22,7 @@ class TestControlIdToKey: - """Tests for the OSPS control ID → badge key transform.""" + """Tests for the OSPS control ID -> badge key transform.""" def test_basic_transform(self): assert control_id_to_key("OSPS-AC-01.01") == "osps_ac_01_01" @@ -67,7 +55,7 @@ def test_result_is_lowercase(self): class TestStatusToBadgeStatus: - """Tests for the darnit status → badge status mapping.""" + """Tests for the darnit status -> badge status mapping.""" def test_pass_maps_to_met(self): assert status_to_badge_status("PASS") == "Met" @@ -140,47 +128,61 @@ def test_url_contains_justification_for_fail(self): # --- Justification policy: only Met/Unmet get justification ------------- def test_warn_result_has_no_justification(self): - """WARN maps to ? and must never include a justification param.""" + """WARN maps to %3F and must never include a justification param.""" results = [{"id": "OSPS-VM-01.01", "status": "WARN", "details": "Could not verify"}] output = generate_badge_url(results, "https://github.com/example/repo") - assert "osps_vm_01_01_status=%3F" in output or "osps_vm_01_01_status=?" in output + # urlencode(quote_via=quote) always percent-encodes '?' -> '%3F' + assert "osps_vm_01_01_status=%3F" in output assert "osps_vm_01_01_justification" not in output def test_na_result_has_no_justification(self): - """NA maps to N/A and must never include a justification param.""" + """NA maps to N%2FA and must never include a justification param.""" results = [{"id": "OSPS-BR-04.01", "status": "NA", "details": "Not applicable"}] output = generate_badge_url(results, "https://github.com/example/repo") + # urlencode(quote_via=quote) always percent-encodes '/' -> '%2F' + assert "osps_br_04_01_status=N%2FA" in output assert "osps_br_04_01_justification" not in output - def test_warn_status_string_na_has_no_justification(self): + def test_na_slash_result_has_no_justification(self): """N/A status also suppresses justification.""" results = [{"id": "OSPS-BR-04.01", "status": "N/A", "details": "Not applicable"}] output = generate_badge_url(results, "https://github.com/example/repo") + assert "osps_br_04_01_status=N%2FA" in output assert "osps_br_04_01_justification" not in output - # --- URL budget cap ------------------------------------------------------ + # --- URL length ceiling -------------------------------------------------- + + def test_url_under_8000_chars_for_62_controls(self): + """URL must stay under 8000 chars for a full 62-control OSPS audit.""" + # Simulate 62 controls with typical short justifications + domains = ["AC", "BR", "DO", "GV", "QA", "VM"] + results = [] + for i, domain in enumerate(domains): + for j in range(1, 11): + control_num = f"{j:02d}" + results.append({ + "id": f"OSPS-{domain}-{control_num}.01", + "status": "FAIL", + "details": f"Remediation required for {domain}-{control_num}.01 control", + }) + if len(results) == 62: + break + if len(results) == 62: + break - def test_url_within_6kb_budget(self): - """Total URL must never exceed the 6 KB budget.""" - many_results = [ - { - "id": f"OSPS-AC-{i:02d}.01", - "status": "FAIL", - "details": "X" * 600, # each detail is 600 chars before truncation - } - for i in range(1, 30) - ] - output = generate_badge_url(many_results, "https://github.com/example/repo") + output = generate_badge_url(results, "https://github.com/example/repo") url_line = [line for line in output.splitlines() if line.startswith("https://")][-1] - assert len(url_line.encode()) <= 6 * 1024 + assert len(url_line) <= 8000, ( + f"URL is {len(url_line)} chars, exceeds 8000-char ceiling" + ) def test_all_status_params_present_even_when_budget_exhausted(self): - """Even when the budget is exhausted, every _status param must appear.""" + """Even when the URL budget is exhausted, every _status param must appear.""" many_results = [ { "id": f"OSPS-AC-{i:02d}.01", "status": "FAIL", - "details": "X" * 600, + "details": "X" * 300, } for i in range(1, 30) ] @@ -197,7 +199,7 @@ def test_empty_results_still_returns_url(self): def test_no_project_url_shows_warning(self): output = generate_badge_url(self._SIMPLE_RESULTS, "") - assert "⚠️" in output + assert "WARNING:" in output def test_no_project_url_no_url_param(self): """When project_url is empty, the url= query param should be absent.""" @@ -206,11 +208,13 @@ def test_no_project_url_no_url_param(self): assert "as=edit" in url_line assert "url=" not in url_line - def test_justification_truncated_at_500_chars(self): - long_details = "A" * 600 + def test_justification_truncated_at_200_chars(self): + """Justification longer than 200 chars is truncated and ends with ...""" + long_details = "A" * 300 results = [{"id": "OSPS-AC-01.01", "status": "PASS", "details": long_details}] output = generate_badge_url(results, "https://github.com/example/repo") - assert "…" in output or "%E2%80%A6" in output # ellipsis or its URL-encoded form + # '...' is plain ASCII; check it appears in the output + assert "..." in output def test_result_missing_details_no_justification_param(self): results = [{"id": "OSPS-AC-01.01", "status": "PASS"}] @@ -247,14 +251,20 @@ def test_result_with_missing_status_skipped(self): # --------------------------------------------------------------------------- -# Integration: import from package __init__ +# Integration: re-export from formatters package __init__ # --------------------------------------------------------------------------- def test_badge_exports_available_from_formatters_package(): - """Ensure the badge symbols are re-exported from the formatters package.""" - assert callable(_gbu) - assert callable(_ctk) - assert callable(_stbs) - assert isinstance(_BADGE_BASE_URL, str) - assert "bestpractices.dev" in _BADGE_BASE_URL + """Ensure the badge symbols are re-exported from the formatters package. + + This test uses the names already imported at module level from + darnit_baseline.formatters (not .formatters.badge). If the re-export + breaks, every test in this file that uses those names will fail, which + is the correct signal. + """ + assert callable(generate_badge_url) + assert callable(control_id_to_key) + assert callable(status_to_badge_status) + assert isinstance(BADGE_BASE_URL, str) + assert "bestpractices.dev" in BADGE_BASE_URL From 58f32a44090951adbbc3c9a33ea91836b1192c88 Mon Sep 17 00:00:00 2001 From: jaydeep869 Date: Sun, 19 Jul 2026 18:47:50 +0530 Subject: [PATCH 5/5] fix(badge): replace em-dashes with ASCII -- in badge.py Replace three U+2014 em-dashes with plain ASCII double-hyphens (--) to keep rendering consistent across all terminals and diffs clean: - Line 50 comment: inconclusive -- cannot assert Met or Unmet - Line 181 comment: Budget exhausted -- skip remaining justifications - Line 193 header string: OpenSSF Best Practices Badge -- Automation Proposal Requested by mlieberman85 in final review round. Signed-off-by: jaydeep869 --- .../darnit-baseline/src/darnit_baseline/formatters/badge.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py b/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py index 8096682d..afec2ba1 100644 --- a/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py +++ b/packages/darnit-baseline/src/darnit_baseline/formatters/badge.py @@ -47,7 +47,7 @@ _STATUS_MAP: dict[str, str] = { "PASS": "Met", "FAIL": "Unmet", - "WARN": "?", # inconclusive — cannot assert Met or Unmet + "WARN": "?", # inconclusive -- cannot assert Met or Unmet "NA": "N/A", "N/A": "N/A", } @@ -178,7 +178,7 @@ def generate_badge_url( cost = len(f"&{encoded_pair}".encode()) if remaining_budget < cost: - # Budget exhausted — skip remaining justifications + # Budget exhausted -- skip remaining justifications break remaining_budget -= cost @@ -190,7 +190,7 @@ def generate_badge_url( url = f"{BADGE_BASE_URL}?{query}" header_lines = [ - "## OpenSSF Best Practices Badge — Automation Proposal", + "## OpenSSF Best Practices Badge -- Automation Proposal", "", "Follow this link to pre-fill your badge entry with the audit results.", "Review each field, then submit to claim your badge.",