Skip to content

feat(baseline): add Best Practices Badge automation-proposal URL output format#319

Open
Jaydeep869 wants to merge 5 commits into
darnitdevorg:mainfrom
Jaydeep869:feat/badge-output-format
Open

feat(baseline): add Best Practices Badge automation-proposal URL output format#319
Jaydeep869 wants to merge 5 commits into
darnitdevorg:mainfrom
Jaydeep869:feat/badge-output-format

Conversation

@Jaydeep869

Copy link
Copy Markdown
Contributor

Closes #283

This adds a badge output format to darnit audit and the audit_openssf_baseline MCP tool. It generates a ready-to-follow OpenSSF Best Practices Badge automation-proposal URL pre-filled with the audit results.

Usage

CLI

darnit audit -o badge .
darnit audit -o badge --project-url https://github.com/example/repo .

MCP tool

audit_openssf_baseline(output_format="badge")
audit_openssf_baseline(output_format="badge", project_url="https://github.com/example/repo")

How it works

Control ID transform: OSPS-AC-01.01 becomes osps_ac_01_01 (lowercase, hyphens and dots replaced with underscores).

Status mapping follows Marc's proposal from the issue:

darnit status badge status
PASS Met
FAIL Unmet
WARN ? (inconclusive)
NA / N/A N/A

The url= parameter is taken from --project-url if given, otherwise auto-detected from the git remote as https://github.com/{owner}/{repo}. A warning is shown when it cannot be determined.

Justifications (the details field) are included per control and capped at 500 characters to keep URLs practical.

Changes

packages/darnit-baseline/src/darnit_baseline/formatters/badge.py — new module with three pure functions: control_id_to_key, status_to_badge_status, generate_badge_url

packages/darnit-baseline/src/darnit_baseline/formatters/__init__.py — export the new symbols

packages/darnit-baseline/src/darnit_baseline/tools.py — add output_format="badge" branch and optional project_url parameter to audit_openssf_baseline

packages/darnit/src/darnit/cli.py — add badge to -o/--output choices and add --project-url flag

tests/darnit_baseline/formatters/test_badge_formatter.py — 30 unit tests covering all status mappings, key transforms, URL structure, and edge cases

All 30 new tests pass. No existing tests broken by this change.

Signed-off-by: jaydeep869 jaydeeppokhariya2106@gmail.com

…ut format

Closes darnitdevorg#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 <wheeler@dwheeler.com>
Signed-off-by: jaydeep869 <jaydeeppokhariya2106@gmail.com>
@Jaydeep869
Jaydeep869 requested a review from mlieberman85 as a code owner July 11, 2026 14:40
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 <jaydeeppokhariya2106@gmail.com>
@Marc-cn

