reference Flags

--dataflow-traces

Accepted by
opengrep scan, opengrep ci
See also
mode: taint, taint_intrafile, --sarif, --matching-explanations, --output-enclosing-context

Adds to each finding of a taint rule the path the tainted data took from the source to the sink. Without the flag, the text output shows only the sink.

Reading a trace

A trace is a list of steps, one per line, each with a label and the line of code it refers to:

LabelStep
sourceThe code that a source of the rule matches.
throughA variable that holds the tainted data.
through callA call that the data goes through, entering the called function by an argument and leaving it by its return value or by a field of an argument.
source in callA call to a function that contains the source.
sink in callA call to a function that contains the sink.
paramThe parameter by which the data enters a called function.
returnThe return by which the data leaves a called function.
sinkThe code that a sink of the rule matches.

The steps inside a called function are drawn under its call, one level deeper. A function called within a called function is nested in turn. These steps appear only when the analysis follows calls: with the rule option taint_intrafile, the flag --taint-intrafile, or the cross-file analysis of --enable-interfile. Otherwise, a call appears as a single through step.

Examples

The path from source to sink

rule.yaml
rules:  - id: input-to-eval    mode: taint    pattern-sources:      - pattern: input(...)    pattern-sinks:      - pattern: eval(...)    message: input reaches eval    languages: [python]    severity: ERROR
app.py
x = input()y = xeval(y)
Command and result
$ opengrep scan -q --config rule.yaml --dataflow-traces app.py
app.py

  error  input-to-eval
  input reaches eval

     3: eval(y)
        │
        ├─ source           1: x = input()
        ├─ through          1: x = input()
        ├─ through          2: y = x
        └─ sink             3: eval(y)

Through a function call

sql.yaml
rules:  - id: sql-injection    mode: taint    pattern-sources:      - pattern: request.args.get(...)    pattern-sinks:      - pattern: cursor.execute(...)    message: user input reaches a SQL query    languages: [python]    severity: ERROR
search.py
def build_query(name):    query = "SELECT * FROM users WHERE name = '" + name + "'"    return querydef search():    name = request.args.get("name")    query = build_query(name)    cursor.execute(query)
Command and result
$ opengrep scan -q --config sql.yaml --dataflow-traces search.py
search.py

  error  sql-injection
  user input reaches a SQL query

     8: cursor.execute(query)
        │
        ├─ source           6: name = request.args.get("name")
        ├─ through          6: name = request.args.get("name")
        ├─ through          7: query = build_query(name)
        └─ sink             8: cursor.execute(query)
$ opengrep scan -q --config sql.yaml --dataflow-traces --taint-intrafile search.py
search.py

  error  sql-injection
  user input reaches a SQL query

     8: cursor.execute(query)
        │
        ├─ source           6: name = request.args.get("name")
        ├─ through          6: name = request.args.get("name")
        ├─ through call     7: query = build_query(name)
        │  ├─ param            1: def build_query(name):
        │  ├─ through          2: query = "SELECT * FROM users WHERE name = '" + name + "'"
        │  └─ return           3: return query
        ├─ through          7: query = build_query(name)
        └─ sink             8: cursor.execute(query)

The first scan treats the call to build_query as a single step. With --taint-intrafile, the trace also shows the path inside the function.

A source and a sink in called functions

sql.yaml
rules:  - id: sql-injection    mode: taint    pattern-sources:      - pattern: request.args.get(...)    pattern-sinks:      - pattern: cursor.execute(...)    message: user input reaches a SQL query    languages: [python]    severity: ERROR
calls.py
def get_name():    name = request.args.get("name")    return namedef run_query(name):    sql = "SELECT * FROM users WHERE name = '" + name + "'"    cursor.execute(sql)def search():    run_query(get_name())
Command and result
$ opengrep scan -q --config sql.yaml --dataflow-traces --taint-intrafile calls.py
calls.py

  error  sql-injection
  user input reaches a SQL query

     7: cursor.execute(sql)
        │
        ├─ source in call   10: run_query(get_name())
        │  ├─ source            2: name = request.args.get("name")
        │  ├─ through           2: name = request.args.get("name")
        │  └─ return            3: return name
        └─ sink in call     10: run_query(get_name())
           ├─ param             5: def run_query(name):
           ├─ through           6: sql = "SELECT * FROM users WHERE name = '" + name + "'"
           └─ sink              7: cursor.execute(sql)

The finding is reported at the sink, inside run_query. The trace starts at the call in search that connects the two functions.

JSON schema

In the JSON output, each result of a taint rule holds its trace in extra.dataflow_trace, with or without the flag. The trace is a list of steps in execution order, and each step is an object with these fields:

