Skip to content
Open
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
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ __pycache__/
.coverage
.mypy_cache/
dist/
!dist/
!dist/**
build/
node_modules/
*.log
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/release-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
strategy:
fail-fast: false
matrix:
package: [darnit, darnit-baseline, darnit-gittuf, darnit-mcp]
package: [darnit-core, darnit-baseline, darnit-csl, darnit-gittuf, darnit-reproducibility, darnit-mcp]
steps:
- name: Checkout (for public-packages.txt sanity check)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
Expand Down Expand Up @@ -145,9 +145,15 @@ jobs:
"darnit-baseline")
"$venv/bin/python" -c "import darnit_baseline; print('darnit_baseline imports OK')"
;;
"darnit-csl")
"$venv/bin/python" -c "import darnit_csl; print('darnit_csl imports OK')"
;;
"darnit-gittuf")
"$venv/bin/python" -c "import darnit_gittuf; print('darnit_gittuf imports OK')"
;;
"darnit-reproducibility")
"$venv/bin/python" -c "import darnit_reproducibility; print('darnit_reproducibility imports OK')"
;;
*)
echo "::error::No smoke recipe defined for package '$PKG'"
exit 1
Expand Down
111 changes: 70 additions & 41 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ name: Release
# What this workflow does today:
# 1. preflight - tag/version parity, clean tree, tests, lint, sync
# 2. build - per-package wheels + sdists to a shared artifact
# 3. publish-* - upload each public package to PyPI via API token
# 3. publish-* - upload each public package to PyPI/TestPyPI via Trusted Publishing
# 4. container - build + push + cosign-sign the multi-arch container image
# 5. release - create the GitHub Release with install instructions
# 6. finalize - summary line to the run's step summary
#
# Auth model: PyPI publishes use the `PYPI_API_TOKEN` repo secret (account-
# scoped). Trusted-publisher setup is nicer long-term but requires per-project
# UI config in PyPI, which was blocking. Container signing uses cosign keyless
# OIDC as before; that path does not depend on PyPI auth.
# Auth model: PyPI publishes use PyPI Trusted Publishing (GitHub OIDC), so no
# long-lived PyPI API token is required. Container signing uses cosign keyless
# OIDC as before.
#
# Version format: `vX.Y.Z` or `vX.Y.ZrcN` (no dash before rc; matches PEP 440
# canonical form and the preflight parse-tag regex). Pre-release tags are
Expand Down Expand Up @@ -218,21 +217,21 @@ jobs:
if-no-files-found: error
retention-days: 7

# Publish jobs use `password: ${{ secrets.PYPI_API_TOKEN }}` (account-scoped
# token). Migrate to project-scoped tokens once the projects exist on PyPI.
#
# Sequenced via `needs:` because darnit-baseline, darnit-gittuf,
# Sequenced via `needs:` because darnit-baseline, darnit-csl, darnit-gittuf,
# darnit-reproducibility, and darnit-mcp declare `darnit-core>=...` runtime
# deps; darnit-mcp additionally depends on the other four. Publishing them
# before the deps are on the index briefly produces unresolvable wheels.
# deps; darnit-mcp additionally depends on baseline, gittuf, and
# reproducibility. Publishing them before the deps are on the index briefly
# produces unresolvable wheels.

publish-darnit-core:
name: Publish darnit-core to PyPI
needs: [preflight, build]
runs-on: ubuntu-latest
environment: release
timeout-minutes: 10
permissions:
contents: read
id-token: write
steps:
- name: Download dist artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
Expand All @@ -244,16 +243,19 @@ jobs:
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: dist/darnit-core/
password: ${{ secrets.PYPI_API_TOKEN }}
repository-url: ${{ needs.preflight.outputs.is_prerelease == 'true' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }}
skip-existing: true
attestations: true

publish-darnit-baseline:
name: Publish darnit-baseline to PyPI
needs: [preflight, build, publish-darnit-core]
runs-on: ubuntu-latest
environment: release
timeout-minutes: 10
permissions:
contents: read
id-token: write
steps:
- name: Download dist artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
Expand All @@ -265,16 +267,43 @@ jobs:
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: dist/darnit-baseline/
password: ${{ secrets.PYPI_API_TOKEN }}
repository-url: ${{ needs.preflight.outputs.is_prerelease == 'true' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }}
skip-existing: true
attestations: true

publish-darnit-csl:
name: Publish darnit-csl to PyPI
needs: [preflight, build, publish-darnit-core]
runs-on: ubuntu-latest
environment: release
timeout-minutes: 10
permissions:
contents: read
id-token: write
steps:
- name: Download dist artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: dist/

- name: Publish darnit-csl
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: dist/darnit-csl/
repository-url: ${{ needs.preflight.outputs.is_prerelease == 'true' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }}
skip-existing: true
attestations: true

publish-darnit-gittuf:
name: Publish darnit-gittuf to PyPI
needs: [preflight, build, publish-darnit-core]
runs-on: ubuntu-latest
environment: release
timeout-minutes: 10
permissions:
contents: read
id-token: write
steps:
- name: Download dist artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
Expand All @@ -286,16 +315,19 @@ jobs:
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: dist/darnit-gittuf/
password: ${{ secrets.PYPI_API_TOKEN }}
repository-url: ${{ needs.preflight.outputs.is_prerelease == 'true' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }}
skip-existing: true
attestations: true

publish-darnit-reproducibility:
name: Publish darnit-reproducibility to PyPI
needs: [preflight, build, publish-darnit-core]
runs-on: ubuntu-latest
environment: release
timeout-minutes: 10
permissions:
contents: read
id-token: write
steps:
- name: Download dist artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
Expand All @@ -307,21 +339,25 @@ jobs:
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: dist/darnit-reproducibility/
password: ${{ secrets.PYPI_API_TOKEN }}
repository-url: ${{ needs.preflight.outputs.is_prerelease == 'true' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }}
skip-existing: true
attestations: true

publish-darnit-mcp:
name: Publish darnit-mcp to PyPI
needs:
- preflight
- build
- publish-darnit-baseline
- publish-darnit-csl
- publish-darnit-gittuf
- publish-darnit-reproducibility
runs-on: ubuntu-latest
environment: release
timeout-minutes: 10
permissions:
contents: read
id-token: write
steps:
- name: Download dist artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
Expand All @@ -333,17 +369,19 @@ jobs:
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: dist/darnit-mcp/
password: ${{ secrets.PYPI_API_TOKEN }}
repository-url: ${{ needs.preflight.outputs.is_prerelease == 'true' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }}
skip-existing: true
attestations: true

# Build + push + cosign-sign the multi-arch container image. Depends on
# publish-darnit-mcp because the Dockerfile does `pip install
# darnit-mcp==<version>` at build time - the package must be live before
# the image build can resolve it.
# Build + push + cosign-sign the multi-arch container image. Depends on the
# PyPI publish jobs so the image only ships after the public package set is
# uploaded. The Dockerfile installs Darnit packages from this run's build
# artifacts and resolves only external dependencies from PyPI.
container_build_push:
name: Build, push, and sign container image
needs:
- preflight
- build
- publish-darnit-mcp
runs-on: ubuntu-latest
timeout-minutes: 20
Expand Down Expand Up @@ -375,6 +413,12 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Download dist artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: dist/

- name: Compute image tags
id: tags
env:
Expand All @@ -398,26 +442,6 @@ jobs:
} >> "$GITHUB_OUTPUT"
echo "Tagging: $tags"

- name: Wait for darnit-mcp on PyPI
env:
VERSION: ${{ needs.preflight.outputs.version }}
run: |
set -euo pipefail
# PyPI's CDN typically caches the index for a few minutes after a
# new upload. Poll up to 2 minutes so the image build is not racing
# publication.
url="https://pypi.org/pypi/darnit-mcp/${VERSION}/json"
for attempt in $(seq 1 24); do
if curl -fsSL "$url" >/dev/null 2>&1; then
echo "darnit-mcp ${VERSION} is visible on PyPI (attempt $attempt)"
exit 0
fi
echo "Waiting for darnit-mcp ${VERSION} on PyPI (attempt $attempt/24)..."
sleep 5
done
echo "::error::darnit-mcp ${VERSION} did not appear on PyPI within 2 minutes"
exit 1

- name: Build and push multi-arch image
id: build_push
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v6
Expand Down Expand Up @@ -469,9 +493,11 @@ jobs:
if [ "$IS_PRERELEASE" = "true" ]; then
flags="--prerelease"
title="darnit ${VERSION} (pre-release)"
pypi_install="pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ --pre darnit-mcp==${VERSION}"
else
flags="--latest"
title="darnit ${VERSION}"
pypi_install="pip install darnit-mcp==${VERSION}"
fi
body_file=$(mktemp)
cat > "$body_file" <<EOF
Expand All @@ -482,7 +508,7 @@ jobs:
## Install

- **MCP (Claude Code / Claude Desktop):** \`darnit install\` writes an \`uvx --from darnit-mcp\` config for you. See [docs/install/](https://github.com/${GITHUB_REPOSITORY}/tree/${TAG}/docs/install).
- **PyPI:** \`pip install darnit-mcp==${VERSION}\` (installs the meta-package plus \`darnit-core\`, \`darnit-baseline\`, \`darnit-gittuf\`, \`darnit-reproducibility\`).
- **PyPI:** \`${pypi_install}\` (installs the meta-package plus \`darnit-core\`, \`darnit-baseline\`, \`darnit-gittuf\`, and \`darnit-reproducibility\`).
- **Container:** \`docker pull ghcr.io/${owner_lc}/darnit:${TAG}\`.
- **Standalone binary / Homebrew:** not yet in this release. Tracked for follow-ups.

Expand Down Expand Up @@ -511,6 +537,7 @@ jobs:
- preflight
- publish-darnit-core
- publish-darnit-baseline
- publish-darnit-csl
- publish-darnit-gittuf
- publish-darnit-reproducibility
- publish-darnit-mcp
Expand All @@ -526,6 +553,7 @@ jobs:
VERSION: ${{ needs.preflight.outputs.version }}
PYPI_CORE: ${{ needs.publish-darnit-core.result }}
PYPI_BASELINE: ${{ needs.publish-darnit-baseline.result }}
PYPI_CSL: ${{ needs.publish-darnit-csl.result }}
PYPI_GITTUF: ${{ needs.publish-darnit-gittuf.result }}
PYPI_REPRO: ${{ needs.publish-darnit-reproducibility.result }}
PYPI_MCP: ${{ needs.publish-darnit-mcp.result }}
Expand All @@ -539,6 +567,7 @@ jobs:
echo "|---|---|"
echo "| pypi-darnit-core | $PYPI_CORE |"
echo "| pypi-darnit-baseline | $PYPI_BASELINE |"
echo "| pypi-darnit-csl | $PYPI_CSL |"
echo "| pypi-darnit-gittuf | $PYPI_GITTUF |"
echo "| pypi-darnit-reproducibility | $PYPI_REPRO |"
echo "| pypi-darnit-mcp | $PYPI_MCP |"
Expand Down
29 changes: 13 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,21 @@ The Baseline isn't just about security—it covers testing requirements, build p
## Installation

> [!NOTE]
> PyPI, pipx, `uv tool install`, container, Homebrew, and standalone-binary
> channels are not published yet - tracked in
> [#229](https://github.com/kusari-oss/darnit/issues/229) (which depends on
> PyPI publishing, [#228](https://github.com/kusari-oss/darnit/issues/228)).
> Until those land, install from source with [`uv`](https://docs.astral.sh/uv/):
> PyPI packages are published as `darnit-mcp` plus the workspace packages it
> depends on. `pipx` or `uv tool install` is the recommended path for most
> users. Native install paths such as Homebrew and standalone binaries remain
> tracked in [#229](https://github.com/darnitdevorg/darnit/issues/229).

```bash
git clone https://github.com/kusari-oss/darnit
cd darnit
uv sync
pipx install darnit-mcp
# or
uv tool install darnit-mcp

uv run darnit audit /path/to/repo
uv run darnit serve --framework openssf-baseline # MCP server mode
darnit audit /path/to/repo
darnit serve --framework openssf-baseline # MCP server mode
```

Once [#229](https://github.com/kusari-oss/darnit/issues/229) lands, `pipx install darnit-mcp`
and `uv tool install darnit-mcp` will become the recommended end-user paths, with
container, Homebrew, standalone-binary, and Claude Code plugin channels documented in
For source installs, pre-releases, containers, and other channels, see
[`docs/install/README.md`](docs/install/README.md).
### Optional: Opengrep for taint analysis

Expand Down Expand Up @@ -93,12 +90,12 @@ The tree-sitter discovery pipeline is tuned for **web-service shapes**. What wor
| Go HTTP service (`net/http`, chi, gorilla) | Thin — HTTP route registration + `sql.Open` only |
| Go CLI built on [`spf13/cobra`](https://github.com/spf13/cobra) | Moderate — command families discovered, STRIDE assigned heuristically by import set; `needs reviewer attention` marker on every finding ([feature 014](specs/014-cobra-threat-model/spec.md)) |
| YAML / GitHub Actions workflows | Some — overly-broad permissions and similar config issues |
| Python CLI frameworks (argparse, click, typer) | Not modeled — sibling to the Go cobra work ([#264](https://github.com/kusari-oss/darnit/issues/264)) |
| Other Go CLI frameworks (urfave/cli, kingpin) / message handlers / gRPC | Not modeled — out of scope for the cobra pass ([#262](https://github.com/kusari-oss/darnit/issues/262)) |
| Python CLI frameworks (argparse, click, typer) | Not modeled — sibling to the Go cobra work ([#264](https://github.com/darnitdevorg/darnit/issues/264)) |
| Other Go CLI frameworks (urfave/cli, kingpin) / message handlers / gRPC | Not modeled — out of scope for the cobra pass ([#262](https://github.com/darnitdevorg/darnit/issues/262)) |
| Crypto/signing **client** libraries (sigstore-python, in-toto) | Out of scope — these call out rather than receive; entry-point queries don't fire |
| Systems software, daemons, libraries, ML pipelines | Not modeled |

If your project doesn't match a supported shape, the generator will still write a report — but it will likely show "Total findings: 0" because no entry points were discovered. That's a coverage gap on our side, not a clean bill of health. Expanding the query set is [tracked in our issue tracker](https://github.com/kusari-oss/darnit/issues?q=is%3Aissue+threat-model+coverage).
If your project doesn't match a supported shape, the generator will still write a report — but it will likely show "Total findings: 0" because no entry points were discovered. That's a coverage gap on our side, not a clean bill of health. Expanding the query set is [tracked in our issue tracker](https://github.com/darnitdevorg/darnit/issues?q=is%3Aissue+threat-model+coverage).

## How to Use Darnit

Expand Down
8 changes: 4 additions & 4 deletions docs/install/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ If you're not sure: **`pipx install darnit-mcp`** works for most users and is th

- **One `darnit` command** on PATH after install. Use `darnit audit`, `darnit remediate`, `darnit list-controls`, etc.
- **Same version, same artifact identity.** A release tag (`v0.1.0`) produces a Sigstore-signed PyPI wheel, a cosign-signed container image, four cosign-signed binaries, a Homebrew formula bump, and a Claude Code plugin zip — all derived from the same tagged commit. `darnit --version` reports the same string regardless of which channel installed it.
- **Verifiable signing identity.** Every channel ships with a signature you can verify back to `kusari-oss/darnit`'s `release.yml` workflow. The exact verification command differs per channel; each page has it.
- **Verifiable signing identity.** Every channel ships with a signature you can verify back to `darnitdevorg/darnit`'s `release.yml` workflow. The exact verification command differs per channel; each page has it.

## What differs

Expand All @@ -44,13 +44,13 @@ Every install page has a "Verify" section with the exact command. The common sha
# PyPI (Sigstore)
python -m sigstore verify identity \
--bundle <bundle> \
--cert-identity-regexp '^https://github\.com/kusari-oss/darnit/' \
--cert-identity-regexp '^https://github\.com/darnitdevorg/darnit/' \
--cert-oidc-issuer https://token.actions.githubusercontent.com \
<artifact>

# Container / binary (cosign)
cosign verify[-blob] \
--certificate-identity-regexp '^https://github\.com/kusari-oss/darnit/' \
--certificate-identity-regexp '^https://github\.com/darnitdevorg/darnit/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
<artifact>
```
Expand All @@ -62,7 +62,7 @@ If any of these fail with "identity mismatch", **do not trust the artifact** —
Occasionally a release will succeed on some channels and fail on others (PyPI succeeded, container build failed, etc.). When that happens:

- The successful channels stay published — they were signed correctly.
- A `release-failure` issue appears on the [upstream repo](https://github.com/kusari-oss/darnit/labels/release-failure) naming the failed channel.
- A `release-failure` issue appears on the [upstream repo](https://github.com/darnitdevorg/darnit/labels/release-failure) naming the failed channel.
- The release notes include a per-channel timing table; channels that failed or exceeded the SC-007 budget are flagged.

If you're trying to use a channel that's behind, check the latest release notes and the `release-failure` label.
Expand Down
Loading
Loading