Marc-cn commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Tested end-to-end @Jaydeep869 the formatter works, the git-remote auto-detect works, and the pure functions + tests are solid. Three changes needed before merge:

  1. Don't import darnit_baseline from core. packages/darnit/src/darnit/cli.py now imports darnit_baseline.formatters.badge, but darnit-core doesn't (and shouldn't) depend on darnit-baseline, so darnit audit -o badge ImportErrors on a core-only install, it only works in our monorepo venv. Also, darnit audit runs any framework, and badge output only makes sense for openssf-baseline. Simplest fix: drop the cli.py changes and keep this as an MCP-tool feature (audit_openssf_baseline(output_format="badge")), which is where it lives anyway

  2. Cap the total URL length. I ran it on a nearly-empty repo and got a 7.6KB URL, already at common 8KB server limits, and real repos with long FAIL details will exceed it. Add a total budget (say 6KB): always keep every _status param, then add justifications only while they fit

  3. Drop justifications for ? results. On a machine without gh installed, ~20 badge fields get pre-filled with "Command not found: gh" as their justificatio that would end up in a real badge submission. Only include justification text for Met/Unmet; ERROR and WARN should map to ? with no justification

Thanks!

Three changes based on review comments on PR darnitdevorg#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 <jaydeeppokhariya2106@gmail.com>
@Jaydeep869

Copy link
Copy Markdown
Contributor Author

Hey @Marc-cn done, can you review it again.

@mlieberman85 mlieberman85 left a comment

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.

Overall this looks decent — the badge formatter itself is clean, well-tested, and does what #283 asks for. One architectural issue to sort out plus a handful of smaller items.

Dependency direction.

packages/darnit/src/darnit/cli.py:207-211 imports from darnit_baseline.formatters.badge import generate_badge_url. That's the framework package (darnit) reaching into one of its plugin modules (darnit_baseline). Once that import is there, darnit no longer stands alone — it depends on the plugin it's supposed to host, and the "framework loads plugins" arrow gets a return arrow pointing the other way.

This is called out explicitly in docs/getting-started/framework-development.md under "Separation Rules" (around line 46 today): the framework must not import implementation packages, and cross-boundary communication should go through the ComplianceImplementation protocol or entry points. The badge output format and --project-url flag are baseline-specific concepts (OSPS control IDs, Best Practices Badge site), so they don't naturally belong in the generic framework CLI anyway.

Two ways to fix this that I'd be happy with:

  • Add a badge-output primitive in core darnit that downstream modules call into. The framework exposes a generic "audit-results-to-URL-form" mechanism (or something similar with generic keys/values), and the baseline module supplies the OSPS-specific mapping and the Best Practices Badge base URL. Cleaner long-term shape and unblocks future badge-adjacent formats, but a bigger PR.
  • Keep the badge feature purely in darnit-baseline for now and abstract later. Drop the packages/darnit/src/darnit/cli.py changes; keep the badge logic and the output_format=\"badge\" branch in the baseline MCP tool (audit_openssf_baseline) where it already lives. Users invoke it via the MCP tool; CLI access can come back later once we have the right protocol. Smallest change, ships the feature, defers the abstraction.

I'd lean toward the second — it lands the value from #283 without committing to an abstraction shape we haven't designed yet. But I'm open to either.

ASCII-only in file content.

packages/darnit-baseline/src/darnit_baseline/formatters/badge.py uses `...` (horizontal ellipsis) for truncation and a warning emoji in the header. Please replace with plain ASCII (`...` and something like `WARNING:` respectively). Keeps rendering consistent across terminals and diffs clean.

URL length ceiling.

OSPS Baseline has ~62 controls. At a 500-char per-control justification cap plus keys plus URL-encoding overhead, a fully populated URL can easily land in the 30-50K character range. Practical browser URL limits are around 8K-32K depending on browser and server; the badge site's own tolerance isn't documented in #283. Two concrete asks:

  • Reduce `_MAX_JUSTIFICATION_LEN` to something like 200. The badge form expects short human-readable justifications, not evidence dumps.
  • Add a test asserting the generated URL length stays under a fixed ceiling (say 8000 chars) for a 62-control audit with typical justifications. Turns "URL might be too long" into a hard signal.

Test-file import cleanup.

`tests/darnit_baseline/formatters/test_badge_formatter.py:11-25` imports the same four symbols twice — once from `darnit_baseline.formatters` with `_`-prefixed aliases, once from `darnit_baseline.formatters.badge` with real names. The aliased copies are only used in the last test that verifies the re-export. Simpler: import once from `darnit_baseline.formatters` with real names and let every test use those references — if the re-export breaks, all tests break, which is the correct signal.

Assertion sloppiness in encoding tests.

`test_url_with_warn_result` and `test_url_with_na_result` accept either the URL-encoded (`%3F`, `%2F`) or unencoded form of the status value. Since `urlencode(..., quote_via=quote)` always percent-encodes reserved characters, please assert the specific encoded form — otherwise a regression that ships raw `?` or `/` in the URL query would slip past.

Nits (take or leave):

  • `--project-url` accepts any string; a bad value silently produces a broken badge URL. A cheap check that it starts with `http://` or `https://` at argparse time would help.
  • `re.sub(r"[-.]", "_", control_id).lower()` — two `str.replace()` calls would be equivalent and clearer for two literal characters. Minor.

Nice work overall. Once the dependency direction is sorted plus the ASCII fix and URL-length cap, this should be in good shape.

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 <jaydeeppokhariya2106@gmail.com>
@Jaydeep869
Jaydeep869 requested a review from mlieberman85 July 12, 2026 20:59
@mlieberman85

Copy link
Copy Markdown
Contributor

Thanks for the follow-up commit — most of the review is addressed cleanly (cli.py no longer imports darnit_baseline, _MAX_JUSTIFICATION_LEN down to 200, URL-length ceiling test in place, encoding assertions strict, imports deduplicated).

One remaining ASCII issue: packages/darnit-baseline/src/darnit_baseline/formatters/badge.py still has three em-dashes (U+2014) that need to be plain -- or reworded:

  • Line 51 (comment): # inconclusive -- cannot assert Met or Unmet
  • Line 182 (comment): # Budget exhausted -- skip remaining justifications
  • Line 194 (header string): "## OpenSSF Best Practices Badge -- Automation Proposal"

Once those are ASCII, this is good to merge.

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 <jaydeeppokhariya2106@gmail.com>
@Jaydeep869

Copy link
Copy Markdown
Contributor Author

hey @mlieberman85 @Marc-cn i have updated the pr as you told can you review it again.

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.

Add baseline status report to best practices badge site using automation proposal

3 participants