FieldValue
kindThe kind of step: source, through, through_call, source_in_call, sink_in_call, parameter, return or sink. These are the labels of the text output, with _ for a space and parameter for param.
locationThe path of the file, and the start and end of the code. Each has a line and a col, counted from 1, and an offset from the start of the file, counted from 0. end is the position after the last character.
contentThe code at the location, as written in the file; for a call, the name of the called function.
childrenFor a call, the steps inside the called function, in the same form; for other steps, an empty list.

A trace in the JSON output

rule.yaml
rules:  - id: input-to-eval    mode: taint    options:      taint_intrafile: true    pattern-sources:      - pattern: input(...)    pattern-sinks:      - pattern: eval(...)    message: input reaches eval    languages: [python]    severity: ERROR
app.py
def run(code):    eval(code)run(input())
Command and result
$ opengrep scan -q --config rule.yaml --dataflow-traces app.py
app.py

  error  input-to-eval
  input reaches eval

     2: eval(code)
        │
        ├─ source           4: run(input())
        └─ sink in call     4: run(input())
           ├─ param            1: def run(code):
           └─ sink             2: eval(code)
$ opengrep scan --config rule.yaml --json app.py | jq .
{
  "version": "X.Y.Z",
  "results": [
    {
      "check_id": "input-to-eval",
      "path": "app.py",
      "start": {
        "line": 2,
        "col": 5,
        "offset": 19
      },
      "end": {
        "line": 2,
        "col": 15,
        "offset": 29
      },
      "extra": {
        "metavars": {},
        "message": "input reaches eval",
        "metadata": {},
        "severity": "ERROR",
        "fingerprint": "d41d752bf0b1ea72530217bd06cc03b7d24d71fb0d4af0ec9374a71b66caf9c82c17a25f10ae71f05a05fe5fc45177fabbd13b8dd7b991b62d833d75e641d928_0",
        "lines": "    eval(code)",
        "is_ignored": false,
        "validation_state": "NO_VALIDATOR",
        "dataflow_trace": [
          {
            "kind": "source",
            "location": {
              "path": "app.py",
              "start": {
                "line": 4,
                "col": 5,
                "offset": 35
              },
              "end": {
                "line": 4,
                "col": 12,
                "offset": 42
              }
            },
            "content": "input()",
            "children": []
          },
          {
            "kind": "sink_in_call",
            "location": {
              "path": "app.py",
              "start": {
                "line": 4,
                "col": 1,
                "offset": 31
              },
              "end": {
                "line": 4,
                "col": 4,
                "offset": 34
              }
            },
            "content": "run",
            "children": [
              {
                "kind": "parameter",
                "location": {
                  "path": "app.py",
                  "start": {
                    "line": 1,
                    "col": 9,
                    "offset": 8
                  },
                  "end": {
                    "line": 1,
                    "col": 13,
                    "offset": 12
                  }
                },
                "content": "code",
                "children": []
              },
              {
                "kind": "sink",
                "location": {
                  "path": "app.py",
                  "start": {
                    "line": 2,
                    "col": 5,
                    "offset": 19
                  },
                  "end": {
                    "line": 2,
                    "col": 15,
                    "offset": 29
                  }
                },
                "content": "eval(code)",
                "children": []
              }
            ]
          }
        ],
        "engine_kind": "Opengrep"
      }
    }
  ],
  "errors": [],
  "paths": {
    "scanned": [
      "app.py"
    ]
  },
  "interfile_languages_used": [],
  "skipped_rules": []
}

SARIF schema

With the flag, each result of a taint rule in the SARIF output has a codeFlows list with one code flow. The results of a rule that runs across files with --enable-interfile have it without the flag as well.

The message of the code flow names the file and line of the source and of the finding, as in Untrusted dataflow from app.py:4 to app.py:2. The code flow has one thread flow, whose locations are the steps of the trace in execution order, each call followed by the steps inside the called function. Each location has these fields:

FieldValue
nestingLevel0 for a step of the trace, one more for each call that the step is inside.
kinds["call"] for a call, ["enter"] for a parameter and ["exit"] for a return; absent for the other steps.
location.message.textThe kind of step, the code at the location and its file and line, as in Source: 'input()' @ 'app.py:4'; for a call, the code is the name of the called function. The kind is Source: for a source, Call: for a call, Sink: for a sink and Propagator : for the other steps.
location.physicalLocationThe file, in artifactLocation.uri, and the region of the code: startLine, startColumn, endLine and endColumn, the code in snippet.text, and the same message.

A trace in the SARIF output

