promptgraph-mcp 2.4.5 → 2.4.7
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.
- package/github-import.js +28 -0
- package/index.js +47 -14
- package/indexer.js +14 -7
- package/package.json +1 -1
- package/parser.js +21 -115
- package/registry/training/bad/accepts-HISTORY.md +250 -0
- package/registry/training/bad/argparse-CHANGELOG.md +185 -0
- package/registry/training/bad/balanced-match-LICENSE.md +23 -0
- package/registry/training/bad/better-sqlite3-README.md +99 -0
- package/registry/training/bad/bindings-LICENSE.md +22 -0
- package/registry/training/bad/bl-LICENSE.md +13 -0
- package/registry/training/bad/body-parser-README.md +494 -0
- package/registry/training/bad/bytes-HISTORY.md +97 -0
- package/registry/training/bad/camelcase-README.md +135 -0
- package/registry/training/bad/chai-README.md +162 -0
- package/registry/training/bad/cli-progress-LICENSE.md +24 -0
- package/registry/training/bad/content-type-HISTORY.md +29 -0
- package/registry/training/bad/cookie-SECURITY.md +25 -0
- package/registry/training/bad/cookie-signature-HISTORY.md +70 -0
- package/registry/training/bad/cors-README.md +277 -0
- package/registry/training/bad/cross-spawn-README.md +89 -0
- package/registry/training/bad/deep-extend-CHANGELOG.md +46 -0
- package/registry/training/bad/depd-HISTORY.md +103 -0
- package/registry/training/bad/esprima-README.md +46 -0
- package/registry/training/bad/etag-HISTORY.md +83 -0
- package/registry/training/bad/expect-type-SECURITY.md +14 -0
- package/registry/training/bad/finalhandler-HISTORY.md +239 -0
- package/registry/training/bad/fresh-HISTORY.md +80 -0
- package/registry/training/bad/glob-LICENSE.md +63 -0
- package/registry/training/bad/hono-README.md +85 -0
- package/registry/training/bad/http-errors-HISTORY.md +186 -0
- package/registry/training/bad/jose-LICENSE.md +21 -0
- package/registry/training/bad/jose-README.md +153 -0
- package/registry/training/bad/js-yaml-README.md +299 -0
- package/registry/training/bad/json-schema-typed-LICENSE.md +57 -0
- package/registry/training/bad/json-stringify-safe-CHANGELOG.md +14 -0
- package/registry/training/bad/lru-cache-LICENSE.md +55 -0
- package/registry/training/bad/media-typer-HISTORY.md +50 -0
- package/registry/training/bad/minimatch-LICENSE.md +55 -0
- package/registry/training/bad/minimist-CHANGELOG.md +298 -0
- package/registry/training/bad/minimist-README.md +121 -0
- package/registry/training/bad/ms-LICENSE.md +21 -0
- package/registry/training/bad/negotiator-HISTORY.md +114 -0
- package/registry/training/bad/on-finished-HISTORY.md +98 -0
- package/registry/training/bad/pkce-challenge-CHANGELOG.md +114 -0
- package/registry/training/bad/postcss-README.md +28 -0
- package/registry/training/bad/prebuild-install-CHANGELOG.md +131 -0
- package/registry/training/bad/proxy-addr-HISTORY.md +161 -0
- package/registry/training/bad/qs-CHANGELOG.md +822 -0
- package/registry/training/bad/rc-README.md +227 -0
- package/registry/training/bad/readable-stream-CONTRIBUTING.md +38 -0
- package/registry/training/bad/router-HISTORY.md +228 -0
- package/registry/training/bad/semver-README.md +680 -0
- package/registry/training/bad/send-README.md +317 -0
- package/registry/training/bad/serve-static-README.md +253 -0
- package/registry/training/bad/statuses-HISTORY.md +87 -0
- package/registry/training/bad/type-is-HISTORY.md +292 -0
- package/registry/training/bad/vary-HISTORY.md +39 -0
- package/registry/training/bad/vite-LICENSE.md +2230 -0
- package/registry/training/bad/which-CHANGELOG.md +166 -0
- package/registry/training/bad/zod-README.md +191 -0
- package/registry/training/good/skills-store-autopilot.md +85 -0
- package/registry/training/good/skills-store-bot-builder.md +70 -0
- package/registry/training/good/skills-store-chain.md +136 -0
- package/registry/training/good/skills-store-evolve.md +100 -0
- package/registry/training/good/skills-store-game.md +27 -0
- package/registry/training/good/skills-store-hunt.md +102 -0
- package/registry/training/good/skills-store-intel.md +56 -0
- package/registry/training/good/skills-store-memory-gc.md +58 -0
- package/registry/training/good/skills-store-pcsort.md +207 -0
- package/registry/training/good/skills-store-pickup.md +60 -0
- package/registry/training/good/skills-store-recon.md +141 -0
- package/registry/training/good/skills-store-remember.md +64 -0
- package/registry/training/good/skills-store-report.md +117 -0
- package/registry/training/good/skills-store-router.md +225 -0
- package/registry/training/good/skills-store-search.md +168 -0
- package/registry/training/good/skills-store-surface.md +53 -0
- package/registry/training/good/skills-store-token-scan.md +141 -0
- package/registry/training/good/skills-store-triage.md +97 -0
- package/registry/training/good/skills-store-unity.md +733 -0
- package/registry/training/good/skills-store-validate.md +135 -0
- package/registry/training/good/skills-store-web3-audit.md +217 -0
- package/src/filter/classifier.js +88 -0
- package/src/filter/cluster.js +52 -0
- package/src/filter/dedup.js +36 -0
- package/src/filter/hard-filter.js +57 -0
- package/src/filter/train.js +64 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: validate
|
|
3
|
+
description: Validate a finding — runs 7-Question Gate + 4-gate checklist. Kills weak findings before report writing. Prevents N/A submissions that hurt validity ratio. Usage: /validate
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /validate
|
|
7
|
+
|
|
8
|
+
Run full validation on the current finding before writing a report.
|
|
9
|
+
|
|
10
|
+
## What This Does
|
|
11
|
+
|
|
12
|
+
1. Runs 7-Question Gate (one wrong answer = kill it)
|
|
13
|
+
2. Checks against the always-rejected list
|
|
14
|
+
3. Runs 4 pre-submission gates
|
|
15
|
+
4. Outputs: PASS (write the report) or KILL (move on)
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
/validate
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Describe the finding when prompted. Include:
|
|
24
|
+
- The endpoint
|
|
25
|
+
- The bug class
|
|
26
|
+
- What the PoC shows
|
|
27
|
+
- The target program
|
|
28
|
+
|
|
29
|
+
## The 7-Question Gate
|
|
30
|
+
|
|
31
|
+
Answer each. ONE wrong answer = STOP.
|
|
32
|
+
|
|
33
|
+
### Q1: Can I demonstrate this step-by-step RIGHT NOW?
|
|
34
|
+
|
|
35
|
+
Write this out:
|
|
36
|
+
```
|
|
37
|
+
1. Setup: I need [own account / another user's ID / no account]
|
|
38
|
+
2. Request: [exact HTTP method, URL, headers, body]
|
|
39
|
+
3. Result: Response shows [exact data / action completed]
|
|
40
|
+
4. Impact: Real consequence is [account takeover / PII exposed / money stolen]
|
|
41
|
+
5. Cost: Time: [X min], Capital: [$0 / $X]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If step 2 is "I need to look at the code more" → KILL IT.
|
|
45
|
+
|
|
46
|
+
### Q2: Is the impact accepted by this program?
|
|
47
|
+
|
|
48
|
+
Check program scope page. Is your bug class listed? Is it excluded?
|
|
49
|
+
|
|
50
|
+
### Q3: Is the vulnerable asset in scope?
|
|
51
|
+
|
|
52
|
+
Exact domain in scope? Not staging/dev? Not a third-party service?
|
|
53
|
+
|
|
54
|
+
### Q4: Does it need admin or privileged access that an attacker can't get?
|
|
55
|
+
|
|
56
|
+
"Admin can do X" → KILL IT.
|
|
57
|
+
"Regular user can do X that only admin should" → valid.
|
|
58
|
+
|
|
59
|
+
### Q5: Is this known or documented behavior?
|
|
60
|
+
|
|
61
|
+
Search disclosed reports + changelog + API docs.
|
|
62
|
+
|
|
63
|
+
### Q6: Can you prove impact beyond "technically possible"?
|
|
64
|
+
|
|
65
|
+
- XSS → actual cookie value in exfil request, not just alert()
|
|
66
|
+
- SSRF → response body from internal service, not just DNS callback
|
|
67
|
+
- IDOR → actual other-user's private data in response, not just 200 status
|
|
68
|
+
|
|
69
|
+
### Q7: Is this on the never-submit list?
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
Missing headers, GraphQL introspection alone, clickjacking without PoC,
|
|
73
|
+
self-XSS, open redirect alone, SSRF DNS-only, logout CSRF, banner disclosure,
|
|
74
|
+
rate limit on non-critical forms, missing cookie flags alone...
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If yes → KILL IT unless you have a chain.
|
|
78
|
+
|
|
79
|
+
## Check: Conditionally Valid?
|
|
80
|
+
|
|
81
|
+
If it's on the never-submit list, can you chain it?
|
|
82
|
+
|
|
83
|
+
| You Have | Chain Available? |
|
|
84
|
+
|---|---|
|
|
85
|
+
| Open redirect | + OAuth code theft → ATO? |
|
|
86
|
+
| SSRF DNS-only | + internal service data? |
|
|
87
|
+
| Clickjacking | + sensitive action + PoC? |
|
|
88
|
+
| CORS wildcard | + credentialed data exfil? |
|
|
89
|
+
| Prompt injection | + IDOR → other user's data? |
|
|
90
|
+
|
|
91
|
+
If no chain → KILL IT. If chain confirmed → report both together.
|
|
92
|
+
|
|
93
|
+
## 4 Gates — All Must Pass
|
|
94
|
+
|
|
95
|
+
**Gate 0 (30 sec):**
|
|
96
|
+
```
|
|
97
|
+
[ ] Confirmed with real HTTP requests (not just code reading)
|
|
98
|
+
[ ] In scope (checked program page)
|
|
99
|
+
[ ] Reproducible from scratch
|
|
100
|
+
[ ] Evidence captured
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Gate 1 — Impact (2 min):**
|
|
104
|
+
```
|
|
105
|
+
[ ] Can answer "What does attacker walk away with?"
|
|
106
|
+
[ ] More than "sees non-sensitive data"
|
|
107
|
+
[ ] Real victim exists
|
|
108
|
+
[ ] No unlikely preconditions
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Gate 2 — Dedup (5 min):**
|
|
112
|
+
```
|
|
113
|
+
[ ] Searched HackerOne Hacktivity for endpoint + bug class
|
|
114
|
+
[ ] Searched GitHub issues
|
|
115
|
+
[ ] Read 5 most recent disclosed reports
|
|
116
|
+
[ ] Not in changelog as known issue
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Gate 3 — Report quality (10 min):**
|
|
120
|
+
```
|
|
121
|
+
[ ] Title formula: [Class] in [Endpoint] allows [actor] to [impact]
|
|
122
|
+
[ ] Steps have exact HTTP request
|
|
123
|
+
[ ] Evidence shows actual impact
|
|
124
|
+
[ ] CVSS calculated
|
|
125
|
+
[ ] Fix: 1-2 concrete sentences
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Output
|
|
129
|
+
|
|
130
|
+
**PASS:** "All 7 questions pass. All 4 gates pass. Proceed to /report."
|
|
131
|
+
|
|
132
|
+
**KILL:** "Q[N] fails because [reason]. Kill this finding. Reason: [explanation]. Move on."
|
|
133
|
+
|
|
134
|
+
**DOWNGRADE:** "Q6 only shows technical possibility. Downgrade from High to Medium. Requires showing actual data exfil in PoC."
|
|
135
|
+
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web3-audit
|
|
3
|
+
description: Smart contract security audit — runs through 10 bug class checklist (accounting desync, access control, incomplete path, off-by-one, oracle errors, ERC4626, reentrancy, flash loan, signature replay, proxy/upgrade). Applies pre-dive kill signals first. Generates Foundry PoC template for confirmed findings. Usage: /web3-audit <contract.sol>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /web3-audit
|
|
7
|
+
|
|
8
|
+
Smart contract security audit using the 10-bug-class methodology.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
/web3-audit VulnerableContract.sol
|
|
14
|
+
/web3-audit https://github.com/protocol/contracts
|
|
15
|
+
/web3-audit [paste contract code]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Step 0: Pre-Dive Kill Signals
|
|
19
|
+
|
|
20
|
+
ALWAYS check these BEFORE reading any code:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
1. TVL < $500K → max payout too low for effort → SKIP
|
|
24
|
+
2. 2+ top-tier audits (Halborn, ToB, Cyfrin, OZ) on simple protocol → SKIP
|
|
25
|
+
3. Protocol < 500 lines, single A→B→C flow → minimal attack surface → SKIP
|
|
26
|
+
4. max_payout = min(10% × TVL, program_cap) → if < $10K → SKIP
|
|
27
|
+
|
|
28
|
+
Formula: Is [TVL * 10%] > [hours I'll spend * hourly rate]? If not, skip.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Only proceed if score >= 6/10:
|
|
32
|
+
- TVL > $10M: +2
|
|
33
|
+
- Immunefi Critical >= $50K: +2
|
|
34
|
+
- No top-tier audit on current version: +2
|
|
35
|
+
- < 30 days since deploy: +1
|
|
36
|
+
- Protocol you've hunted before: +1
|
|
37
|
+
- Upgradeable proxies present: +1
|
|
38
|
+
|
|
39
|
+
## Step 1: Accounting State Desynchronization (28% of Criticals)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Find accounting variables
|
|
43
|
+
grep -rn "totalSupply\|totalShares\|totalAssets\|totalDebt\|cumulativeReward" contracts/
|
|
44
|
+
|
|
45
|
+
# Find ALL early returns in critical functions
|
|
46
|
+
grep -rn "\breturn\b" contracts/ -B3 | grep -B3 "if\b"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Check: For each early return in claim/redeem/withdraw functions:
|
|
50
|
+
- Which state variables are updated in the normal path?
|
|
51
|
+
- Are ALL of them also updated in the early return path?
|
|
52
|
+
- If A updated but B isn't → potential desync bug
|
|
53
|
+
|
|
54
|
+
## Step 2: Access Control (19% of Criticals)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Sibling function families — do ALL have same modifier set?
|
|
58
|
+
grep -rn "function vote\|function poke\|function reset\|function update\|function claim\|function harvest" contracts/ -A2
|
|
59
|
+
|
|
60
|
+
# Ownership check: existence vs ownership
|
|
61
|
+
grep -rn "_requireOwned\|ownerOf\|_isApprovedOrOwner" contracts/ -B5
|
|
62
|
+
|
|
63
|
+
# Silent modifiers (if without revert)
|
|
64
|
+
grep -rn "modifier\b" contracts/ -A8 | grep -B3 "if (" | grep -v "require\|revert"
|
|
65
|
+
|
|
66
|
+
# Uninitialized proxy
|
|
67
|
+
grep -rn "function initialize\b" contracts/ -A3
|
|
68
|
+
grep -rn "_disableInitializers()" contracts/
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Check: Does EVERY sibling function in a family have the SAME modifiers?
|
|
72
|
+
|
|
73
|
+
## Step 3: Incomplete Code Path (17% of Criticals)
|
|
74
|
+
|
|
75
|
+
The function family comparison test:
|
|
76
|
+
```
|
|
77
|
+
1. List all state changes in function A (deposit/place/create)
|
|
78
|
+
2. List all state changes in function B (withdraw/update/cancel)
|
|
79
|
+
3. For each state change in A: does B have the corresponding reverse?
|
|
80
|
+
4. For each token transfer in A: does B have the corresponding refund?
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
grep -rn "safeApprove\b" contracts/ # safeApprove without zero-reset?
|
|
85
|
+
grep -rn "delete\b" contracts/ -B5 # delete before operation completes?
|
|
86
|
+
grep -rn "function deposit\|function mint\|function withdraw\|function redeem" contracts/ -A10
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Step 4: Off-By-One (22% of Highs)
|
|
90
|
+
|
|
91
|
+
Mental test: For EVERY `if (A > B)`: "What happens when A == B?" Is that correct?
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Boundary comparisons
|
|
95
|
+
grep -rn "Period\|Epoch\|Deadline\|period\|epoch\|deadline" contracts/ -A3 | grep "[<>][^=]"
|
|
96
|
+
|
|
97
|
+
# Loop breaks
|
|
98
|
+
grep -rn "\bbreak\b" contracts/ -B10
|
|
99
|
+
|
|
100
|
+
# Array bounds
|
|
101
|
+
grep -rn "\.length\s*-\s*1\|i\s*<=\s*.*\.length\b" contracts/
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Step 5: Oracle / Price Manipulation
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Missing staleness check
|
|
108
|
+
grep -rn "latestRoundData" contracts/ -A5 | grep -v "updatedAt\|timestamp"
|
|
109
|
+
|
|
110
|
+
# Pyth confidence interval
|
|
111
|
+
grep -rn "getPriceUnsafe\|getPrice\b" contracts/ -A8 | grep -v "conf\|confidence"
|
|
112
|
+
|
|
113
|
+
# TWAP windows
|
|
114
|
+
grep -rn "secondsAgo\|TWAP\|cardinality" contracts/ -A5
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Check:
|
|
118
|
+
- Is staleness checked? (`require(block.timestamp - updatedAt <= MAX_AGE)`)
|
|
119
|
+
- Is Pyth confidence interval checked? (`require(conf * 10 <= price)`)
|
|
120
|
+
- Is TWAP window > 1800 seconds (30 min)?
|
|
121
|
+
|
|
122
|
+
## Step 6: ERC4626 Vaults
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
grep -rn "function deposit\|function mint\|function withdraw\|function redeem" contracts/ -A10
|
|
126
|
+
grep -rn "function transfer\|function transferFrom" contracts/ -A15
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Check:
|
|
130
|
+
- Does `mint()` call the same validation as `deposit()`?
|
|
131
|
+
- Does `transfer()` move lock records along with shares?
|
|
132
|
+
- Is there a `_decimalsOffset()` virtual shares defense against first-depositor attack?
|
|
133
|
+
|
|
134
|
+
## Step 7: Reentrancy
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Effects after interactions
|
|
138
|
+
grep -rn "\.call{value\|safeTransfer\|transfer(" contracts/ -B10 | grep -v "require\|revert"
|
|
139
|
+
|
|
140
|
+
# Missing nonReentrant
|
|
141
|
+
grep -rn "function withdraw\|function redeem\|function claim" contracts/ -A2 | grep -v "nonReentrant"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Check: Does every function that transfers ETH or ERC20 follow CEI order?
|
|
145
|
+
(Checks → Effects → Interactions)
|
|
146
|
+
|
|
147
|
+
## Step 8: Flash Loan Oracle Manipulation
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
grep -rn "getReserves\|getAmountsOut\|slot0\b" contracts/ -A5
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Check: Any spot price reading from Uniswap reserves/slot0? → flash loan manipulatable.
|
|
154
|
+
|
|
155
|
+
## Step 9: Signature Replay
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
grep -rn "ecrecover\|ECDSA\.recover" contracts/ -B20
|
|
159
|
+
grep -rn "nonce\|_nonces\|nonces\[" contracts/
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Check: Does signed hash include: nonce + chainId + contract address?
|
|
163
|
+
|
|
164
|
+
## Step 10: Proxy / Upgrade
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
grep -rn "function initialize\b\|_disableInitializers\|initializer" contracts/
|
|
168
|
+
grep -rn "delegatecall\b" contracts/ -B3
|
|
169
|
+
grep -rn "0x360894\|_IMPLEMENTATION_SLOT\|EIP1967" contracts/
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Check:
|
|
173
|
+
- `_disableInitializers()` in implementation constructor?
|
|
174
|
+
- Implementation can't be initialized directly by attacker?
|
|
175
|
+
- Storage layout compatible between versions?
|
|
176
|
+
|
|
177
|
+
## Confirming a Finding
|
|
178
|
+
|
|
179
|
+
Apply the 7-Question Gate:
|
|
180
|
+
```
|
|
181
|
+
1. Can I demonstrate this with a Foundry test?
|
|
182
|
+
2. What is the financial impact (quantify in $)?
|
|
183
|
+
3. Is this in the Immunefi scope?
|
|
184
|
+
4. Is it a known issue or acknowledged behavior?
|
|
185
|
+
5. Does my Foundry PoC actually run? (forge test -vvvv)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Foundry PoC Template
|
|
189
|
+
|
|
190
|
+
```solidity
|
|
191
|
+
// SPDX-License-Identifier: MIT
|
|
192
|
+
pragma solidity ^0.8.0;
|
|
193
|
+
import "forge-std/Test.sol";
|
|
194
|
+
import "../src/VulnerableContract.sol";
|
|
195
|
+
|
|
196
|
+
contract ExploitTest is Test {
|
|
197
|
+
VulnerableContract target;
|
|
198
|
+
address attacker = makeAddr("attacker");
|
|
199
|
+
|
|
200
|
+
function setUp() public {
|
|
201
|
+
vm.createSelectFork("mainnet", BLOCK_NUMBER);
|
|
202
|
+
target = VulnerableContract(TARGET_ADDRESS);
|
|
203
|
+
deal(address(token), attacker, INITIAL_BALANCE);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function test_exploit() public {
|
|
207
|
+
uint256 before = token.balanceOf(attacker);
|
|
208
|
+
vm.startPrank(attacker);
|
|
209
|
+
// Execute exploit
|
|
210
|
+
vm.stopPrank();
|
|
211
|
+
assertGt(token.balanceOf(attacker), before, "Exploit failed");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Run: `forge test --match-test test_exploit -vvvv`
|
|
217
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { cosineSimilarity } from '../../embedder.js';
|
|
2
|
+
|
|
3
|
+
const SKILL_THRESHOLD = 0.35;
|
|
4
|
+
const UNSURE_THRESHOLD = 0.15;
|
|
5
|
+
|
|
6
|
+
const DOC_FIRST_HEADERS = /^(overview|introduction|about|background|welcome|getting started|what is|why |table of contents|toc|foreword|preface|readme)/i;
|
|
7
|
+
const INSTRUCTION_HEADERS = /^#{1,3}\s+(steps?|usage|instructions?|how\s+to|when\s+to\s+use|workflow|process|procedure|example|examples?|commands?|output|result)/i;
|
|
8
|
+
const IMPERATIVE_HEADERS = /\b(run|use|apply|execute|check|debug|fix|create|add|remove|deploy|test|write|generate|analyze|review|refactor|optimize|configure|setup|install|scan|audit|validate|search|find|extract|parse)\b/i;
|
|
9
|
+
|
|
10
|
+
export function getFeatureVector(raw) {
|
|
11
|
+
const lines = raw.split('\n');
|
|
12
|
+
const nonEmpty = lines.filter(l => l.trim());
|
|
13
|
+
const headers = nonEmpty.filter(l => /^#{1,3}\s/.test(l));
|
|
14
|
+
const paragraphs = raw.split(/\n\n+/).filter(p => p.trim() && !p.trim().startsWith('#'));
|
|
15
|
+
const words = raw.toLowerCase().split(/\s+/).filter(w => w.length > 3);
|
|
16
|
+
|
|
17
|
+
const firstHeader = headers[0]?.replace(/^#+\s*/, '') || '';
|
|
18
|
+
const longProse = paragraphs.filter(p => p.split(' ').length > 60 && !/```/.test(p));
|
|
19
|
+
const wordRatio = words.length > 80 ? new Set(words).size / words.length : 1;
|
|
20
|
+
|
|
21
|
+
return [
|
|
22
|
+
Number(raw.length >= 150),
|
|
23
|
+
Number(headers.length >= 1),
|
|
24
|
+
Number(lines.some(l => INSTRUCTION_HEADERS.test(l))),
|
|
25
|
+
Number(headers.some(h => IMPERATIVE_HEADERS.test(h))),
|
|
26
|
+
Number(raw.includes('```') || raw.includes(' ')),
|
|
27
|
+
Number(nonEmpty.some(l => /^\d+\.\s/.test(l))),
|
|
28
|
+
Number(nonEmpty.some(l => /^[-*+]\s/.test(l))),
|
|
29
|
+
Number(headers.length >= 2),
|
|
30
|
+
Number(headers.length >= 4),
|
|
31
|
+
Number(DOC_FIRST_HEADERS.test(firstHeader)) ? 1 : 0,
|
|
32
|
+
Number(longProse.length > paragraphs.length * 0.6 && paragraphs.length > 3) ? 1 : 0,
|
|
33
|
+
Number(wordRatio >= 0.22) ? 0 : 1,
|
|
34
|
+
Number(raw.length < 150) ? 1 : 0,
|
|
35
|
+
Number(headers.length < 1) ? 1 : 0,
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Rule A override: if centroid says reject/unsure but file clearly looks like
|
|
41
|
+
* a skill by content (code + instructions + list + verbs, no SKILL in name/path),
|
|
42
|
+
* override to skill. Catches adversarial wrong-heading cases with 0 FP cost.
|
|
43
|
+
*/
|
|
44
|
+
function ruleA(rawVec, raw, filePath) {
|
|
45
|
+
const fv = getFeatureVector(raw);
|
|
46
|
+
const hasCode = fv[4];
|
|
47
|
+
const hasInstructions = fv[2];
|
|
48
|
+
const hasNumberedList = fv[5];
|
|
49
|
+
const hasVerb = fv[3];
|
|
50
|
+
const filename = filePath ? filePath.split(/[/\\]/).pop().replace(/\.md$/i, '').toUpperCase() : '';
|
|
51
|
+
const pathUpper = filePath ? filePath.toUpperCase() : '';
|
|
52
|
+
const hasSkillName = filename.includes('SKILL');
|
|
53
|
+
const hasSkillPath = /SKILL/.test(pathUpper);
|
|
54
|
+
|
|
55
|
+
if (hasCode && hasInstructions && hasNumberedList && hasVerb && !hasSkillName && !hasSkillPath) {
|
|
56
|
+
return { label: 'skill', score: 0.6, method: 'ruleA' };
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function classify(rawVec, centroids, raw, filePath) {
|
|
62
|
+
if (!centroids) {
|
|
63
|
+
return { label: 'skill', score: 1, method: 'fallback' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const goodSim = centroids.good ? cosineSimilarity(rawVec, centroids.good) : 0;
|
|
67
|
+
const badSim = centroids.bad ? cosineSimilarity(rawVec, centroids.bad) : 0;
|
|
68
|
+
|
|
69
|
+
const rawScore = goodSim - badSim;
|
|
70
|
+
const score = (rawScore + 1) / 2;
|
|
71
|
+
|
|
72
|
+
if (score >= SKILL_THRESHOLD) {
|
|
73
|
+
return { label: 'skill', score, goodSim, badSim, method: 'centroid' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Below threshold — try Rule A content override
|
|
77
|
+
if (raw && filePath) {
|
|
78
|
+
const override = ruleA(rawVec, raw, filePath);
|
|
79
|
+
if (override) return override;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (score >= UNSURE_THRESHOLD) {
|
|
83
|
+
return { label: 'unsure', score, goodSim, badSim, method: 'centroid' };
|
|
84
|
+
}
|
|
85
|
+
return { label: 'reject', score, goodSim, badSim, method: 'centroid' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { SKILL_THRESHOLD, UNSURE_THRESHOLD };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { cosineSimilarity } from '../../embedder.js';
|
|
2
|
+
|
|
3
|
+
const INFERENCE_THRESHOLD = 0.35;
|
|
4
|
+
|
|
5
|
+
export function findClusters(skills, embeddings) {
|
|
6
|
+
if (!skills.length || !embeddings) return [];
|
|
7
|
+
const clusters = [];
|
|
8
|
+
const assigned = new Set();
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < skills.length; i++) {
|
|
11
|
+
if (assigned.has(i)) continue;
|
|
12
|
+
const cluster = { centroid: embeddings[i], members: [skills[i]], indices: [i] };
|
|
13
|
+
assigned.add(i);
|
|
14
|
+
|
|
15
|
+
for (let j = i + 1; j < skills.length; j++) {
|
|
16
|
+
if (assigned.has(j)) continue;
|
|
17
|
+
if (cosineSimilarity(embeddings[i], embeddings[j]) >= INFERENCE_THRESHOLD) {
|
|
18
|
+
cluster.members.push(skills[j]);
|
|
19
|
+
cluster.indices.push(j);
|
|
20
|
+
assigned.add(j);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
clusters.push(cluster);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return clusters;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function expandResults(annResults, db) {
|
|
31
|
+
if (!annResults || annResults.length === 0) return annResults;
|
|
32
|
+
|
|
33
|
+
const seenIds = new Set();
|
|
34
|
+
const expanded = [];
|
|
35
|
+
|
|
36
|
+
for (const r of annResults) {
|
|
37
|
+
if (!seenIds.has(r.skill_id)) {
|
|
38
|
+
seenIds.add(r.skill_id);
|
|
39
|
+
expanded.push(r);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const callees = db.prepare('SELECT to_skill FROM edges WHERE from_skill = ?').all(r.skill_id);
|
|
43
|
+
for (const c of callees) {
|
|
44
|
+
if (!seenIds.has(c.to_skill)) {
|
|
45
|
+
seenIds.add(c.to_skill);
|
|
46
|
+
expanded.push({ skill_id: c.to_skill, score: r.score * 0.85 });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return expanded.sort((a, b) => b.score - a.score);
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { cosineSimilarity } from '../../embedder.js';
|
|
2
|
+
|
|
3
|
+
const DEDUP_THRESHOLD = 0.97;
|
|
4
|
+
|
|
5
|
+
export function dedup(skills, embeddings) {
|
|
6
|
+
if (!skills.length) return { skills: [], embeddings: [] };
|
|
7
|
+
if (embeddings && embeddings.length !== skills.length) {
|
|
8
|
+
throw new Error('embeddings length must match skills length');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const keep = [true]; // first skill always kept
|
|
12
|
+
const kept = [0];
|
|
13
|
+
|
|
14
|
+
for (let i = 1; i < skills.length; i++) {
|
|
15
|
+
let dup = false;
|
|
16
|
+
if (embeddings) {
|
|
17
|
+
for (const j of kept) {
|
|
18
|
+
if (cosineSimilarity(embeddings[i], embeddings[j]) >= DEDUP_THRESHOLD) {
|
|
19
|
+
dup = true;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (dup) {
|
|
25
|
+
keep.push(false);
|
|
26
|
+
} else {
|
|
27
|
+
keep.push(true);
|
|
28
|
+
kept.push(i);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
skills: skills.filter((_, i) => keep[i]),
|
|
34
|
+
embeddings: embeddings ? embeddings.filter((_, i) => keep[i]) : undefined,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
const SKIP_FILENAMES = new Set([
|
|
4
|
+
'readme', 'changelog', 'license', 'contributing', 'code-of-conduct',
|
|
5
|
+
'security', 'authors', 'credits', 'install', 'installation', 'usage',
|
|
6
|
+
'engagements', 'contributors', 'maintainers', 'acknowledgements',
|
|
7
|
+
'faq', 'glossary', 'index', 'overview', 'summary', 'roadmap', 'todo',
|
|
8
|
+
'notes', 'template', 'example', 'sample', 'demo', 'getting-started',
|
|
9
|
+
'quickstart', 'guide', 'tutorial', 'walkthrough', 'architecture',
|
|
10
|
+
'design', 'spec', 'specification', 'requirements', 'privacy', 'terms',
|
|
11
|
+
'disclaimer', 'notice', 'copying', 'warranty', 'codeofconduct',
|
|
12
|
+
'pull_request_template', 'issue_template', 'funding',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const SKIP_FILENAME_RE = /^(_|\.)|^v?\d+[\.\-]\d+|^\d{4}[\-_]\d{2}|^readme|^license|^changelog|^contributing|^code.of.conduct|^security|^authors|^credits|^disclaimer|^notice|^copying|^warranty|^promotion|^funding/i;
|
|
16
|
+
|
|
17
|
+
const SKIP_DIRS = new Set([
|
|
18
|
+
'.github', 'docs', 'doc', 'documentation', 'examples', 'example',
|
|
19
|
+
'tests', 'test', '__tests__', 'spec', 'fixtures', 'assets', 'images',
|
|
20
|
+
'img', 'screenshots', 'media', 'static', 'public', 'dist', 'build',
|
|
21
|
+
'node_modules', 'vendor', 'third_party',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const BADGE_RE = /!\[.*\]\(https?:\/\/(img\.shields\.io|badge\.fury|travis-ci|github\.com\/[^)]+\/badge)/i;
|
|
25
|
+
const README_HEADER_RE = /^#\s*readme\b/i;
|
|
26
|
+
|
|
27
|
+
export function hardFilter(filePath, raw) {
|
|
28
|
+
const parts = filePath.replace(/\\/g, '/').split('/');
|
|
29
|
+
const filename = parts[parts.length - 1];
|
|
30
|
+
const base = filename.replace(/\.md$/i, '').toLowerCase();
|
|
31
|
+
|
|
32
|
+
if (SKIP_FILENAMES.has(base)) {
|
|
33
|
+
return { pass: false, reason: `skip filename: ${base}` };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (SKIP_FILENAME_RE.test(base)) {
|
|
37
|
+
return { pass: false, reason: `skip filename pattern: ${base}` };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const part of parts.slice(0, -1)) {
|
|
41
|
+
if (SKIP_DIRS.has(part.toLowerCase())) {
|
|
42
|
+
return { pass: false, reason: `skip dir: ${part}` };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (raw) {
|
|
47
|
+
const firstLines = raw.trimStart().slice(0, 300);
|
|
48
|
+
if (README_HEADER_RE.test(firstLines)) {
|
|
49
|
+
return { pass: false, reason: 'starts with # Readme' };
|
|
50
|
+
}
|
|
51
|
+
if (BADGE_RE.test(firstLines)) {
|
|
52
|
+
return { pass: false, reason: 'badge-only content' };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { pass: true };
|
|
57
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { globSync } from 'glob';
|
|
5
|
+
import { embedBatch } from '../../embedder.js';
|
|
6
|
+
|
|
7
|
+
const TRAINING_DIR = path.resolve(new URL('../../registry/training', import.meta.url).pathname);
|
|
8
|
+
const MODEL_PATH = path.join(os.homedir(), '.claude', '.promptgraph', 'model.json');
|
|
9
|
+
|
|
10
|
+
function readAllMd(dir) {
|
|
11
|
+
const files = globSync(`${dir}/**/*.md`);
|
|
12
|
+
return files.map(f => fs.readFileSync(f, 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function meanVector(vectors) {
|
|
16
|
+
if (!vectors.length) return null;
|
|
17
|
+
const dim = vectors[0].length;
|
|
18
|
+
const centroid = new Float32Array(dim);
|
|
19
|
+
for (const v of vectors) {
|
|
20
|
+
for (let i = 0; i < dim; i++) centroid[i] += v[i];
|
|
21
|
+
}
|
|
22
|
+
for (let i = 0; i < dim; i++) centroid[i] /= vectors.length;
|
|
23
|
+
return Array.from(centroid);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function train() {
|
|
27
|
+
const goodDir = path.join(TRAINING_DIR, 'good');
|
|
28
|
+
const badDir = path.join(TRAINING_DIR, 'bad');
|
|
29
|
+
|
|
30
|
+
const goodTexts = readAllMd(goodDir);
|
|
31
|
+
const badTexts = readAllMd(badDir);
|
|
32
|
+
|
|
33
|
+
if (goodTexts.length < 1 || badTexts.length < 1) {
|
|
34
|
+
throw new Error(`Need at least 1 good and 1 bad training example (good: ${goodTexts.length}, bad: ${badTexts.length})`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const allTexts = [...goodTexts, ...badTexts];
|
|
38
|
+
const allVecs = await embedBatch(allTexts);
|
|
39
|
+
|
|
40
|
+
const goodVecs = allVecs.slice(0, goodTexts.length);
|
|
41
|
+
const badVecs = allVecs.slice(goodTexts.length);
|
|
42
|
+
|
|
43
|
+
const model = {
|
|
44
|
+
version: 1,
|
|
45
|
+
good: meanVector(goodVecs),
|
|
46
|
+
bad: meanVector(badVecs),
|
|
47
|
+
counts: { good: goodTexts.length, bad: badTexts.length },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
fs.mkdirSync(path.dirname(MODEL_PATH), { recursive: true });
|
|
51
|
+
fs.writeFileSync(MODEL_PATH, JSON.stringify(model));
|
|
52
|
+
return model;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function loadModel() {
|
|
56
|
+
try {
|
|
57
|
+
const raw = fs.readFileSync(MODEL_PATH, 'utf8');
|
|
58
|
+
return JSON.parse(raw);
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { MODEL_PATH };
|