Flow Go taint through full := assignment RHS

ober

1f55031e50607a2b7ed7c3706ce16a6bfbbc6d0b

diff --git a/lib/semgrep/scan.sls b/lib/semgrep/scan.sls
index 1c2808d..dad8671 100644
--- a/lib/semgrep/scan.sls
+++ b/lib/semgrep/scan.sls
@@ -31112,6 +31112,103 @@
                        (scan-positive-pattern-entry rule (cons 'pattern pattern) language path source
                          target-root))))
               (implicit-assignment-patterns language))))
+  (def (go-simple-assignment-parts source first line-end)
+       (and (< first line-end)
+            (let ([c0 (string-ref source first)])
+              (or (char-alphabetic? c0) (char=? c0 #\_)))
+            (let loop ([i (+ first 1)])
+              (cond
+                [(and (< i line-end)
+                      (identifier-token-char? (string-ref source i)))
+                 (loop (+ i 1))]
+                [else
+                 (let* ([l-end i] [j (skip-horizontal-forward source i)])
+                   (cond
+                     [(and (< (+ j 1) line-end)
+                           (char=? (string-ref source j) #\:)
+                           (char=? (string-ref source (+ j 1)) #\=))
+                      (cons l-end (+ j 2))]
+                     [(and (< j line-end)
+                           (char=? (string-ref source j) #\=)
+                           (or (>= (+ j 1) line-end)
+                               (not (char=?
+                                      (string-ref source (+ j 1))
+                                      #\=))))
+                      (cons l-end (+ j 1))]
+                     [else #f]))]))))
+  (def (go-assignment-rhs-end source r-start line-end)
+       (let* ([raw-end (go-line-trimmed-end
+                         source
+                         r-start
+                         line-end)]
+              [comment (string-find-substring-from source "//" r-start)]
+              [content-end (if (and comment (< comment raw-end))
+                               comment
+                               raw-end)])
+         (let loop ([e content-end])
+           (if (and (> e r-start)
+                    (let ([c (string-ref source (- e 1))])
+                      (or (char=? c #\space) (char=? c #\tab))))
+               (loop (- e 1))
+               e))))
+  (def (scan-go-implicit-assignment-propagators
+         rule
+         path
+         source)
+       (let ([len (string-length source)])
+         (let loop ([line-start 0] [acc '()])
+           (if (> line-start len)
+               (reverse acc)
+               (let* ([line-end (line-end-after source line-start)]
+                      [first (line-first-nonspace
+                               source
+                               line-start
+                               line-end)]
+                      [parts (go-simple-assignment-parts
+                               source
+                               first
+                               line-end)]
+                      [next (if (< line-end len)
+                                (+ line-end 1)
+                                (+ len 1))])
+                 (if (not parts)
+                     (loop next acc)
+                     (let* ([l-end (car parts)]
+                            [op-end (cdr parts)]
+                            [r-start (skip-horizontal-forward
+                                       source
+                                       op-end)]
+                            [r-end (go-assignment-rhs-end
+                                     source
+                                     r-start
+                                     line-end)])
+                       (if (<= r-end r-start)
+                           (loop next acc)
+                           (let* ([l-binding (metavariable-binding-for-range
+                                               "L"
+                                               source
+                                               first
+                                               l-end)]
+                                  [r-binding (metavariable-binding-for-range
+                                               "R"
+                                               source
+                                               r-start
+                                               r-end)]
+                                  [finding (finding-for-range-with-bindings rule path source first r-end
+                                             (list
+                                               (cons "L" l-binding)
+                                               (cons "R" r-binding)))]
+                                  [propagator (list (cons 'finding finding)
+                                                (cons 'from "$R")
+                                                (cons 'to "$L")
+                                                (cons 'by-side-effect #t)
+                                                (cons 'label #f)
+                                                (cons 'requires #f)
+                                                (cons 'replace-labels #f)
+                                                (cons
+                                                  'implicit-assignment
+                                                  #t))])
+                             (loop next (cons propagator acc)))))))))))
   (def (label-token-char? ch)
        (or (char-alphabetic? ch)
            (char-numeric? ch)
@@ -32874,8 +32971,15 @@
               [propagators (scan-taint-propagators rule
                              (alist-ref/default taint 'propagators '())
                              language path source target-root)]
-              [implicit-propagators (scan-implicit-assignment-propagators rule language path source
-                                      target-root)]
+              [implicit-propagators (append
+                                      (scan-implicit-assignment-propagators rule language path source
+                                        target-root)
+                                      (if (go-language? language)
+                                          (scan-go-implicit-assignment-propagators
+                                            rule
+                                            path
+                                            source)
+                                          '()))]
               [sanitizers (scan-taint-specs rule (alist-ref/default taint 'sanitizers '())
                             language path source target-root #f)]
               [sources (expand-taint-sources rule initial-sources source-matches
diff --git a/src/.jerbuild-hashes b/src/.jerbuild-hashes
index 3b9d596..db52f43 100644
--- 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" . "351112D8D439582D")
-  ("src/semgrep/output/text.ss" . "BE476CB84B807FBA")
-  ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1")
+  ("src/semgrep/scan.ss" . "3C8AA38427B2EE0A")
   ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0")
   ("src/semgrep/rule.ss" . "E12C108153C181FA")
+  ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1")
+  ("src/semgrep/output/text.ss" . "BE476CB84B807FBA")
   ("src/semgrep/match/structural.ss" . "6FE77014EE9FDCE4")
   ("src/semgrep/main.ss" . "A4EC9E7F2A09D25E")
   ("src/semgrep/cli.ss" . "EBDC4B1DAD3F13CC"))
diff --git a/src/semgrep/scan.ss b/src/semgrep/scan.ss
index eb6ce21..1f16ad9 100644
--- a/src/semgrep/scan.ss
+++ b/src/semgrep/scan.ss
@@ -31089,6 +31089,81 @@
                          target-root))))
               (implicit-assignment-patterns language))))
 
+;; Go's `$L := $R` / `$L = $R` matching binds $R to only the head token of a
+;; call RHS (e.g. `make` in `items := make(global.Items, ...)`), so taint does
+;; not flow when the tainted value is nested inside the right-hand expression.
+;; These helpers build implicit-assignment propagators whose `$R` spans the
+;; whole right-hand side, so a tainted token anywhere in it propagates to `$L`.
+(def (go-simple-assignment-parts source first line-end)
+  (and (< first line-end)
+       (let ([c0 (string-ref source first)])
+         (or (char-alphabetic? c0) (char=? c0 #\_)))
+       (let loop ([i (+ first 1)])
+         (cond
+           [(and (< i line-end) (identifier-token-char? (string-ref source i)))
+            (loop (+ i 1))]
+           [else
+            (let* ([l-end i]
+                   [j (skip-horizontal-forward source i)])
+              (cond
+                [(and (< (+ j 1) line-end)
+                      (char=? (string-ref source j) #\:)
+                      (char=? (string-ref source (+ j 1)) #\=))
+                 (cons l-end (+ j 2))]
+                [(and (< j line-end)
+                      (char=? (string-ref source j) #\=)
+                      (or (>= (+ j 1) line-end)
+                          (not (char=? (string-ref source (+ j 1)) #\=))))
+                 (cons l-end (+ j 1))]
+                [else #f]))]))))
+
+(def (go-assignment-rhs-end source r-start line-end)
+  (let* ([raw-end (go-line-trimmed-end source r-start line-end)]
+         [comment (string-find-substring-from source "//" r-start)]
+         [content-end (if (and comment (< comment raw-end)) comment raw-end)])
+    (let loop ([e content-end])
+      (if (and (> e r-start)
+               (let ([c (string-ref source (- e 1))])
+                 (or (char=? c #\space) (char=? c #\tab))))
+          (loop (- e 1))
+          e))))
+
+(def (scan-go-implicit-assignment-propagators rule path source)
+  (let ([len (string-length source)])
+    (let loop ([line-start 0] [acc '()])
+      (if (> line-start len)
+          (reverse acc)
+          (let* ([line-end (line-end-after source line-start)]
+                 [first (line-first-nonspace source line-start line-end)]
+                 [parts (go-simple-assignment-parts source first line-end)]
+                 [next (if (< line-end len) (+ line-end 1) (+ len 1))])
+            (if (not parts)
+                (loop next acc)
+                (let* ([l-end (car parts)]
+                       [op-end (cdr parts)]
+                       [r-start (skip-horizontal-forward source op-end)]
+                       [r-end (go-assignment-rhs-end source r-start line-end)])
+                  (if (<= r-end r-start)
+                      (loop next acc)
+                      (let* ([l-binding (metavariable-binding-for-range
+                                          "L" source first l-end)]
+                             [r-binding (metavariable-binding-for-range
+                                          "R" source r-start r-end)]
+                             [finding (finding-for-range-with-bindings
+                                        rule path source first r-end
+                                        (list (cons "L" l-binding)
+                                              (cons "R" r-binding)))]
+                             [propagator (list
+                                           (cons 'finding finding)
+                                           (cons 'from "$R")
+                                           (cons 'to "$L")
+                                           (cons 'by-side-effect #t)
+                                           (cons 'label #f)
+                                           (cons 'requires #f)
+                                           (cons 'replace-labels #f)
+                                           (cons 'implicit-assignment #t))])
+                        (loop next (cons propagator acc)))))))))))
+
 (def (label-token-char? ch)
   (or (char-alphabetic? ch)
       (char-numeric? ch)
@@ -32844,12 +32919,16 @@
                         source
                         target-root)]
          [implicit-propagators
-         (scan-implicit-assignment-propagators
-            rule
-            language
-            path
-            source
-            target-root)]
+          (append
+            (scan-implicit-assignment-propagators
+              rule
+              language
+              path
+              source
+              target-root)
+            (if (go-language? language)
+                (scan-go-implicit-assignment-propagators rule path source)
+                '()))]
          [sanitizers (scan-taint-specs
                        rule
                        (alist-ref/default taint 'sanitizers '())
diff --git a/tests/smoke.ss b/tests/smoke.ss
index aedc586..28194d3 100644
--- a/tests/smoke.ss
+++ b/tests/smoke.ss
@@ -4261,6 +4261,18 @@
     (check (length findings) => 1)
     (check (finding-start-line (car findings)) => 5)))
 
+(test-case "scan Go taint flows through := assignment and nested call"
+  (let* ([taint-config
+          "rules:\n  - id: demo.taint.go.shortvar\n    mode: taint\n    languages: [go]\n    message: go shortvar taint\n    severity: WARNING\n    pattern-sources:\n      - pattern: getGlobal(...)\n    pattern-sanitizers:\n      - pattern: sanitizeGlobal(...)\n    pattern-sinks:\n      - pattern: sink(...)\n"]
+         [findings
+          (scan-config-string
+            taint-config
+            "go"
+            "demo.go"
+            "func f() {\n\tg := getGlobal()\n\titems := make(g.Items, len(g.items))\n\tsink(items)\n\tclean := sanitizeGlobal(g)\n\tsink(clean)\n}\n")])
+    (check (length findings) => 1)
+    (check (finding-start-line (car findings)) => 4)))
+
 (test-case "scan PHP taint ignores sink after break but resumes at case"
   (let* ([taint-config
           "rules:\n  - id: demo.taint.php.break\n    mode: taint\n    languages: [php]\n    message: unreachable php taint\n    severity: WARNING\n    pattern-sources:\n      - pattern: $source\n    pattern-sinks:\n      - pattern: sink(...)\n"]