diff-sentry 0.10.0__tar.gz → 0.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/.claude/rules/monitoring.md +8 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/.env.example +2 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/AGENTS.md +3 -3
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/CHANGELOG.md +7 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/PKG-INFO +2 -2
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/README.md +1 -1
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/action.yml +1 -1
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/__init__.py +1 -1
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/cli.py +30 -6
- diff_sentry-0.11.0/diff_sentry/images.py +628 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/docs/release-watch.md +22 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/pyproject.toml +1 -1
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_adversarial_input.py +1 -1
- diff_sentry-0.11.0/tests/test_images.py +505 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/uv.lock +1 -1
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/.claude/rules/detect-setup.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/.claude/rules/handoff.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/.claude/rules/indicators.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/.claude/rules/rubric-and-eval.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/.claude/skills/release/SKILL.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/.github/workflows/ci.yml +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/.github/workflows/release.yml +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/.gitignore +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/.python-version +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/LICENSE +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/VENDOR.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/__main__.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/actors.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/assemble.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/config.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/dbutil.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/deep_classify.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/detect.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/emit.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/fetch_tool.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/hosts.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/indicators.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/ingest.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/linewide.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/normalize.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/releases.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/response.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/rl_export.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/rubric.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/schema.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/shelllex.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/skills/ci-and-workflows.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/skills/concealment-and-provenance.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/skills/developer-autorun.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/skills/llm-directed-text.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/skills/package-and-build-hooks.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/skills/runtime-droppers-and-exfil.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/skills/triage-a-change.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/snapshot.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/diff_sentry/watch.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/docs/development.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/docs/how-it-works.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/docs/limits.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/docs/watch.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/README.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/diff_sentry_eval/__init__.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/diff_sentry_eval/__main__.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/diff_sentry_eval/cli.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/diff_sentry_eval/judge.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/diff_sentry_eval/schema.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/diff_sentry_eval/score.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/diff_sentry_eval/taskset.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/pyproject.toml +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/taskset.example.json +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/tests/conftest.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/tests/test_boundary.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/tests/test_cli.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/tests/test_judge.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/tests/test_schema.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/tests/test_score.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/eval/tests/test_taskset.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/DESIGN.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/README.md +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/diff_sentry_studio/__init__.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/diff_sentry_studio/app.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/diff_sentry_studio/iterations.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/diff_sentry_studio/live.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/diff_sentry_studio/mapper.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/pyproject.toml +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/static/app.js +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/static/index.html +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/static/replay-core.js +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/static/run-core.js +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/static/style.css +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/static/trajectory.js +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/static/vendor/fonts/jetbrains-mono-400.woff2 +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/static/vendor/fonts/jetbrains-mono-700.woff2 +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/tests/replay-core.test.js +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/tests/run-core.test.js +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/tests/static-contract.test.js +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/tests/test_app.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/tests/test_iterations.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/tests/test_live.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/tests/test_mapper.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/studio/tests/trajectory.test.js +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/__init__.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/conftest.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/corpus/change_corpus.json +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/corpus/hackerbot_claw_campaign.json +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/corpus/hackerbot_claw_incident.json +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/fixtures/known-malicious.diff +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/oracle/__init__.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/oracle/grammar.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/oracle/harness.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/oracle/lexrunner.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/oracle/programs.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/oracle/runner.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_actors.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_assemble.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_campaign_replay.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_cli.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_cli_watch.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_config.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_deep_classify.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_detect.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_detection_quality.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_dogfood_gate.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_emit.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_enclosure.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_export.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_fetch_tool.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_file_scope.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_file_structure.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_forward_offline.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_gate_edges.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_grounding.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_hosts.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_incident_hackerbot_claw.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_indicators.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_ingest.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_ingest_metadata.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_injection_offline.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_inline_exec_and_bidi.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_linewide.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_lookalike_owner.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_metadata_rules.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_normalize.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_oracle.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_oracle_harness.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_provenance.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_python_loader.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_releases.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_response.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_rubric.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_schema.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_shelllex.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_snapshot.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_subscription.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_watch.py +0 -0
- {diff_sentry-0.10.0 → diff_sentry-0.11.0}/tests/test_watch_pool.py +0 -0
|
@@ -52,3 +52,11 @@ paths:
|
|
|
52
52
|
- **Paths are where pip puts them.** A wheel's `x.data/purelib/` and `platlib/` map to the site-packages root, `x.data/scripts/` to `{scripts}/`, `x.dist-info/` to `{dist-info}/`; an sdist member loses its top directory, and one at the root keeps its path (pip builds from the root when the members share no top directory). `RECORD`, `WHEEL` and `METADATA` of a wheel, and `PKG-INFO` and `*.egg-info/` of an sdist, are derived and not diffed; `Requires-Dist` is read from `METADATA`.
|
|
53
53
|
- **The previous release** is the highest version below the new one with a file that is not yanked; a pre-release is compared only when the new one is a pre-release too. Members are compared by content across all artifacts of a kind, so a payload in one platform's wheel shows.
|
|
54
54
|
- **State is per file.** First sight of a package (even an empty one) records every file as the baseline. After that, each file not yet recorded is judged with the other new files of its release, against the previous release, so a wheel uploaded after its release was judged, or added to an old release, is judged too. A file is recorded only after its comparison is screened, and the index's ETag only after every new file of it is: a watcher stopped mid-comparison judges the same files on its next pass. A failed comparison is retried on every pass (the ETag is not sent while one is pending), superseded when the files to judge change, and kept as `error` after five, with an alert that it was not compared. A `--check` runs under `...-check` and never stands in for the watcher's judgment. An alert the SIEM did not accept is re-sent from `screened/` a few per poll.
|
|
55
|
+
|
|
56
|
+
## Image watch (`images.py`, `release-watch --images`)
|
|
57
|
+
|
|
58
|
+
- **Provenance only, never a layer.** `image_provenance` reads an image's index, each BuildKit attestation manifest and its SLSA provenance layer; `provenance_facts` keeps the source repository (`vcs:source`; `owner/name` only for a URL whose host is exactly github.com, any other source as written, so a lookalike is only judged between GitHub repositories), revision, building repository, workflow without its ref, ref and ref protection. An attestation counts only when its `vnd.docker.reference.digest` is a platform image of the same index and the provenance's `subject` names that image (BuildKit always writes one); a platform image no attestation names is a note. A layer's content is a known gap.
|
|
59
|
+
- **Judged against what is known, never against the last image.** A repository holds images from several workflows, so `images.known` holds the sources, the builders (repository and workflow) and the workflows seen building from protected refs: seeded at first sight from the watched floating tags and the newest `_SAMPLED_VERSIONS` version tags, and extended by every image judged clean. An image that signals never extends it, so one bad image cannot vouch for the next. On GHCR the namespace (`_namespace`) is a known owner from the start. `scan_image` grades by owner, since one organisation's repositories share a trust boundary: a source or building repository whose owner imitates a known owner is `image-source-lookalike` / `image-builder-lookalike` (critical, via `indicators.lookalike_owner`); one of an unknown owner is `image-source-changed` / `image-builder-changed` (high); another repository or workflow of a known owner is `medium` (`image-workflow-changed`, a nightly or branch build); an unprotected ref under a workflow known protected is `image-ref-unprotected` (high); an image without provenance while the repository's images carry it, a single manifest included, is `image-provenance-dropped` (high); notes are `release-not-fully-read` (high). A first-sight sample whose owners imitate each other is screened as `image-source-lookalike` (critical) and learns neither: both owners are kept as `known.suspect`, every image of the repository is `image-owner-unresolved` (high) and none is learned until the state is reset, and the suspect owners count among those an image's owner is checked for imitating (`_without_lookalikes`). The first-sight alert has a fixed run id and an `image_judgments` row, so it is sent once and re-sent when refused. An empty owner (Docker Hub has no namespace; a build outside GitHub Actions has no building repository) is never a known owner, and a first sight that read nothing and learned nothing is no baseline (retried next poll). The provenance is unsigned: a match proves nothing, only a mismatch is evidence.
|
|
60
|
+
- **What is judged**: a new tag (cosign's `sha256-<digest>.sig/.att/.sbom` skipped, newest first by the version numbers a tag starts with, at most `_MAX_NEW_TAGS` a poll), a watched floating tag that points at another digest, and one of the newest `_RECHECKED_VERSIONS` plain release tags (`_releases`: no `-nightly` or `-rc` suffix, so one version's pre-releases cannot crowd the current release out) pushed again. The first-sight sample takes the newest plain releases the same way. Digests are read with HEAD requests, which Docker Hub does not count toward its pull limit.
|
|
61
|
+
- **Registries are read anonymously and only from their own hosts**: GHCR and Docker Hub (`registry-1.docker.io`), a Bearer token asked for only from a realm on an allowed host, kept per registry and repository and asked for again on a 401 (Docker Hub's expire in minutes), a blob redirect followed only to their CDN hosts (`pkg-containers.githubusercontent.com`, Docker Hub's `production.cloudfront.docker.com` and `production.cloudflare.docker.com`); every response is bounded. A 429 (`RateLimited`) stops that image's poll and counts as no failure.
|
|
62
|
+
- **Like releases**, a comparison that fails is retried on later polls and, after five, screened as not compared (`release-not-fully-read`, which alerts); an alert the SIEM did not accept is re-sent from `screened/` a few per poll.
|
|
@@ -79,3 +79,5 @@ DS_API_KEY=sk-...
|
|
|
79
79
|
# DS_RELEASE_PACKAGES=litellm,requests # release-watch: PyPI projects (or --packages / --packages-file)
|
|
80
80
|
# DS_RELEASE_INTERVAL=300 # release-watch: seconds between polls of each project's index
|
|
81
81
|
# DS_RELEASE_MAX_ARTIFACT_MB=256 # release-watch: a larger wheel or sdist is not compared, which alerts
|
|
82
|
+
# DS_RELEASE_IMAGES=ghcr.io/owner/name # release-watch: container images compared by build provenance (or --images)
|
|
83
|
+
# DS_RELEASE_IMAGE_TAGS=latest # release-watch: floating tags whose moves are judged (or --image-tags)
|
|
@@ -31,10 +31,10 @@ rlm-harness is an exact PyPI pin (`rlm-harness==X.Y.Z` in `pyproject.toml`, lock
|
|
|
31
31
|
- **The budget is hard: `max_retries=1`, no whole-RLM retry inside a run.** One change = one trajectory, so the trace stays valid training data. (`watch` may run a change again, but only when the previous attempt produced no result at all, an infrastructure failure; `cli.run` resets that run id's trace first, so exactly one trajectory per run id survives.) A failed run is infra before it is a schema bug; see `.claude/rules/detect-setup.md` for how the failure names itself.
|
|
32
32
|
- **A second model-judgement is a TOOL, never the sub-LM.** `deep_classify` (on rlm-harness's `make_model_tool`) is the swappable second-stage seam the planner chooses to call, so the decision is a `tool_call` in the trajectory. The analyst intercept (`intercept_sub_lm`) is tracing-only, zero transforms. Swapping the backend touches only `classify_backend` / `_selfclassify_chat`. The planner passes distilled findings, never the whole diff.
|
|
33
33
|
- **Sandboxed code builds no sub-agents.** In dspy 3.4.1 and later, sandboxed code can construct dspy programs that run on the host, call the analyst, can drive the task's tools, and record as one `sub_call` with no inner turns. The kit's default allows it, so `setup` passes `sandbox_sub_agents=False` explicitly, and production never injects an interpreter (which would bypass it); `test_program_offers_no_sub_agents` holds the behaviour.
|
|
34
|
-
- **GitHub ingest, monitoring, and SIEM emission are host-side plumbing, never planner tools.** `ingest.py` shells out to `gh` host-side (transport injectable); `emit.emit_signal` POSTs and never raises; `watch`, `actors` and `snapshot` poll, remember, and archive outside the trajectory,
|
|
34
|
+
- **GitHub ingest, monitoring, and SIEM emission are host-side plumbing, never planner tools.** `ingest.py` shells out to `gh` host-side (transport injectable); `emit.emit_signal` POSTs and never raises; `watch`, `actors` and `snapshot` poll, remember, and archive outside the trajectory, `releases` reads PyPI's own hosts only, as data that is never installed, and `images` reads the GHCR and Docker Hub APIs only (manifests and provenance, never a layer). Only choices the policy makes are tools; delivering a finished result is plumbing. What the store learns reaches the signal only as provenance facts through `scan_provenance`, never as a second derivation (see `.claude/rules/monitoring.md`).
|
|
35
35
|
- **This is a ROLLOUT source: trajectories, not reward.** `rl_export` passes `reward=None`; labels read the assembled verdict and metrics are objective effort counters. `deep_classify` tool_calls train the classifier; every other action is the planner's. A metric keeps ONE meaning across rlm-harness versions (the corpus mixes them; `run_start.payload.rlm_harness` names the writer): a quantity a newer kit makes observable gets a NEW key that is None where the trace cannot say (e.g. `analyst_failures`). The ATLAS rubric and the eval member stay reward-free too; see `.claude/rules/rubric-and-eval.md`.
|
|
36
36
|
- **Attack knowledge lives in `diff_sentry/skills/`, not the prompt.** The skill catalog is injected (`load_skills_as_tools(discovery="inject")`) and `read_skill(name)` pulls a body on demand. Fix a wrong convention by editing or adding a skill; `detect.INSTRUCTIONS` holds only identity, the MISSION frame, the tool cost model, the triage loop, and output-gating rules. Skills ship via `packages = ["diff_sentry"]`; a force-include duplicates them and breaks the build.
|
|
37
|
-
- **Keep the dspy-free modules dspy-free.** `config`, `schema`, `normalize`, `indicators`, `shelllex`, `linewide`, `assemble`, `response`, `emit`, `ingest`, `hosts`, `dbutil`, `actors`, `snapshot`, `watch`, `releases`, `rl_export`, and `rubric` must not import dspy at module top, and `import diff_sentry` must not import dspy (`ClassifyChange` / `setup` / `run` / `screen` / `detect_from_event` are lazy PEP 562 re-exports).
|
|
37
|
+
- **Keep the dspy-free modules dspy-free.** `config`, `schema`, `normalize`, `indicators`, `shelllex`, `linewide`, `assemble`, `response`, `emit`, `ingest`, `hosts`, `dbutil`, `actors`, `snapshot`, `watch`, `releases`, `images`, `rl_export`, and `rubric` must not import dspy at module top, and `import diff_sentry` must not import dspy (`ClassifyChange` / `setup` / `run` / `screen` / `detect_from_event` are lazy PEP 562 re-exports).
|
|
38
38
|
- **The trace self-describes the run.** `run_start` meta carries the normalized event, instructions, source echo, `baseline_indicators`, `emit_on`, role→model names, budgets, and the rubric skeleton, so an offline re-render/export re-derives the same `signal` (reading `emit_on` from meta, never current config). Any new per-run config that affects read-time derivation must ride in meta.
|
|
39
39
|
|
|
40
40
|
## Topic guides
|
|
@@ -54,5 +54,5 @@ Claude Code loads each rule automatically when you touch a matching file; other
|
|
|
54
54
|
|
|
55
55
|
- rlm-harness owns the generic cores: `make_model_tool` (chat → retry → validate → circuit-break) under `deep_classify`, and the SSRF primitives (`is_safe_url`, `resolved_host_is_safe`, `parse_cidrs`) under `fetch_tool`, which adds the GitHub allowlist, the httpx provider, and tracing.
|
|
56
56
|
- When this consumer needs a workaround, fix the reusable gap in rlm-harness generically; never special-case diff-sentry there. Consumer values (`DS_*` roles, the verdict schema, indicator rules, the SIEM payload shape) stay here.
|
|
57
|
-
- The deterministic tier ships as `cli.screen`, `watch` and `release-watch` (host-side, in front of `cli.run`). Next increments: comparing a release's sdist with its git tag, and npm releases beside PyPI's; an optional small single-shot model call inside that tier; a GH Archive backfill for account history older than the Events API's 30 days; per-repository workflow awareness (which repos run `issue_comment` or `pull_request_target` workflows) to weight the slash-command rule.
|
|
57
|
+
- The deterministic tier ships as `cli.screen`, `watch` and `release-watch` (host-side, in front of `cli.run`). Next increments: comparing a release's sdist with its git tag, an image's new layers by content, and npm releases beside PyPI's; an optional small single-shot model call inside that tier; a GH Archive backfill for account history older than the Events API's 30 days; per-repository workflow awareness (which repos run `issue_comment` or `pull_request_target` workflows) to weight the slash-command rule.
|
|
58
58
|
- Two workspace members, never in the `diff_sentry` wheel: `studio/` (`diff-sentry-studio`, the detection console, behind its `live` extra) and `eval/` (`diff-sentry-eval`, the reward-free scorecard). Both read the trace / `DetectionResponse` contract through the public surface only. Keep the root wheel at `packages = ["diff_sentry"]` so `uv build` never sweeps them in.
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to diff-sentry, a BewAIre-style detector that classifies one GitHub change (PR, issue or push) for malicious intent on [`rlm-harness`](https://github.com/qazbnm456/rlm-harness).
|
|
4
4
|
|
|
5
|
+
## 0.11.0
|
|
6
|
+
|
|
7
|
+
Upgrade from 0.10.0: `release-watch` can now watch container images. The GitHub Action's scan is unchanged, nothing needs configuring, and `rlm-harness==1.22.0` is unchanged.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`release-watch --images` compares GHCR and Docker Hub images by their build provenance.** litellm's 1.107.0 images, and the `latest` and `main-stable` tags moved onto them, carried the same payload as the wheels. Each new tag, each move of a watched floating tag, and each re-push of a recent release is judged against the repositories and workflows the image is known to be built from and by. A source or building repository whose owner imitates a known one is `critical`. An unknown owner, a build from an unprotected ref by a workflow that used protected ones, or an image without the provenance the repository's images carry is `high`. Another workflow of a known owner, such as a nightly build, is `medium`. Images are read anonymously and never pulled; the provenance is unsigned, so only a mismatch is evidence, and a layer's content is not read. See [Container images](docs/release-watch.md#container-images).
|
|
11
|
+
|
|
5
12
|
## 0.10.0
|
|
6
13
|
|
|
7
14
|
Upgrade from 0.9.0: the GitHub Action now flags a GitHub owner that imitates your repository's own, so a workflow change it passed before can fail it now, and `release-watch` compares who published each release. Nothing needs configuring: the Action reads the repository from `GITHUB_REPOSITORY`. `rlm-harness==1.22.0` is unchanged.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: diff-sentry
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Catch malicious pull requests before they merge: scan a diff for supply-chain attack patterns with deterministic evidence a model cannot suppress.
|
|
5
5
|
Author: Boik Su
|
|
6
6
|
License-Expression: MIT
|
|
@@ -43,7 +43,7 @@ jobs:
|
|
|
43
43
|
- uses: actions/checkout@v7
|
|
44
44
|
with:
|
|
45
45
|
fetch-depth: 0 # the scan diffs against the PR base
|
|
46
|
-
- uses: qazbnm456/diff-sentry@v0.
|
|
46
|
+
- uses: qazbnm456/diff-sentry@v0.11.0
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
That is the whole setup. The job fails when an indicator at or above `high` fires. Add `push` to `on:` to also scan commits pushed straight to a branch. A push that bypasses branch protection can also delete this workflow, so `watch` and `release-watch` read the repository and its releases from outside.
|
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
- uses: actions/checkout@v7
|
|
22
22
|
with:
|
|
23
23
|
fetch-depth: 0 # the scan diffs against the PR base
|
|
24
|
-
- uses: qazbnm456/diff-sentry@v0.
|
|
24
|
+
- uses: qazbnm456/diff-sentry@v0.11.0
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
That is the whole setup. The job fails when an indicator at or above `high` fires. Add `push` to `on:` to also scan commits pushed straight to a branch. A push that bypasses branch protection can also delete this workflow, so `watch` and `release-watch` read the repository and its releases from outside.
|
|
@@ -40,7 +40,7 @@ inputs:
|
|
|
40
40
|
that PR rather than by the ones already on main.
|
|
41
41
|
required: false
|
|
42
42
|
# KEEP IN STEP WITH [project].version. release.yml fails the build if this drifts from the tag being released, because a stale default here would silently run an old scanner under a new action tag.
|
|
43
|
-
default: '0.
|
|
43
|
+
default: '0.11.0'
|
|
44
44
|
|
|
45
45
|
outputs:
|
|
46
46
|
failed:
|
|
@@ -14,6 +14,7 @@ import json
|
|
|
14
14
|
import math
|
|
15
15
|
import os
|
|
16
16
|
import sys
|
|
17
|
+
import time
|
|
17
18
|
from collections.abc import Callable
|
|
18
19
|
from dataclasses import dataclass
|
|
19
20
|
|
|
@@ -77,6 +78,7 @@ async def detect_from_event(
|
|
|
77
78
|
|
|
78
79
|
def _baseline_indicators(event: dict, *, content_hits: list[IndicatorHit] | None = None) -> list[dict]:
|
|
79
80
|
"""The host-side deterministic evidence recorded in run_start meta BEFORE the planner runs: the text detectors over the raw content ∪ the provenance detectors over the ingest facts. Both re-source into the evidence union on read (MF3), so a false-benign SUBMIT can skew the verdict but never suppress these."""
|
|
81
|
+
from .images import scan_image
|
|
80
82
|
from .indicators import keep_worse, scan_change, scan_provenance
|
|
81
83
|
from .releases import release_scope, scan_release, scan_release_change
|
|
82
84
|
|
|
@@ -84,7 +86,8 @@ def _baseline_indicators(event: dict, *, content_hits: list[IndicatorHit] | None
|
|
|
84
86
|
release = event.get("kind") == "release"
|
|
85
87
|
scan = scan_release_change if release else scan_change
|
|
86
88
|
hits = (scan(event) if content_hits is None else release_scope(list(content_hits)) if release else list(content_hits)) \
|
|
87
|
-
+ scan_provenance(event.get("provenance") or {}) + scan_release(event.get("release") or {})
|
|
89
|
+
+ scan_provenance(event.get("provenance") or {}) + scan_release(event.get("release") or {}) \
|
|
90
|
+
+ scan_image((event.get("release") or {}).get("image") or {})
|
|
88
91
|
# Hits an EARLIER version of this same change raised ride along, so an edit that removes the payload cannot also remove the evidence (the watcher supplies them; a change scanned on its own has none).
|
|
89
92
|
for raw in event.get("prior_indicators") or []:
|
|
90
93
|
if isinstance(raw, IndicatorHit):
|
|
@@ -333,6 +336,7 @@ def _cmd_release_watch(args) -> int:
|
|
|
333
336
|
import logging
|
|
334
337
|
import signal
|
|
335
338
|
|
|
339
|
+
from .images import ImageWatcher
|
|
336
340
|
from .releases import ReleaseWatcher, parse_packages
|
|
337
341
|
|
|
338
342
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
@@ -347,7 +351,15 @@ def _cmd_release_watch(args) -> int:
|
|
|
347
351
|
packages = parse_packages(texts)
|
|
348
352
|
if args.check:
|
|
349
353
|
packages = parse_packages([args.check[0]])
|
|
350
|
-
|
|
354
|
+
images = [i for text in [*args.images, os.getenv("DS_RELEASE_IMAGES", "")] for i in text.replace(",", " ").split()]
|
|
355
|
+
floating = tuple(args.image_tags or os.getenv("DS_RELEASE_IMAGE_TAGS", "latest").replace(",", " ").split())
|
|
356
|
+
image_watcher = ImageWatcher([] if args.check else images, state_dir=args.state, out_dir=args.out,
|
|
357
|
+
floating=floating, emit=not args.no_emit)
|
|
358
|
+
try:
|
|
359
|
+
watcher = ReleaseWatcher(packages, state_dir=args.state, out_dir=args.out, emit=not args.no_emit)
|
|
360
|
+
except BaseException:
|
|
361
|
+
image_watcher.close()
|
|
362
|
+
raise
|
|
351
363
|
except (OSError, ValueError, RuntimeError) as exc:
|
|
352
364
|
print(f"release-watch: {exc}", file=sys.stderr)
|
|
353
365
|
return 2
|
|
@@ -356,22 +368,29 @@ def _cmd_release_watch(args) -> int:
|
|
|
356
368
|
result = watcher.check(packages[0], args.check[1])
|
|
357
369
|
print(json.dumps(result, indent=2, default=str))
|
|
358
370
|
return 1 if "error" in result else 0
|
|
359
|
-
if not packages:
|
|
360
|
-
print("release-watch:
|
|
371
|
+
if not packages and not images:
|
|
372
|
+
print("release-watch: nothing to watch (--packages, --packages-file, --images, or DS_RELEASE_PACKAGES / "
|
|
373
|
+
"DS_RELEASE_IMAGES)", file=sys.stderr)
|
|
361
374
|
return 2
|
|
362
375
|
if args.once:
|
|
363
|
-
for result in watcher.poll_once():
|
|
376
|
+
for result in watcher.poll_once() + image_watcher.poll_once():
|
|
364
377
|
print(json.dumps(result, default=str))
|
|
365
378
|
return 0
|
|
366
379
|
stopping = []
|
|
367
380
|
signal.signal(signal.SIGTERM, lambda *_: stopping.append(True))
|
|
368
381
|
try:
|
|
369
|
-
|
|
382
|
+
while not stopping:
|
|
383
|
+
started = time.monotonic()
|
|
384
|
+
watcher.poll_once()
|
|
385
|
+
image_watcher.poll_once()
|
|
386
|
+
while not stopping and time.monotonic() - started < args.interval:
|
|
387
|
+
time.sleep(min(1.0, args.interval))
|
|
370
388
|
except KeyboardInterrupt:
|
|
371
389
|
pass
|
|
372
390
|
return 0
|
|
373
391
|
finally:
|
|
374
392
|
watcher.close()
|
|
393
|
+
image_watcher.close()
|
|
375
394
|
|
|
376
395
|
|
|
377
396
|
def _cmd_render(args) -> int:
|
|
@@ -636,6 +655,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
636
655
|
help="compare each new release of watched PyPI packages with the one before it (no model)")
|
|
637
656
|
rw.add_argument("--packages", nargs="+", default=[], help="PyPI project names (or set DS_RELEASE_PACKAGES)")
|
|
638
657
|
rw.add_argument("--packages-file", help="a file of project names, one per line (# comments allowed)")
|
|
658
|
+
rw.add_argument("--images", nargs="+", default=[],
|
|
659
|
+
help="container images (ghcr.io/owner/name, docker.io/owner/name) whose tags are compared by provenance "
|
|
660
|
+
"(or set DS_RELEASE_IMAGES)")
|
|
661
|
+
rw.add_argument("--image-tags", nargs="+", default=None,
|
|
662
|
+
help="floating tags whose moves are judged (DS_RELEASE_IMAGE_TAGS, default: latest)")
|
|
639
663
|
rw.add_argument("--out", default="./output")
|
|
640
664
|
rw.add_argument("--state", default=os.getenv("DS_WATCH_STATE_DIR", "./output/state"),
|
|
641
665
|
help="state directory: releases.db, snapshots/")
|