rule.yaml
rules:  - id: input-to-eval    mode: taint    options:      taint_intrafile: true    pattern-sources:      - pattern: input(...)    pattern-sinks:      - pattern: eval(...)    message: input reaches eval    languages: [python]    severity: ERROR
app.py
def run(code):    eval(code)run(input())
Command and result
$ opengrep scan --config rule.yaml --sarif --dataflow-traces app.py | jq .
{
  "version": "2.1.0",
  "runs": [
    {
      "invocations": [
        {
          "executionSuccessful": true,
          "toolExecutionNotifications": []
        }
      ],
      "results": [
        {
          "codeFlows": [
            {
              "message": {
                "text": "Untrusted dataflow from app.py:4 to app.py:2"
              },
              "threadFlows": [
                {
                  "locations": [
                    {
                      "location": {
                        "message": {
                          "text": "Source: 'input()' @ 'app.py:4'"
                        },
                        "physicalLocation": {
                          "artifactLocation": {
                            "uri": "app.py"
                          },
                          "region": {
                            "endColumn": 12,
                            "endLine": 4,
                            "message": {
                              "text": "Source: 'input()' @ 'app.py:4'"
                            },
                            "snippet": {
                              "text": "input()"
                            },
                            "startColumn": 5,
                            "startLine": 4
                          }
                        }
                      },
                      "nestingLevel": 0
                    },
                    {
                      "kinds": [
                        "call"
                      ],
                      "location": {
                        "message": {
                          "text": "Call: 'run' @ 'app.py:4'"
                        },
                        "physicalLocation": {
                          "artifactLocation": {
                            "uri": "app.py"
                          },
                          "region": {
                            "endColumn": 4,
                            "endLine": 4,
                            "message": {
                              "text": "Call: 'run' @ 'app.py:4'"
                            },
                            "snippet": {
                              "text": "run"
                            },
                            "startColumn": 1,
                            "startLine": 4
                          }
                        }
                      },
                      "nestingLevel": 0
                    },
                    {
                      "kinds": [
                        "enter"
                      ],
                      "location": {
                        "message": {
                          "text": "Propagator : 'code' @ 'app.py:1'"
                        },
                        "physicalLocation": {
                          "artifactLocation": {
                            "uri": "app.py"
                          },
                          "region": {
                            "endColumn": 13,
                            "endLine": 1,
                            "message": {
                              "text": "Propagator : 'code' @ 'app.py:1'"
                            },
                            "snippet": {
                              "text": "code"
                            },
                            "startColumn": 9,
                            "startLine": 1
                          }
                        }
                      },
                      "nestingLevel": 1
                    },
                    {
                      "location": {
                        "message": {
                          "text": "Sink: 'eval(code)' @ 'app.py:2'"
                        },
                        "physicalLocation": {
                          "artifactLocation": {
                            "uri": "app.py"
                          },
                          "region": {
                            "endColumn": 15,
                            "endLine": 2,
                            "message": {
                              "text": "Sink: 'eval(code)' @ 'app.py:2'"
                            },
                            "snippet": {
                              "text": "eval(code)"
                            },
                            "startColumn": 5,
                            "startLine": 2
                          }
                        }
                      },
                      "nestingLevel": 1
                    }
                  ]
                }
              ]
            }
          ],
          "fingerprints": {
            "matchBasedId/v1": "d41d752bf0b1ea72530217bd06cc03b7d24d71fb0d4af0ec9374a71b66caf9c82c17a25f10ae71f05a05fe5fc45177fabbd13b8dd7b991b62d833d75e641d928_0"
          },
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": {
                  "uri": "app.py",
                  "uriBaseId": "%SRCROOT%"
                },
                "region": {
                  "endColumn": 15,
                  "endLine": 2,
                  "snippet": {
                    "text": "    eval(code)"
                  },
                  "startColumn": 5,
                  "startLine": 2
                }
              }
            }
          ],
          "message": {
            "text": "input reaches eval"
          },
          "properties": {},
          "ruleId": "input-to-eval"
        }
      ],
      "tool": {
        "driver": {
          "name": "Opengrep",
          "rules": [
            {
              "defaultConfiguration": {
                "level": "error"
              },
              "fullDescription": {
                "text": "input reaches eval"
              },
              "help": {
                "markdown": "input reaches eval",
                "text": "input reaches eval"
              },
              "id": "input-to-eval",
              "name": "input-to-eval",
              "properties": {
                "precision": "very-high",
                "tags": []
              },
              "shortDescription": {
                "text": "Opengrep Finding: input-to-eval"
              }
            }
          ],
          "semanticVersion": "X.Y.Z"
        }
      }
    }
  ],
  "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json"
}