Scope JavaScript trailing inside matches
ober
5abcf3d7cd9af8c467d02441629f73995a82b6d7
--- a/HANDOFF_OPUS_4_8.md +++ b/HANDOFF_OPUS_4_8.md @@ -1,24 +1,24 @@ # Opus 4.8 Handoff: jerboa-semgrep Semgrep Parity -Date: 2026-05-30 05:52 MDT +Date: 2026-05-30 06:21 MDT Workspace: `/Users/user/mine/jerboa-semgrep` Sibling upstream Semgrep checkout: `/Users/user/mine/semgrep` Packaged Semgrep oracle: `/Users/user/.local/bin/semgrep` Branch: `main` -Base HEAD before this handoff-only checkpoint: -`355e728 Respect taint metavariable unification` +Base HEAD before this checkpoint: +`8dd414c Refresh Opus parity handoff` The user wants the pure Jerboa Semgrep port carried forward until it reaches -Semgrep parity. Do not treat this handoff as completion. This file is a -detailed continuation memo for Opus 4.8 and intentionally preserves the next -known failing case, the current baseline, the recent design decisions, and the -exact validation commands. +Semgrep parity. Do not treat this handoff as completion. This checkpoint closes +the upstream JavaScript taint `sanitized_by_side_effect` mismatch by bounding +the JavaScript trailing-ellipsis `pattern-inside` fallback to the lexical block +that contains the base match. ## Immediate State -The worktree was clean at `355e728` before this handoff edit. The latest code -checkpoint already committed the implementation work for -`taint_unify_mvars`; this handoff commit should modify only this file. +The worktree was clean at `8dd414c` before this checkpoint. The implementation +change is in `src/semgrep/scan.ss`; `lib/semgrep/scan.sls` and +`src/.jerbuild-hashes` were regenerated by `make test`. Current headline: @@ -27,10 +27,10 @@ Current headline: - Promoted JavaScript pattern oracle: 91 passed / 0 mismatched. - Promoted Python pattern oracle: 116 passed / 0 mismatched. - Local oracle: 42 passed / 0 failed. -- Smoke suite: 302 tests / 302 passed. +- Smoke suite: 303 tests / 303 passed. - Broad same-basename upstream `tests/rules` sweep: 437 passed / 0 mismatched / 0 Jerboa errors, with 3 packaged-Semgrep current errors. -- Upstream `tests/tainting_rules/js` sweep: 6 passed / 5 mismatched / +- Upstream `tests/tainting_rules/js` sweep: 7 passed / 4 mismatched / 0 Jerboa errors / 0 current errors. Semgrep parity is not reached. The cleanest active frontier is JavaScript taint @@ -95,9 +95,90 @@ Operational defaults: - Added smoke coverage: `scan JavaScript taint unifies propagated source metavariables`. +`8dd414c Refresh Opus parity handoff` + +- Handoff-only checkpoint after the `taint_unify_mvars` fix. +- Preserved the investigation notes for `sanitized_by_side_effect`. + +This checkpoint: + +- Adds `javascript-matching-close-brace` and `javascript-enclosing-block-end` + in `src/semgrep/scan.ss`. +- Makes `inside-after-clause-apply` require after-clause candidates to end + inside the base match's enclosing JavaScript block. +- Adds smoke coverage: + `scan JavaScript taint sanitizer after verify stays in function scope`. +- Closes upstream JS tainting-rule case `sanitized_by_side_effect`. +- JS tainting-rule sweep is now 7 passed / 4 mismatched. + +This checkpoint modifies: + +```text +HANDOFF_OPUS_4_8.md +src/semgrep/scan.ss +lib/semgrep/scan.sls +src/.jerbuild-hashes +tests/smoke.ss +``` + ## Latest Code Change Details -The latest code checkpoint fixed `metavar_eq_simple`. The upstream shape was: +### Scoped Trailing-Ellipsis `pattern-inside` + +Semgrep treats a JavaScript `pattern-inside` such as: + +```yaml +pattern-inside: | + $JWT.verify($TOKEN, ...) + ... +``` + +as a region after the verifier inside the relevant lexical block. Jerboa's +fallback for this shape previously checked only that the base match appeared +before the candidate. That let a verifier in one function act as context for a +candidate in a later function. + +The upstream `sanitized_by_side_effect` rule exposes this: + +```javascript +const jwt = require('jsonwebtoken'); + +function ok(token, key) { + jwt.verify(token, key); + jwt.decode(token, true); // sanitized +} + +const ok2 = (token, key) => { + jwt.verify(token, key); + jwt.decode(token, true); // sanitized +}; + +function bad_different_token(token, key) { + token2 = getToken(); + jwt.verify(token2, key); + jwt.decode(token, true); // must report +} +``` + +Jerboa was incorrectly allowing the verifier from `ok2` to satisfy the +sanitizer `pattern-inside` for the later `bad_different_token` sink. + +The scanner now: + +- Tracks the nearest unmatched `{` before the base finding, ignoring braces in + JavaScript strings and line/block comments. +- Finds the corresponding close brace with `javascript-matching-close-brace`. +- Falls back to EOF when the base finding is top-level. +- Requires the candidate's end offset to be within that scope before merging + metavariable bindings in `inside-after-clause-apply`. + +This keeps top-level trailing-ellipsis context such as +`$JWT = require('jsonwebtoken'); ...` file-wide, while preventing a verifier in +one function body from sanitizing a token in a later function. + +### Previous Checkpoint: Propagated Taint Unification + +The previous code checkpoint fixed `metavar_eq_simple`. The upstream shape was: ```javascript var source1 = get(A) @@ -148,10 +229,10 @@ Smoke: make test ``` -Result from the last code checkpoint: +Result: ```text -302 tests, 302 passed, 0 failed +303 tests, 303 passed, 0 failed ``` Focused upstream JS taint `metavar_eq_simple`: @@ -214,7 +295,7 @@ The 3 current-side errors are packaged-Semgrep schema failures for `anywhere_global`, `anywhere_include`, and `anywhere_metavar`; they are not Jerboa mismatches. -Fresh handoff confirmation for `sanitized_by_side_effect`: +Focused upstream JS taint `sanitized_by_side_effect`: ```sh SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \ @@ -226,13 +307,10 @@ tests/oracle/upstream-sweep.sh Result: ```text -upstream-sweep: 0 passed, 1 mismatched, 0 jerboa errors, 0 current errors, 1 compared +upstream-sweep: 1 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 1 compared ``` -The missing Jerboa finding is Semgrep's line 31 finding in -`sanitized_by_side_effect.js`. - -Fresh handoff confirmation for full JS tainting rules: +Full JS tainting-rule subdirectory: ```sh SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \ @@ -243,12 +321,12 @@ LIST_MISMATCHES=1 MAX_DIFFS=80 tests/oracle/upstream-sweep.sh Result: ```text -upstream-sweep: 6 passed, 5 mismatched, 0 jerboa errors, 0 current errors, 11 compared +upstream-sweep: 7 passed, 4 mismatched, 0 jerboa errors, 0 current errors, 11 compared ``` ## Remaining JS Taint Mismatches -The 5 remaining mismatches under `/Users/user/mine/semgrep/tests/tainting_rules/js` +The 4 remaining mismatches under `/Users/user/mine/semgrep/tests/tainting_rules/js` are: 1. `await` @@ -272,23 +350,16 @@ reports both `A` and `B` messages at both sinks. Jerboa keeps only the locally matching binding at each sink. Likely gap: conditional-expression sources need multiple alternative taint states/metavariable environments. -4. `sanitized_by_side_effect` - -Semgrep reports `jwt.decode(token, true)` at line 31 because only `token2` was -verified in that function. Jerboa misses that finding by over-sanitizing -`token`. This is the best next target because the root cause is now fairly -well isolated. - -5. `simpl_nodejs_eval` +4. `simpl_nodejs_eval` Jerboa misses the direct concat sink `s.run('lol(' + req.query.userInput + ')', cb)` and has a one-column overlong range on the template-literal sink. Likely gaps: direct string concatenation taint into a focused sink expression, plus template literal range trimming. -## Best Next Target: `sanitized_by_side_effect` +## Completed Target: `sanitized_by_side_effect` -Start with this command: +Verification command: ```sh SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \ @@ -297,7 +368,7 @@ CASE_REGEX='^(sanitized_by_side_effect)$' LIST_MISMATCHES=1 MAX_DIFFS=180 \ tests/oracle/upstream-sweep.sh ``` -Current diff: +Old diff before this checkpoint: ```diff - (finding "jwt-decode-without-verify" ".../sanitized_by_side_effect.js" 31 20 728 31 25 733 ...) @@ -352,13 +423,19 @@ function bad_different_token(token, key) { } ``` -Observed behavior: +Observed behavior before this checkpoint: - Semgrep reports lines 6 and 31. - Jerboa reports only line 6. - Therefore Jerboa is incorrectly treating some prior `jwt.verify(...)` as a sanitizer for the later `jwt.decode(token, ...)` at line 31. +Current behavior after this checkpoint: + +- Focused `sanitized_by_side_effect` passes. +- Full JS tainting rules improved from 6 passed / 5 mismatched to + 7 passed / 4 mismatched. + ### Important Investigation Already Done Do not start from the assumption that by-side-effect sanitizer identity alone is @@ -419,7 +496,7 @@ that candidate when the sink token is `token`. The incorrect line 31 sanitizer is likely coming from the earlier line 19 verify in `ok2`, because the after-clause fallback checks ordering only. -### Probable Patch Point +### Patch Applied Relevant code in [src/semgrep/scan.ss](/Users/user/mine/jerboa-semgrep/src/semgrep/scan.ss): @@ -430,7 +507,7 @@ Relevant code in [src/semgrep/scan.ss](/Users/user/mine/jerboa-semgrep/src/semgr - `patterns-clause-apply*` uses the trailing-ellipsis fallback around lines 29256 and 29273. -Current shape of `inside-after-clause-apply`: +Previous shape of `inside-after-clause-apply`: ```scheme (def (inside-after-clause-apply rule candidate findings source) @@ -448,13 +525,13 @@ Current shape of `inside-after-clause-apply`: ...))) ``` -That helper needs a lexical/scope bound, not just `before?`. +That helper needed a lexical/scope bound, not just `before?`. -Suggested implementation direction: +Implementation applied: -- Add a helper that returns the end of the smallest enclosing JavaScript block +- Added a helper that returns the end of the smallest enclosing JavaScript block for the base finding, or EOF if there is no enclosing block. -- Use it in `inside-after-clause-apply` so a candidate must be after the base +- Used it in `inside-after-clause-apply` so a candidate must be after the base finding and before that lexical end. - For line 19 in `ok2`, the lexical end should be the `};` around line 24, so it must not sanitize line 31. @@ -467,7 +544,7 @@ track `{` positions outside comments and strings, then call `find-matching-close-brace` on the top stack entry. Existing code already has small brace scanners; do not add a full parser unless necessary. -One possible shape: +Implemented shape: ```scheme (def (javascript-enclosing-block-end source offset) @@ -499,10 +576,9 @@ Risk to watch: a naive nearest-brace helper may pick an object literal instead of a block. That is probably still safer than file-wide leakage for this fallback, but the broad `tests/rules` sweep should be rerun. -### Smoke Test to Add With That Fix +### Smoke Test Added -Add a focused smoke test near the other JavaScript taint sanitizer tests. A -minimal shape: +Added a focused smoke test near the other JavaScript taint sanitizer tests: ```scheme (test-case "scan JavaScript taint sanitizer after verify stays in function scope" @@ -518,13 +594,9 @@ minimal shape: (check (finding-start-line (car findings)) => 8))) ``` -If that minimal source pattern is too broad in practice, include the -`require('jsonwebtoken')` inside-clause from the upstream rule and assert only -the line 8 decode token survives. - -### Required Validation After This Patch +### Validation Run For This Patch -Run these before committing a fix: +These were run before committing the fix: ```sh make test @@ -542,7 +614,7 @@ LIST_MISMATCHES=1 MAX_DIFFS=40 tests/oracle/upstream-sweep.sh git diff --check ``` -Expected improvement if the fix is right: +Actual improvement: - Focused `sanitized_by_side_effect`: 1 passed / 0 mismatched. - Full JS tainting rules: 7 passed / 4 mismatched. @@ -550,10 +622,9 @@ Expected improvement if the fix is right: - Broad `tests/rules`: still 0 mismatches, with the same 3 current-side packaged-Semgrep errors unless upstream/current behavior changes. -## Alternative Next Targets +## Recommended Next Target -If `sanitized_by_side_effect` unexpectedly fans out, the next most tractable -target is probably `await`: +The next most tractable target is probably `await`: ```sh SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \ --- a/lib/semgrep/scan.sls +++ b/lib/semgrep/scan.sls @@ -21163,6 +21163,108 @@ (string-trim (substring pattern 0 ellipsis-line)))]) (and base (not (string=? base "")) (cons 'pattern base))))) + (def (javascript-matching-close-brace source open-index) + (let ([len (string-length source)]) + (and (>= open-index 0) + (< open-index len) + (char=? (string-ref source open-index) #\{) + (let loop ([i open-index] + [depth 0] + [state 'normal] + [escaped? #f]) + (cond + [(>= i len) #f] + [(eq? state 'normal) + (let ([ch (string-ref source i)]) + (cond + [(char=? ch #\") (loop (+ i 1) depth 'double #f)] + [(char=? ch #\') (loop (+ i 1) depth 'single #f)] + [(char=? ch #\`) (loop (+ i 1) depth 'backtick #f)] + [(and (< (+ i 1) len) + (char=? ch #\/) + (char=? (string-ref source (+ i 1)) #\/)) + (loop (line-end-after source i) depth state #f)] + [(and (< (+ i 1) len) + (char=? ch #\/) + (char=? (string-ref source (+ i 1)) #\*)) + (loop (+ i 2) depth 'block-comment #f)] + [(char=? ch #\{) + (loop (+ i 1) (+ depth 1) state #f)] + [(char=? ch #\}) + (if (= depth 1) + (+ i 1) + (loop (+ i 1) (max 0 (- depth 1)) state #f))] + [else (loop (+ i 1) depth state #f)]))] + [(eq? state 'block-comment) + (if (and (< (+ i 1) len) + (char=? (string-ref source i) #\*) + (char=? (string-ref source (+ i 1)) #\/)) + (loop (+ i 2) depth 'normal #f) + (loop (+ i 1) depth state #f))] + [escaped? (loop (+ i 1) depth state #f)] + [(char=? (string-ref source i) #\\) + (loop (+ i 1) depth state #t)] + [(and (eq? state 'double) + (char=? (string-ref source i) #\")) + (loop (+ i 1) depth 'normal #f)] + [(and (eq? state 'single) + (char=? (string-ref source i) #\')) + (loop (+ i 1) depth 'normal #f)] + [(and (eq? state 'backtick) + (char=? (string-ref source i) #\`)) + (loop (+ i 1) depth 'normal #f)] + [else (loop (+ i 1) depth state #f)]))))) + (def (javascript-enclosing-block-end source offset) + (let* ([len (string-length source)] + [limit (min offset len)]) + (let loop ([i 0] [stack '()] [state 'normal] [escaped? #f]) + (cond + [(>= i limit) + (let ([open (and (not (null? stack)) (car stack))]) + (or (and open + (javascript-matching-close-brace source open)) + len))] + [(eq? state 'normal) + (let ([ch (string-ref source i)]) + (cond + [(char=? ch #\") (loop (+ i 1) stack 'double #f)] + [(char=? ch #\') (loop (+ i 1) stack 'single #f)] + [(char=? ch #\`) (loop (+ i 1) stack 'backtick #f)] + [(and (< (+ i 1) limit) + (char=? ch #\/) + (char=? (string-ref source (+ i 1)) #\/)) + (loop (line-end-after source i) stack state #f)] + [(and (< (+ i 1) limit) + (char=? ch #\/) + (char=? (string-ref source (+ i 1)) #\*)) + (loop (+ i 2) stack 'block-comment #f)] + [(char=? ch #\{) (loop (+ i 1) (cons i stack) state #f)] + [(char=? ch #\}) + (loop + (+ i 1) + (if (null? stack) stack (cdr stack)) + state + #f)] + [else (loop (+ i 1) stack state #f)]))] + [(eq? state 'block-comment) + (if (and (< (+ i 1) limit) + (char=? (string-ref source i) #\*) + (char=? (string-ref source (+ i 1)) #\/)) + (loop (+ i 2) stack 'normal #f) + (loop (+ i 1) stack state #f))] + [escaped? (loop (+ i 1) stack state #f)] + [(char=? (string-ref source i) #\\) + (loop (+ i 1) stack state #t)] + [(and (eq? state 'double) + (char=? (string-ref source i) #\")) + (loop (+ i 1) stack 'normal #f)] + [(and (eq? state 'single) + (char=? (string-ref source i) #\')) + (loop (+ i 1) stack 'normal #f)] + [(and (eq? state 'backtick) + (char=? (string-ref source i) #\`)) + (loop (+ i 1) stack 'normal #f)] + [else (loop (+ i 1) stack state #f)])))) (def (inside-after-clause-apply rule candidate @@ -21184,7 +21286,14 @@ (let* ([finding (car xs)] [before? (< (finding-end-offset finding) (finding-start-offset candidate))] - [merged (and before? + [scope-end (and before? + (javascript-enclosing-block-end + source + (finding-start-offset finding)))] + [within? (and scope-end + (<= (finding-end-offset candidate) + scope-end))] + [merged (and within? (merge-binding-list (finding-metavars candidate) (finding-metavars finding)))] --- a/src/.jerbuild-hashes +++ b/src/.jerbuild-hashes @@ -3,11 +3,11 @@ ("src/semgrep/output/json.ss" . "293881CFA2ADB7BC") ("src/semgrep/lang.ss" . "6982E07679D20836") ("src/semgrep/parse/parse-target.ss" . "E74854DDDACF6BA") - ("src/semgrep/scan.ss" . "C428896282E9AB09") - ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1") - ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") + ("src/semgrep/scan.ss" . "26462685AB67B945") ("src/semgrep/rule.ss" . "E12C108153C181FA") ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0") + ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") + ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1") ("src/semgrep/match/structural.ss" . "F7B63A9A6FA028B") ("src/semgrep/main.ss" . "A4EC9E7F2A09D25E") ("src/semgrep/cli.ss" . "EBDC4B1DAD3F13CC")) --- a/src/semgrep/scan.ss +++ b/src/semgrep/scan.ss @@ -21227,6 +21227,106 @@ (not (string=? base "")) (cons 'pattern base))))) +(def (javascript-matching-close-brace source open-index) + (let ([len (string-length source)]) + (and (>= open-index 0) + (< open-index len) + (char=? (string-ref source open-index) #\{) + (let loop ([i open-index] + [depth 0] + [state 'normal] + [escaped? #f]) + (cond + [(>= i len) #f] + [(eq? state 'normal) + (let ([ch (string-ref source i)]) + (cond + [(char=? ch #\") (loop (+ i 1) depth 'double #f)] + [(char=? ch #\') (loop (+ i 1) depth 'single #f)] + [(char=? ch #\`) (loop (+ i 1) depth 'backtick #f)] + [(and (< (+ i 1) len) + (char=? ch #\/) + (char=? (string-ref source (+ i 1)) #\/)) + (loop (line-end-after source i) depth state #f)] + [(and (< (+ i 1) len) + (char=? ch #\/) + (char=? (string-ref source (+ i 1)) #\*)) + (loop (+ i 2) depth 'block-comment #f)] + [(char=? ch #\{) + (loop (+ i 1) (+ depth 1) state #f)] + [(char=? ch #\}) + (if (= depth 1) + (+ i 1) + (loop (+ i 1) (max 0 (- depth 1)) state #f))] + [else (loop (+ i 1) depth state #f)]))] + [(eq? state 'block-comment) + (if (and (< (+ i 1) len) + (char=? (string-ref source i) #\*) + (char=? (string-ref source (+ i 1)) #\/)) + (loop (+ i 2) depth 'normal #f) + (loop (+ i 1) depth state #f))] + [escaped? (loop (+ i 1) depth state #f)] + [(char=? (string-ref source i) #\\) + (loop (+ i 1) depth state #t)] + [(and (eq? state 'double) (char=? (string-ref source i) #\")) + (loop (+ i 1) depth 'normal #f)] + [(and (eq? state 'single) (char=? (string-ref source i) #\')) + (loop (+ i 1) depth 'normal #f)] + [(and (eq? state 'backtick) (char=? (string-ref source i) #\`)) + (loop (+ i 1) depth 'normal #f)] + [else (loop (+ i 1) depth state #f)]))))) + +(def (javascript-enclosing-block-end source offset) + (let* ([len (string-length source)] + [limit (min offset len)]) + (let loop ([i 0] + [stack '()] + [state 'normal] + [escaped? #f]) + (cond + [(>= i limit) + (let ([open (and (not (null? stack)) (car stack))]) + (or (and open (javascript-matching-close-brace source open)) + len))] + [(eq? state 'normal) + (let ([ch (string-ref source i)]) + (cond + [(char=? ch #\") (loop (+ i 1) stack 'double #f)] + [(char=? ch #\') (loop (+ i 1) stack 'single #f)] + [(char=? ch #\`) (loop (+ i 1) stack 'backtick #f)] + [(and (< (+ i 1) limit) + (char=? ch #\/) + (char=? (string-ref source (+ i 1)) #\/)) + (loop (line-end-after source i) stack state #f)] + [(and (< (+ i 1) limit) + (char=? ch #\/) + (char=? (string-ref source (+ i 1)) #\*)) + (loop (+ i 2) stack 'block-comment #f)] + [(char=? ch #\{) + (loop (+ i 1) (cons i stack) state #f)] + [(char=? ch #\}) + (loop (+ i 1) + (if (null? stack) stack (cdr stack)) + state + #f)] + [else (loop (+ i 1) stack state #f)]))] + [(eq? state 'block-comment) + (if (and (< (+ i 1) limit) + (char=? (string-ref source i) #\*) + (char=? (string-ref source (+ i 1)) #\/)) + (loop (+ i 2) stack 'normal #f) + (loop (+ i 1) stack state #f))] + [escaped? (loop (+ i 1) stack state #f)] + [(char=? (string-ref source i) #\\) + (loop (+ i 1) stack state #t)] + [(and (eq? state 'double) (char=? (string-ref source i) #\")) + (loop (+ i 1) stack 'normal #f)] + [(and (eq? state 'single) (char=? (string-ref source i) #\')) + (loop (+ i 1) stack 'normal #f)] + [(and (eq? state 'backtick) (char=? (string-ref source i) #\`)) + (loop (+ i 1) stack 'normal #f)] + [else (loop (+ i 1) stack state #f)])))) + (def (inside-after-clause-apply rule candidate findings source) (let loop ([xs findings] [best #f] [best-start #f] [best-bindings #f]) (cond @@ -21237,8 +21337,15 @@ (let* ([finding (car xs)] [before? (< (finding-end-offset finding) (finding-start-offset candidate))] - [merged + [scope-end (and before? + (javascript-enclosing-block-end + source + (finding-start-offset finding)))] + [within? (and scope-end + (<= (finding-end-offset candidate) scope-end))] + [merged + (and within? (merge-binding-list (finding-metavars candidate) (finding-metavars finding)))] --- a/tests/smoke.ss +++ b/tests/smoke.ss @@ -4139,6 +4139,18 @@ (check (length findings) => 1) (check (finding-start-line (car findings)) => 2))) +(test-case "scan JavaScript taint sanitizer after verify stays in function scope" + (let* ([taint-config + "rules:\n - id: demo.taint.jwt.verify\n mode: taint\n languages: [javascript]\n message: jwt token\n severity: WARNING\n pattern-sources:\n - pattern: $TOKEN\n pattern-sanitizers:\n - patterns:\n - pattern-inside: |\n $JWT.verify($TOKEN, ...)\n ...\n - pattern: $TOKEN\n pattern-sinks:\n - patterns:\n - pattern: $JWT.decode($TOKEN, ...)\n - pattern: $TOKEN\n"] + [findings + (scan-config-string + taint-config + "javascript" + "demo.js" + "function ok(token, key) {\n jwt.verify(token, key);\n jwt.decode(token, true);\n}\nfunction bad(token, key) {\n token2 = getToken();\n jwt.verify(token2, key);\n jwt.decode(token, true);\n}\n")]) + (check (length findings) => 1) + (check (finding-start-line (car findings)) => 8))) + (test-case "scan JavaScript taint object destructuring assignment" (let* ([taint-config "rules:\n - id: demo.taint.js.destructure\n mode: taint\n languages: [javascript]\n message: destructured taint\n severity: WARNING\n pattern-sources:\n - pattern: user.dataValues\n pattern-sinks:\n - pattern: sink(...)\n"]