Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ Thumbs.db
project.toml
!example.*.toml
!example.*.yaml
# Test fixtures with tracked .baseline.toml. The top-level rule above
# would otherwise strip them and CI would auto-pick a different
# framework than the fixture expects (harness/parity tests).
!tests/darnit/harness/fixtures/**/.baseline.toml

# Logs
*.log
Expand Down
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"feature_directory": "specs/024-cmd-run-e2e-tests"}
{"feature_directory": "specs/026-darnit-harness"}
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ else:

## Technology Stack
- **Language**: Python 3.11+ (targets 3.11/3.12)
- **Core deps**: FastMCP, Pydantic >=2.0, PyYAML, cel-python
- **Core deps**: FastMCP (via `mcp>=1.23,<2`), Pydantic >=2.0, PyYAML, cel-python, pydantic-ai-slim[anthropic] (required runtime dep as of RFC-0001 Stage 1 / feature 025)
- **Threat model**: tree-sitter, tree-sitter-language-pack (Python/JS/Go/YAML grammars)
- **Attestation**: sigstore, in-toto (optional)
- **Config**: TOML framework configs, `.project/project.yaml` (YAML), `.baseline.toml` (user overrides)
Expand All @@ -369,11 +369,14 @@ else:
- Filesystem only. Composition is resolved in-memory at framework-config load time; no new persistent state. (013-plugin-composition)

## Recent Changes
- 026-darnit-harness: adds `darnit harness` subcommand -- end-to-end audit driver with in-band LLM dispatch (fleet-operator + CI-integrated persona). Consumes `ANTHROPIC_API_KEY` from env; dispatches PENDING_LLM results via `PydanticAILLMStep`. Non-interactive by default; batch answers via pluggable `AnswerSource` Protocol with auto-discovery of `.project/project.yaml` + `--answers` override. Markdown + JSON reports. Four documented exit codes (0/1/2/3) plus grep-able stderr summary. New `darnit.harness` subpackage (`driver`, `answer_sources`, `report`, `exit_codes`).
- 025-rfc0001-stage1: RFC-0001 Stage 1. Adds `authority` (`dispositive`|`suggestive`|`asserted`) to every step + result; per-phase Check execution rule ensures only dispositive/asserted results conclude a control (LLM output alone cannot manufacture a PASS). New `darnit.core.action_plan` module exposes `next_action`/`submit_result` as a public typed protocol; `agent.graph.route()` becomes a thin adapter. MCP surface adds `run_next_action`/`submit_action_result` tools (client-owned state). Baseline attestation predicate gains a per-result `authority` field additively within v1. `pydantic-ai-slim[anthropic]` becomes a required runtime dep.
- 024-cmd-run-e2e-tests: E2E baseline for `darnit run` pinning header/footer/count/exit-code contract; used as the mechanical regression guarantee for Stage 1's `cmd_run` code path.
- 021-fix-config-path: framework TOMLs (openssf-baseline.toml, gittuf.toml, reproducibility.toml) moved into `src/<module>/`; `get_framework_config_path()` uses `importlib.resources`. Wheel installs now find the TOML; editable installs unchanged.
- 012-packaging-distribution: Added Python 3.11/3.12 (workspace targets) plus bash for release scripts and GitHub Actions YAML + `shiv` (binary builder), `cosign` (image + binary signing), `syft` (SBOM generation), `docker buildx` (multi-arch images), `gh` CLI (release creation), Sigstore-action (PyPI wheel signing via `pypa/gh-action-pypi-publish`). No new runtime dependencies in any darnit Python package.

<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan:
[`specs/024-cmd-run-e2e-tests/plan.md`](specs/024-cmd-run-e2e-tests/plan.md)
[`specs/026-darnit-harness/plan.md`](specs/026-darnit-harness/plan.md)
<!-- SPECKIT END -->
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

This module builds the in-toto attestation predicate for
OpenSSF Baseline assessment results.

