Advance taint control parity
ober
7beb039040f6587b83af90f9308e64bac534a146
--- a/HANDOFF_OPUS_4_8.md +++ b/HANDOFF_OPUS_4_8.md @@ -65,7 +65,7 @@ make test Result: ```text -176 tests, 176 passed, 0 failed +178 tests, 178 passed, 0 failed ``` Local oracle: @@ -83,13 +83,13 @@ oracle: 42 passed, 0 failed Focused upstream cases fixed in this checkpoint: ```sh -SEMGREP_CURRENT=/Users/user/.local/bin/semgrep CASE_REGEX='^(taint_best_fit_sink3|taint_clean_in_try_no_finally)$' LIST_MISMATCHES=1 MAX_DIFFS=160 tests/oracle/upstream-sweep.sh +SEMGREP_CURRENT=/Users/user/.local/bin/semgrep CASE_REGEX='^(taint_control|taint_flask|taint_imported_func)$' LIST_MISMATCHES=1 MAX_DIFFS=160 tests/oracle/upstream-sweep.sh ``` Result: ```text -upstream-sweep: 2 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 2 compared +upstream-sweep: 3 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 3 compared ``` Focused safe-option/propagation guardrail: @@ -113,102 +113,67 @@ SEMGREP_CURRENT=/Users/user/.local/bin/semgrep MAX_CASES=220 LIST_MISMATCHES=1 M Result: ```text -upstream-sweep: 183 passed, 35 mismatched, 0 jerboa errors, 2 current errors, 220 compared +upstream-sweep: 186 passed, 32 mismatched, 0 jerboa errors, 2 current errors, 220 compared ``` Important progress markers: - The first 160 upstream pairs are still clean for Jerboa: 158 passed, 0 mismatched, 0 Jerboa errors, 2 packaged-Semgrep current errors. -- The first-220 sweep improved from 37 mismatches to 35 mismatches in this +- The first-220 sweep improved from 35 mismatches to 32 mismatches in this checkpoint. -- `taint_best_fit_sink3` and `taint_clean_in_try_no_finally` are no longer part +- `taint_control`, `taint_flask`, and `taint_imported_func` are no longer part of the mismatch frontier. - The 2 non-passing current errors in the first-160/first-220 windows are packaged-Semgrep oracle errors, not Jerboa scanner errors. ## What Changed In This Checkpoint -The current checkpoint extends the taint best-fit/sanitizer work. It adds -support for `taint_assume_safe_functions` in Python focused sink shapes and -prevents later implicit assignments from resurrecting token taint after a -compatible sanitizer has run. The source changes are in `src/semgrep/scan.ss`; -`make test` mirrored them into `lib/semgrep/scan.sls`. +The current checkpoint adds control-taint reachability and Python import-alias +pattern matching for taint specs. The source changes are in +`src/semgrep/scan.ss`; `make test` mirrored them into +`lib/semgrep/scan.sls`. New implementation landmarks: ```text -token-text-inside-python-conditional-test? -token-source-inside-python-conditional-test? -finding-range-contains-span? -sanitizer-covers-token-span? -token-text-fully-sanitized-in-sink? -token-source-fully-sanitized-in-sink? -taint-assume-safe-functions? -call-name-start-before-open -matching-close-paren-index -finding-focused-to-whole-binding? -safe-function-wrapper-in-text? -taint-safe-function-use? +taint-state-control? +python-import-local-pattern-candidates +scan-python-import-local-pattern +scan-python-import-local-pattern-with-bindings ``` Behavior added: -- Bare-token taint sources are suppressed when the only relevant use in a - Python conditional expression is inside the conditional test, e.g. - `sink(safe if isset(source) else '')`. -- Nested sanitizer calls now block token taint when the sanitizer is contained - inside the sink and covers the token occurrence, e.g. - `sink(sanitizer(source) if isset(safe) else '')`. -- Token sinks are treated occurrence-by-occurrence for sanitizer coverage: - a sink is suppressed only when every occurrence of the token in the sink is - covered by a sanitizer. This avoids over-generalizing to mixed shapes like - `sink(sanitizer(source), source)`. -- Non-exact sink reach was tightened for token states so a later same-named - token source cannot taint an earlier sink merely because the text matches. - Token sources now still need to be before the sink or physically contained in - it when the non-exact sink branch is used. -- With `taint_assume_safe_functions: true`, taint nested under an ordinary - function result is suppressed, e.g. `sink(not_a_propagator(tainted()))`, - while direct sink arguments such as `sink(tainted())` still report. -- The safe-function check handles focused sink findings by recognizing when the - finding range is exactly the focused metavariable binding. -- Propagation through later implicit assignments is blocked when the source was - sanitized before the assignment RHS, so `id = int(id)` prevents later - `some_object = id` / `csv_file = ...` propagation from reviving the old - `request` taint. - -Existing sanitizer logic changed: - -- In `sanitizer-blocks?`, the nested-token branch now checks whether the sink - contains the sanitizer, instead of incorrectly checking whether the sanitizer - starts before the sink. This matters for `sink(sanitizer(source))` shapes. +- `control: true` taint sources now carry a control bit in the taint state and + can reach later sinks in the same simple function scope by order/control + reachability rather than value containment. +- The control bit is preserved through label changes and participates in + taint-state identity, so label/requires logic does not merge control and + ordinary value taint states. +- Python patterns written with imported fully qualified names can match local + imported calls. For example, a rule pattern + `framework.db_access.mysql_update($SINK)` now matches target code after + `from framework import db_access` as `db_access.mysql_update(params)`. +- The Python import-local fallback returns `#f` when it has no findings, so it + does not short-circuit later structural/symbolic matchers in fallback `or` + chains. +- Clean reassignment killing now applies to any source whose source text equals + the assigned LHS, not only token-marked states. This helps focused + function-parameter sources and variable-like source patterns. Smoke coverage added: ```text -scan taint sanitizer blocks conditional sink token -scan taint assume safe functions blocks wrapper -scan taint sanitizer blocks later assignment propagation -``` - -That smoke test mirrors upstream `taint_best_fit_sink2.py`: - -```python -sink(sanitizer(source) if isset(source) else '') -sink(sanitizer(source) if isset(safe) else '') -sink(sanitizer(safe) if isset(source) else '') -sink(safe if isset(source) else '') +scan taint control source reaches later sinks +scan Python taint imported fully qualified sink ``` -Expected result: zero findings. - ## Current Upstream Frontier -The first-220 sweep currently has these 35 Jerboa mismatches: +The first-220 sweep currently has these 32 Jerboa mismatches: ```text -taint_control taint_exact_sources taint_exception taint_field_sensitive1 @@ -219,8 +184,6 @@ taint_field_sensitive6 taint_field_sensitive7 taint_field_sensitive8 taint_final_globals1 -taint_flask -taint_imported_func taint_labels2 taint_labels3 taint_lambda2 @@ -245,76 +208,38 @@ taint_react taint_safe_comparisons ``` -The next case to investigate is `taint_control`. +The next case to investigate is `taint_exact_sources`. Focused command: ```sh -SEMGREP_CURRENT=/Users/user/.local/bin/semgrep CASE_REGEX='^taint_control$' LIST_MISMATCHES=1 MAX_DIFFS=200 tests/oracle/upstream-sweep.sh +SEMGREP_CURRENT=/Users/user/.local/bin/semgrep CASE_REGEX='^taint_exact_sources$' LIST_MISMATCHES=1 MAX_DIFFS=240 tests/oracle/upstream-sweep.sh ``` Current normalized diff: ```diff -@@ -1,3 +0,0 @@ --(finding "test" "/Users/user/mine/semgrep/tests/rules/taint_control.py" 14 5 140 14 11 146 "WARNING" "Test" "") --(finding "test" "/Users/user/mine/semgrep/tests/rules/taint_control.py" 22 5 228 22 11 234 "WARNING" "Test" "") --(finding "test" "/Users/user/mine/semgrep/tests/rules/taint_control.py" 6 3 58 6 9 64 "WARNING" "Test" "") -``` - -Rule: - -```yaml -rules: - - id: test - languages: - - python - severity: WARNING - mode: taint - message: Test - pattern-sources: - - control: true - pattern: source(...) - pattern-sinks: - - pattern: sink(...) -``` - -Target: - -```python -def test1(): - source() - foo() - bar() - #ruleid: test - sink() - -def test2(): - source() - foo() - bar() - if baz(): - #ruleid: test - sink() - -def test3(): - if foo(): - source() - bar() - if baz(): - #ruleid: test - sink() +@@ -1,5 +1,7 @@ ++(finding "sql-injection" "/Users/user/mine/semgrep/tests/rules/taint_exact_sources.py" 23 36 776 23 42 782 "WARNING" "Semgrep found a match" "") + (finding "sql-injection" "/Users/user/mine/semgrep/tests/rules/taint_exact_sources.py" 28 28 990 28 34 996 "WARNING" "Semgrep found a match" "") + (finding "sql-injection" "/Users/user/mine/semgrep/tests/rules/taint_exact_sources.py" 33 32 1161 33 38 1167 "WARNING" "Semgrep found a match" "") ++(finding "sql-injection" "/Users/user/mine/semgrep/tests/rules/taint_exact_sources.py" 41 28 1483 41 34 1489 "WARNING" "Semgrep found a match" "") + (finding "sql-injection" "/Users/user/mine/semgrep/tests/rules/taint_exact_sources.py" 49 28 1654 49 34 1660 "WARNING" "Semgrep found a match" "") + (finding "sql-injection" "/Users/user/mine/semgrep/tests/rules/taint_exact_sources.py" 60 28 2029 60 34 2035 "WARNING" "Semgrep found a match" "") + (finding "sql-injection" "/Users/user/mine/semgrep/tests/rules/taint_exact_sources.py" 69 28 2232 69 34 2238 "WARNING" "Semgrep found a match" "") ``` Interpretation for the next fix: -- Jerboa currently reports zero findings for control-taint sources. -- Semgrep reports all three sinks after a `control: true` source in the same - function/control region. -- The parser already records `control` on taint source specs; the next patch - should inspect `taint-state-for-spec`, source reachability, and label - propagation to model control taint without making ordinary value taint leak - across unrelated functions. +- The Python import-local fallback moved this case from all-missing findings to + mostly matching the expected sink family. +- Remaining false positives are line 23 (`results = db_access.mysql_dict(params)`) + after a clean assignment to `params["sql"]`, and line 41 after a clean + assignment to `params["name"]`. +- Continue in the assignment-kill / field-sensitivity area. The broad + source-text-equals-LHS kill is not enough because later self-use and indexed + assignment semantics still need to distinguish clean field writes from + tainting writes. ## Resolved First-160 Taint Cluster @@ -484,8 +409,9 @@ Use the target extension that actually exists for the case (`.py`, `.js`, ## Immediate Next Step -Start with `taint_control`. Add a smoke test for `control: true` sources where -`source()` causes later `sink()` calls in the same function to report, including -inside a later `if` block. Then patch taint reachability so control-taint flows -through control/order within the same simple function scope without changing -ordinary value-taint behavior. +Start with `taint_exact_sources`. The remaining work is not import matching; +that now works. Focus on suppressing the two false positives at lines 23 and +41 while preserving the expected reports at lines 28, 33, 49, 60, and 69. +Likely areas are assignment-kill ordering, indexed-field sensitivity, and +whether clean indexed assignments should block the original focused parameter +source until a later genuinely tainted write re-taints the container. --- a/lib/semgrep/scan.sls +++ b/lib/semgrep/scan.sls @@ -2799,6 +2799,70 @@ (string=? language "python2") (string=? language "python3") (string=? language "py"))) + (def (replace-imported-fqn-pattern pattern local fqn) + (let ([fqn-len (string-length fqn)]) + (let loop ([start 0]) + (let ([index (string-find-substring-from + pattern + fqn + start)]) + (and index + (let ([end (+ index fqn-len)]) + (if (and (token-start-boundary? pattern index) + (token-end-boundary? pattern end)) + (replace-range pattern index end local) + (loop (+ index 1))))))))) + (def (python-import-local-pattern-candidates source pattern) + (unique-string-list + (let import-loop ([imports (python-import-map source)] + [acc '()]) + (if (null? imports) + acc + (let* ([entry (car imports)] + [local (car entry)] + [fqns (cdr entry)]) + (import-loop + (cdr imports) + (let fqn-loop ([xs fqns] [inner acc]) + (if (null? xs) + inner + (fqn-loop + (cdr xs) + (cons + (replace-imported-fqn-pattern + pattern + local + (car xs)) + inner)))))))))) + (def (scan-python-import-local-pattern rule language path + source target-root pattern) + (and (symbolic-python-like-language? language) + (let ([patterns (python-import-local-pattern-candidates + source + pattern)]) + (and (not (null? patterns)) + (let ([findings (apply + append + (map (lambda (candidate) + (scan-structural-pattern rule language path source + target-root candidate)) + patterns))]) + (and (not (null? findings)) findings)))))) + (def (scan-python-import-local-pattern-with-bindings rule language path source target-root pattern + initial-bindings) + (and (symbolic-python-like-language? language) + (let ([patterns (python-import-local-pattern-candidates + source + pattern)]) + (and (not (null? patterns)) + (let ([findings (apply + append + (map (lambda (candidate) + (scan-structural-pattern-with-bindings rule language path source + target-root candidate + initial-bindings)) + patterns))]) + (and (not (null? findings)) findings)))))) (def (symbolic-assignment-line-offset source start) (if (and (< start (string-length source)) (char=? (string-ref source start) #\newline)) @@ -4199,6 +4263,8 @@ path source (cdr entry))) + (scan-python-import-local-pattern rule language path source + target-root (cdr entry)) (scan-symbolic-propagation-pattern rule language path source (cdr entry)) (scan-structural-pattern rule language path source @@ -4373,6 +4439,8 @@ path source (cdr entry))) + (scan-python-import-local-pattern-with-bindings rule language path source target-root (cdr entry) + initial-bindings) (scan-symbolic-propagation-pattern rule language path source (cdr entry)) (scan-structural-pattern-with-bindings rule language path source target-root (cdr entry) @@ -7622,10 +7690,11 @@ target-root) (scan-patterns-clauses rule (rule-pattern rule) language path source target-root)) - (def (taint-state finding labels exact? token? contained?) + (def (taint-state finding labels exact? token? contained? + control?) (list (cons 'finding finding) (cons 'labels labels) (cons 'exact exact?) (cons 'token token?) - (cons 'contained contained?))) + (cons 'contained contained?) (cons 'control control?))) (def (taint-state-finding state) (alist-ref/default state 'finding #f)) (def (taint-state-labels state) @@ -7636,10 +7705,13 @@ (and (alist-ref/default state 'token #f) #t)) (def (taint-state-contained? state) (and (alist-ref/default state 'contained #f) #t)) + (def (taint-state-control? state) + (and (alist-ref/default state 'control #f) #t)) (def (taint-state-with-labels state labels) (taint-state (taint-state-finding state) labels (taint-state-exact? state) (taint-state-token? state) - (taint-state-contained? state))) + (taint-state-contained? state) + (taint-state-control? state))) (def (taint-by-side-effect? value) (cond [(not value) #f] @@ -7695,7 +7767,7 @@ (or side-effect? (taint-function-parameter-source-spec? spec) (taint-bare-identifier-source-spec? spec)) - #f))) + #f (alist-ref/default spec 'control #f)))) (def (scan-taint-specs rule specs language path source target-root default-label) (apply @@ -7973,24 +8045,29 @@ source-text) (let ([source (taint-state-finding source-state)]) (and source - (if (taint-state-exact? source-state) - (finding-range-equal? source sink) - (or (finding-range-contains? sink source) - (and (taint-state-contained? source-state) - (finding-range-contains? sink source)) - (finding-text-equals-any-binding? - source - sink - source-text) - (finding-text-equals-any-binding? - sink - source - source-text) - (and (taint-state-token? source-state) - (source-token-in-sink? source sink source-text)) - (and (null? (finding-metavars source)) - (null? (finding-metavars sink)) - (finding-range-contains? sink source))))))) + (if (taint-state-control? source-state) + #t + (if (taint-state-exact? source-state) + (finding-range-equal? source sink) + (or (finding-range-contains? sink source) + (and (taint-state-contained? source-state) + (finding-range-contains? sink source)) + (finding-text-equals-any-binding? + source + sink + source-text) + (finding-text-equals-any-binding? + sink + source + source-text) + (and (taint-state-token? source-state) + (source-token-in-sink? + source + sink + source-text)) + (and (null? (finding-metavars source)) + (null? (finding-metavars sink)) + (finding-range-contains? sink source)))))))) (def (taint-assume-safe-booleans? rule) (rule-option-enabled? rule "taint_assume_safe_booleans")) (def (taint-assume-safe-numbers? rule) @@ -8426,8 +8503,7 @@ (finding-metavariable-binding assignment-finding "$L"))]) - (and (taint-state-token? source-state) - source + (and source assignment-finding sink to-binding @@ -8753,6 +8829,8 @@ (taint-state-token? existing)) (eq? (taint-state-contained? state) (taint-state-contained? existing)) + (eq? (taint-state-control? state) + (taint-state-control? existing)) (same-label-set? (taint-state-labels state) (taint-state-labels existing)))) @@ -8826,7 +8904,7 @@ (and merged (taint-state (finding-with-bindings rule focused merged source) (propagator-output-labels source-state propagator) #f - side-effect? (not side-effect?))))) + side-effect? (not side-effect?) #f)))) (def (new-required-sources source-matches known source) (let loop ([matches source-matches] [acc '()]) (if (null? matches) --- a/src/.jerbuild-hashes +++ b/src/.jerbuild-hashes @@ -3,11 +3,11 @@ ("src/semgrep/output/json.ss" . "293881CFA2ADB7BC") ("src/semgrep/lang.ss" . "7E5441BD00A7F1D4") ("src/semgrep/parse/parse-target.ss" . "E74854DDDACF6BA") - ("src/semgrep/scan.ss" . "E1CA04F548FE150") - ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1") - ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") + ("src/semgrep/scan.ss" . "471D16BC922EC36F") ("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" . "D56FC2D2EB449BA6")) --- a/src/semgrep/scan.ss +++ b/src/semgrep/scan.ss @@ -2927,6 +2927,86 @@ (string=? language "python3") (string=? language "py"))) +(def (replace-imported-fqn-pattern pattern local fqn) + (let ([fqn-len (string-length fqn)]) + (let loop ([start 0]) + (let ([index (string-find-substring-from pattern fqn start)]) + (and index + (let ([end (+ index fqn-len)]) + (if (and (token-start-boundary? pattern index) + (token-end-boundary? pattern end)) + (replace-range pattern index end local) + (loop (+ index 1))))))))) + +(def (python-import-local-pattern-candidates source pattern) + (unique-string-list + (let import-loop ([imports (python-import-map source)] [acc '()]) + (if (null? imports) + acc + (let* ([entry (car imports)] + [local (car entry)] + [fqns (cdr entry)]) + (import-loop + (cdr imports) + (let fqn-loop ([xs fqns] [inner acc]) + (if (null? xs) + inner + (fqn-loop + (cdr xs) + (cons (replace-imported-fqn-pattern + pattern + local + (car xs)) + inner)))))))))) + +(def (scan-python-import-local-pattern + rule + language + path + source + target-root + pattern) + (and (symbolic-python-like-language? language) + (let ([patterns (python-import-local-pattern-candidates source pattern)]) + (and (not (null? patterns)) + (let ([findings + (apply append + (map (lambda (candidate) + (scan-structural-pattern + rule + language + path + source + target-root + candidate)) + patterns))]) + (and (not (null? findings)) findings)))))) + +(def (scan-python-import-local-pattern-with-bindings + rule + language + path + source + target-root + pattern + initial-bindings) + (and (symbolic-python-like-language? language) + (let ([patterns (python-import-local-pattern-candidates source pattern)]) + (and (not (null? patterns)) + (let ([findings + (apply append + (map (lambda (candidate) + (scan-structural-pattern-with-bindings + rule + language + path + source + target-root + candidate + initial-bindings)) + patterns))]) + (and (not (null? findings)) findings)))))) + (def (symbolic-assignment-line-offset source start) (if (and (< start (string-length source)) (char=? (string-ref source start) #\newline)) @@ -4359,6 +4439,13 @@ path source (cdr entry))) + (scan-python-import-local-pattern + rule + language + path + source + target-root + (cdr entry)) (scan-symbolic-propagation-pattern rule language @@ -4590,6 +4677,14 @@ path source (cdr entry))) + (scan-python-import-local-pattern-with-bindings + rule + language + path + source + target-root + (cdr entry) + initial-bindings) (scan-symbolic-propagation-pattern rule language @@ -8068,12 +8163,13 @@ source target-root)) -(def (taint-state finding labels exact? token? contained?) +(def (taint-state finding labels exact? token? contained? control?) (list (cons 'finding finding) (cons 'labels labels) (cons 'exact exact?) (cons 'token token?) - (cons 'contained contained?))) + (cons 'contained contained?) + (cons 'control control?))) (def (taint-state-finding state) (alist-ref/default state 'finding #f)) @@ -8090,13 +8186,17 @@ (def (taint-state-contained? state) (and (alist-ref/default state 'contained #f) #t)) +(def (taint-state-control? state) + (and (alist-ref/default state 'control #f) #t)) + (def (taint-state-with-labels state labels) (taint-state (taint-state-finding state) labels (taint-state-exact? state) (taint-state-token? state) - (taint-state-contained? state))) + (taint-state-contained? state) + (taint-state-control? state))) (def (taint-by-side-effect? value) (cond @@ -8163,7 +8263,8 @@ (or side-effect? (taint-function-parameter-source-spec? spec) (taint-bare-identifier-source-spec? spec)) - #f))) + #f + (alist-ref/default spec 'control #f)))) (def (scan-taint-specs rule specs language path source target-root default-label) (apply append @@ -8477,18 +8578,20 @@ (def (source-compatible-with-sink? source-state sink source-text) (let ([source (taint-state-finding source-state)]) (and source - (if (taint-state-exact? source-state) - (finding-range-equal? source sink) - (or (finding-range-contains? sink source) - (and (taint-state-contained? source-state) - (finding-range-contains? sink source)) - (finding-text-equals-any-binding? source sink source-text) - (finding-text-equals-any-binding? sink source source-text) - (and (taint-state-token? source-state) - (source-token-in-sink? source sink source-text)) - (and (null? (finding-metavars source)) - (null? (finding-metavars sink)) - (finding-range-contains? sink source))))))) + (if (taint-state-control? source-state) + #t + (if (taint-state-exact? source-state) + (finding-range-equal? source sink) + (or (finding-range-contains? sink source) + (and (taint-state-contained? source-state) + (finding-range-contains? sink source)) + (finding-text-equals-any-binding? source sink source-text) + (finding-text-equals-any-binding? sink source source-text) + (and (taint-state-token? source-state) + (source-token-in-sink? source sink source-text)) + (and (null? (finding-metavars source)) + (null? (finding-metavars sink)) + (finding-range-contains? sink source)))))))) (def (taint-assume-safe-booleans? rule) (rule-option-enabled? rule "taint_assume_safe_booleans")) @@ -8879,8 +8982,7 @@ (finding-metavariable-binding assignment-finding "$L"))]) - (and (taint-state-token? source-state) - source + (and source assignment-finding sink to-binding @@ -9202,6 +9304,8 @@ (taint-state-token? existing)) (eq? (taint-state-contained? state) (taint-state-contained? existing)) + (eq? (taint-state-control? state) + (taint-state-control? existing)) (same-label-set? (taint-state-labels state) (taint-state-labels existing)))) states)) @@ -9271,7 +9375,8 @@ (propagator-output-labels source-state propagator) #f side-effect? - (not side-effect?))))) + (not side-effect?) + #f)))) (def (new-required-sources source-matches known source) (let loop ([matches source-matches] [acc '()]) --- a/tests/smoke.ss +++ b/tests/smoke.ss @@ -1424,6 +1424,33 @@ (check (length findings) => 1) (check (finding-start-line (car findings)) => 1))) +(test-case "scan taint control source reaches later sinks" + (let* ([taint-config + "rules:\n - id: demo.taint.control\n mode: taint\n languages: [python]\n message: control taint\n severity: WARNING\n pattern-sources:\n - control: true\n pattern: source(...)\n pattern-sinks:\n - pattern: sink(...)\n"] + [findings + (scan-config-string + taint-config + "python" + "demo.py" + "def test1():\n source()\n foo()\n bar()\n sink()\n\ndef test2():\n source()\n foo()\n bar()\n if baz():\n sink()\n\ndef test3():\n if foo():\n source()\n bar()\n if baz():\n sink()\n")]) + (check (length findings) => 3) + (check (finding-start-line (car findings)) => 5) + (check (finding-start-line (cadr findings)) => 12) + (check (finding-start-line (caddr findings)) => 19))) + +(test-case "scan Python taint imported fully qualified sink" + (let* ([taint-config + "rules:\n - id: demo.taint.python-imported-sink\n mode: taint\n languages: [python]\n message: imported sink\n severity: WARNING\n pattern-sources:\n - patterns:\n - pattern: |\n def $FN(..., $PARAMS, ...):\n ...\n - focus-metavariable: $PARAMS\n pattern-sinks:\n - patterns:\n - pattern: framework.db_access.mysql_update($SINK)\n - focus-metavariable: $SINK\n"] + [findings + (scan-config-string + taint-config + "python" + "demo.py" + "from framework import db_access\n\ndef fn(params):\n db_access.mysql_update(params)\n")]) + (check (length findings) => 1) + (check (finding-start-line (car findings)) => 4) + (check (finding-start-col (car findings)) => 28))) + (test-case "scan taint implicit assignment propagation" (let* ([taint-config "rules:\n - id: demo.taint.assignment\n mode: taint\n languages: [python]\n message: assigned taint\n severity: WARNING\n pattern-sources:\n - pattern: source()\n pattern-sinks:\n - pattern: sink($X)\n"]