RFC-0001 Stage 1 (feature 025 T046, T054): each result entry now carries
an ``authority`` field ("dispositive" | "suggestive" | "asserted"). The
predicate type string ``https://openssf.org/baseline/assessment/v1`` does
NOT change; the addition is field-additive within v1 per Q2 clarification.
Consumers with permissive schemas continue to load unchanged; consumers
with field-strict validation must update. See
specs/025-rfc0001-stage1/contracts/attestation-authority-field.md.
"""

from datetime import UTC, datetime
Expand All @@ -19,7 +27,7 @@ def build_assessment_predicate(
level: int,
results: list[dict[str, Any]],
project_config: Optional["ProjectConfig"],
adapters_used: list[str]
adapters_used: list[str],
) -> dict[str, Any]:
"""Build the assessment attestation predicate.

Expand All @@ -40,25 +48,25 @@ def build_assessment_predicate(
Dictionary containing the attestation predicate
"""
# Count results by status
passes = [r for r in results if r['status'] == 'PASS']
fails = [r for r in results if r['status'] == 'FAIL']
warns = [r for r in results if r['status'] == 'WARN']
nas = [r for r in results if r['status'] == 'N/A']
errors = [r for r in results if r['status'] == 'ERROR']
passes = [r for r in results if r["status"] == "PASS"]
fails = [r for r in results if r["status"] == "FAIL"]
warns = [r for r in results if r["status"] == "WARN"]
nas = [r for r in results if r["status"] == "N/A"]
errors = [r for r in results if r["status"] == "ERROR"]

# Calculate level compliance
levels = {}
for lvl in [1, 2, 3]:
if lvl <= level:
lvl_results = [r for r in results if r.get('level', 1) == lvl]
lvl_passes = len([r for r in lvl_results if r['status'] == 'PASS'])
lvl_results = [r for r in results if r.get("level", 1) == lvl]
lvl_passes = len([r for r in lvl_results if r["status"] == "PASS"])
lvl_total = len(lvl_results)
lvl_fails = len([r for r in lvl_results if r['status'] == 'FAIL'])
lvl_fails = len([r for r in lvl_results if r["status"] == "FAIL"])
levels[str(lvl)] = {
"total": lvl_total,
"passed": lvl_passes,
"failed": lvl_fails,
"compliant": lvl_fails == 0
"compliant": lvl_fails == 0,
}

# Determine highest compliant level
Expand All @@ -73,49 +81,45 @@ def build_assessment_predicate(
controls = []
for r in results:
control = {
"id": r['id'],
"level": r.get('level', 1),
"category": r['id'].split('-')[1] if '-' in r['id'] else "UNKNOWN",
"status": r['status'],
"message": r.get('details', ''),
"id": r["id"],
"level": r.get("level", 1),
"category": r["id"].split("-")[1] if "-" in r["id"] else "UNKNOWN",
"status": r["status"],
"message": r.get("details", ""),
}
if r.get('evidence'):
control["evidence"] = r['evidence']
if r.get('source'):
control["source"] = r['source']
if r.get("evidence"):
control["evidence"] = r["evidence"]
if r.get("source"):
control["source"] = r["source"]
else:
control["source"] = "builtin"
# RFC-0001 Stage 1 (feature 025 T046): additive `authority` field.
# Present when the result carries one; absent for results loaded
# from a pre-Stage-1 serialized state. Per contract T2, a Stage-1
# producer emits authority for every result it generates.
if r.get("authority") is not None:
control["authority"] = r["authority"]
controls.append(control)

# Build configuration section
config_section = {
"project_type": project_config.project_type if project_config else "software",
"adapters_used": adapters_used or ["builtin"]
"adapters_used": adapters_used or ["builtin"],
}

if project_config:
excluded = []
for control_id, override in project_config.control_overrides.items():
if override.get('status') == 'n/a':
if override.get("status") == "n/a":
excluded.append(control_id)
if excluded:
config_section["excluded_controls"] = excluded

predicate = {
"assessor": {
"name": "openssf-baseline-mcp",
"version": "0.1.0",
"uri": "https://github.com/ossf/baseline-mcp"
},
"assessor": {"name": "openssf-baseline-mcp", "version": "0.1.0", "uri": "https://github.com/ossf/baseline-mcp"},
"timestamp": datetime.now(UTC).isoformat(),
"baseline": {
"version": "2025.10.10",
"specification": "https://baseline.openssf.org/versions/2025-10-10"
},
"repository": {
"url": f"https://github.com/{owner}/{repo}",
"commit": commit
},
"baseline": {"version": "2025.10.10", "specification": "https://baseline.openssf.org/versions/2025-10-10"},
"repository": {"url": f"https://github.com/{owner}/{repo}", "commit": commit},
"configuration": config_section,
"summary": {
"level_assessed": level,
Expand All @@ -125,10 +129,10 @@ def build_assessment_predicate(
"failed": len(fails),
"warnings": len(warns),
"not_applicable": len(nas),
"errors": len(errors)
"errors": len(errors),
},
"levels": levels,
"controls": controls
"controls": controls,
}

if ref:
Expand Down
33 changes: 22 additions & 11 deletions packages/darnit-baseline/src/darnit_baseline/implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,25 @@ def get_controls_by_level(self, level: int) -> list[ControlSpec]:
domain = control.domain
if domain is None and control.tags:
domain = control.tags.get("domain", "")
controls.append(ControlSpec(
control_id=control_id,
name=control.name,
description=control.description or "",
level=level,
domain=domain or (control_id.split("-")[1] if "-" in control_id else "UNKNOWN"),
metadata={
"full": control.description or "",
"help_uri": control.docs_url or f"https://baseline.openssf.org/versions/2025-10-10#{control_id}",
}
))
controls.append(
ControlSpec(
control_id=control_id,
name=control.name,
description=control.description or "",
level=level,
domain=domain or (control_id.split("-")[1] if "-" in control_id else "UNKNOWN"),
# Preserve TOML tags on the ControlSpec so downstream
# consumers (e.g., tag-based filtering, feature 025's
# STAGE1-REF-* opt-out from OSPS-format tests) can see
# them. ControlSpec.__post_init__ still adds level/domain.
tags=dict(control.tags) if control.tags else {},
metadata={
"full": control.description or "",
"help_uri": control.docs_url
or f"https://baseline.openssf.org/versions/2025-10-10#{control_id}",
},
)
)
return controls

def get_rules_catalog(self) -> dict[str, Any]:
Expand Down Expand Up @@ -220,6 +228,9 @@ def register_handlers(self) -> None:
phase="deterministic",
handler_fn=generate_threat_model_handler,
description="Generate dynamic STRIDE threat model",
# RFC-0001 Stage 1: threat-model generation observes ground
# truth (file produced or not). Explicitly dispositive.
default_authority="dispositive",
)
sieve_registry.set_plugin_context(None)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4359,6 +4359,48 @@ overwrite = false
[controls."OSPS-SA-03.02".remediation.project_update]
set = { "security.threat_model.path" = "docs/threatmodel/SUMMARY.md" }

# =============================================================================
# RFC-0001 Stage 1 Reference Control (feature 025 T044)
# =============================================================================
# STAGE1-REF-SECURITY-01 exercises the full Check/Collect/Remediate flow
# with all three authority levels: dispositive file_exists, suggestive
# llm_extract, asserted manual confirmation. See
# specs/025-rfc0001-stage1/research.md R5 for the design rationale.
#
# Kept as a distinct STAGE1-REF-* id (rather than adapting an existing
# OSPS-* control) so the Stage 1 acceptance gate is decoupled from
# baseline evolution and can be removed cleanly if Stage 2 replaces it.

[controls."STAGE1-REF-SECURITY-01"]
name = "SecurityPolicyReference"
level = 1
domain = "VM"
description = "RFC-0001 Stage 1 reference control: SECURITY.md discovery + LLM-suggested contact + confirmation"
tags = { level = 1, domain = "VM", "stage1-ref" = true }

# Dispositive file_exists FIRST so a repo with SECURITY.md concludes PASS
# without ever calling the LLM. Under the harness's `stop_on_llm=True`
# semantics, an ll m_extract-first ordering returns PENDING_LLM and the
# sieve never continues to file_exists -- the control lands on WARN,
# never PASS. Reviewer flagged this on PR #365 as a level-1 control
# that could never PASS.
#
# NOTE: this ordering sacrifices the "suggestive proposes a candidate
# contact even when file_exists concludes FAIL" property. A future
# feature can add a "suggestive-runs-after-termination" semantic to the
# orchestrator so both properties hold; documented for follow-up.
[[controls."STAGE1-REF-SECURITY-01".passes]]
handler = "file_exists"
files = ["SECURITY.md", "docs/SECURITY.md", ".github/SECURITY.md"]
authority = "dispositive"

[[controls."STAGE1-REF-SECURITY-01".passes]]
handler = "llm_extract"
prompt = "Scan the repository's README and documentation for security-contact information. Propose a contact string suitable for a SECURITY.md."
files = ["README.md", "README", "docs/**/*.md"]
target_key = "security_contact"
authority = "suggestive"

# =============================================================================
# MCP Server Configuration
# =============================================================================
Expand Down
5 changes: 5 additions & 0 deletions packages/darnit-gittuf/src/darnit_gittuf/implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,22 @@ def register_sieve_handlers(self) -> None:
registry = get_sieve_handler_registry()
registry.set_plugin_context(self.name)

# RFC-0001 Stage 1: both handlers observe ground truth
# (gittuf verify-ref and commit signature presence). Explicitly
# dispositive so a passing result concludes the control.
registry.register(
"gittuf_verify_policy",
phase="deterministic",
handler_fn=handlers.gittuf_verify_policy_handler,
description="Run gittuf verify-ref HEAD",
default_authority="dispositive",
)
registry.register(
"gittuf_commits_signed",
phase="deterministic",
handler_fn=handlers.gittuf_commits_signed_handler,
description="Check last 5 commits for cryptographic signatures",
default_authority="dispositive",
)

registry.set_plugin_context(None)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,35 +113,44 @@ def register_sieve_handlers(self) -> None:
registry = get_sieve_handler_registry()
registry.set_plugin_context(self.name)

# RFC-0001 Stage 1: all five handlers observe ground truth (lock
# files, Dockerfiles, CI workflow contents). Explicitly dispositive
# so passing results conclude the control instead of falling
# through to WARN via the suggestive default.
registry.register(
"repro_deps_pinned",
phase="deterministic",
handler_fn=handlers.repro_deps_pinned_handler,
description="Check for lock files indicating pinned dependencies",
default_authority="dispositive",
)
registry.register(
"repro_build_env_declared",
phase="deterministic",
handler_fn=handlers.repro_build_env_declared_handler,
description="Check for Dockerfile, Nix flake, or similar env declaration",
default_authority="dispositive",
)
registry.register(
"repro_hermetic_build",
phase="pattern",
handler_fn=handlers.repro_hermetic_build_handler,
description="Scan CI workflows for live network fetches during build",
default_authority="dispositive",
)
registry.register(
"repro_provenance_exists",
phase="pattern",
handler_fn=handlers.repro_provenance_exists_handler,
description="Check CI workflows for sigstore/SLSA provenance steps",
default_authority="dispositive",
)
registry.register(
"repro_bit_for_bit",
phase="pattern",
handler_fn=handlers.repro_bit_for_bit_handler,
description="Check for SOURCE_DATE_EPOCH and reprotest signals",
default_authority="dispositive",
)

registry.set_plugin_context(None)
5 changes: 5 additions & 0 deletions packages/darnit/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ dependencies = [
"cel-python>=0.5.0", # CEL expression evaluation for pass logic
"tomli>=2.0.0;python_version<'3.11'",
"tomllib-stubs>=0.1.0;python_version<'3.11'",
# RFC-0001 Stage 1 (feature 025): default LLMStep implementation. Required
# runtime dependency; LLM-assisted checks are core product functionality
# and there is no shipping "no-LLM" install tier. Swappable at code time
# via the LLMStep Protocol (single-file replacement), not via install flag.
"pydantic-ai-slim[anthropic]>=0.0.14",
]

[project.urls]
Expand Down
Loading
Loading