Repository: semgrep/semgrep (language: python)

Issue:
 integrate tree-sitter java parser into sgrep



Discussion / hints:
there is already a Java parser in  sgrep; it parses 98% code.
the title should be integrate tree-sitter java parser into sgrep

Final code lives in https://github.com/returntocorp/ocaml-tree-sitter/tree/master/proto
Closing until broader tree-sitter work lands. Java is already supported. CC @DrewDennison @nbrahms 

Fix patch:
diff --git a/changelog.d/code-7750.added b/changelog.d/code-7750.added
new file mode 100644
index 000000000000..433d706a5afb
--- /dev/null
+++ b/changelog.d/code-7750.added
@@ -0,0 +1,4 @@
+Semgrep Pro engine now resolves method invocations on abstract classes. In
+addition to the existing resolution for interface method invocations, this
+change further enhances dataflow tracking accuracy for dynamic method
+invocations.
diff --git a/changelog.d/saf-1687.fixed b/changelog.d/saf-1687.fixed
new file mode 100644
index 000000000000..aa92b744b7e0
--- /dev/null
+++ b/changelog.d/saf-1687.fixed
@@ -0,0 +1,2 @@
+Target file selection in git projects: files containing special characters
+(according to git) are now scanned correctly instead of being ignored.
diff --git a/cli/src/semdep/parsers/cargo.py b/cli/src/semdep/parsers/cargo.py
index 391742cfd4c0..d09de2695bd1 100644
--- a/cli/src/semdep/parsers/cargo.py
+++ b/cli/src/semdep/parsers/cargo.py
@@ -29,6 +29,7 @@ def parse_dep(s: str, raw_lines: List[str]) -> FoundDependency:
             resolved_url=None,
             allowed_hashes=hash,
             transitivity=Transitivity(Unknown()),
+            manifest_path=None,
             lockfile_path=Fpath(str(lockfile_path)),
             line_number=raw_lines.index(lines[0]) + 1,
         )
diff --git a/cli/src/semdep/parsers/composer.py b/cli/src/semdep/parsers/composer.py
index 9ca7709eb073..378043c65e68 100644
--- a/cli/src/semdep/parsers/composer.py
+++ b/cli/src/semdep/parsers/composer.py
@@ -119,6 +119,7 @@ def parse_composer_lock(
                 transitivity=transitivity(manifest_deps, [package]),
                 line_number=line_number,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             )
         )
     # Return the output list containing FoundDependency objects
diff --git a/cli/src/semdep/parsers/gem.py b/cli/src/semdep/parsers/gem.py
index d6779f56e6cb..5af09e644ef3 100644
--- a/cli/src/semdep/parsers/gem.py
+++ b/cli/src/semdep/parsers/gem.py
@@ -116,6 +116,7 @@ def parse_gemfile(
                     transitivity=transitivity(manifest_deps, [dep[0]]),
                     line_number=line_number,
                     lockfile_path=Fpath(str(lockfile_path)),
+                    manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
                 )
             )
     return output, errors
diff --git a/cli/src/semdep/parsers/go_mod.py b/cli/src/semdep/parsers/go_mod.py
index cf47e405c7b5..76acc142ea69 100644
--- a/cli/src/semdep/parsers/go_mod.py
+++ b/cli/src/semdep/parsers/go_mod.py
@@ -106,6 +106,9 @@ def parse_go_mod(
                             line_number=line_number,
                             resolved_url=package,  # Go package names are URLs
                             lockfile_path=Fpath(str(lockfile_path)),
+                            manifest_path=Fpath(str(manifest_path))
+                            if manifest_path
+                            else None,
                         )
                     )
     return [d for d in output if (d.package, d.version) not in exclude], errors
diff --git a/cli/src/semdep/parsers/gradle.py b/cli/src/semdep/parsers/gradle.py
index 8b29129649d9..d876d9abce0b 100644
--- a/cli/src/semdep/parsers/gradle.py
+++ b/cli/src/semdep/parsers/gradle.py
@@ -100,6 +100,7 @@ def parse_gradle(
                 transitivity=transitivity(parsed_manifest, [package]),
                 line_number=line_number,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             )
         )
     return output, errors
diff --git a/cli/src/semdep/parsers/mix.py b/cli/src/semdep/parsers/mix.py
index 92de091144f1..4061f167557c 100644
--- a/cli/src/semdep/parsers/mix.py
+++ b/cli/src/semdep/parsers/mix.py
@@ -212,6 +212,7 @@ def _build_found_dependencies(
     lockfile_path: Path,
     direct_deps: set[str],
     lockfile_deps: list[tuple[int, tuple[str, str]] | None],
+    manifest_path: Path | None,
 ) -> list[FoundDependency]:
     result = []
     for dep in lockfile_deps:
@@ -227,6 +228,7 @@ def _build_found_dependencies(
                 transitivity=transitivity(direct_deps, [package]),
                 line_number=line_number,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             )
         )
 
@@ -254,7 +256,10 @@ def parse_mix(
         [x for x in parsed_manifest if isinstance(x, tuple)]
     )
     found_deps = _build_found_dependencies(
-        lockfile_path, direct_deps, [x for x in parsed_lockfile if isinstance(x, tuple)]
+        lockfile_path,
+        direct_deps,
+        [x for x in parsed_lockfile if isinstance(x, tuple)],
+        manifest_path,
     )
 
     return found_deps, errors
diff --git a/cli/src/semdep/parsers/package_lock.py b/cli/src/semdep/parsers/package_lock.py
index 64490fa03cc1..66d8c5d37127 100644
--- a/cli/src/semdep/parsers/package_lock.py
+++ b/cli/src/semdep/parsers/package_lock.py
@@ -48,6 +48,7 @@ def parse_package_name(package_path: str) -> str:
 def parse_packages_field(
     lockfile_path: Path,
     deps: Dict[str, JSON],
+    manifest_path: Optional[Path],
 ) -> List[FoundDependency]:
     try:
         manifest_deps = set(deps[""].as_dict()["dependencies"].as_dict().keys())
@@ -85,6 +86,7 @@ def parse_packages_field(
                 else transitivity(manifest_deps, [package_name]),
                 line_number=dep_json.line_number,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             )
         )
     return output
@@ -95,6 +97,7 @@ def parse_dependencies_field(
     deps: Dict[str, JSON],
     manifest_deps: Optional[Set[str]],
     nested: bool,
+    manifest_path: Optional[Path],
 ) -> List[FoundDependency]:
     # Dependency dicts in a package-lock.json can be nested:
     # {"foo" : {stuff, "dependencies": {"bar": stuff, "dependencies": {"baz": stuff}}}}
@@ -127,13 +130,18 @@ def parse_dependencies_field(
                 else transitivity(manifest_deps, [package]),
                 line_number=dep_json.line_number,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             )
         )
         nested_deps = fields.get("dependencies")
         if nested_deps:
             output.extend(
                 parse_dependencies_field(
-                    lockfile_path, nested_deps.as_dict(), manifest_deps, True
+                    lockfile_path,
+                    nested_deps.as_dict(),
+                    manifest_deps,
+                    True,
+                    manifest_path,
                 )
             )
     return output
@@ -167,7 +175,10 @@ def parse_package_lock(
         if deps is None:
             logger.debug("Found package-lock with no 'packages'")
             return [], errors
-        return parse_packages_field(lockfile_path, deps.as_dict()), errors
+        return (
+            parse_packages_field(lockfile_path, deps.as_dict(), manifest_path),
+            errors,
+        )
     else:
         deps = lockfile_json.get("dependencies")
         if deps is None:
@@ -186,7 +197,7 @@ def parse_package_lock(
 
         return (
             parse_dependencies_field(
-                lockfile_path, deps.as_dict(), manifest_deps, False
+                lockfile_path, deps.as_dict(), manifest_deps, False, manifest_path
             ),
             errors,
         )
diff --git a/cli/src/semdep/parsers/packages_lock_c_sharp.py b/cli/src/semdep/parsers/packages_lock_c_sharp.py
index b2f9f438b2f6..657b0baf239b 100644
--- a/cli/src/semdep/parsers/packages_lock_c_sharp.py
+++ b/cli/src/semdep/parsers/packages_lock_c_sharp.py
@@ -38,7 +38,7 @@ def map_to_transitivity(type_value: Optional[str]) -> Transitivity:
 
 
 def parse_dependencies_field(
-    lockfile_path: Path, deps: Dict[str, JSON]
+    lockfile_path: Path, deps: Dict[str, JSON], manifest_path: Optional[Path]
 ) -> List[FoundDependency]:
     output = []
 
@@ -66,6 +66,7 @@ def parse_dependencies_field(
                     transitivity=map_to_transitivity(transitivity_str),
                     line_number=package_json.line_number,
                     lockfile_path=Fpath(str(lockfile_path)),
+                    manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
                 )
             )
 
@@ -91,4 +92,7 @@ def parse_packages_lock(
         logger.warn("Found packages.lock.json with no 'dependencies'")
         return [], errors
 
-    return parse_dependencies_field(lockfile_path, deps.as_dict()), errors
+    return (
+        parse_dependencies_field(lockfile_path, deps.as_dict(), _manifest_path),
+        errors,
+    )
diff --git a/cli/src/semdep/parsers/pipfile.py b/cli/src/semdep/parsers/pipfile.py
index febdfbb6852c..08748a5827f3 100644
--- a/cli/src/semdep/parsers/pipfile.py
+++ b/cli/src/semdep/parsers/pipfile.py
@@ -129,6 +129,7 @@ def extract_pipfile_hashes(
                 ),
                 line_number=dep_json.line_number,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             )
         )
     return output, errors
diff --git a/cli/src/semdep/parsers/pnpm.py b/cli/src/semdep/parsers/pnpm.py
index afe8cba47d5d..1877f227e464 100644
--- a/cli/src/semdep/parsers/pnpm.py
+++ b/cli/src/semdep/parsers/pnpm.py
@@ -61,7 +61,7 @@ def parse_package_key_post_9(key: str) -> Optional[Tuple[str, str]]:
 
 
 def parse_pnpm(
-    lockfile_path: Path, _: Optional[Path]
+    lockfile_path: Path, manifest_path: Optional[Path]
 ) -> Tuple[List[FoundDependency], List[DependencyParserError]]:
     parsed_lockfile, parsed_manifest, errors = safe_parse_lockfile_and_manifest(
         DependencyFileToParse(
@@ -140,6 +140,7 @@ def parse_pnpm(
                 line_number=line_number,
                 allowed_hashes={},
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             )
         )
     return output, errors
diff --git a/cli/src/semdep/parsers/poetry.py b/cli/src/semdep/parsers/poetry.py
index d2009fd6e456..dee0eb97eb19 100644
--- a/cli/src/semdep/parsers/poetry.py
+++ b/cli/src/semdep/parsers/poetry.py
@@ -226,6 +226,7 @@ def parse_poetry(
                 ),
                 line_number=dep["name"].line_number,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             )
         )
     return output, errors
diff --git a/cli/src/semdep/parsers/pom_tree.py b/cli/src/semdep/parsers/pom_tree.py
index fee2a1fce37a..6e6324cc3396 100644
--- a/cli/src/semdep/parsers/pom_tree.py
+++ b/cli/src/semdep/parsers/pom_tree.py
@@ -110,7 +110,7 @@ def get_children(deps: List[Any]) -> List[ParsedDependency]:
 
 
 def parse_pom_tree(
-    tree_path: Path, _: Optional[Path]
+    tree_path: Path, manifest_path: Optional[Path]
 ) -> Tuple[List[FoundDependency], List[DependencyParserError]]:
     parsed_lockfile, parsed_manifest, errors = safe_parse_lockfile_and_manifest(
         DependencyFileToParse(tree_path, pom_tree, ScaParserName(Pomtree())),
@@ -139,6 +139,7 @@ def parse_pom_tree(
                 line_number=match.line_number,
                 children=match.children,
                 lockfile_path=Fpath(str(tree_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             )
         )
     return output, errors
diff --git a/cli/src/semdep/parsers/pubspec_lock.py b/cli/src/semdep/parsers/pubspec_lock.py
index 79ce0603431d..5aaeb652c8fb 100644
--- a/cli/src/semdep/parsers/pubspec_lock.py
+++ b/cli/src/semdep/parsers/pubspec_lock.py
@@ -24,7 +24,7 @@
 
 
 def parse_pubspec_lock(
-    lockfile_path: Path, _: Optional[Path]
+    lockfile_path: Path, manifest_path: Optional[Path]
 ) -> Tuple[List[FoundDependency], List[DependencyParserError]]:
     parsed_lockfile, _, errors = safe_parse_lockfile_and_manifest(
         DependencyFileToParse(
@@ -64,6 +64,7 @@ def parse_pubspec_lock(
                     line_number=key.span.start.line,
                     allowed_hashes={},
                     lockfile_path=Fpath(str(lockfile_path)),
+                    manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
                 )
             )
     except KeyError:
diff --git a/cli/src/semdep/parsers/requirements.py b/cli/src/semdep/parsers/requirements.py
index 91b74c409299..8503c8b855dd 100644
--- a/cli/src/semdep/parsers/requirements.py
+++ b/cli/src/semdep/parsers/requirements.py
@@ -144,6 +144,7 @@ def parse_requirements(
                 transitivity=transitivity(manifest_deps, [package]),
                 line_number=line_number,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             )
         )
     return output, errors
diff --git a/cli/src/semdep/parsers/swiftpm.py b/cli/src/semdep/parsers/swiftpm.py
index e3dd28cbcd43..80c8b1e1fef3 100644
--- a/cli/src/semdep/parsers/swiftpm.py
+++ b/cli/src/semdep/parsers/swiftpm.py
@@ -111,7 +111,10 @@
 
 
 def parse_swiftpm_v2(
-    lockfile_path: Path, lockfile: Dict[str, JSON], direct_deps: Set[str]
+    lockfile_path: Path,
+    lockfile: Dict[str, JSON],
+    direct_deps: Set[str],
+    manifest_path: Optional[Path],
 ) -> List[FoundDependency]:
     result = []
 
@@ -151,6 +154,7 @@ def parse_swiftpm_v2(
                 git_ref=revision.as_str() if revision else None,
                 resolved_url=repository_url.as_str() if repository_url else None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             )
         )
 
@@ -158,7 +162,10 @@ def parse_swiftpm_v2(
 
 
 def parse_swiftpm_v1(
-    lockfile_path: Path, lockfile: Dict[str, JSON], direct_deps: Set[str]
+    lockfile_path: Path,
+    lockfile: Dict[str, JSON],
+    direct_deps: Set[str],
+    manifest_path: Optional[Path],
 ) -> List[FoundDependency]:
     result = []
 
@@ -199,6 +206,7 @@ def parse_swiftpm_v1(
                 git_ref=revision.as_str() if revision else None,
                 resolved_url=repository_url.as_str() if repository_url else None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             )
         )
 
@@ -248,9 +256,13 @@ def parse_package_resolved(
 
     all_deps = []
     if lockfile_version_int == 1:
-        all_deps = parse_swiftpm_v1(lockfile_path, lockfile_json, direct_deps)
+        all_deps = parse_swiftpm_v1(
+            lockfile_path, lockfile_json, direct_deps, manifest_path
+        )
     elif lockfile_version_int == 2:
-        all_deps = parse_swiftpm_v2(lockfile_path, lockfile_json, direct_deps)
+        all_deps = parse_swiftpm_v2(
+            lockfile_path, lockfile_json, direct_deps, manifest_path
+        )
     else:
         errors.append(
             DependencyParserError(
diff --git a/cli/src/semdep/parsers/yarn.py b/cli/src/semdep/parsers/yarn.py
index 57f237f55269..abd8f6fcd66a 100644
--- a/cli/src/semdep/parsers/yarn.py
+++ b/cli/src/semdep/parsers/yarn.py
@@ -271,6 +271,7 @@ def parse_yarn(
                 transitivity=transitivity(manifest_deps, sources),
                 line_number=line_number,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             )
         )
     return output, errors
diff --git a/cli/src/semgrep/console_scripts/entrypoint.py b/cli/src/semgrep/console_scripts/entrypoint.py
index da0e0ec396c8..c1f0a3f7d58d 100755
--- a/cli/src/semgrep/console_scripts/entrypoint.py
+++ b/cli/src/semgrep/console_scripts/entrypoint.py
@@ -149,10 +149,7 @@ def exec_pysemgrep():
 # they'll get the old behavior.
 def exec_osemgrep():
     argv = sys.argv
-    if (
-        any(pro_flag in argv for pro_flag in PRO_FLAGS)
-        or "--beta-testing-secrets-enabled" in argv
-    ):
+    if any(pro_flag in argv for pro_flag in PRO_FLAGS):
         try:
             path = find_semgrep_core_path(
                 pro=True,
diff --git a/cli/src/semgrep/formatter/json.py b/cli/src/semgrep/formatter/json.py
index 9881bd4f0e72..ee53a0a8c7fc 100644
--- a/cli/src/semgrep/formatter/json.py
+++ b/cli/src/semgrep/formatter/json.py
@@ -1,22 +1,16 @@
-import json
 from typing import Any
 from typing import Iterable
 from typing import Mapping
 from typing import Sequence
 
 import semgrep.formatter.base as base
+import semgrep.rpc_call
 import semgrep.semgrep_interfaces.semgrep_output_v1 as out
 from semgrep.error import SemgrepError
 from semgrep.rule import Rule
 from semgrep.rule_match import RuleMatch
 
 
-# This is for converting instances of classes generated by atdpy, which
-# all have a 'to_json' method.
-def to_json(x: Any) -> Any:
-    return x.to_json()
-
-
 class JsonFormatter(base.BaseFormatter):
     def format(
         self,
@@ -30,5 +24,9 @@ def format(
         output = base.to_CliOutput(
             rule_matches, semgrep_structured_errors, cli_output_extra
         )
-        # Sort keys for predictable output. This helps with snapshot tests, etc.
-        return json.dumps(output.to_json(), sort_keys=True, default=to_json)
+        # alt: we could call json.dumps() directly here, but more consistent with
+        # the other formatters to call instead rpc_call.format (this will also
+        # help to avoid code duplication when we need to some post-processing of
+        # the JSON output in osmegrep).
+        # old: return json.dumps(output.to_json(), sort_keys=True, default=to_json)
+        return semgrep.rpc_call.format(out.OutputFormat(out.Json()), ctx, output)
diff --git a/cli/src/semgrep/semgrep_interfaces b/cli/src/semgrep/semgrep_interfaces
index 14462086a61d..64dd682a52ed 160000
--- a/cli/src/semgrep/semgrep_interfaces
+++ b/cli/src/semgrep/semgrep_interfaces
@@ -1,1 +1,1 @@
-Subproject commit 14462086a61d29cafd5e0b19c3ccd950dad9643f
+Subproject commit 64dd682a52edb70b47ad52687bc1c3e0bc4e8c3b
diff --git a/cli/src/semgrep/target_manager.py b/cli/src/semgrep/target_manager.py
index d64076346f3c..2ced01d3378c 100644
--- a/cli/src/semgrep/target_manager.py
+++ b/cli/src/semgrep/target_manager.py
@@ -438,9 +438,9 @@ def _is_valid_file(self, path: Path) -> bool:
         """
         return self._is_valid_file_or_dir(path) and path.is_file()
 
-    def _parse_git_output(self, output: str) -> FrozenSet[Path]:
+    def _parse_git_output_nulsep(self, output: str) -> FrozenSet[Path]:
         """
-        Convert a newline delimited list of files to a set of path objects
+        Convert a null-delimited list of files to a set of path objects
         prepends curr_dir to all paths in said list
 
         If list is empty then returns an empty set
@@ -450,7 +450,7 @@ def _parse_git_output(self, output: str) -> FrozenSet[Path]:
         if output:
             files = frozenset(
                 p
-                for p in (self.path / elem for elem in output.strip().split("\n"))
+                for p in (self.path / elem for elem in output.split("\x00") if elem)
                 if self._is_valid_file(p)
             )
         return files
@@ -477,22 +477,23 @@ def files_from_git_ls(self) -> FrozenSet[Path]:
         )
 
         # Tracked files
-        tracked_output = run_git_command(["git", "ls-files"])
+        tracked_output = run_git_command(["git", "ls-files", "-z"])
 
         # Untracked but not ignored files
         untracked_output = run_git_command(
             [
                 "git",
                 "ls-files",
+                "-z",
                 "--others",
                 "--exclude-standard",
             ]
         )
 
-        deleted_output = run_git_command(["git", "ls-files", "--deleted"])
-        tracked = self._parse_git_output(tracked_output)
-        untracked_unignored = self._parse_git_output(untracked_output)
-        deleted = self._parse_git_output(deleted_output)
+        deleted_output = run_git_command(["git", "ls-files", "-z", "--deleted"])
+        tracked = self._parse_git_output_nulsep(tracked_output)
+        untracked_unignored = self._parse_git_output_nulsep(untracked_output)
+        deleted = self._parse_git_output_nulsep(deleted_output)
         return frozenset(tracked | untracked_unignored - deleted)
 
     def files_from_filesystem(self) -> FrozenSet[Path]:
diff --git a/libs/ast_generic/AST_generic_helpers.ml b/libs/ast_generic/AST_generic_helpers.ml
index 0de553776237..37f624bde9df 100644
--- a/libs/ast_generic/AST_generic_helpers.ml
+++ b/libs/ast_generic/AST_generic_helpers.ml
@@ -349,6 +349,11 @@ let has_keyword_attr kwd attrs =
        | KeywordAttr (kwd2, _) -> kwd =*= kwd2
        | _ -> false)
 
+let id_of_name = function
+  | G.Id (id, id_info) -> (id, id_info)
+  | G.IdQualified { G.name_last = id, _typeargs; name_info = id_info; _ } ->
+      (id, id_info)
+
 let name_is_global = function
   | Global (* OSS *)
   | GlobalName _ (* Pro *)
diff --git a/libs/ast_generic/AST_generic_helpers.mli b/libs/ast_generic/AST_generic_helpers.mli
index bb5259d7ee01..b96169285462 100644
--- a/libs/ast_generic/AST_generic_helpers.mli
+++ b/libs/ast_generic/AST_generic_helpers.mli
@@ -76,6 +76,7 @@ val dotted_ident_of_name : AST_generic.name -> AST_generic.dotted_ident
 
 (* misc *)
 
+val id_of_name : AST_generic.name -> AST_generic.ident * AST_generic.id_info
 val name_is_global : AST_generic.resolved_name_kind -> bool
 
 val parameter_to_catch_exn_opt :
diff --git a/libs/git_wrapper/Git_wrapper.ml b/libs/git_wrapper/Git_wrapper.ml
index ae8efe74753f..51be7963b6cb 100644
--- a/libs/git_wrapper/Git_wrapper.ml
+++ b/libs/git_wrapper/Git_wrapper.ml
@@ -322,20 +322,26 @@ let string_of_ls_files_kind (kind : ls_files_kind) =
   | Cached -> "--cached"
   | Others -> "--others"
 
+(* Parse the output of 'git ls-files -z' *)
+let parse_nul_separated_list_of_files str =
+  String.split_on_char '\000' str |> List.filter (( <> ) "")
+
 let ls_files ?(cwd = Fpath.v ".") ?(exclude_standard = false) ?(kinds = [])
     root_paths =
   let roots = root_paths |> List_.map Fpath.to_string in
   let kinds = kinds |> List_.map string_of_ls_files_kind in
   let cmd =
     ( git,
-      [ "-C"; !!cwd; "ls-files" ]
+      (* Unlike the default, '-z' causes the file path to not be quoted
+         when they contain special characters, simplifying parsing. *)
+      [ "-C"; !!cwd; "ls-files"; "-z" ]
       @ kinds
       @ flag "--exclude-standard" exclude_standard
       @ roots )
   in
   let files =
-    match UCmd.lines_of_run ~trim:true cmd with
-    | Ok (files, (_, `Exited 0)) -> files
+    match UCmd.string_of_run ~trim:true cmd with
+    | Ok (data, (_, `Exited 0)) -> parse_nul_separated_list_of_files data
     | _ -> raise (Error "Could not get files from git ls-files")
   in
   files |> Fpath_.of_strings
diff --git a/libs/git_wrapper/Unit_git_wrapper.ml b/libs/git_wrapper/Unit_git_wrapper.ml
index d06d650a91c2..39c0dde15d3c 100644
--- a/libs/git_wrapper/Unit_git_wrapper.ml
+++ b/libs/git_wrapper/Unit_git_wrapper.ml
@@ -33,7 +33,17 @@ let test_ls_files_relative ~mk_cwd ~mk_scanning_root () =
   let repo_files =
     Testutil_files.
       [
-        dir "a" [ dir "b" [ file "target" ] ]; dir "x" [ dir "y" [ file "z" ] ];
+        dir "a"
+          [
+            dir "b"
+              [
+                file "target";
+                file "zero\u{200B}width-space";
+                (* smiley "😊" *)
+                file "\u{1F60A}";
+              ];
+          ];
+        dir "x" [ dir "y" [ file "z" ] ];
       ]
   in
   Testutil_git.with_git_repo ~verbose:true repo_files (fun _cwd ->
diff --git a/libs/process_limits/Unit_memory_limit.ml b/libs/process_limits/Unit_memory_limit.ml
index 42627da32f2d..74983f54da7d 100644
--- a/libs/process_limits/Unit_memory_limit.ml
+++ b/libs/process_limits/Unit_memory_limit.ml
@@ -87,7 +87,8 @@ let test_memory_limit_with_stack caps =
 let skip_if_ocaml_5 tests =
   tests
   |> List_.map (fun test ->
-         if Sys.ocaml_release.major = 5 then Testo.update ~skipped:true test
+         if Sys.ocaml_release.major = 5 then
+           Testo.update ~skipped:(Some "ocaml 5") test
          else test)
 
 let tests (caps : < Cap.memory_limit >) =
diff --git a/libs/tracing/Tracing.mli b/libs/tracing/Tracing.mli
index f4e9aa301982..a62869583a2b 100644
--- a/libs/tracing/Tracing.mli
+++ b/libs/tracing/Tracing.mli
@@ -124,8 +124,8 @@ val configure_tracing :
     NOTE: this will set the active trace endpoint to whatever is passed. This
     endpoint will be used when restarting tracing via [restart_tracing] *)
 
-val stop_tracing : unit -> unit
-(** [stop_tracing ()] explicitly shuts down the Otel collector. If tracing has
+val stop_tracing : exit_active_spans:bool -> unit -> unit
+(** [stop_tracing ~exit_active_spans ()] explicitly shuts down the Otel collector. If tracing has
     been setup this MUST be called before forking (such as in {!Parmap}), or you
     WILL experience random segfaults. This is safe to call multiple times in a
     row. See [restart_tracing] to continue tracing after calling this.
diff --git a/libs/tracing/js/Tracing.ml b/libs/tracing/js/Tracing.ml
index a1ea5a3a326c..9767113549ed 100644
--- a/libs/tracing/js/Tracing.ml
+++ b/libs/tracing/js/Tracing.ml
@@ -89,7 +89,7 @@ let otel_reporter : Logs.reporter = Logs.nop_reporter
 (* Entry points for setting up tracing *)
 (*****************************************************************************)
 
-let stop_tracing () = ()
+let stop_tracing ~exit_active_spans:_ () = ()
 let restart_tracing () = ()
 
 let configure_tracing ?(attrs = []) ?(env = "") ?(version = "")
diff --git a/libs/tracing/unix/Tracing.ml b/libs/tracing/unix/Tracing.ml
index 16c8e09a032c..4e354719c338 100644
--- a/libs/tracing/unix/Tracing.ml
+++ b/libs/tracing/unix/Tracing.ml
@@ -260,10 +260,12 @@ let otel_reporter : Logs.reporter =
 (*****************************************************************************)
 (* Span/Event entrypoints *)
 (*****************************************************************************)
-
+(* Essentially
+   https://github.com/imandra-ai/ocaml-opentelemetry/blob/fdee7fe2dd1f91a8d1f78d6ce20d2bc86d555444/src/core/opentelemetry.ml#L980-L993
+   We should switch to this once it's released! *)
 let trace_exn sp exn =
   let e = Exception.catch exn in
-  let exn_type = Printexc.to_string_default exn in
+  let exn_type = Printexc.exn_slot_name exn in
   let exn_msg = Printexc.to_string exn in
   let exn_stacktrace =
     e |> Exception.get_trace |> Printexc.raw_backtrace_to_string
@@ -356,6 +358,50 @@ let log_trace_message () =
 (* Entry points for setting up tracing *)
 (*****************************************************************************)
 
+(* Safe to call whenever *)
+let stop_tracing ~exit_active_spans () =
+  (* hack: get the backend so we can easily stop tracing at any time. See
+     [with_paused_tracing] for why we want the option to do this
+
+     See the following for why we do it this way:
+     https://github.com/imandra-ai/ocaml-opentelemetry/issues/70
+  *)
+  Otel.Collector.get_backend ()
+  |> Option.iter (fun backend ->
+         Log.info (fun m -> m "Stopping tracing");
+         let module Backend = (val backend : Otel.Collector.BACKEND) in
+         Trace_core.shutdown ();
+         (* A bit hacky also... here we use the internal trace backend to get
+            all active spans, and then exit them, and then send them *)
+         (if exit_active_spans then
+            let active_spans =
+              let active_span_tbl =
+                (Opentelemetry_trace.Internal.Active_spans.get ()).tbl
+              in
+              Opentelemetry_trace.Internal.Active_span_tbl.to_seq
+                active_span_tbl
+              |> List.of_seq
+              |> List.sort
+                   (* Sort by start time so we can exit them in order *)
+                   (fun
+                     ((_, span_begin) :
+                       _ * Opentelemetry_trace.Internal.span_begin)
+                     (_, span_begin')
+                   ->
+                     Int64_.compare span_begin.start_time span_begin'.start_time)
+              |> List_.map (fun (span, span_begin) ->
+                     Opentelemetry_trace.Internal.exit_span' span span_begin)
+            in
+            Otel.Trace.emit active_spans);
+         Backend.tick ();
+         Otel.Collector.set_backend (module Otel.Collector.Noop_backend);
+
+         (* Cleanup doesn't seem to always send so let's tick one more time to
+            flush, see:
+            https://github.com/imandra-ai/ocaml-opentelemetry/issues/69
+         *)
+         Backend.cleanup ())
+
 (* setup_otel sets the Otel tracing backend and Trace_core tracing backend *)
 let setup_otel trace_endpoint =
   (* nosemgrep: no-logs-in-library *)
@@ -366,13 +412,14 @@ let setup_otel trace_endpoint =
   (* hack: let's just keep track of the endpoint for if we restart tracing
      instead of having to pass it down everywhere. We will assume that we will
      only ever report to one endpoint for the lifetime of the program *)
-  (* coupling: [restart_tracing] *)
   active_endpoint := Some trace_endpoint;
   (* This forwards the spans from Trace to the Opentelemetry collector *)
+  (* coupling: if we change the backend here, make sure to update with_span and
+     restart_tracing to not use Opentelemetry_trace/Trace_core! *)
   Opentelemetry_trace.setup_with_otel_backend otel_backend
 
 (* Set according to README of https://github.com/imandra-ai/ocaml-opentelemetry/ *)
-let configure_tracing ?(attrs : (string * user_data) list = []) ?(env = "local")
+let configure_tracing ?(attrs : (string * user_data) list = []) ?(env = "prod")
     ?version service_name trace_endpoint =
   Otel.Globals.service_name := service_name;
   Otel.Globals.default_span_kind := Otel.Span.Span_kind_internal;
@@ -389,26 +436,6 @@ let configure_tracing ?(attrs : (string * user_data) list = []) ?(env = "local")
   Ambient_context.set_storage_provider (Ambient_context_lwt.storage ());
   setup_otel trace_endpoint
 
-let stop_tracing () =
-  (* hack: get the backend so we can easily stop tracing at any time. See
-     [with_paused_tracing] for why we want the option to do this
-
-     See the following for why we do it this way:
-     https://github.com/imandra-ai/ocaml-opentelemetry/issues/70
-  *)
-  Otel.Collector.get_backend ()
-  |> Option.iter (fun backend ->
-         Log.info (fun m -> m "Stopping tracing");
-         let module Backend = (val backend : Otel.Collector.BACKEND) in
-         Backend.tick ();
-         Trace_core.shutdown ();
-         Otel.Collector.set_backend (module Otel.Collector.Noop_backend);
-         (* Cleanup doesn't seem to always send so let's tick one more time to
-            flush, see:
-            https://github.com/imandra-ai/ocaml-opentelemetry/issues/69
-         *)
-         Backend.cleanup ())
-
 let restart_tracing () =
   (* We must re-initialize the randomness on restart since this usually happens
      after a parmap fork. If we don't do this then all parmap forks will have
@@ -429,7 +456,8 @@ let restart_tracing () =
    See https://github.com/imandra-ai/ocaml-opentelemetry/issues/68
 *)
 let with_tracing_paused f =
-  stop_tracing ();
+  (* Don't exit current spans here since we only want to pause *)
+  stop_tracing ~exit_active_spans:false ();
   Common.protect ~finally:restart_tracing f
 
 let with_tracing fname data f =
@@ -461,7 +489,16 @@ let with_tracing fname data f =
     log_trace_message ();
     f sp
   in
-  Common.protect ~finally:stop_tracing f'
+  (* coupling: [restart_tracing] *)
+  (* Ensure the otel backend always flushes traces before exiting! Normally
+     tracing stops + everything is flushed when `with_tracing` exits, but this
+     ensures it also happens when an unhandled exception occurs, or in the event
+     that Stdlib.exit is called before the user can call `stop_tracing`.
+     stop_tracing is safe to call multiple times and is a noop if tracing is not
+     setup
+  *)
+  Stdlib.at_exit (stop_tracing ~exit_active_spans:true);
+  Common.protect ~finally:(stop_tracing ~exit_active_spans:true) f'
 
 (* Alt: using cohttp_lwt (we probably want to do this when we switch to Eio w/ *)
 (* their compatibility layer)
diff --git a/src/core_scan/Parmap_targets.ml b/src/core_scan/Parmap_targets.ml
index e12fb6007622..a7546d811835 100644
--- a/src/core_scan/Parmap_targets.ml
+++ b/src/core_scan/Parmap_targets.ml
@@ -96,7 +96,7 @@ let init job =
 let finalize () =
   (* Stop tracing to ensure traces are flushed *)
   (* NOTE: this only stops tracing in the child *)
-  Tracing.stop_tracing ()
+  Tracing.stop_tracing ~exit_active_spans:false ()
 
 (* Run jobs in parallel, using number of cores specified with -j *)
 let map_targets__run_in_forked_process_do_not_modify_globals caps (ncores : int)
diff --git a/src/engine/Match_taint_spec.ml b/src/engine/Match_taint_spec.ml
index 18e7266c0200..d62b9b29e942 100644
--- a/src/engine/Match_taint_spec.ml
+++ b/src/engine/Match_taint_spec.ml
@@ -13,7 +13,7 @@
  * LICENSE for more details.
  *)
 open Common
-open Fpath_.Operators (* e.g. !! *)
+open Fpath_.Operators
 module Log = Log_tainting.Log
 module G = AST_generic
 module ME = Matching_explanation
@@ -64,7 +64,7 @@ module D = Dataflow_tainting
  *)
 
 type propagator_match = {
-  id : D.var;
+  id : Taint_rule_inst.var;
   rwm : RM.t;
   from : Range.t;
   to_ : Range.t;
@@ -78,13 +78,6 @@ type spec_matches = {
   sinks : (RM.t * R.taint_sink) list;
 }
 
-type spec_predicates = {
-  is_source : G.any -> R.taint_source Taint_spec_match.t list;
-  is_propagator : G.any -> D.a_propagator Taint_spec_match.t list;
-  is_sanitizer : G.any -> R.taint_sanitizer Taint_spec_match.t list;
-  is_sink : G.any -> R.taint_sink Taint_spec_match.t list;
-}
-
 (*****************************************************************************)
 (* Hooks *)
 (*****************************************************************************)
@@ -381,7 +374,7 @@ let is_exact_match ~match_range r =
 
 let mk_propagator_match rule (prop : propagator_match) var kind r =
   let spec_pm = RM.range_to_pattern_match_adjusted rule prop.rwm in
-  let spec : D.a_propagator = { kind; prop = prop.spec; var } in
+  let spec : Taint_rule_inst.a_propagator = { kind; prop = prop.spec; var } in
   {
     Taint_spec_match.spec;
     spec_id = prop.spec.propagator_id;
@@ -394,7 +387,7 @@ let mk_propagator_match rule (prop : propagator_match) var kind r =
  * `pattern-propagators`. Matches must be exact (overlap > 0.99) to make
  * taint propagation more precise and predictable. *)
 let any_is_in_propagators_matches_OSS rule matches any :
-    D.a_propagator Taint_spec_match.t list =
+    Taint_rule_inst.a_propagator Taint_spec_match.t list =
   match range_of_any any with
   | None -> []
   | Some r ->
@@ -411,23 +404,24 @@ let any_is_in_propagators_matches_OSS rule matches any :
 let mk_taint_spec_match_preds rule matches =
   match !hook_mk_taint_spec_match_preds with
   | None ->
-      {
-        is_source =
-          (fun any ->
-            any_is_in_matches_OSS rule matches.sources any
-              ~get_id:(fun (ts : R.taint_source) -> ts.source_id));
-        is_propagator =
-          (fun any ->
-            any_is_in_propagators_matches_OSS rule matches.propagators any);
-        is_sanitizer =
-          (fun any ->
-            any_is_in_matches_OSS rule matches.sanitizers any
-              ~get_id:(fun (ts : R.taint_sanitizer) -> ts.sanitizer_id));
-        is_sink =
-          (fun any ->
-            any_is_in_matches_OSS rule matches.sinks any
-              ~get_id:(fun (ts : R.taint_sink) -> ts.sink_id));
-      }
+      Taint_rule_inst.
+        {
+          is_source =
+            (fun any ->
+              any_is_in_matches_OSS rule matches.sources any
+                ~get_id:(fun (ts : R.taint_source) -> ts.source_id));
+          is_propagator =
+            (fun any ->
+              any_is_in_propagators_matches_OSS rule matches.propagators any);
+          is_sanitizer =
+            (fun any ->
+              any_is_in_matches_OSS rule matches.sanitizers any
+                ~get_id:(fun (ts : R.taint_sanitizer) -> ts.sanitizer_id));
+          is_sink =
+            (fun any ->
+              any_is_in_matches_OSS rule matches.sinks any
+                ~get_id:(fun (ts : R.taint_sink) -> ts.sink_id));
+        }
   | Some hook -> hook rule matches
 
 (*****************************************************************************)
@@ -437,31 +431,28 @@ let mk_taint_spec_match_preds rule matches =
 let default_effect_handler _fun_name new_effects = new_effects
 
 let taint_config_of_rule ~per_file_formula_cache
-    ?(handle_effects = default_effect_handler) xconf file ast_and_errors
+    ?(handle_effects = default_effect_handler) xconf lang file ast_and_errors
     ({ mode = `Taint spec; _ } as rule : R.taint_rule) =
   let spec_matches, expls =
-    spec_matches_of_taint_rule ~per_file_formula_cache xconf file ast_and_errors
-      rule
+    spec_matches_of_taint_rule ~per_file_formula_cache xconf !!file
+      ast_and_errors rule
   in
-  let file = Fpath.v file in
   let xconf = Match_env.adjust_xconfig_with_rule_options xconf rule.options in
-  let config = xconf.config in
-  let { is_source; is_propagator; is_sanitizer; is_sink } =
-    mk_taint_spec_match_preds rule spec_matches
-  in
-  ( {
-      Dataflow_tainting.filepath = !!file;
-      rule_id = fst rule.R.id;
-      track_control =
-        spec.sources |> snd
-        |> List.exists (fun (src : R.taint_source) -> src.source_control);
-      is_source;
-      is_propagator;
-      is_sanitizer;
-      is_sink;
-      unify_mvars = config.taint_unify_mvars;
-      handle_effects;
-    },
+  let options = xconf.config in
+  let preds = mk_taint_spec_match_preds rule spec_matches in
+  ( Taint_rule_inst.
+      {
+        lang;
+        file;
+        rule_id = fst rule.R.id;
+        options;
+        track_control =
+          spec.sources |> snd
+          |> List.exists (fun (src : R.taint_source) -> src.source_control);
+        preds;
+        handle_effects;
+        java_props_cache = Hashtbl.create 30;
+      },
     spec_matches,
     expls )
 [@@trace_trace]
diff --git a/src/engine/Match_taint_spec.mli b/src/engine/Match_taint_spec.mli
index dcef6d601eb5..93b3f3a05491 100644
--- a/src/engine/Match_taint_spec.mli
+++ b/src/engine/Match_taint_spec.mli
@@ -1,5 +1,5 @@
 type propagator_match = {
-  id : Dataflow_tainting.var;
+  id : Taint_rule_inst.var;
       (** An unique identifier for the propagator match. This is used as an
    * auxiliary variable to store the taints flowing from `from` to `to`. *)
   rwm : Range_with_metavars.t;
@@ -20,17 +20,8 @@ type spec_matches = {
       (** Ranges matched by `pattern-sinks:` *)
 }
 
-type spec_predicates = {
-  is_source : AST_generic.any -> Rule.taint_source Taint_spec_match.t list;
-  is_propagator :
-    AST_generic.any -> Dataflow_tainting.a_propagator Taint_spec_match.t list;
-  is_sanitizer :
-    AST_generic.any -> Rule.taint_sanitizer Taint_spec_match.t list;
-  is_sink : AST_generic.any -> Rule.taint_sink Taint_spec_match.t list;
-}
-
 val hook_mk_taint_spec_match_preds :
-  (Rule.rule -> spec_matches -> spec_predicates) option ref
+  (Rule.rule -> spec_matches -> Taint_rule_inst.spec_predicates) option ref
 
 (* It could be a private function, but it is also used by Deep Semgrep. *)
 (* This [formula_cache] argument is exposed here because this function is also
@@ -40,7 +31,7 @@ val hook_mk_taint_spec_match_preds :
 *)
 val taint_config_of_rule :
   per_file_formula_cache:Formula_cache.t ->
-  ?handle_effects:Dataflow_tainting.effects_handler
+  ?handle_effects:Taint_rule_inst.effects_handler
     (** Use 'handle_effects' to e.g. apply hash-consing (see 'Deep_tainting'), or
         to do some side-effect if needed.
 
@@ -48,10 +39,11 @@ val taint_config_of_rule :
           by side-effect (no pun intended), however this is not needed now because
           'Dataflow_tainting.fixpoint' already returns the set of taint effects. *) ->
   Match_env.xconfig ->
-  string (* filename *) ->
+  Lang.t ->
+  Fpath.t ->
   AST_generic.program * Tok.location list ->
   Rule.taint_rule ->
-  Dataflow_tainting.config * spec_matches * Matching_explanation.t list
+  Taint_rule_inst.t * spec_matches * Matching_explanation.t list
 
 (* Exposed for Pro *)
 
@@ -64,4 +56,4 @@ val mk_propagator_match :
   string ->
   [ `From | `To ] ->
   Range.t ->
-  Dataflow_tainting.a_propagator Taint_spec_match.t
+  Taint_rule_inst.a_propagator Taint_spec_match.t
diff --git a/src/engine/Match_tainting_mode.ml b/src/engine/Match_tainting_mode.ml
index 3886b67aad97..28368dd7590c 100644
--- a/src/engine/Match_tainting_mode.ml
+++ b/src/engine/Match_tainting_mode.ml
@@ -226,19 +226,17 @@ let pms_of_effect ~match_on (effect : Effect.t) =
 (* Main entry points *)
 (*****************************************************************************)
 
-let check_var_def lang options taint_config env id ii expr =
+let check_var_def (taint_inst : Taint_rule_inst.t) env id ii expr =
   let name = AST_to_IL.var_of_id_info id ii in
   let assign =
     G.Assign (G.N (G.Id (id, ii)) |> G.e, Tok.fake_tok (snd id) "=", expr)
     |> G.e |> G.exprstmt
   in
-  let xs = AST_to_IL.stmt lang assign in
+  let xs = AST_to_IL.stmt taint_inst.lang assign in
   let cfg, lambdas = CFG_build.cfg_of_stmts xs in
   let effects, end_mapping =
     (* There could be taint effects indeed, e.g. if 'expr' is `sink(taint)`. *)
-    let java_props_cache = D.mk_empty_java_props_cache () in
-    Dataflow_tainting.fixpoint ~in_env:env lang options taint_config
-      java_props_cache
+    Dataflow_tainting.fixpoint taint_inst ~in_env:env
       IL.{ params = []; cfg; lambdas }
   in
   let out_env = end_mapping.(cfg.exit).Dataflow_core.out_env in
@@ -246,11 +244,11 @@ let check_var_def lang options taint_config env id ii expr =
   let xtaint = Lval_env.find_lval_xtaint out_env lval in
   (xtaint, effects)
 
-let add_to_env_aux lang options taint_config env id ii opt_expr =
+let add_to_env_aux (taint_inst : Taint_rule_inst.t) env id ii opt_expr =
   let var = AST_to_IL.var_of_id_info id ii in
-  let var_type = Typing.resolved_type_of_id_info lang var.id_info in
+  let var_type = Typing.resolved_type_of_id_info taint_inst.lang var.id_info in
   let id_taints =
-    taint_config.D.is_source (G.Tk (snd id))
+    taint_inst.preds.is_source (G.Tk (snd id))
     |> List_.map (fun (x : _ Taint_spec_match.t) -> (x.spec_pm, x.spec))
     (* These sources come from the parameters to a function,
         which are not within the normal control flow of a code.
@@ -261,28 +259,25 @@ let add_to_env_aux lang options taint_config env id ii opt_expr =
   let expr_taints, expr_effects =
     match opt_expr with
     | Some e ->
-        let xtaint, effects =
-          check_var_def lang options taint_config env id ii e
-        in
+        let xtaint, effects = check_var_def taint_inst env id ii e in
         (Xtaint.to_taints xtaint, effects)
     | None -> (T.Taint_set.empty, Effects.empty)
   in
   let taints = id_taints |> T.Taint_set.union expr_taints in
   let taints =
-    Dataflow_tainting.drop_taints_if_bool_or_number options taints var_type
+    Dataflow_tainting.drop_taints_if_bool_or_number taint_inst.options taints
+      var_type
   in
   let env = env |> Lval_env.add (IL_helpers.lval_of_var var) taints in
   (env, expr_effects)
 
-let add_to_env lang options taint_config (env, effects) id id_info opt_expr =
-  let env, new_effects =
-    add_to_env_aux lang options taint_config env id id_info opt_expr
-  in
+let add_to_env taint_inst (env, effects) id id_info opt_expr =
+  let env, new_effects = add_to_env_aux taint_inst env id id_info opt_expr in
   (env, Effects.union new_effects effects)
 
-let mk_fun_input_env lang options taint_config ?(glob_env = Lval_env.empty)
+let mk_fun_input_env taint_inst ?(glob_env = Lval_env.empty)
     (fdef : IL.function_definition) =
-  let add_to_env = add_to_env lang options taint_config in
+  let add_to_env = add_to_env taint_inst in
   let in_env =
     (* For each argument, check if it's a source and, if so, add it to the input
      * environment. *)
@@ -340,8 +335,8 @@ let is_global (id_info : G.id_info) =
   let* kind, _sid = !(id_info.id_resolved) in
   Some (H.name_is_global kind)
 
-let mk_file_env lang options taint_config ast =
-  let add_to_env = add_to_env lang options taint_config in
+let mk_file_env taint_inst ast =
+  let add_to_env = add_to_env taint_inst in
   let env = ref (Lval_env.empty, Effects.empty) in
   let visitor =
     object (_self : 'self)
@@ -375,21 +370,16 @@ let mk_file_env lang options taint_config ast =
   visitor#visit_program env ast;
   !env
 
-let check_fundef lang options taint_config opt_ent ctx ?glob_env
-    java_props_cache fdef =
+let check_fundef (taint_inst : Taint_rule_inst.t) opt_ent ctx ?glob_env fdef =
   let name =
     let* ent = opt_ent in
-    let* name = AST_to_IL.name_of_entity ent in
-    Some (IL.str_of_name name)
+    AST_to_IL.name_of_entity ent
   in
-  let fdef = AST_to_IL.function_definition lang ~ctx fdef in
+  let fdef = AST_to_IL.function_definition taint_inst.lang ~ctx fdef in
   let fcfg = CFG_build.cfg_of_fdef fdef in
-  let in_env, env_effects =
-    mk_fun_input_env lang options taint_config ?glob_env fdef
-  in
+  let in_env, env_effects = mk_fun_input_env taint_inst ?glob_env fdef in
   let effects, mapping =
-    Dataflow_tainting.fixpoint ~in_env ?name lang options taint_config
-      java_props_cache fcfg
+    Dataflow_tainting.fixpoint taint_inst ~in_env ?name fcfg
   in
   let effects = Effects.union env_effects effects in
   (fcfg, effects, mapping)
@@ -412,9 +402,12 @@ let check_rule per_file_formula_cache (rule : R.taint_rule) match_hook
            let effect_pms = pms_of_effect ~match_on effect in
            matches := List.rev_append effect_pms !matches)
   in
-
-  let { path = { internal_path_to_content; _ }; xlang; lazy_ast_and_errors; _ }
-      : Xtarget.t =
+  let {
+    path = { internal_path_to_content = file; _ };
+    xlang;
+    lazy_ast_and_errors;
+    _;
+  } : Xtarget.t =
     xtarget
   in
   let lang =
@@ -430,15 +423,15 @@ let check_rule per_file_formula_cache (rule : R.taint_rule) match_hook
   in
   (* TODO: 'debug_taint' should just be part of 'res'
      * (i.e., add a "debugging" field to 'Report.match_result'). *)
-  let taint_config, _TODO_debug_taint, expls =
-    Match_taint_spec.taint_config_of_rule ~per_file_formula_cache xconf
-      !!internal_path_to_content (ast, []) rule
+  let taint_inst, _TODO_debug_taint, expls =
+    Match_taint_spec.taint_config_of_rule ~per_file_formula_cache xconf lang
+      file (ast, []) rule
   in
 
   (match !hook_setup_hook_function_taint_signature with
   | None -> ()
   | Some setup_hook_function_taint_signature ->
-      setup_hook_function_taint_signature xconf rule taint_config xtarget);
+      setup_hook_function_taint_signature rule taint_inst xtarget);
 
   (* FIXME: This is no longer needed, now we can just check the type 'n'. *)
   let ctx = ref AST_to_IL.empty_ctx in
@@ -450,17 +443,14 @@ let check_rule per_file_formula_cache (rule : R.taint_rule) match_hook
       | __else__ -> ())
     ast;
 
-  let java_props_cache = Dataflow_tainting.mk_empty_java_props_cache () in
-
-  let glob_env, glob_effects = mk_file_env lang xconf.config taint_config ast in
+  let glob_env, glob_effects = mk_file_env taint_inst ast in
   record_matches glob_effects;
 
   (* Check each function definition. *)
   Visit_function_defs.visit
     (fun opt_ent fdef ->
       let _flow, fdef_effects, _mapping =
-        check_fundef lang xconf.config taint_config opt_ent !ctx ~glob_env
-          java_props_cache fdef
+        check_fundef taint_inst opt_ent !ctx ~glob_env fdef
       in
       record_matches fdef_effects)
     ast;
@@ -473,12 +463,10 @@ let check_rule per_file_formula_cache (rule : R.taint_rule) match_hook
         |> List_.map (function G.F x -> x)
         |> G.stmt1
       in
-      let stmts = AST_to_IL.stmt lang fields in
+      let stmts = AST_to_IL.stmt taint_inst.lang fields in
       let cfg, lambdas = CFG_build.cfg_of_stmts stmts in
       let init_effects, _mapping =
-        Dataflow_tainting.fixpoint lang xconf.config taint_config
-          java_props_cache
-          IL.{ params = []; cfg; lambdas }
+        Dataflow_tainting.fixpoint taint_inst IL.{ params = []; cfg; lambdas }
       in
       record_matches init_effects)
     ast;
@@ -489,12 +477,10 @@ let check_rule per_file_formula_cache (rule : R.taint_rule) match_hook
    * treat the program itself as an anonymous function. *)
   let (), match_time =
     Common.with_time (fun () ->
-        let xs = AST_to_IL.stmt lang (G.stmt1 ast) in
+        let xs = AST_to_IL.stmt taint_inst.lang (G.stmt1 ast) in
         let cfg, lambdas = CFG_build.cfg_of_stmts xs in
         let top_effects, _mapping =
-          Dataflow_tainting.fixpoint lang xconf.config taint_config
-            java_props_cache
-            IL.{ params = []; cfg; lambdas }
+          Dataflow_tainting.fixpoint taint_inst IL.{ params = []; cfg; lambdas }
         in
         record_matches top_effects)
   in
diff --git a/src/engine/Match_tainting_mode.mli b/src/engine/Match_tainting_mode.mli
index 65b556d2feb5..3f3373daaff3 100644
--- a/src/engine/Match_tainting_mode.mli
+++ b/src/engine/Match_tainting_mode.mli
@@ -1,11 +1,5 @@
 val hook_setup_hook_function_taint_signature :
-  (Match_env.xconfig ->
-  Rule.taint_rule ->
-  Dataflow_tainting.config ->
-  Xtarget.t ->
-  unit)
-  option
-  ref
+  (Rule.taint_rule -> Taint_rule_inst.t -> Xtarget.t -> unit) option ref
 (** This is used for intra-file inter-procedural taint-tracking, and the idea is
   * that this hook will do top-sorting and infer the signature of each function
   * in the file, and while doing this it will also setup
@@ -21,9 +15,7 @@ val hook_setup_hook_function_taint_signature :
   *)
 
 val mk_fun_input_env :
-  Language.t ->
-  Rule_options_t.t ->
-  Dataflow_tainting.config ->
+  Taint_rule_inst.t ->
   ?glob_env:Taint_lval_env.t ->
   IL.function_definition ->
   Taint_lval_env.t * Shape_and_sig.Effects.t
@@ -33,13 +25,10 @@ val mk_fun_input_env :
  * It is exposed to be used by inter-file taint analysis in Pro.  *)
 
 val check_fundef :
-  Lang.t ->
-  Rule_options.t ->
-  Dataflow_tainting.config ->
+  Taint_rule_inst.t ->
   AST_generic.entity option (** entity being analyzed *) ->
   AST_to_IL.ctx ->
   ?glob_env:Taint_lval_env.t ->
-  Dataflow_tainting.java_props_cache ->
   AST_generic.function_definition ->
   IL.fun_cfg * Shape_and_sig.Effects.t * Dataflow_tainting.mapping
 (** Check a function definition using a [Dataflow_tainting.config] (which can
diff --git a/src/osemgrep/cli_ci/Github_metadata.ml b/src/osemgrep/cli_ci/Github_metadata.ml
index 4ba887239e5a..22475c599001 100644
--- a/src/osemgrep/cli_ci/Github_metadata.ml
+++ b/src/osemgrep/cli_ci/Github_metadata.ml
@@ -16,6 +16,8 @@ module Cmdl = Cmdliner.Cmd
 
 (* See https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables *)
 
+(* TODO: don't start field names with an underscore because it prevents
+   some warnings (e.g. unused variable) *)
 type env = {
   (* actually GITHUB_EVENT_PATH *)
   _GITHUB_EVENT_JSON : Yojson.Basic.t;
diff --git a/src/osemgrep/cli_scan/Test_scan_subcommand.ml b/src/osemgrep/cli_scan/Test_scan_subcommand.ml
index d908570c16ca..cca47967a720 100644
--- a/src/osemgrep/cli_scan/Test_scan_subcommand.ml
+++ b/src/osemgrep/cli_scan/Test_scan_subcommand.ml
@@ -69,6 +69,8 @@ let normalize =
 let without_settings f =
   Semgrep_envvars.with_envvar "SEMGREP_SETTINGS_FILE" "nosettings.yaml" f
 
+(* Please run all tests with this to ensure reproducibility from one
+   host to another. *)
 let with_env_app_token ?(token = dummy_app_token) f =
   Semgrep_envvars.with_envvar "SEMGREP_APP_TOKEN" token f
 
@@ -99,9 +101,8 @@ let test_nosettings ~env_app_token_set () =
     "default settings loaded with app token and no env" true
     (settings_with_no_include_env = Semgrep_settings.default)
 
-let test_basic_output (caps : Scan_subcommand.caps) : Testo.t =
-  t ~checked_output:(Testo.stdxxx ()) ~normalize __FUNCTION__ (fun () ->
-      Logs.app (fun m -> m "Snapshot for %s" __FUNCTION__);
+let test_basic_output (caps : Scan_subcommand.caps) () =
+  with_env_app_token (fun () ->
       let repo_files =
         [
           F.File ("rules.yml", eqeq_basic_content);
@@ -118,9 +119,20 @@ let test_basic_output (caps : Scan_subcommand.caps) : Testo.t =
           in
           Exit_code.Check.ok exit_code))
 
-let test_basic_verbose_output (caps : Scan_subcommand.caps) : Testo.t =
-  t ~checked_output:(Testo.stdxxx ()) ~normalize __FUNCTION__ (fun () ->
-      Logs.app (fun m -> m "Snapshot for %s" __FUNCTION__);
+(* This test fails for me (Martin) when run alone with e.g.
+
+     ./test -s "basic verbose output"
+
+   In this case, it fails to print these two lines that it normally prints
+   when run as part of the full test suite ('./test'):
+
+     [<MASKED TIMESTAMP>][INFO]: Running external command: 'git' 'ls-remote' '--get-url'
+     [<MASKED TIMESTAMP>][INFO]: error output: fatal: No remote configured to list refs from.
+
+   TODO: figure out why and fix it
+*)
+let test_basic_verbose_output (caps : Scan_subcommand.caps) () =
+  with_env_app_token (fun () ->
       let repo_files =
         [
           F.File ("rules.yml", eqeq_basic_content);
@@ -153,6 +165,10 @@ let tests (caps : < Scan_subcommand.caps >) =
       t "no semgrep settings file with env set" (fun () ->
           without_settings (fun () ->
               with_env_app_token (test_nosettings ~env_app_token_set:true)));
-      test_basic_output caps;
-      test_basic_verbose_output caps;
+      t "basic output" ~checked_output:(Testo.stdxxx ()) ~normalize
+        (test_basic_output caps);
+      t "basic verbose output"
+        ~skipped:"captured output depends on which tests run before it"
+        ~checked_output:(Testo.stdxxx ()) ~normalize
+        (test_basic_verbose_output caps);
     ]
diff --git a/src/osemgrep/configuring/Semgrep_envvars.ml b/src/osemgrep/configuring/Semgrep_envvars.ml
index c326bae3dd15..94968bf13ec7 100644
--- a/src/osemgrep/configuring/Semgrep_envvars.ml
+++ b/src/osemgrep/configuring/Semgrep_envvars.ml
@@ -39,10 +39,27 @@ let%test_unit "Semgrep_envvars.(/)" =
   [%test_eq: Base.string] ("a/b/" / "c/d" / "foo.c") "a/b/c/d/foo.c"
 *)
 
-let env_opt var = Sys.getenv_opt var
+(*
+   Treat environment variables with an empty value as if they were unset.
 
-let env_or conv var default =
+   Since OCaml doesn't provide an 'unsetenv' function (which exists in libc),
+   tests that that set environment variable temporarily can't unset them,
+   leaving them with the empty value instead.
+*)
+let env_opt var =
   match Sys.getenv_opt var with
+  | Some "" -> None
+  | x -> x
+
+(*****************************************************************************)
+(* Don't use Sys.getenv* or Unix.getenv* starting from here!  *)
+(*****************************************************************************)
+(*
+   TODO: ensure that the whole application uses our 'env_opt' function.
+*)
+
+let env_or conv var default =
+  match env_opt var with
   | None -> default
   | Some x -> conv x
 
@@ -103,9 +120,11 @@ let of_current_sys_env () : t =
       (* In windows USERPROFILE=C:\Users\<user> *)
       if Sys.win32 then "USERPROFILE" else "XDG_CONFIG_HOME"
     in
-    match Sys.getenv_opt home_env_var with
+    match env_opt home_env_var with
     | Some x when Sys.is_directory x -> Fpath.v x
-    | _else_ -> Fpath.v (env_or (fun x -> x) "HOME" "/")
+    | Some _
+    | None ->
+        Fpath.v (env_or (fun x -> x) "HOME" "/")
   in
   let user_dot_semgrep_dir = user_home_dir / ".semgrep" in
   {
diff --git a/src/osemgrep/configuring/Semgrep_settings.ml b/src/osemgrep/configuring/Semgrep_settings.ml
index 681ea13a260d..21d37bd2b58c 100644
--- a/src/osemgrep/configuring/Semgrep_settings.ml
+++ b/src/osemgrep/configuring/Semgrep_settings.ml
@@ -85,8 +85,8 @@ let set_api_token_from_env settings =
      as ocaml cannot unset environment variables *)
   | Some token when Auth.well_formed token ->
       Logs.info (fun m ->
-          m "Found API token in environment variables '%s'"
-            (Auth.string_of_token token));
+          (* SECURITY: Don't log authentication tokens! *)
+          m "Found API token in environment variables");
       { settings with api_token = Some token }
   | Some _ ->
       Logs.info (fun m ->
diff --git a/src/osemgrep/reporting/Output.ml b/src/osemgrep/reporting/Output.ml
index 1198f5f25593..44b9e8c58149 100644
--- a/src/osemgrep/reporting/Output.ml
+++ b/src/osemgrep/reporting/Output.ml
@@ -75,15 +75,19 @@ let string_of_severity (severity : Out.match_severity) : string =
 (*****************************************************************************)
 
 (* called also from RPC_return.ml *)
-let format (kind : Output_format.t) (cli_output : Out.cli_output) : string list
-    =
+let format (kind : Output_format.t) (ctx : Out.format_context)
+    (cli_output : Out.cli_output) : string list =
+  (* TODO: use is_logged_in for the logged_in gated export fields *)
+  ignore ctx;
   match kind with
   | Text
-  | Json
   | Sarif
   | Files_with_matches
   | Incremental ->
       failwith (spf "format not supported here: %s" (Output_format.show kind))
+  | Json ->
+      (* TODO? Sort keys for predictable output. Helps with snapshot tests? *)
+      [ Out.string_of_cli_output cli_output ]
   | Junit_xml -> [ Junit_xml_output.junit_xml_output cli_output ]
   | Gitlab_sast ->
       let gitlab_sast_json = Gitlab_output.sast_output cli_output.results in
@@ -159,19 +163,18 @@ let format (kind : Output_format.t) (cli_output : Out.cli_output) : string list
                  String.concat ":" parts)
 
 let dispatch_output_format (caps : < Cap.stdout >) (conf : conf)
-    (runtime_params : Out.format_context) (cli_output : Out.cli_output)
+    (ctx : Out.format_context) (cli_output : Out.cli_output)
     (hrules : Rule.hrules) : unit =
   let print = CapConsole.print caps#stdout in
   match conf.output_format with
-  | Vim -> format Vim cli_output |> List.iter print
-  | Emacs -> format Emacs cli_output |> List.iter print
-  | Junit_xml -> format Junit_xml cli_output |> List.iter print
-  | Gitlab_sast -> format Gitlab_sast cli_output |> List.iter print
-  | Gitlab_secrets -> format Gitlab_secrets cli_output |> List.iter print
-  | Json ->
-      (* TOPORT? Sort keys for predictable output. Helps with snapshot tests *)
-      let s = Out.string_of_cli_output cli_output in
-      print s
+  (* matches have already been displayed in a file_match_results_hook *)
+  | Incremental -> ()
+  | Vim -> format Vim ctx cli_output |> List.iter print
+  | Emacs -> format Emacs ctx cli_output |> List.iter print
+  | Junit_xml -> format Junit_xml ctx cli_output |> List.iter print
+  | Gitlab_sast -> format Gitlab_sast ctx cli_output |> List.iter print
+  | Gitlab_secrets -> format Gitlab_secrets ctx cli_output |> List.iter print
+  | Json -> format Json ctx cli_output |> List.iter print
   | Text ->
       (* TODO: we should switch to Fmt_.with_buffer_to_string +
        * some CapConsole.print_no_nl, but then is_atty fail on
@@ -181,8 +184,6 @@ let dispatch_output_format (caps : < Cap.stdout >) (conf : conf)
         ~max_lines_per_finding:conf.max_lines_per_finding
           (* nosemgrep: forbid-console *)
         ~color_output:conf.force_color Format.std_formatter cli_output
-  (* matches have already been displayed in a file_match_results_hook *)
-  | Incremental -> ()
   | Sarif ->
       let engine_label, is_pro =
         match cli_output.engine_requested with
@@ -192,8 +193,7 @@ let dispatch_output_format (caps : < Cap.stdout >) (conf : conf)
         | Some `PRO -> ("PRO", true)
       in
       let hide_nudge =
-        runtime_params.is_logged_in || is_pro
-        || not runtime_params.is_using_registry
+        ctx.is_logged_in || is_pro || not ctx.is_using_registry
       in
       let sarif_json =
         Sarif_output.sarif_output hide_nudge engine_label
diff --git a/src/osemgrep/reporting/Output.mli b/src/osemgrep/reporting/Output.mli
index 9337c36a0078..22b5001c40df 100644
--- a/src/osemgrep/reporting/Output.mli
+++ b/src/osemgrep/reporting/Output.mli
@@ -50,4 +50,5 @@ val output_result :
 val preprocess_result : fixed_lines:bool -> Core_runner.result -> Out.cli_output
 
 (* used by RPC_return.ml for Vim/Emacs/Junit_xml/Gitlab_xxx for now *)
-val format : Output_format.t -> Out.cli_output -> string list
+val format :
+  Output_format.t -> Out.format_context -> Out.cli_output -> string list
diff --git a/src/osemgrep/reporting/Sarif_output.ml b/src/osemgrep/reporting/Sarif_output.ml
index 3a114674c01e..cfcce059ddff 100644
--- a/src/osemgrep/reporting/Sarif_output.ml
+++ b/src/osemgrep/reporting/Sarif_output.ml
@@ -1,6 +1,6 @@
 open Common
-module OutT = Semgrep_output_v1_t
-module Sarif_v = Sarif.Sarif_v_2_1_0_v
+module Out = Semgrep_output_v1_t
+module Sarif = Sarif.Sarif_v_2_1_0_v
 
 (*****************************************************************************)
 (* Prelude *)
@@ -16,7 +16,8 @@ module Sarif_v = Sarif.Sarif_v_2_1_0_v
 (*****************************************************************************)
 
 (* See the "level" property in the spec *)
-let sarif_severity_of_severity : _ -> Sarif_v.notification_level = function
+let severity_of_severity sev : Sarif.notification_level =
+  match sev with
   | `Info
   | `Low ->
       `Note
@@ -32,21 +33,21 @@ let sarif_severity_of_severity : _ -> Sarif_v.notification_level = function
   | `Inventory ->
       raise Todo
 
-let message ?markdown text = Sarif_v.create_message ?markdown ~text ()
+let message ?markdown text = Sarif.create_message ?markdown ~text ()
 
 let multiformat_message ?markdown text =
-  Sarif_v.create_multiformat_message_string ?markdown ~text ()
+  Sarif.create_multiformat_message_string ?markdown ~text ()
 
-let region ?message ?snippet (start : OutT.position) (end_ : OutT.position) =
+let region ?message ?snippet (start : Out.position) (end_ : Out.position) =
   (* The sarif package is a bit annoying by using int64 for posititons *)
   let start_line = Int64.of_int start.line
   and start_column = Int64.of_int start.col
   and end_line = Int64.of_int end_.line
   and end_column = Int64.of_int end_.col in
   let snippet =
-    Option.map (fun text -> Sarif_v.create_artifact_content ~text ()) snippet
+    Option.map (fun text -> Sarif.create_artifact_content ~text ()) snippet
   in
-  Sarif_v.create_region ~start_line ~start_column ~end_line ~end_column ?message
+  Sarif.create_region ~start_line ~start_column ~end_line ~end_column ?message
     ?snippet ()
 
 let tags_of_metadata metadata =
@@ -175,14 +176,14 @@ let rules hide_nudge (hrules : Rule.hrules) =
           | [] -> ""
           | xs -> "\n\n<b>References:</b>\n" ^ String.concat "" xs
         in
-        Sarif_v.create_reporting_descriptor
+        Sarif.create_reporting_descriptor
           ~id:(Rule_ID.to_string rule_id)
           ~name:(Rule_ID.to_string rule_id)
           ~short_description:(multiformat_message short_description)
           ~full_description:(multiformat_message rule.message)
           ~default_configuration:
-            (Sarif_v.create_reporting_configuration
-               ~level:(sarif_severity_of_severity rule.severity)
+            (Sarif.create_reporting_configuration
+               ~level:(severity_of_severity rule.severity)
                ())
           ~help:
             (multiformat_message
@@ -194,7 +195,7 @@ let rules hide_nudge (hrules : Rule.hrules) =
   in
   List.of_seq rules
 
-let sarif_fixes (cli_match : OutT.cli_match) =
+let sarif_fixes (cli_match : Out.cli_match) =
   match cli_match.extra.fixed_lines with
   | None -> None
   | Some fixed_lines ->
@@ -203,50 +204,50 @@ let sarif_fixes (cli_match : OutT.cli_match) =
       in
       let fix =
         let artifact_change =
-          Sarif_v.create_artifact_change
+          Sarif.create_artifact_change
             ~artifact_location:
-              (Sarif_v.create_artifact_location
+              (Sarif.create_artifact_location
                  ~uri:(Fpath.to_string cli_match.path)
                  ())
             ~replacements:
               [
-                Sarif_v.create_replacement
+                Sarif.create_replacement
                   ~deleted_region:(region cli_match.start cli_match.end_)
                   ~inserted_content:
-                    (Sarif_v.create_artifact_content
+                    (Sarif.create_artifact_content
                        ~text:(String.concat "\n" fixed_lines)
                        ())
                   ();
               ]
             ()
         in
-        Sarif_v.create_fix ~description:(message description_text)
+        Sarif.create_fix ~description:(message description_text)
           ~artifact_changes:[ artifact_change ] ()
       in
       Some [ fix ]
 
-let thread_flow_location (cli_match : OutT.cli_match) message
-    (location : OutT.location) content nesting_level =
+let thread_flow_location (cli_match : Out.cli_match) message
+    (location : Out.location) content nesting_level =
   let location =
-    Sarif_v.create_location ~message
+    Sarif.create_location ~message
       ~physical_location:
-        (Sarif_v.create_physical_location
+        (Sarif.create_physical_location
            ~region:
              (region ~message ~snippet:content location.start location.end_)
            ~artifact_location:
-             (Sarif_v.create_artifact_location
+             (Sarif.create_artifact_location
                 ~uri:(Fpath.to_string cli_match.path)
                 ())
            ())
       ()
   in
-  Sarif_v.create_thread_flow_location
+  Sarif.create_thread_flow_location
     ~nesting_level:(Int64.of_int nesting_level)
     ~location ()
 
 let intermediate_var_locations cli_match intermediate_vars =
   intermediate_vars
-  |> List_.map (fun ({ location; content } : OutT.match_intermediate_var) ->
+  |> List_.map (fun ({ location; content } : Out.match_intermediate_var) ->
          let propagation_message_text =
            spf "Propagator : '%s' @ '%s:%d'" content
              (Fpath.to_string location.path)
@@ -256,8 +257,8 @@ let intermediate_var_locations cli_match intermediate_vars =
          thread_flow_location cli_match propagation_message_text location
            content 0)
 
-let thread_flows (cli_match : OutT.cli_match)
-    (dataflow_trace : OutT.match_dataflow_trace) (location : OutT.location)
+let thread_flows (cli_match : Out.cli_match)
+    (dataflow_trace : Out.match_dataflow_trace) (location : Out.location)
     content =
   (* TODO from sarif.py: deal with taint sink *)
   let intermediate_vars = dataflow_trace.intermediate_vars in
@@ -286,26 +287,26 @@ let thread_flows (cli_match : OutT.cli_match)
     in
     thread_flow_location cli_match sink_message_text
       {
-        OutT.start = cli_match.start;
+        Out.start = cli_match.start;
         end_ = cli_match.end_;
         path = cli_match.path;
       }
       cli_match.extra.lines 1
   in
   [
-    Sarif_v.create_thread_flow
+    Sarif.create_thread_flow
       ~locations:
         ((source_flow_location :: intermediate_var_locations)
         @ [ sink_flow_location ])
       ();
   ]
 
-let sarif_codeflow (cli_match : OutT.cli_match) =
+let sarif_codeflow (cli_match : Out.cli_match) =
   match cli_match.extra.dataflow_trace with
   | None
-  | Some { OutT.taint_source = None; _ } ->
+  | Some { Out.taint_source = None; _ } ->
       None
-  | Some { OutT.taint_source = Some (CliCall _); _ } ->
+  | Some { Out.taint_source = Some (CliCall _); _ } ->
       Logs.err (fun m ->
           m
             "Emitting SARIF output for unsupported dataflow trace (source is a \
@@ -327,18 +328,18 @@ let sarif_codeflow (cli_match : OutT.cli_match) =
       in
       Some
         [
-          Sarif_v.create_code_flow
+          Sarif.create_code_flow
             ~message:(message code_flow_message)
             ~thread_flows ();
         ]
 
-let results show_dataflow_traces (cli_output : OutT.cli_output) =
-  let result (cli_match : OutT.cli_match) =
+let results show_dataflow_traces (cli_output : Out.cli_output) =
+  let result (cli_match : Out.cli_match) =
     let location =
       let physical_location =
-        Sarif_v.create_physical_location
+        Sarif.create_physical_location
           ~artifact_location:
-            (Sarif_v.create_artifact_location
+            (Sarif.create_artifact_location
                ~uri:(Fpath.to_string cli_match.path)
                ~uri_base_id:"%SRCROOT%" ())
           ~region:
@@ -346,20 +347,20 @@ let results show_dataflow_traces (cli_output : OutT.cli_output) =
                cli_match.end_)
           ()
       in
-      Sarif_v.create_location ~physical_location ()
+      Sarif.create_location ~physical_location ()
     in
     let suppressions =
       match cli_match.extra.is_ignored with
       | None
       | Some false ->
           None
-      | Some true -> Some [ Sarif_v.create_suppression ~kind:`InSource () ]
+      | Some true -> Some [ Sarif.create_suppression ~kind:`InSource () ]
     in
     let fixes = sarif_fixes cli_match in
     let code_flows =
       if show_dataflow_traces then sarif_codeflow cli_match else None
     in
-    Sarif_v.create_result
+    Sarif.create_result
       ~rule_id:(Rule_ID.to_string cli_match.check_id)
       ~message:(message cli_match.extra.message)
       ~locations:[ location ]
@@ -368,8 +369,8 @@ let results show_dataflow_traces (cli_output : OutT.cli_output) =
   in
   List_.map result cli_output.results
 
-let error_to_sarif_notification (e : OutT.cli_error) =
-  let level = sarif_severity_of_severity e.level in
+let error_to_sarif_notification (e : Out.cli_error) =
+  let level = severity_of_severity e.level in
   let message =
     Option.value
       ~default:
@@ -380,18 +381,18 @@ let error_to_sarif_notification (e : OutT.cli_error) =
     |> message
   in
   let descriptor =
-    Sarif_v.create_reporting_descriptor_reference
+    Sarif.create_reporting_descriptor_reference
       ~id:(Error.string_of_error_type e.type_)
       ()
   in
-  Sarif_v.create_notification ~message ~descriptor ~level ()
+  Sarif.create_notification ~message ~descriptor ~level ()
 
 (*****************************************************************************)
 (* Entry point *)
 (*****************************************************************************)
 
 let sarif_output ~hide_nudge ~engine_label ~show_dataflow_traces hrules
-    (cli_output : OutT.cli_output) =
+    (cli_output : Out.cli_output) =
   let sarif_schema =
     "https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json"
   in
@@ -399,11 +400,11 @@ let sarif_output ~hide_nudge ~engine_label ~show_dataflow_traces hrules
     let rules = rules hide_nudge hrules in
     let tool =
       let driver =
-        Sarif_v.create_tool_component
+        Sarif.create_tool_component
           ~name:(spf "Semgrep %s" engine_label)
           ~semantic_version:Version.version ~rules ()
       in
-      Sarif_v.create_tool ~driver ()
+      Sarif.create_tool ~driver ()
     in
     let results = results show_dataflow_traces cli_output in
     let invocation =
@@ -411,10 +412,10 @@ let sarif_output ~hide_nudge ~engine_label ~show_dataflow_traces hrules
       let tool_execution_notifications =
         List_.map error_to_sarif_notification cli_output.errors
       in
-      Sarif_v.create_invocation ~execution_successful:true
+      Sarif.create_invocation ~execution_successful:true
         ~tool_execution_notifications ()
     in
-    Sarif_v.create_run ~tool ~results ~invocations:[ invocation ] ()
+    Sarif.create_run ~tool ~results ~invocations:[ invocation ] ()
   in
-  Sarif_v.create_sarif_json_schema ~version:`TwoDotOneDotZero
-    ~schema:sarif_schema ~runs:[ run ] ()
+  Sarif.create_sarif_json_schema ~version:`TwoDotOneDotZero ~schema:sarif_schema
+    ~runs:[ run ] ()
diff --git a/src/rpc/RPC_return.ml b/src/rpc/RPC_return.ml
index 73e0bd289d35..d2a846cd2a2a 100644
--- a/src/rpc/RPC_return.ml
+++ b/src/rpc/RPC_return.ml
@@ -41,9 +41,7 @@ let autofix (dryrun : bool) (edits : Out.edit list) :
 
 let format (kind : Out.output_format) (ctx : Out.format_context)
     (cli_output : Out.cli_output) : string =
-  (* TODO: use is_logged_in for the logged_in gated export fields *)
-  ignore ctx;
-  let xs = Output.format kind cli_output in
+  let xs = Output.format kind ctx cli_output in
   String.concat "\n" xs
 
 let sarif_format _caps hide_nudge engine_label show_dataflow_traces
diff --git a/src/tainting/Dataflow_tainting.ml b/src/tainting/Dataflow_tainting.ml
index c24fadea5a67..d8c7bfdc85ef 100644
--- a/src/tainting/Dataflow_tainting.ml
+++ b/src/tainting/Dataflow_tainting.ml
@@ -13,6 +13,7 @@
  * LICENSE for more details.
  *)
 open Common
+open Fpath_.Operators
 open IL
 module Log = Log_tainting.Log
 module G = AST_generic
@@ -27,6 +28,7 @@ module T = Taint
 module Lval_env = Taint_lval_env
 module Taints = T.Taint_set
 module TM = Taint_spec_match
+module TRI = Taint_rule_inst
 module S = Shape_and_sig.Shape
 module Shape = Taint_shape
 module Effect = Shape_and_sig.Effect
@@ -73,50 +75,18 @@ let transfer_tag = Log_tainting.transfer_tag
 (* Types *)
 (*****************************************************************************)
 
-type var = Var_env.var
-
-type a_propagator = {
-  kind : [ `From | `To ];
-  prop : R.taint_propagator;
-  var : var;
-}
-
-type effects_handler = var option -> Effect.t list -> Effect.t list
-
-type config = {
-  filepath : string;
-  rule_id : Rule_ID.t;
-  track_control : bool;
-  is_source : G.any -> R.taint_source TM.t list;
-  is_propagator : AST_generic.any -> a_propagator TM.t list;
-  is_sink : G.any -> R.taint_sink TM.t list;
-  is_sanitizer : G.any -> R.taint_sanitizer TM.t list;
-      (* NOTE [is_sanitizer]:
-       * A sanitizer is more "extreme" than you may expect. When a piece of code is
-       * "sanitized" Semgrep will just not check it. For example, something like
-       * `sanitize(sink(tainted))` will not yield any finding.
-       * *)
-  unify_mvars : bool;
-  handle_effects : effects_handler;
-}
-
 type mapping = Lval_env.t D.mapping
-
-(* HACK: Tracks tainted functions intrafile. *)
-type fun_env = (var, Taints.t) Hashtbl.t
 type java_props_cache = (string * G.SId.t, IL.name) Hashtbl.t
 
 let mk_empty_java_props_cache () = Hashtbl.create 30
 
 (* THINK: Separate read-only enviroment into a new a "cfg" type? *)
 type env = {
-  lang : Lang.t;
-  options : Rule_options.t;
-  config : config;
-  fun_name : var option;
+  taint_inst : Taint_rule_inst.t;
+  fun_name : IL.name option;
+  in_lambda : IL.name option;
   lval_env : Lval_env.t;
   best_matches : TM.Best_matches.t;
-  java_props : java_props_cache;
   effects_acc : Effects.t ref;
 }
 
@@ -133,18 +103,18 @@ let hook_check_tainted_at_exit_sinks = ref None
 (*****************************************************************************)
 
 let propagate_through_functions env =
-  (not env.options.taint_assume_safe_functions)
-  && not env.options.taint_only_propagate_through_assignments
+  (not env.taint_inst.options.taint_assume_safe_functions)
+  && not env.taint_inst.options.taint_only_propagate_through_assignments
 
 let propagate_through_indexes env =
-  (not env.options.taint_assume_safe_indexes)
-  && not env.options.taint_only_propagate_through_assignments
+  (not env.taint_inst.options.taint_assume_safe_indexes)
+  && not env.taint_inst.options.taint_only_propagate_through_assignments
 
 (*****************************************************************************)
 (* Helpers *)
 (*****************************************************************************)
 
-let log_timeout_warning config opt_name timeout =
+let log_timeout_warning (taint_inst : Taint_rule_inst.t) opt_name timeout =
   match timeout with
   | `Ok -> ()
   | `Timeout ->
@@ -153,8 +123,9 @@ let log_timeout_warning config opt_name timeout =
           m
             "Fixpoint timeout while performing taint analysis [rule: %s file: \
              %s func: %s]"
-            (Rule_ID.to_string config.rule_id)
-            config.filepath (opt_name ||| "???"))
+            (Rule_ID.to_string taint_inst.rule_id)
+            !!(taint_inst.file)
+            (Option.map IL.str_of_name opt_name ||| "???"))
 
 let map_check_expr env check_expr xs =
   let rev_taints_and_shapes, lval_env =
@@ -181,7 +152,8 @@ let union_map_taints_and_vars env check xs =
          (Taints.empty, env.lval_env)
   in
   let taints =
-    if env.options.taint_only_propagate_through_assignments then Taints.empty
+    if env.taint_inst.options.taint_only_propagate_through_assignments then
+      Taints.empty
     else taints
   in
   (taints, lval_env)
@@ -197,13 +169,13 @@ let gather_all_taints_in_args_taints args_taints =
        Taints.empty
 
 let any_is_best_sanitizer env any =
-  env.config.is_sanitizer any
+  env.taint_inst.preds.is_sanitizer any
   |> List.filter (fun (m : R.taint_sanitizer TM.t) ->
          (not m.spec.sanitizer_exact) || TM.is_best_match env.best_matches m)
 
 (* TODO: We could return source matches already split by `by-side-effect` here ? *)
 let any_is_best_source ?(is_lval = false) env any =
-  env.config.is_source any
+  env.taint_inst.preds.is_source any
   |> List.filter (fun (m : R.taint_source TM.t) ->
          (* Remove sources that should match exactly but do not here. *)
          match m.spec.source_by_side_effect with
@@ -215,24 +187,27 @@ let any_is_best_source ?(is_lval = false) env any =
              (not m.spec.source_exact) || TM.is_best_match env.best_matches m)
 
 let any_is_best_sink env any =
-  env.config.is_sink any
+  env.taint_inst.preds.is_sink any
   |> List.filter (fun (tm : R.taint_sink TM.t) ->
          (* at-exit sinks are handled in 'check_tainted_at_exit_sinks' *)
          (not tm.spec.sink_at_exit) && TM.is_best_match env.best_matches tm)
 
-let orig_is_source config orig = config.is_source (any_of_orig orig)
+let orig_is_source (taint_inst : Taint_rule_inst.t) orig =
+  taint_inst.preds.is_source (any_of_orig orig)
 
 let orig_is_best_source env orig : R.taint_source TM.t list =
   any_is_best_source env (any_of_orig orig)
 [@@profiling]
 
-let orig_is_sanitizer config orig = config.is_sanitizer (any_of_orig orig)
+let orig_is_sanitizer (taint_inst : Taint_rule_inst.t) orig =
+  taint_inst.preds.is_sanitizer (any_of_orig orig)
 
 let orig_is_best_sanitizer env orig =
   any_is_best_sanitizer env (any_of_orig orig)
 [@@profiling]
 
-let orig_is_sink config orig = config.is_sink (any_of_orig orig)
+let orig_is_sink (taint_inst : Taint_rule_inst.t) orig =
+  taint_inst.preds.is_sink (any_of_orig orig)
 
 let orig_is_best_sink env orig = any_is_best_sink env (any_of_orig orig)
 [@@profiling]
@@ -255,7 +230,7 @@ let lval_is_best_sanitizer env lval =
 let lval_is_sink env lval =
   (* TODO: This should be = any_is_best_sink env (any_of_lval lval)
    *    but see tests/rules/TODO_taint_messy_sink. *)
-  env.config.is_sink (any_of_lval lval)
+  env.taint_inst.preds.is_sink (any_of_lval lval)
   |> List.filter (fun (tm : R.taint_sink TM.t) ->
          (* at-exit sinks are handled in 'check_tainted_at_exit_sinks' *)
          not tm.spec.sink_at_exit)
@@ -284,7 +259,7 @@ let taints_of_matches env ~incoming sources =
 
 let record_effects env new_effects =
   if not (List_.null new_effects) then
-    let new_effects = env.config.handle_effects env.fun_name new_effects in
+    let new_effects = env.taint_inst.handle_effects env.fun_name new_effects in
     env.effects_acc := Effects.add_list new_effects !(env.effects_acc)
 
 let unify_mvars_sets env mvars1 mvars2 =
@@ -295,8 +270,10 @@ let unify_mvars_sets env mvars1 mvars2 =
         match List.assoc_opt mvar mvars2 with
         | None -> Some ((mvar, mval) :: xs)
         | Some mval' ->
-            if Matching_generic.equal_ast_bound_code env.options mval mval' then
-              Some ((mvar, mval) :: xs)
+            if
+              Matching_generic.equal_ast_bound_code env.taint_inst.options mval
+                mval'
+            then Some ((mvar, mval) :: xs)
             else None)
       (Some []) mvars1
   in
@@ -340,7 +317,8 @@ let merge_source_mvars env bindings =
          | Some (Some mval') ->
              if
                not
-                 (Matching_generic.equal_ast_bound_code env.options mval mval')
+                 (Matching_generic.equal_ast_bound_code env.taint_inst.options
+                    mval mval')
              then Hashtbl.remove bindings_tbl mvar);
   (* After this, the only surviving bindings should be those where
      there was no conflict between bindings in different sources.
@@ -357,7 +335,7 @@ let merge_source_mvars env bindings =
 
 (* Merge source's and sink's bound metavariables. *)
 let merge_source_sink_mvars env source_mvars sink_mvars =
-  if env.config.unify_mvars then
+  if env.taint_inst.options.taint_unify_mvars then
     (* This used to be the default, but it turned out to be confusing even for
      * r2c's security team! Typically you think of `pattern-sources` and
      * `pattern-sinks` as independent. We keep this option mainly for
@@ -402,14 +380,14 @@ let get_control_taints_to_return env =
 let type_of_lval env lval =
   match lval with
   | { base = Var x; rev_offset = [] } ->
-      Typing.resolved_type_of_id_info env.lang x.id_info
+      Typing.resolved_type_of_id_info env.taint_inst.lang x.id_info
   | { base = _; rev_offset = { o = Dot fld; _ } :: _ } ->
-      Typing.resolved_type_of_id_info env.lang fld.id_info
+      Typing.resolved_type_of_id_info env.taint_inst.lang fld.id_info
   | __else__ -> Type.NoType
 
 let type_of_expr env e =
   match e.eorig with
-  | SameAs eorig -> Typing.type_of_expr env.lang eorig |> fst
+  | SameAs eorig -> Typing.type_of_expr env.taint_inst.lang eorig |> fst
   | __else__ -> Type.NoType
 
 (* We only check this at a few key places to avoid calling `type_of_expr` too
@@ -438,14 +416,14 @@ let drop_taints_if_bool_or_number (options : Rule_options.t) taints ty =
  *)
 let check_type_and_drop_taints_if_bool_or_number env taints type_of_x x =
   if
-    (env.options.taint_assume_safe_booleans
-   || env.options.taint_assume_safe_numbers)
+    (env.taint_inst.options.taint_assume_safe_booleans
+   || env.taint_inst.options.taint_assume_safe_numbers)
     && not (Taints.is_empty taints)
   then
     match type_of_x env x with
     | Type.Function (_, return_ty) ->
-        drop_taints_if_bool_or_number env.options taints return_ty
-    | ty -> drop_taints_if_bool_or_number env.options taints ty
+        drop_taints_if_bool_or_number env.taint_inst.options taints return_ty
+    | ty -> drop_taints_if_bool_or_number env.taint_inst.options taints ty
   else taints
 
 (*****************************************************************************)
@@ -532,7 +510,9 @@ let effects_of_tainted_sink env taints_with_traces (sink : Effect.sink) :
          behavior is that one of the `$X` bindings is chosen arbitrarily. We will
          try to keep this behavior here.
       *)
-      if env.config.unify_mvars || Option.is_none sink.rule_sink.sink_requires
+      if
+        env.taint_inst.options.taint_unify_mvars
+        || Option.is_none sink.rule_sink.sink_requires
       then
         taints_and_bindings
         |> List_.filter_map (fun (t, bindings) ->
@@ -615,7 +595,8 @@ let effects_of_call_func_arg fun_exp fun_shape args_taints =
 
 let lookup_signature env fun_exp =
   match (!hook_function_taint_signature, fun_exp) with
-  | Some hook, { e = Fetch _f; eorig = SameAs eorig } -> hook env.config eorig
+  | Some hook, { e = Fetch _f; eorig = SameAs eorig } ->
+      hook env.taint_inst eorig
   | __else__ -> None
 
 (*****************************************************************************)
@@ -676,7 +657,10 @@ let fix_poly_taint_with_field env lval xtaint =
     else lval
   in
   (* TODO: Aren't we missing here C# and Go ? *)
-  if env.lang =*= Lang.Java || Lang.is_js env.lang || env.lang =*= Lang.Python
+  if
+    env.taint_inst.lang =*= Lang.Java
+    || Lang.is_js env.taint_inst.lang
+    || env.taint_inst.lang =*= Lang.Python
   then
     match xtaint with
     | `Sanitized
@@ -782,10 +766,10 @@ let handle_taint_propagators env thing taints shape =
       | `Exp exp -> any_of_orig exp.eorig
       | `Ins ins -> any_of_orig ins.iorig
     in
-    env.config.is_propagator any
+    env.taint_inst.preds.is_propagator any
   in
   let propagate_froms, propagate_tos =
-    List.partition (fun p -> p.TM.spec.kind =*= `From) propagators
+    List.partition (fun p -> p.TM.spec.TRI.kind =*= `From) propagators
   in
   let lval_env =
     (* `thing` is the source (the "from") of propagation, we add its taints to
@@ -822,7 +806,7 @@ let handle_taint_propagators env thing taints shape =
         *)
         match
           T.solve_precondition ~ignore_poly_taint:false ~taints
-            (R.get_propagator_precondition prop.TM.spec.prop)
+            (R.get_propagator_precondition prop.TM.spec.TRI.prop)
         with
         | Some true ->
             (* If we have an output label, change the incoming taints to be
@@ -850,7 +834,7 @@ let handle_taint_propagators env thing taints shape =
     List.fold_left
       (fun (taints_in_acc, lval_env) prop ->
         let opt_propagated, lval_env =
-          Lval_env.propagate_from prop.TM.spec.var lval_env
+          Lval_env.propagate_from prop.TM.spec.TRI.var lval_env
         in
         let taints_from_prop =
           match opt_propagated with
@@ -858,7 +842,7 @@ let handle_taint_propagators env thing taints shape =
           | Some taints -> taints
         in
         let lval_env =
-          if prop.spec.prop.propagator_by_side_effect then
+          if prop.spec.TRI.prop.propagator_by_side_effect then
             match thing with
             (* If `thing` is an l-value of the form `x.a.b.c`, then taint can be
              *  propagated by side-effect. A pattern-propagator may use this to
@@ -955,21 +939,16 @@ and propagate_taint_via_java_getters_and_setters_without_definition env e args
    _;
   }
   (* We check for the "get"/"set" prefix below. *)
-    when env.lang =*= Lang.Java && String.length method_str > 3 -> (
+    when env.taint_inst.lang =*= Lang.Java && String.length method_str > 3 -> (
       let mk_prop_lval () =
         (* e.g. getFooBar/setFooBar -> fooBar *)
         let prop_str =
           String.uncapitalize_ascii (Str.string_after method_str 3)
         in
         let prop_name =
-          (* FIXME: Pro should be autogenerating definitions for these getters/setters,
-           * but that seems to hurt performance and it's still unclear why, so instead
-           * we give taint access to Pro typing info via 'hook_find_attribute_in_class'
-           * and look for the property corresponding to the getter/setter.
-           *
-           * On very large files, allocating a new name every time can have a perf impact,
-           * so we cache them. *)
-          match Hashtbl.find_opt env.java_props (prop_str, sid) with
+          match
+            Hashtbl.find_opt env.taint_inst.java_props_cache (prop_str, sid)
+          with
           | Some prop_name -> prop_name
           | None -> (
               let mk_default_prop_name () =
@@ -980,7 +959,8 @@ and propagate_taint_via_java_getters_and_setters_without_definition env e args
                     id_info = G.empty_id_info ();
                   }
                 in
-                Hashtbl.add env.java_props (prop_str, sid) prop_name;
+                Hashtbl.add env.taint_inst.java_props_cache (prop_str, sid)
+                  prop_name;
                 prop_name
               in
               match (!(obj.id_info.id_type), !hook_find_attribute_in_class) with
@@ -989,7 +969,8 @@ and propagate_taint_via_java_getters_and_setters_without_definition env e args
                   | None -> mk_default_prop_name ()
                   | Some prop_name ->
                       let prop_name = AST_to_IL.var_of_name prop_name in
-                      Hashtbl.add env.java_props (prop_str, sid) prop_name;
+                      Hashtbl.add env.taint_inst.java_props_cache
+                        (prop_str, sid) prop_name;
                       prop_name)
               | __else__ -> mk_default_prop_name ())
         in
@@ -1065,7 +1046,7 @@ and check_tainted_lval_aux env (lval : IL.lval) :
             (sub_new_taints, sub_in_env, sub_shape, lval_env)
       in
       let sub_new_taints, sub_in_env =
-        if env.options.taint_only_propagate_through_assignments then
+        if env.taint_inst.options.taint_only_propagate_through_assignments then
           match sub_in_env with
           | `Sanitized -> (Taints.empty, `Sanitized)
           | `Clean
@@ -1121,7 +1102,7 @@ and check_tainted_lval_aux env (lval : IL.lval) :
       in
       (* Check taint propagators. *)
       let taints_incoming (* TODO: find a better name *) =
-        if env.options.taint_only_propagate_through_assignments then
+        if env.taint_inst.options.taint_only_propagate_through_assignments then
           taints_from_sources
         else
           sub_new_taints
@@ -1217,8 +1198,8 @@ and check_tainted_expr env exp : Taints.t * S.shape * Lval_env.t =
           |> Taints.union (gather_all_taints_in_args_taints args_taints)
         in
         let all_args_taints =
-          if env.options.taint_only_propagate_through_assignments then
-            Taints.empty
+          if env.taint_inst.options.taint_only_propagate_through_assignments
+          then Taints.empty
           else all_args_taints
         in
         let op_taints =
@@ -1238,7 +1219,8 @@ and check_tainted_expr env exp : Taints.t * S.shape * Lval_env.t =
           | G.NotIn
           | G.Is
           | G.NotIs ->
-              if env.options.taint_assume_safe_comparisons then Taints.empty
+              if env.taint_inst.options.taint_assume_safe_comparisons then
+                Taints.empty
               else all_args_taints
           | G.And
           | G.Or
@@ -1534,8 +1516,8 @@ let check_tainted_instr env instr : Taints.t * S.shape * Lval_env.t =
         in
         (* We add the taint of the function itselt (i.e., 'e_taints') too. *)
         let all_call_taints =
-          if env.options.taint_only_propagate_through_assignments then
-            call_taints
+          if env.taint_inst.options.taint_only_propagate_through_assignments
+          then call_taints
           else Taints.union e_taints call_taints
         in
         let all_call_taints =
@@ -1558,8 +1540,8 @@ let check_tainted_instr env instr : Taints.t * S.shape * Lval_env.t =
               |> Taints.union (gather_all_taints_in_args_taints args_taints)
             in
             let all_args_taints =
-              if env.options.taint_only_propagate_through_assignments then
-                Taints.empty
+              if env.taint_inst.options.taint_only_propagate_through_assignments
+              then Taints.empty
               else all_args_taints
             in
             (all_args_taints, Bot, lval_env))
@@ -1573,8 +1555,8 @@ let check_tainted_instr env instr : Taints.t * S.shape * Lval_env.t =
           |> Taints.union (gather_all_taints_in_args_taints args_taints)
         in
         let all_args_taints =
-          if env.options.taint_only_propagate_through_assignments then
-            Taints.empty
+          if env.taint_inst.options.taint_only_propagate_through_assignments
+          then Taints.empty
           else all_args_taints
         in
         (all_args_taints, Bot, lval_env)
@@ -1587,8 +1569,8 @@ let check_tainted_instr env instr : Taints.t * S.shape * Lval_env.t =
           |> Taints.union (gather_all_taints_in_args_taints args_taints)
         in
         let all_args_taints =
-          if env.options.taint_only_propagate_through_assignments then
-            Taints.empty
+          if env.taint_inst.options.taint_only_propagate_through_assignments
+          then Taints.empty
           else all_args_taints
         in
         (all_args_taints, Bot, lval_env)
@@ -1694,7 +1676,7 @@ let check_tainted_control_at_exit node env =
            * in the call trace of a finding. *)
           match env.fun_name with
           | None -> G.fake "return"
-          | Some name -> G.fake (name ^ "/return")
+          | Some name -> G.fake (IL.str_of_name name ^ "/return")
         in
         let effects =
           effects_of_tainted_return env Taints.empty Bot return_tok
@@ -1705,7 +1687,7 @@ let check_tainted_at_exit_sinks node env =
   match !hook_check_tainted_at_exit_sinks with
   | None -> ()
   | Some hook -> (
-      match hook env.config env.lval_env node with
+      match hook env.taint_inst env.lval_env node with
       | None -> ()
       | Some (taints_at_exit, sink_matches_at_exit) ->
           effects_of_tainted_sinks env taints_at_exit sink_matches_at_exit
@@ -1729,6 +1711,33 @@ let input_env ~enter_env ~(flow : F.cfg) mapping ni =
       | [ penv ] -> penv
       | penv1 :: penvs -> List.fold_left Lval_env.union penv1 penvs)
 
+let mk_lambda_in_env env lcfg =
+  (* We do some processing of the lambda parameters but it's mainly
+   * to enable taint propagation, e.g.
+   *
+   *     obj.do_something(lambda x: sink(x))
+   *
+   * so we can propagate taint from `obj` to `x`.
+   *)
+  lcfg.params
+  |> List.fold_left
+       (fun lval_env param ->
+         match param with
+         | Param { pname = var; _ } ->
+             (* This is a *new* variable, so we clean any taint that we may have
+              * attached to it previously. This can happen when a lambda is called
+              * inside a loop. *)
+             let lval_env = Lval_env.clean lval_env (LV.lval_of_var var) in
+             (* Now check if the parameter is itself a taint source. *)
+             let _taints, _shape, lval_env =
+               check_tainted_var { env with lval_env } var
+             in
+             lval_env
+         | PatternParam _ (* TODO *)
+         | FixmeParam ->
+             lval_env)
+       env.lval_env
+
 let rec transfer : env -> fun_cfg:F.fun_cfg -> Lval_env.t D.transfn =
  fun enter_env ~fun_cfg
      (* the transfer function to update the mapping at node index ni *)
@@ -1818,7 +1827,7 @@ let rec transfer : env -> fun_cfg:F.fun_cfg -> Lval_env.t D.transfn =
   check_tainted_at_exit_sinks node env_at_exit;
   Log.debug (fun m ->
       m ~tags:transfer_tag "Taint transfer %s\n  %s:\n  IN:  %s\n  OUT: %s"
-        (env.fun_name ||| "<FUN>")
+        (Option.map IL.str_of_name env.fun_name ||| "<FUN>")
         (Display_IL.short_string_of_node_kind node.F.n)
         (Lval_env.to_string in') (Lval_env.to_string out'));
   { D.in_env = in'; out_env = out' }
@@ -1860,37 +1869,9 @@ and do_lambdas env (lambdas : IL.lambdas_cfgs) node =
   let effects_lambdas, out_envs_lambdas =
     lambdas_in_node
     |> List_.map (fun (name, lcfg) ->
-           let in_env =
-             (* We do some processing of the lambda parameters but it's mainly
-              * to enable taint propagation, e.g.
-              *
-              *     obj.do_something(lambda x: sink(x))
-              *
-              * so we can propagate taint from `obj` to `x`.
-              *)
-             lcfg.params
-             |> List.fold_left
-                  (fun lval_env param ->
-                    match param with
-                    | Param { pname = var; _ } ->
-                        (* This is a *new* variable, so we clean any taint that we may have
-                         * attached to it previously. This can happen when a lambda is called
-                         * inside a loop. *)
-                        let lval_env =
-                          Lval_env.clean lval_env (LV.lval_of_var var)
-                        in
-                        (* Now check if the parameter is itself a taint source. *)
-                        let _taints, _shape, lval_env =
-                          check_tainted_var { env with lval_env } var
-                        in
-                        lval_env
-                    | PatternParam _ (* TODO *)
-                    | FixmeParam ->
-                        lval_env)
-                  env.lval_env
-           in
-           fixpoint_lambda env.lang env.options env.config env.best_matches
-             env.java_props (IL.str_of_name name) lcfg in_env)
+           let in_env = mk_lambda_in_env env lcfg in
+           fixpoint_lambda env.taint_inst env.best_matches env.fun_name name
+             lcfg in_env)
     |> List_.split
   in
   let effects = Effects.union_list effects_lambdas in
@@ -1924,13 +1905,15 @@ and do_lambdas env (lambdas : IL.lambdas_cfgs) node =
   in
   (effects, out_env)
 
-and fixpoint_lambda lang options config best_matches java_props_cache name
-    lambda_cfg in_env : Effects.t * Lval_env.t =
+and fixpoint_lambda taint_inst best_matches fun_name lambda_name lambda_cfg
+    in_env : Effects.t * Lval_env.t =
   Log.debug (fun m ->
-      m "Analyzing lambda %s (%s)" name (Lval_env.to_string in_env));
+      m "Analyzing lambda %s (%s)"
+        (IL.str_of_name lambda_name)
+        (Lval_env.to_string in_env));
   let effects, mapping =
-    fixpoint_aux ~in_env ~name lang options config best_matches java_props_cache
-      lambda_cfg
+    fixpoint_aux taint_inst ~in_env ?name:fun_name ~in_lambda:(Some lambda_name)
+      best_matches lambda_cfg
   in
   let effects =
     effects
@@ -1944,8 +1927,8 @@ and fixpoint_lambda lang options config best_matches java_props_cache name
   let out_env = mapping.(lambda_cfg.cfg.exit).Dataflow_core.out_env in
   (effects, out_env)
 
-and fixpoint_aux ?in_env ?name:opt_name lang options config best_matches
-    java_props fun_cfg =
+and fixpoint_aux taint_inst ?in_env ?name:opt_name ~in_lambda best_matches
+    fun_cfg =
   let flow = fun_cfg.cfg in
   let init_mapping = DataflowX.new_node_array flow Lval_env.empty_inout in
   let enter_lval_env =
@@ -1955,13 +1938,11 @@ and fixpoint_aux ?in_env ?name:opt_name lang options config best_matches
   in
   let env =
     {
-      lang;
-      options;
-      config;
+      taint_inst;
       fun_name = opt_name;
+      in_lambda;
       lval_env = enter_lval_env;
       best_matches;
-      java_props;
       effects_acc = ref Effects.empty;
     }
   in
@@ -1972,7 +1953,7 @@ and fixpoint_aux ?in_env ?name:opt_name lang options config best_matches
       ~eq_env:Lval_env.equal ~init:init_mapping ~trans:(transfer env ~fun_cfg)
       ~forward:true ~flow
   in
-  log_timeout_warning config opt_name timeout;
+  log_timeout_warning taint_inst opt_name timeout;
   let exit_lval_env = end_mapping.(flow.exit).D.out_env in
   effects_from_arg_updates_at_exit enter_lval_env exit_lval_env
   |> record_effects env;
@@ -1983,15 +1964,12 @@ and fixpoint_aux ?in_env ?name:opt_name lang options config best_matches
 (*****************************************************************************)
 
 and (fixpoint :
+      Taint_rule_inst.t ->
       ?in_env:Lval_env.t ->
-      ?name:Var_env.var ->
-      Lang.t ->
-      Rule_options.t ->
-      config ->
-      java_props_cache ->
+      ?name:IL.name ->
       F.fun_cfg ->
       Effects.t * mapping) =
- fun ?in_env ?name:opt_name lang options config java_props fun_cfg ->
+ fun taint_inst ?in_env ?name:opt_name fun_cfg ->
   let best_matches =
     (* Here we compute the "canonical" or "best" source/sanitizer/sink matches,
      * for each source/sanitizer/sink we check whether there is a "best match"
@@ -2000,24 +1978,26 @@ and (fixpoint :
     fun_cfg
     |> TM.best_matches_in_nodes ~sub_matches_of_orig:(fun orig ->
            let sources =
-             orig_is_source config orig |> List.to_seq
+             orig_is_source taint_inst orig
+             |> List.to_seq
              |> Seq.filter (fun (m : R.taint_source TM.t) ->
                     m.spec.source_exact)
              |> Seq.map (fun m -> TM.Any m)
            in
            let sanitizers =
-             orig_is_sanitizer config orig
+             orig_is_sanitizer taint_inst orig
              |> List.to_seq
              |> Seq.filter (fun (m : R.taint_sanitizer TM.t) ->
                     m.spec.sanitizer_exact)
              |> Seq.map (fun m -> TM.Any m)
            in
            let sinks =
-             orig_is_sink config orig |> List.to_seq
+             orig_is_sink taint_inst orig
+             |> List.to_seq
              |> Seq.filter (fun (m : R.taint_sink TM.t) -> m.spec.sink_exact)
              |> Seq.map (fun m -> TM.Any m)
            in
            sources |> Seq.append sanitizers |> Seq.append sinks)
   in
-  fixpoint_aux ?in_env ?name:opt_name lang options config best_matches
-    java_props fun_cfg
+  fixpoint_aux taint_inst ?in_env ?name:opt_name ~in_lambda:None best_matches
+    fun_cfg
diff --git a/src/tainting/Dataflow_tainting.mli b/src/tainting/Dataflow_tainting.mli
index 7ab3cba4f6e0..8d30e608ef25 100644
--- a/src/tainting/Dataflow_tainting.mli
+++ b/src/tainting/Dataflow_tainting.mli
@@ -1,79 +1,7 @@
-type var = Dataflow_var_env.var
-(** A string of the form "<source name>:<sid>". *)
-
-type a_propagator = {
-  kind : [ `From | `To ];
-  prop : Rule.taint_propagator;
-  var : var; (* REMOVE USE prop.id *)
-}
-
-type effects_handler =
-  var option (** function name ('None' if anonymous) *) ->
-  Shape_and_sig.Effect.t list ->
-  Shape_and_sig.Effect.t list
-
-type config = {
-  filepath : string;  (** File under analysis, for Deep Semgrep. *)
-  rule_id : Rule_ID.t;  (** Taint rule id, for Deep Semgrep. *)
-  track_control : bool;
-      (** Whether the rule requires tracking "control taint". *)
-  is_source : AST_generic.any -> Rule.taint_source Taint_spec_match.t list;
-      (** Test whether 'any' is a taint source, this corresponds to
-      * 'pattern-sources:' in taint-mode. *)
-  is_propagator : AST_generic.any -> a_propagator Taint_spec_match.t list;
-      (** Test whether 'any' matches a taint propagator, this corresponds to
-       * 'pattern-propagators:' in taint-mode.
-       *
-       * Propagators allow to specify how taint propagates through side effects.
-       *
-       * Note that we tried to solve this with a hack in returntocorp/semgrep#5150
-       * but it caused a bunch of FPs in semgrep-rules. The hack was essentially
-       * to assume that in `x.f(y)` taint always propagated from `y` to `x`.
-       *
-       * The typical FP was a call that incorrectly tainted an object or module,
-       * that also happened to be part of a sink specification. For example, in
-       * rule ruby.rails.security.audit.avoid-tainted-shell-call the `Shell` class
-       * does not really get tainted even if we call `Shell.cat` on tainted data:
-       *
-       *     # ruleid: avoid-tainted-shell-call
-       *     Shell.cat(params[:filename])
-       *
-       * But with the hack, `Shell` becomes tainted. Later on, when we call
-       * `Shell.cat` on safe data, it triggered an FP. Why? Because the entire
-       * `Shell.cat(...)` was marked as a sink, and `Shell` was considered
-       * tainted!
-       *
-       *     # ok: avoid-tainted-shell-call
-       *     Shell.cat("/var/log/www/access.log")
-       *
-       * Most of these FPs could be prevented by fine tuning pattern-sinks. But
-       * anyhow it's clearly incorrect to taint `Shell`, so a better solution was
-       * needed (hence `pattern-propagators`).
-       *)
-  is_sink : AST_generic.any -> Rule.taint_sink Taint_spec_match.t list;
-      (** Test whether 'any' is a sink, this corresponds to 'pattern-sinks:'
-      * in taint-mode. *)
-  is_sanitizer :
-    AST_generic.any -> Rule.taint_sanitizer Taint_spec_match.t list;
-      (** Test whether 'any' is a sanitizer, this corresponds to
-      * 'pattern-sanitizers:' in taint-mode. *)
-  unify_mvars : bool;  (** Unify metavariables in sources and sinks? *)
-  handle_effects : effects_handler;  (** Callback to report effects. *)
-}
-(** Taint rule instantiated for a given file.
-  *
-  * For a source to taint a sink, the bindings of both source and sink must be
-  * unifiable. See 'unify_meta_envs'. *)
-
 type mapping = Taint_lval_env.t Dataflow_core.mapping
 (** Mapping from variables to taint sources (if the variable is tainted).
   * If a variable is not in the map, then it's not tainted. *)
 
-type fun_env = (var, Taint.Taint_set.t) Hashtbl.t
-(** Set of functions known to act as taint sources (their output is
-  * tainted). This is used for a HACK to do some poor-man's intrafile
-  * interprocedural taint tracking. TO BE DEPRECATED. *)
-
 type java_props_cache
 (** When we encounter getters/setters without a definition, we need to resolve them
   * to their corresponding property, we cache the results here. *)
@@ -81,7 +9,9 @@ type java_props_cache
 val mk_empty_java_props_cache : unit -> java_props_cache
 
 val hook_function_taint_signature :
-  (config -> AST_generic.expr -> Shape_and_sig.Signature.t option) option ref
+  (Taint_rule_inst.t -> AST_generic.expr -> Shape_and_sig.Signature.t option)
+  option
+  ref
 (** Pro inter-file (aka deep) *)
 
 val hook_find_attribute_in_class :
@@ -89,7 +19,7 @@ val hook_find_attribute_in_class :
 (** Pro inter-file (aka deep) *)
 
 val hook_check_tainted_at_exit_sinks :
-  (config ->
+  (Taint_rule_inst.t ->
   Taint_lval_env.t ->
   IL.node ->
   (Taint.taints * Shape_and_sig.Effect.sink list) option)
@@ -98,12 +28,9 @@ val hook_check_tainted_at_exit_sinks :
 (** Pro: support for `at-exit: true` sinks *)
 
 val fixpoint :
+  Taint_rule_inst.t ->
   ?in_env:Taint_lval_env.t ->
-  ?name:var ->
-  Lang.t ->
-  Rule_options.t ->
-  config ->
-  java_props_cache ->
+  ?name:IL.name ->
   IL.fun_cfg ->
   Shape_and_sig.Effects.t * mapping
 (** Main entry point, [fixpoint config cfg] returns a mapping (effectively a set)
@@ -117,4 +44,4 @@ val fixpoint :
 
 (* TODO: Move to module 'Taint' maybe. *)
 val drop_taints_if_bool_or_number :
-  Rule_options_t.t -> Taint.Taint_set.t -> 'a Type.t -> Taint.Taint_set.t
+  Rule_options.t -> Taint.Taint_set.t -> 'a Type.t -> Taint.Taint_set.t
diff --git a/src/tainting/Taint_rule_inst.ml b/src/tainting/Taint_rule_inst.ml
new file mode 100644
index 000000000000..fe8206f7e8b4
--- /dev/null
+++ b/src/tainting/Taint_rule_inst.ml
@@ -0,0 +1,112 @@
+(* Yoann Padioleau, Iago Abal
+ *
+ * Copyright (C) 2019-2024 Semgrep Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * version 2.1 as published by the Free Software Foundation, with the
+ * special exception on linking described in file LICENSE.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the file
+ * LICENSE for more details.
+ *)
+
+(* A taint rule inst(ance).
+ *
+ * This is the "instantiation" of a taint rule for an specific file. A taint rule
+ * consists of several pattern formulas that specify what is a source/sink/etc.
+ * We instantiate a taint rule by matching those formulas on the file, obtaining
+ * a set of "predicates" (see type 'spec_predicates' below) that allow us to label
+ * an arbitrary sub-AST in the file as being a source/sink/etc or not. Once we have
+ * these predicates, we are then able to run the actual taint analysis.
+ *
+ * Module 'Match_taint_spec' generates a taint rule instance given a taint rule
+ * and a file, and 'Dataflow_tainting' runs the actual taint analysis given a
+ * taint rule intance and a function from that file.
+ *)
+
+type var = Dataflow_var_env.var
+(** A string of the form "<source name>:<sid>". *)
+
+type a_propagator = {
+  kind : [ `From | `To ];
+  prop : Rule.taint_propagator;
+  var : var; (* TODO: Use prop.id instead ? *)
+}
+
+type spec_predicates = {
+  is_source : AST_generic.any -> Rule.taint_source Taint_spec_match.t list;
+      (** Test whether 'any' is a taint source, this corresponds to
+      * 'pattern-sources:' in taint-mode. *)
+  is_propagator : AST_generic.any -> a_propagator Taint_spec_match.t list;
+      (** Test whether 'any' matches a taint propagator, this corresponds to
+       * 'pattern-propagators:' in taint-mode.
+       *
+       * Propagators allow to specify how taint propagates through side effects.
+       *
+       * Note that we tried to solve this with a hack in returntocorp/semgrep#5150
+       * but it caused a bunch of FPs in semgrep-rules. The hack was essentially
+       * to assume that in `x.f(y)` taint always propagated from `y` to `x`.
+       *
+       * The typical FP was a call that incorrectly tainted an object or module,
+       * that also happened to be part of a sink specification. For example, in
+       * rule ruby.rails.security.audit.avoid-tainted-shell-call the `Shell` class
+       * does not really get tainted even if we call `Shell.cat` on tainted data:
+       *
+       *     # ruleid: avoid-tainted-shell-call
+       *     Shell.cat(params[:filename])
+       *
+       * But with the hack, `Shell` becomes tainted. Later on, when we call
+       * `Shell.cat` on safe data, it triggered an FP. Why? Because the entire
+       * `Shell.cat(...)` was marked as a sink, and `Shell` was considered
+       * tainted!
+       *
+       *     # ok: avoid-tainted-shell-call
+       *     Shell.cat("/var/log/www/access.log")
+       *
+       * Most of these FPs could be prevented by fine tuning pattern-sinks. But
+       * anyhow it's clearly incorrect to taint `Shell`, so a better solution was
+       * needed (hence `pattern-propagators`).
+       *)
+  is_sanitizer :
+    AST_generic.any -> Rule.taint_sanitizer Taint_spec_match.t list;
+      (** Test whether 'any' is a sanitizer, this corresponds to
+      * 'pattern-sanitizers:' in taint-mode. *)
+  is_sink : AST_generic.any -> Rule.taint_sink Taint_spec_match.t list;
+      (** Test whether 'any' is a sink, this corresponds to 'pattern-sinks:'
+      * in taint-mode. *)
+}
+
+type effects_handler =
+  IL.name option (** name of the function definition ('None' if anonymous) *) ->
+  Shape_and_sig.Effect.t list ->
+  Shape_and_sig.Effect.t list
+
+type java_props_cache = (string * AST_generic.SId.t, IL.name) Hashtbl.t
+
+type t = {
+  lang : Lang.t;
+  file : Fpath.t;  (** File under analysis, for Deep Semgrep. *)
+  rule_id : Rule_ID.t;  (** Taint rule id, for Deep Semgrep. *)
+  options : Rule_options.t;
+  track_control : bool;
+      (** Whether the rule requires tracking "control taint". If it does not,
+       * then we avoid adding control taint-variables to environment. *)
+  preds : spec_predicates;
+  handle_effects : effects_handler;  (** Callback to report effects. *)
+  java_props_cache : java_props_cache;
+      (** Pro should be autogenerating definitions for these getters/setters,
+    * but that seems to hurt performance and it's still unclear why, so instead
+    * we give taint access to Pro typing info through a hook
+    * ('Dataflow_tainting.hook_find_attribute_in_class') and look for the
+    * property corresponding to the getter/setter.
+    *
+    * On very large files, allocating a new name every time could have a perf
+    * impact, so we cache them. *)
+}
+(** Taint rule instantiated for a given file.
+  *
+  * For a source to taint a sink, the bindings of both source and sink must be
+  * unifiable. See 'Dataflow_tainting.unify_meta_envs'. *)
diff --git a/src/targeting/Find_targets.ml b/src/targeting/Find_targets.ml
index c9a197ced1d9..f27fd1372b8c 100644
--- a/src/targeting/Find_targets.ml
+++ b/src/targeting/Find_targets.ml
@@ -254,7 +254,11 @@ let filter_path (ign : Gitignore.filter)
       (* We need to filter those paths ASAP otherwise we can get some exn later
        * when trying to process targets that actually do not exist.
        *)
-      | exception Unix.Unix_error (_err, _fun, _info) -> Ignore_silently)
+      | exception Unix.Unix_error (err, _fun, _info) ->
+          Log.debug (fun m ->
+              m "lstat: system error on file '%s': %s" !!fpath
+                (Unix.error_message err));
+          Ignore_silently)
 
 (*
    Filter a pre-expanded list of target files, such as a list of files
@@ -282,7 +286,8 @@ let filter_paths
          (* shouldn't happen if we work on the output of 'git ls-files *)
          | Dir -> ()
          | Skip x -> skip x
-         | Ignore_silently -> ());
+         | Ignore_silently ->
+             Log.debug (fun m -> m "ignore silently: %s" !!(fppath.fpath)));
   (Fppath_set.of_list !selected_paths, !skipped)
 
 let filter_size_and_minified max_target_bytes exclude_minified_files paths =


Test patch:
diff --git a/cli/tests/conftest.py b/cli/tests/conftest.py
index 37e5656d9810..ee30fd09fdfb 100644
--- a/cli/tests/conftest.py
+++ b/cli/tests/conftest.py
@@ -301,6 +301,14 @@ def mask_variable_text(
     return text
 
 
+# GIT_CONFIG_NOGLOBAL=true prevents reading the user's git configuration
+# which varies from one developer to another and causes variable output.
+def create_git_repo() -> None:
+    os.system("GIT_CONFIG_NOGLOBAL=true git init")
+    os.system("GIT_CONFIG_NOGLOBAL=true git add .")
+    os.system("GIT_CONFIG_NOGLOBAL=true git commit -m 'add files'")
+
+
 ##############################################################################
 # Calls to semgrep
 ##############################################################################
diff --git a/cli/tests/default/e2e-other/parsers/test_mix.py b/cli/tests/default/e2e-other/parsers/test_mix.py
index 1258467cb812..e229b0803e85 100644
--- a/cli/tests/default/e2e-other/parsers/test_mix.py
+++ b/cli/tests/default/e2e-other/parsers/test_mix.py
@@ -405,6 +405,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="cowboy",
@@ -417,6 +418,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="cowlib",
@@ -429,6 +431,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="ecpool",
@@ -441,6 +444,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="ehttpc",
@@ -453,6 +457,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="ex_aws",
@@ -465,6 +470,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="ex_aws_lambda",
@@ -477,6 +483,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="ex_aws_s3",
@@ -489,6 +496,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="ex_aws_ses",
@@ -501,6 +509,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="gproc",
@@ -513,6 +522,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="grpc",
@@ -525,6 +535,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="gun",
@@ -537,6 +548,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="hackney",
@@ -549,6 +561,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="idna",
@@ -561,6 +574,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="jason",
@@ -573,6 +587,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="joken",
@@ -585,6 +600,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="jose",
@@ -597,6 +613,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="metrics",
@@ -609,6 +626,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="mime",
@@ -621,6 +639,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="mimerl",
@@ -633,6 +652,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="parse_trans",
@@ -645,6 +665,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="pbkdf2",
@@ -657,6 +678,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="ranch",
@@ -669,6 +691,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="rocksdb",
@@ -681,6 +704,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="snabbkaffe",
@@ -693,6 +717,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="ssl_verify_fun",
@@ -705,6 +730,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="sweet_xml",
@@ -717,6 +743,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="telemetry",
@@ -729,6 +756,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="typerefl",
@@ -741,6 +769,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="unicode_util_compat",
@@ -753,6 +782,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
             FoundDependency(
                 package="uuid",
@@ -765,6 +795,7 @@ def test_full_mix_parsing(run_semgrep_on_copied_files: RunSemgrep):
                 children=None,
                 git_ref=None,
                 lockfile_path=Fpath(str(lockfile_path)),
+                manifest_path=Fpath(str(manifest_path)) if manifest_path else None,
             ),
         ],
         [],
diff --git a/cli/tests/default/e2e-other/parsers/test_package_lock.py b/cli/tests/default/e2e-other/parsers/test_package_lock.py
index 575a9a8cbefa..d059f440ca8a 100644
--- a/cli/tests/default/e2e-other/parsers/test_package_lock.py
+++ b/cli/tests/default/e2e-other/parsers/test_package_lock.py
@@ -27,7 +27,7 @@ def test_package_lock_v2_parser_ignores_root():
             },
         )
     }
-    parsed_deps = parse_packages_field(lockfile_path, only_root_dep)
+    parsed_deps = parse_packages_field(lockfile_path, only_root_dep, manifest_path=None)
     assert parsed_deps == []
 
 
@@ -141,7 +141,7 @@ def test_package_lock_v2_parser_produces_correct_deps():
         ),
     }
 
-    parsed_deps = parse_packages_field(lockfile_path, v3_deps)
+    parsed_deps = parse_packages_field(lockfile_path, v3_deps, manifest_path=None)
     assert parsed_deps == [
         FoundDependency(
             package="@popperjs/core",
@@ -158,6 +158,7 @@ def test_package_lock_v2_parser_produces_correct_deps():
             children=None,
             git_ref=None,
             lockfile_path=Fpath(str(lockfile_path)),
+            manifest_path=None,
         ),
         FoundDependency(
             package="legacy-bootstrap",
@@ -174,5 +175,6 @@ def test_package_lock_v2_parser_produces_correct_deps():
             children=None,
             git_ref=None,
             lockfile_path=Fpath(str(lockfile_path)),
+            manifest_path=None,
         ),
     ]
diff --git a/cli/tests/default/e2e-other/parsers/test_swiftpm.py b/cli/tests/default/e2e-other/parsers/test_swiftpm.py
index 62903f8a888c..8a6bcfaabf12 100644
--- a/cli/tests/default/e2e-other/parsers/test_swiftpm.py
+++ b/cli/tests/default/e2e-other/parsers/test_swiftpm.py
@@ -301,7 +301,9 @@ def test_swift_lockfile_v1_parser():
     """
     ).as_dict()
 
-    found_deps = swiftpm.parse_swiftpm_v1(lockfile_path, lockfile_v1, {""})
+    found_deps = swiftpm.parse_swiftpm_v1(
+        lockfile_path, lockfile_v1, {""}, manifest_path=None
+    )
     expected_deps = [
         FoundDependency(
             package="curry",
@@ -313,6 +315,7 @@ def test_swift_lockfile_v1_parser():
             git_ref="4331dd50bc1db007db664a23f32e6f3df93d4e1a",
             resolved_url="https://github.com/thoughtbot/Curry.git",
             lockfile_path=Fpath(str(lockfile_path)),
+            manifest_path=None,
         ),
         FoundDependency(
             package="prettycolors",
@@ -324,6 +327,7 @@ def test_swift_lockfile_v1_parser():
             git_ref="afd4553a4db6f656521cfe9b1f70bece2748c7d8",
             resolved_url="https://github.com/jdhealy/PrettyColors.git",
             lockfile_path=Fpath(str(lockfile_path)),
+            manifest_path=None,
         ),
     ]
 
@@ -363,7 +367,9 @@ def test_swift_lockfile_v2_parser():
     """
     ).as_dict()
 
-    found_deps = swiftpm.parse_swiftpm_v2(lockfile_path, lockfile_v2, {""})
+    found_deps = swiftpm.parse_swiftpm_v2(
+        lockfile_path, lockfile_v2, {""}, manifest_path=None
+    )
     expected_deps = [
         FoundDependency(
             package="bson",
@@ -375,6 +381,7 @@ def test_swift_lockfile_v2_parser():
             git_ref="944dfb3b0eb028f477c25ba6a071181de8ab903a",
             resolved_url="https://github.com/orlandos-nl/BSON.git",
             lockfile_path=Fpath(str(lockfile_path)),
+            manifest_path=None,
         ),
         FoundDependency(
             package="dnsclient",
@@ -386,6 +393,7 @@ def test_swift_lockfile_v2_parser():
             git_ref="770249dcb7259c486f2d68c164091b115ccb765f",
             resolved_url="https://github.com/orlandos-nl/DNSClient.git",
             lockfile_path=Fpath(str(lockfile_path)),
+            manifest_path=None,
         ),
     ]
 
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareansi-html.yaml-dependency_awareansi/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareansi-html.yaml-dependency_awareansi/results.txt
index bd0095e34dd0..0cdce8d0b60c 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareansi-html.yaml-dependency_awareansi/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareansi-html.yaml-dependency_awareansi/results.txt
@@ -57,6 +57,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "npm",
               "line_number": 17,
               "lockfile_path": "targets/dependency_aware/ansi/yarn.lock",
+              "manifest_path": "targets/dependency_aware/ansi/package.json",
               "package": "ansi-html",
               "resolved_url": "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz",
               "transitivity": "unknown",
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareawscli_vuln.yaml-dependency_awareawscli-with-manifest/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareawscli_vuln.yaml-dependency_awareawscli-with-manifest/results.txt
index 3454ed193830..5aca4b176817 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareawscli_vuln.yaml-dependency_awareawscli-with-manifest/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareawscli_vuln.yaml-dependency_awareawscli-with-manifest/results.txt
@@ -49,6 +49,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 20,
               "lockfile_path": "targets/dependency_aware/awscli-with-manifest/Pipfile.lock",
+              "manifest_path": "targets/dependency_aware/awscli-with-manifest/Pipfile",
               "package": "awscli",
               "transitivity": "direct",
               "version": "1.11.82"
@@ -100,6 +101,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 20,
               "lockfile_path": "targets/dependency_aware/awscli-with-manifest/Pipfile.lock",
+              "manifest_path": "targets/dependency_aware/awscli-with-manifest/Pipfile",
               "package": "awscli",
               "transitivity": "direct",
               "version": "1.11.82"
@@ -151,6 +153,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 20,
               "lockfile_path": "targets/dependency_aware/awscli-with-manifest/Pipfile.lock",
+              "manifest_path": "targets/dependency_aware/awscli-with-manifest/Pipfile",
               "package": "awscli",
               "transitivity": "direct",
               "version": "1.11.82"
@@ -203,6 +206,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 20,
               "lockfile_path": "targets/dependency_aware/awscli-with-manifest/Pipfile.lock",
+              "manifest_path": "targets/dependency_aware/awscli-with-manifest/Pipfile",
               "package": "awscli",
               "transitivity": "direct",
               "version": "1.11.82"
@@ -255,6 +259,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 20,
               "lockfile_path": "targets/dependency_aware/awscli-with-manifest/Pipfile.lock",
+              "manifest_path": "targets/dependency_aware/awscli-with-manifest/Pipfile",
               "package": "awscli",
               "transitivity": "direct",
               "version": "1.11.82"
@@ -307,6 +312,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 20,
               "lockfile_path": "targets/dependency_aware/awscli-with-manifest/Pipfile.lock",
+              "manifest_path": "targets/dependency_aware/awscli-with-manifest/Pipfile",
               "package": "awscli",
               "transitivity": "direct",
               "version": "1.11.82"
@@ -359,6 +365,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 20,
               "lockfile_path": "targets/dependency_aware/awscli-with-manifest/Pipfile.lock",
+              "manifest_path": "targets/dependency_aware/awscli-with-manifest/Pipfile",
               "package": "awscli",
               "transitivity": "direct",
               "version": "1.11.82"
@@ -411,6 +418,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 20,
               "lockfile_path": "targets/dependency_aware/awscli-with-manifest/Pipfile.lock",
+              "manifest_path": "targets/dependency_aware/awscli-with-manifest/Pipfile",
               "package": "awscli",
               "transitivity": "direct",
               "version": "1.11.82"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaredart-parity.yaml-dependency_awaredart/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaredart-parity.yaml-dependency_awaredart/results.txt
index ef43786441ea..d57447841542 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaredart-parity.yaml-dependency_awaredart/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaredart-parity.yaml-dependency_awaredart/results.txt
@@ -43,6 +43,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pub",
               "line_number": 13,
               "lockfile_path": "targets/dependency_aware/dart/pubspec.lock",
+              "manifest_path": "targets/dependency_aware/dart/pubspec.yaml",
               "package": "async",
               "transitivity": "transitive",
               "version": "2.11.0"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarego-sca.yaml-dependency_awarego/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarego-sca.yaml-dependency_awarego/results.txt
index 553e6f76947d..22230f39f8ed 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarego-sca.yaml-dependency_awarego/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarego-sca.yaml-dependency_awarego/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "gomod",
               "line_number": 17,
               "lockfile_path": "targets/dependency_aware/go/go.mod",
+              "manifest_path": "targets/dependency_aware/go/go.mod",
               "package": "github.com/cheekybits/genny",
               "resolved_url": "github.com/cheekybits/genny",
               "transitivity": "direct",
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarego-sca.yaml-dependency_awarego_multi_newline/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarego-sca.yaml-dependency_awarego_multi_newline/results.txt
index 8cd6acc0bc85..67cd04cb3ebe 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarego-sca.yaml-dependency_awarego_multi_newline/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarego-sca.yaml-dependency_awarego_multi_newline/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "gomod",
               "line_number": 19,
               "lockfile_path": "targets/dependency_aware/go_multi_newline/go.mod",
+              "manifest_path": "targets/dependency_aware/go_multi_newline/go.mod",
               "package": "github.com/cheekybits/genny",
               "resolved_url": "github.com/cheekybits/genny",
               "transitivity": "direct",
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarego-sca.yaml-dependency_awarego_toolchain/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarego-sca.yaml-dependency_awarego_toolchain/results.txt
index 59bbd25d62ef..7134400b5bae 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarego-sca.yaml-dependency_awarego_toolchain/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarego-sca.yaml-dependency_awarego_toolchain/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "gomod",
               "line_number": 19,
               "lockfile_path": "targets/dependency_aware/go_toolchain/go.mod",
+              "manifest_path": "targets/dependency_aware/go_toolchain/go.mod",
               "package": "github.com/cheekybits/genny",
               "resolved_url": "github.com/cheekybits/genny",
               "transitivity": "direct",
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaregradle-guava.yaml-dependency_awaregradle-direct/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaregradle-guava.yaml-dependency_awaregradle-direct/results.txt
index 058c45161b3a..55d2083d2e8b 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaregradle-guava.yaml-dependency_awaregradle-direct/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaregradle-guava.yaml-dependency_awaregradle-direct/results.txt
@@ -43,6 +43,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "maven",
               "line_number": 7,
               "lockfile_path": "targets/dependency_aware/gradle-direct/gradle.lockfile",
+              "manifest_path": "targets/dependency_aware/gradle-direct/build.gradle",
               "package": "com.google.guava:guava",
               "transitivity": "direct",
               "version": "30.1-jre"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle-arbitrary-comment/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle-arbitrary-comment/results.txt
index b52b8ed5813a..8a5ddddb453c 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle-arbitrary-comment/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle-arbitrary-comment/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "maven",
               "line_number": 320,
               "lockfile_path": "targets/dependency_aware/gradle-arbitrary-comment/gradle.lockfile",
+              "manifest_path": "targets/dependency_aware/gradle-arbitrary-comment/build.gradle",
               "package": "org.webjars.npm:swagger-ui-dist",
               "transitivity": "transitive",
               "version": "3.35.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle-no-comment/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle-no-comment/results.txt
index d3116ed687f6..2e78ae9bf994 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle-no-comment/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle-no-comment/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "maven",
               "line_number": 314,
               "lockfile_path": "targets/dependency_aware/gradle-no-comment/gradle.lockfile",
+              "manifest_path": "targets/dependency_aware/gradle-no-comment/build.gradle",
               "package": "org.webjars.npm:swagger-ui-dist",
               "transitivity": "transitive",
               "version": "3.35.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle/results.txt
index 835af883be56..90056f89eeee 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "maven",
               "line_number": 317,
               "lockfile_path": "targets/dependency_aware/gradle/gradle.lockfile",
+              "manifest_path": "targets/dependency_aware/gradle/build.gradle",
               "package": "org.webjars.npm:swagger-ui-dist",
               "transitivity": "transitive",
               "version": "3.35.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle_trailing_newline/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle_trailing_newline/results.txt
index cc1afe39d2d2..5722f88c8c04 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle_trailing_newline/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejava-gradle-sca.yaml-dependency_awaregradle_trailing_newline/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "maven",
               "line_number": 317,
               "lockfile_path": "targets/dependency_aware/gradle_trailing_newline/gradle/gradle.lockfile",
+              "manifest_path": "targets/dependency_aware/gradle_trailing_newline/gradle/build.gradle",
               "package": "org.webjars.npm:swagger-ui-dist",
               "transitivity": "transitive",
               "version": "3.35.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejs-yarn2-sca.yaml-dependency_awareyarn2/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejs-yarn2-sca.yaml-dependency_awareyarn2/results.txt
index c8b4f5250815..7accd3d8ba4e 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejs-yarn2-sca.yaml-dependency_awareyarn2/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarejs-yarn2-sca.yaml-dependency_awareyarn2/results.txt
@@ -48,6 +48,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "npm",
               "line_number": 34,
               "lockfile_path": "targets/dependency_aware/yarn2/yarn.lock",
+              "manifest_path": "targets/dependency_aware/yarn2/package.json",
               "package": "@babel/core",
               "transitivity": "direct",
               "version": "7.19.6"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarelog4shell.yaml-dependency_awarelog4j/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarelog4shell.yaml-dependency_awarelog4j/results.txt
index 663a81c1fac3..bf6d010b0b3e 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarelog4shell.yaml-dependency_awarelog4j/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarelog4shell.yaml-dependency_awarelog4j/results.txt
@@ -111,6 +111,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "maven",
               "line_number": 3,
               "lockfile_path": "targets/dependency_aware/log4j/maven_dep_tree.txt",
+              "manifest_path": "targets/dependency_aware/log4j/pom.xml",
               "package": "org.apache.logging.log4j:log4j-core",
               "transitivity": "direct",
               "version": "0.0.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarephp-sca.yaml-dependency_awarephp/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarephp-sca.yaml-dependency_awarephp/results.txt
index 6e4554f7b56e..9f4b15716282 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarephp-sca.yaml-dependency_awarephp/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarephp-sca.yaml-dependency_awarephp/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "composer",
               "line_number": 10,
               "lockfile_path": "targets/dependency_aware/php/composer.lock",
+              "manifest_path": "targets/dependency_aware/php/composer.json",
               "package": "bad/package",
               "resolved_url": "https://api.github.com/repos/google/recaptcha/zipball/614f25a9038be4f3f2da7cbfd778dc5b357d2419",
               "transitivity": "direct",
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-pipfile-pyhcl-sca.yaml-dependency_awarepipfile_with_vuln_in_dev-packages/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-pipfile-pyhcl-sca.yaml-dependency_awarepipfile_with_vuln_in_dev-packages/results.txt
index c6bc4a24be9b..cfbc6ab71139 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-pipfile-pyhcl-sca.yaml-dependency_awarepipfile_with_vuln_in_dev-packages/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-pipfile-pyhcl-sca.yaml-dependency_awarepipfile_with_vuln_in_dev-packages/results.txt
@@ -49,6 +49,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 37,
               "lockfile_path": "targets/dependency_aware/pipfile_with_vuln_in_dev-packages/Pipfile.lock",
+              "manifest_path": "targets/dependency_aware/pipfile_with_vuln_in_dev-packages/Pipfile",
               "package": "pyhcl",
               "transitivity": "direct",
               "version": "1.6.0"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-pipfile-sca.yaml-dependency_awarepipfile_with_empty_dev-packages/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-pipfile-sca.yaml-dependency_awarepipfile_with_empty_dev-packages/results.txt
index a2b24deb55e1..d6c30507f3be 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-pipfile-sca.yaml-dependency_awarepipfile_with_empty_dev-packages/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-pipfile-sca.yaml-dependency_awarepipfile_with_empty_dev-packages/results.txt
@@ -49,6 +49,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 29,
               "lockfile_path": "targets/dependency_aware/pipfile_with_empty_dev-packages/Pipfile.lock",
+              "manifest_path": "targets/dependency_aware/pipfile_with_empty_dev-packages/Pipfile",
               "package": "pandas",
               "transitivity": "transitive",
               "version": "1.4.0"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-pipfile-sca.yaml-dependency_awarepipfile_with_one_newline_between_sections/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-pipfile-sca.yaml-dependency_awarepipfile_with_one_newline_between_sections/results.txt
index f2e6d15108f8..cecc23f38907 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-pipfile-sca.yaml-dependency_awarepipfile_with_one_newline_between_sections/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-pipfile-sca.yaml-dependency_awarepipfile_with_one_newline_between_sections/results.txt
@@ -49,6 +49,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 29,
               "lockfile_path": "targets/dependency_aware/pipfile_with_one_newline_between_sections/Pipfile.lock",
+              "manifest_path": "targets/dependency_aware/pipfile_with_one_newline_between_sections/Pipfile",
               "package": "pandas",
               "transitivity": "transitive",
               "version": "1.4.0"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awaremanifest_parse_error/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awaremanifest_parse_error/results.txt
index 4ad309775c73..221990d8c9a3 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awaremanifest_parse_error/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awaremanifest_parse_error/results.txt
@@ -43,6 +43,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 99,
               "lockfile_path": "targets/dependency_aware/manifest_parse_error/poetry.lock",
+              "manifest_path": "targets/dependency_aware/manifest_parse_error/pyproject.toml",
               "package": "faker",
               "transitivity": "unknown",
               "version": "1.0.0"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry/results.txt
index f820d7152b2a..80dadba4c02e 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 17,
               "lockfile_path": "targets/dependency_aware/poetry/poetry.lock",
+              "manifest_path": "targets/dependency_aware/poetry/pyproject.toml",
               "package": "faker",
               "transitivity": "direct",
               "version": "13.11.1"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_comments/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_comments/results.txt
index b4ba8185a7bc..8cd9647f279d 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_comments/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_comments/results.txt
@@ -43,6 +43,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 99,
               "lockfile_path": "targets/dependency_aware/poetry_comments/poetry.lock",
+              "manifest_path": "targets/dependency_aware/poetry_comments/pyproject.toml",
               "package": "faker",
               "transitivity": "direct",
               "version": "1.0.0"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_empty_table/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_empty_table/results.txt
index fc01df2e7edd..a04e7dd0418c 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_empty_table/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_empty_table/results.txt
@@ -43,6 +43,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 103,
               "lockfile_path": "targets/dependency_aware/poetry_empty_table/poetry.lock",
+              "manifest_path": "targets/dependency_aware/poetry_empty_table/pyproject.toml",
               "package": "faker",
               "transitivity": "direct",
               "version": "1.0.0"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_quoted_key/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_quoted_key/results.txt
index 25aca0d354ae..65660cf01e71 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_quoted_key/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_quoted_key/results.txt
@@ -43,6 +43,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 4,
               "lockfile_path": "targets/dependency_aware/poetry_quoted_key/poetry.lock",
+              "manifest_path": "targets/dependency_aware/poetry_quoted_key/pyproject.toml",
               "package": "faker",
               "transitivity": "direct",
               "version": "13.11.1"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_with_arbitrary_starting_comment/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_with_arbitrary_starting_comment/results.txt
index 18f940e93ee6..fa1bd2f437ad 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_with_arbitrary_starting_comment/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_with_arbitrary_starting_comment/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 19,
               "lockfile_path": "targets/dependency_aware/poetry_with_arbitrary_starting_comment/poetry.lock",
+              "manifest_path": "targets/dependency_aware/poetry_with_arbitrary_starting_comment/pyproject.toml",
               "package": "faker",
               "transitivity": "direct",
               "version": "13.11.1"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_with_uppercase_package_name/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_with_uppercase_package_name/results.txt
index 44987c44d55e..ab30ac1706cb 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_with_uppercase_package_name/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-poetry-sca.yaml-dependency_awarepoetry_with_uppercase_package_name/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 17,
               "lockfile_path": "targets/dependency_aware/poetry_with_uppercase_package_name/poetry.lock",
+              "manifest_path": "targets/dependency_aware/poetry_with_uppercase_package_name/pyproject.toml",
               "package": "faker",
               "transitivity": "direct",
               "version": "13.11.1"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements/results.txt
index 04e39fa792ba..50f524bc87f9 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 37,
               "lockfile_path": "targets/dependency_aware/requirements/requirements.txt",
+              "manifest_path": "targets/dependency_aware/requirements/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements3/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements3/results.txt
index ae4480fac5fa..2702412a1694 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements3/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements3/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 37,
               "lockfile_path": "targets/dependency_aware/requirements3/requirements3.txt",
+              "manifest_path": "targets/dependency_aware/requirements3/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_with_uppercase_package_name/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_with_uppercase_package_name/results.txt
index a92f009e9da4..e0af95c5f0bf 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_with_uppercase_package_name/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_with_uppercase_package_name/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 37,
               "lockfile_path": "targets/dependency_aware/requirements_with_uppercase_package_name/requirements.txt",
+              "manifest_path": "targets/dependency_aware/requirements_with_uppercase_package_name/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareswift-sca.yaml-dependency_awareswiftpmv1/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareswift-sca.yaml-dependency_awareswiftpmv1/results.txt
index acff061de8e1..c7a8b01c9c65 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareswift-sca.yaml-dependency_awareswiftpmv1/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareswift-sca.yaml-dependency_awareswiftpmv1/results.txt
@@ -45,6 +45,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "git_ref": "d029d9d39c87bed85b1c50adee7c41795261a192",
               "line_number": 28,
               "lockfile_path": "targets/dependency_aware/swiftpm/v1/Package.resolved",
+              "manifest_path": "targets/dependency_aware/swiftpm/v1/Package.swift",
               "package": "swift-collections",
               "resolved_url": "https://github.com/apple/swift-collections.git",
               "transitivity": "direct",
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareswift-sca.yaml-dependency_awareswiftpmv2/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareswift-sca.yaml-dependency_awareswiftpmv2/results.txt
index 22a85adb423e..48fe6007bbfa 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareswift-sca.yaml-dependency_awareswiftpmv2/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareswift-sca.yaml-dependency_awareswiftpmv2/results.txt
@@ -45,6 +45,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "git_ref": "d029d9d39c87bed85b1c50adee7c41795261a192",
               "line_number": 72,
               "lockfile_path": "targets/dependency_aware/swiftpm/v2/Package.resolved",
+              "manifest_path": "targets/dependency_aware/swiftpm/v2/Package.swift",
               "package": "swift-collections",
               "resolved_url": "https://github.com/apple/swift-collections.git",
               "transitivity": "transitive",
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaretransitive_and_direct.yaml-dependency_awaretransitive_and_directdirect_reachable_transitive_unreachable/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaretransitive_and_direct.yaml-dependency_awaretransitive_and_directdirect_reachable_transitive_unreachable/results.txt
index 03a7bcff14cb..56002277c23f 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaretransitive_and_direct.yaml-dependency_awaretransitive_and_directdirect_reachable_transitive_unreachable/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaretransitive_and_direct.yaml-dependency_awaretransitive_and_directdirect_reachable_transitive_unreachable/results.txt
@@ -48,6 +48,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "npm",
               "line_number": 51,
               "lockfile_path": "targets/dependency_aware/transitive_and_direct/direct_reachable_transitive_unreachable/package-lock.json",
+              "manifest_path": "targets/dependency_aware/transitive_and_direct/direct_reachable_transitive_unreachable/package.json",
               "package": "@types/jquery",
               "resolved_url": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.22.tgz",
               "transitivity": "direct",
@@ -100,6 +101,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "npm",
               "line_number": 40,
               "lockfile_path": "targets/dependency_aware/transitive_and_direct/direct_reachable_transitive_unreachable/package-lock.json",
+              "manifest_path": "targets/dependency_aware/transitive_and_direct/direct_reachable_transitive_unreachable/package.json",
               "package": "@types/jquery",
               "resolved_url": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.22.tgz",
               "transitivity": "transitive",
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaretransitive_and_direct.yaml-dependency_awaretransitive_and_directtransitive_not_reachable_if_direct/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaretransitive_and_direct.yaml-dependency_awaretransitive_and_directtransitive_not_reachable_if_direct/results.txt
index b5a064169b62..29b8e19b3dbf 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaretransitive_and_direct.yaml-dependency_awaretransitive_and_directtransitive_not_reachable_if_direct/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awaretransitive_and_direct.yaml-dependency_awaretransitive_and_directtransitive_not_reachable_if_direct/results.txt
@@ -48,6 +48,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "npm",
               "line_number": 40,
               "lockfile_path": "targets/dependency_aware/transitive_and_direct/transitive_not_reachable_if_direct/package-lock.json",
+              "manifest_path": "targets/dependency_aware/transitive_and_direct/transitive_not_reachable_if_direct/package.json",
               "package": "@types/jquery",
               "resolved_url": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.22.tgz",
               "transitivity": "transitive",
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__pypi_package_name_lowercase/rulesdependency_awarepython-poetry-case-insensitive-package.yaml-dependency_awarepoetry/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__pypi_package_name_lowercase/rulesdependency_awarepython-poetry-case-insensitive-package.yaml-dependency_awarepoetry/results.txt
index 64427cb89415..a8f2927a6538 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__pypi_package_name_lowercase/rulesdependency_awarepython-poetry-case-insensitive-package.yaml-dependency_awarepoetry/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__pypi_package_name_lowercase/rulesdependency_awarepython-poetry-case-insensitive-package.yaml-dependency_awarepoetry/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 17,
               "lockfile_path": "targets/dependency_aware/poetry/poetry.lock",
+              "manifest_path": "targets/dependency_aware/poetry/pyproject.toml",
               "package": "faker",
               "transitivity": "direct",
               "version": "13.11.1"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__pypi_package_name_lowercase/rulesdependency_awarepython-requirements-case-insensitive-package.yaml-dependency_awarerequirements/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__pypi_package_name_lowercase/rulesdependency_awarepython-requirements-case-insensitive-package.yaml-dependency_awarerequirements/results.txt
index 913c31327b92..7d47843fad5a 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__pypi_package_name_lowercase/rulesdependency_awarepython-requirements-case-insensitive-package.yaml-dependency_awarerequirements/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__pypi_package_name_lowercase/rulesdependency_awarepython-requirements-case-insensitive-package.yaml-dependency_awarerequirements/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 37,
               "lockfile_path": "targets/dependency_aware/requirements/requirements.txt",
+              "manifest_path": "targets/dependency_aware/requirements/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement/results.txt
index 304eee7a3b35..4aa08ef6bc13 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 37,
               "lockfile_path": "targets/dependency_aware/requirement/requirement.txt",
+              "manifest_path": "targets/dependency_aware/requirement/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement_pip/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement_pip/results.txt
index 431ca793bfe3..f0fb44436d7b 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement_pip/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement_pip/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 37,
               "lockfile_path": "targets/dependency_aware/requirement_pip/requirement.pip",
+              "manifest_path": "targets/dependency_aware/requirement_pip/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder/results.txt
index b9b913a90a8a..45899a4635ea 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder/results.txt
@@ -45,6 +45,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 1,
               "lockfile_path": "targets/dependency_aware/requirements_folder/requirements/prod.txt",
+              "manifest_path": "targets/dependency_aware/requirements_folder/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_dep_dupes/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_dep_dupes/results.txt
index 49bb672a39b0..449a1141dea0 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_dep_dupes/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_dep_dupes/results.txt
@@ -45,6 +45,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 1,
               "lockfile_path": "targets/dependency_aware/requirements_folder_dep_dupes/requirements/prod.txt",
+              "manifest_path": "targets/dependency_aware/requirements_folder_dep_dupes/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
@@ -92,6 +93,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 1,
               "lockfile_path": "targets/dependency_aware/requirements_folder_dep_dupes/requirements/stage.txt",
+              "manifest_path": "targets/dependency_aware/requirements_folder_dep_dupes/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_no_src/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_no_src/results.txt
index 8b2116fad019..47a5cac51e72 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_no_src/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_no_src/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 1,
               "lockfile_path": "targets/dependency_aware/requirements_folder_no_src/requirements/prod.txt",
+              "manifest_path": "targets/dependency_aware/requirements_folder_no_src/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_similar_deps/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_similar_deps/results.txt
index 0dde9c0ae31e..358cfb0c29aa 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_similar_deps/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_similar_deps/results.txt
@@ -45,6 +45,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 1,
               "lockfile_path": "targets/dependency_aware/requirements_folder_similar_deps/requirements/prod.txt",
+              "manifest_path": "targets/dependency_aware/requirements_folder_similar_deps/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
@@ -92,6 +93,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 82,
               "lockfile_path": "targets/dependency_aware/requirements_folder_similar_deps/requirements/stage.txt",
+              "manifest_path": "targets/dependency_aware/requirements_folder_similar_deps/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.1"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles/results.txt
index ee1574c5d7dd..ebc618a46b0b 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles/results.txt
@@ -46,6 +46,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 1,
               "lockfile_path": "targets/dependency_aware/requirements_multiple_lockfiles/requirements/prod.txt",
+              "manifest_path": "targets/dependency_aware/requirements_multiple_lockfiles/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes/results.txt
index 3234f25d229d..6da3b4e3168d 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes/results.txt
@@ -46,6 +46,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 1,
               "lockfile_path": "targets/dependency_aware/requirements_multiple_lockfiles_dep_dupes/requirements/prod.txt",
+              "manifest_path": "targets/dependency_aware/requirements_multiple_lockfiles_dep_dupes/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
@@ -93,6 +94,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 37,
               "lockfile_path": "targets/dependency_aware/requirements_multiple_lockfiles_dep_dupes/requirements.txt",
+              "manifest_path": "targets/dependency_aware/requirements_multiple_lockfiles_dep_dupes/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes_no_src/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes_no_src/results.txt
index 375ca76df722..4b69d66619b3 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes_no_src/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes_no_src/results.txt
@@ -45,6 +45,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 37,
               "lockfile_path": "targets/dependency_aware/requirements_multiple_lockfiles_dep_dupes_no_src/requirements.txt",
+              "manifest_path": "targets/dependency_aware/requirements_multiple_lockfiles_dep_dupes_no_src/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
@@ -91,6 +92,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 1,
               "lockfile_path": "targets/dependency_aware/requirements_multiple_lockfiles_dep_dupes_no_src/requirements/prod.txt",
+              "manifest_path": "targets/dependency_aware/requirements_multiple_lockfiles_dep_dupes_no_src/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_no_src/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_no_src/results.txt
index 3f2a356db277..45050361cc76 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_no_src/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_no_src/results.txt
@@ -45,6 +45,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 1,
               "lockfile_path": "targets/dependency_aware/requirements_multiple_lockfiles_no_src/requirements/prod.txt",
+              "manifest_path": "targets/dependency_aware/requirements_multiple_lockfiles_no_src/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_similar_deps/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_similar_deps/results.txt
index 7037bd8fce18..834b57219a30 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_similar_deps/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_similar_deps/results.txt
@@ -46,6 +46,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 1,
               "lockfile_path": "targets/dependency_aware/requirements_multiple_lockfiles_similar_deps/requirements/prod.txt",
+              "manifest_path": "targets/dependency_aware/requirements_multiple_lockfiles_similar_deps/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.0"
@@ -93,6 +94,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 37,
               "lockfile_path": "targets/dependency_aware/requirements_multiple_lockfiles_similar_deps/requirements.txt",
+              "manifest_path": "targets/dependency_aware/requirements_multiple_lockfiles_similar_deps/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_pip/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_pip/results.txt
index 4106cb16d76b..11e9c363aae1 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_pip/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_pip/results.txt
@@ -44,6 +44,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 37,
               "lockfile_path": "targets/dependency_aware/requirements_pip/requirements.pip",
+              "manifest_path": "targets/dependency_aware/requirements_pip/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_pip_folder/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_pip_folder/results.txt
index c6b2056abda7..db3db0511166 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_pip_folder/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_pip_folder/results.txt
@@ -45,6 +45,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
               "ecosystem": "pypi",
               "line_number": 1,
               "lockfile_path": "targets/dependency_aware/requirements_pip_folder/requirements/prod.pip",
+              "manifest_path": "targets/dependency_aware/requirements_pip_folder/requirements.in",
               "package": "pandas",
               "transitivity": "direct",
               "version": "1.4.2"
diff --git a/cli/tests/default/e2e/snapshots/test_target_selection/test_target_selection/git/files.list b/cli/tests/default/e2e/snapshots/test_target_selection/test_target_selection/git/files.list
new file mode 100644
index 000000000000..6e8ce078a2c7
--- /dev/null
+++ b/cli/tests/default/e2e/snapshots/test_target_selection/test_target_selection/git/files.list
@@ -0,0 +1,5 @@
+targets/target_selection/hello
+targets/target_selection/quote'/hello
+targets/target_selection/spaces !/hello
+targets/target_selection/~
+targets/target_selection/😊
diff --git a/cli/tests/default/e2e/snapshots/test_target_selection/test_target_selection/nongit/files.list b/cli/tests/default/e2e/snapshots/test_target_selection/test_target_selection/nongit/files.list
new file mode 100644
index 000000000000..6e8ce078a2c7
--- /dev/null
+++ b/cli/tests/default/e2e/snapshots/test_target_selection/test_target_selection/nongit/files.list
@@ -0,0 +1,5 @@
+targets/target_selection/hello
+targets/target_selection/quote'/hello
+targets/target_selection/spaces !/hello
+targets/target_selection/~
+targets/target_selection/😊
diff --git a/cli/tests/default/e2e/targets/target_selection/hello b/cli/tests/default/e2e/targets/target_selection/hello
new file mode 100644
index 000000000000..ce013625030b
--- /dev/null
+++ b/cli/tests/default/e2e/targets/target_selection/hello
@@ -0,0 +1,1 @@
+hello
diff --git a/cli/tests/default/e2e/targets/target_selection/quote'/hello b/cli/tests/default/e2e/targets/target_selection/quote'/hello
new file mode 100644
index 000000000000..ce013625030b
--- /dev/null
+++ b/cli/tests/default/e2e/targets/target_selection/quote'/hello
@@ -0,0 +1,1 @@
+hello
diff --git a/cli/tests/default/e2e/targets/target_selection/spaces !/hello b/cli/tests/default/e2e/targets/target_selection/spaces !/hello
new file mode 100644
index 000000000000..ce013625030b
--- /dev/null
+++ b/cli/tests/default/e2e/targets/target_selection/spaces !/hello
@@ -0,0 +1,1 @@
+hello
diff --git a/cli/tests/default/e2e/targets/target_selection/~ b/cli/tests/default/e2e/targets/target_selection/~
new file mode 100644
index 000000000000..ce013625030b
--- /dev/null
+++ b/cli/tests/default/e2e/targets/target_selection/~
@@ -0,0 +1,1 @@
+hello
diff --git "a/cli/tests/default/e2e/targets/target_selection/\360\237\230\212" "b/cli/tests/default/e2e/targets/target_selection/\360\237\230\212"
new file mode 100644
index 000000000000..ce013625030b
--- /dev/null
+++ "b/cli/tests/default/e2e/targets/target_selection/\360\237\230\212"
@@ -0,0 +1,1 @@
+hello
diff --git a/cli/tests/default/e2e/test_target_selection.py b/cli/tests/default/e2e/test_target_selection.py
new file mode 100644
index 000000000000..e0a6ee00c160
--- /dev/null
+++ b/cli/tests/default/e2e/test_target_selection.py
@@ -0,0 +1,32 @@
+#
+# Test target selection before any rule or language-specific filtering
+#
+import pytest
+from tests.conftest import create_git_repo
+from tests.fixtures import RunSemgrep
+
+
+# Check that a variety of file and folder names are tolerated and selected
+# normally by semgrep scan.
+#
+# Compatibility note: Unix, Windows, and MacOS have different rules for
+# what's allowed as a file name. Here, we only test filenames that work
+# on all platforms.
+#
+# At least one test file name should be quoted
+# by 'git ls-files' e.g. '😊' will be shown as '"\360\237\230\212"'
+#
+@pytest.mark.kinda_slow
+@pytest.mark.parametrize(
+    "prepare_workspace", [lambda: None, create_git_repo], ids=["nongit", "git"]
+)
+def test_target_selection(
+    run_semgrep_on_copied_files: RunSemgrep, prepare_workspace, snapshot
+):
+    stdout, stderr = run_semgrep_on_copied_files(
+        "rules/eqeq.yaml",  # ignored by --x-ls
+        options=["--x-ls"],
+        prepare_workspace=prepare_workspace,
+        target_name="target_selection",
+    )
+    snapshot.assert_match(stdout, "files.list")
diff --git a/libs/testo b/libs/testo
index 043c632157e6..99a0d4f08d9c 160000
--- a/libs/testo
+++ b/libs/testo
@@ -1,1 +1,1 @@
-Subproject commit 043c632157e69a643b86f3d0531336d176f8bc8d
+Subproject commit 99a0d4f08d9cbabc87d10da94a534ce8a1220cd9
diff --git a/scripts/run-core-test b/scripts/run-core-test
index 65bb48cd6589..1d8f07e96786 100755
--- a/scripts/run-core-test
+++ b/scripts/run-core-test
@@ -19,7 +19,9 @@ NC='\033[0m' # No Color
 # Checking the status of the tests and approving the output is done with
 # './test status' and './test approve'. See './test --help' for options.
 # This uses the in-house project testo.
-./test
+#
+# TODO: fix concurrency bugs to remove '-j0'
+./test -j0
 
 # run inline tests
 # TODO: make them part of the testo test program above.
diff --git a/src/engine/tests/Test_dataflow_tainting.ml b/src/engine/tests/Test_dataflow_tainting.ml
index ee9cdac32228..54be93a12f73 100644
--- a/src/engine/tests/Test_dataflow_tainting.ml
+++ b/src/engine/tests/Test_dataflow_tainting.ml
@@ -22,14 +22,11 @@ let pr2_ranges (file : Fpath.t) (rwms : RM.t list) : unit =
          in
          UCommon.pr2 (code_text ^ " @l." ^ line_str))
 
-let test_tainting (lang : Lang.t) (_file : Fpath.t) options config def =
+let test_tainting taint_inst def =
   UCommon.pr2 "\nDataflow";
   UCommon.pr2 "--------";
   let fcfg, _effects_IGNORED, mapping =
-    Match_tainting_mode.check_fundef lang options config None
-      AST_to_IL.empty_ctx
-      (Dataflow_tainting.mk_empty_java_props_cache ())
-      def
+    Match_tainting_mode.check_fundef taint_inst None AST_to_IL.empty_ctx def
   in
   DataflowX.display_mapping fcfg.cfg mapping Taint_lval_env.to_string
 
@@ -70,9 +67,9 @@ let test_dfg_tainting rules_file file =
      for test purposes.
   *)
   let tbl = Formula_cache.mk_specialized_formula_cache [] in
-  let config, spec_matches, _exps =
-    Match_taint_spec.taint_config_of_rule ~per_file_formula_cache:tbl xconf
-      !!file (ast, []) rule
+  let taint_inst, spec_matches, _exps =
+    Match_taint_spec.taint_config_of_rule ~per_file_formula_cache:tbl xconf lang
+      file (ast, []) rule
   in
   UCommon.pr2 "\nSources";
   UCommon.pr2 "-------";
@@ -88,7 +85,7 @@ let test_dfg_tainting rules_file file =
       inherit [_] AST_generic.iter_no_id_info as super
 
       method! visit_function_definition env def =
-        test_tainting lang file xconf.config config def;
+        test_tainting taint_inst def;
         (* go into nested functions *)
         super#visit_function_definition env def
     end
diff --git a/src/tests/Test.ml b/src/tests/Test.ml
index 847aad74572c..f9d703e417a1 100644
--- a/src/tests/Test.ml
+++ b/src/tests/Test.ml
@@ -13,12 +13,62 @@
  * distribute them in their relevant directory (e.g., engine/Unit_engine.ml)
  *)
 
+open Common
+
 let t = Testo.create
 
 (*****************************************************************************)
 (* Helpers *)
 (*****************************************************************************)
 
+let parse_env_entry ~ignore_empty s =
+  match String.index_opt s '=' with
+  | Some i ->
+      let k = String_.safe_sub s 0 i in
+      let v = String_.safe_sub s (i + 1) (String.length s - i - 1) in
+      if ignore_empty && v = "" then None else Some (k, v)
+  | None -> None
+
+(* Get the set of environment variables and their values, optionally
+   excluding empty values. *)
+let get_environment ~ignore_empty () =
+  Unix.environment () |> Array.to_list
+  |> List_.filter_map (parse_env_entry ~ignore_empty)
+  |> Set_.of_list
+
+let string_of_set (set : (string * string) Set_.t) =
+  set |> Set_.elements
+  |> List_.map (fun (k, v) -> spf "%s=%s" k v)
+  |> String.concat ", "
+
+(*
+   Wrap the test function so as to check no environment variables were altered
+   and not restored during the test.
+
+   TODO: if this proves useful, move it to Testo
+*)
+let with_env_check ?(ignore_empty = false) (test : Testo.t) =
+  let func () =
+    let orig_env = get_environment ~ignore_empty () in
+    Common.protect test.func ~finally:(fun () ->
+        let final_env = get_environment ~ignore_empty () in
+        let removed = Set_.diff orig_env final_env in
+        let added = Set_.diff final_env orig_env in
+        if not (Set_.is_empty removed && Set_.is_empty added) then
+          let msg =
+            spf
+              {|One or more environment variables changed during the test.%s
+  * removed bindings: %s
+  * added bindings: %s|}
+              (if ignore_empty then
+                 "\nVariables bound to empty values are treated as unbound."
+               else "")
+              (string_of_set removed) (string_of_set added)
+          in
+          failwith msg)
+  in
+  Testo.update ~func test
+
 let any_gen_of_string str =
   let any = Parse_python.any_of_string str in
   Python_to_generic.any any
@@ -145,7 +195,7 @@ let tests (caps : Cap.all_caps) =
 let tests_with_delayed_error caps =
   try
     Printf.printf "Gathering tests from %s...\n%!" (Sys.getcwd ());
-    let tests = tests caps in
+    let tests = tests caps |> List_.map (with_env_check ~ignore_empty:true) in
     Printf.printf "Done gathering tests.\n%!";
     tests
   with
@@ -160,6 +210,9 @@ let main (caps : Cap.all_caps) : unit =
   (* find the root of the semgrep repo as many of our tests rely on
      'let test_path = "tests/"' to find their test files *)
   let project_root = Test_LS_e2e.project_root () in
+  (* Don't read ~/.gitconfig since it varies from one developer to another,
+     resulting in variable output *)
+  Unix.putenv "GIT_CONFIG_NOGLOBAL" "true";
   Testutil_files.with_chdir project_root (fun () ->
       (* coupling: partial copy of the content of CLI.main() *)
       Core_CLI.register_exception_printers ();
@@ -177,7 +230,7 @@ let main (caps : Cap.all_caps) : unit =
       (* Show log messages produced when building the list of tests *)
       reset ();
       (* let's go *)
-      Testo.interpret_argv ~project_name:"semgrep-core" (fun () ->
+      Testo.interpret_argv ~project_name:"semgrep-core" (fun _env ->
           tests_with_delayed_error caps |> cleanup_before_each_test reset))
 
 let () = Cap.main (fun all_caps -> main all_caps)
diff --git a/tests/snapshots/semgrep-core/17c29a0136d9/stdout b/tests/snapshots/semgrep-core/17c29a0136d9/stdout
index 7fbcf061a011..6ba8417aea92 100644
--- a/tests/snapshots/semgrep-core/17c29a0136d9/stdout
+++ b/tests/snapshots/semgrep-core/17c29a0136d9/stdout
@@ -1,15 +1,21 @@
 --- begin input files ---
 a/b/target
+a/b/zero​width-space
+a/b/😊
 x/y/z
 --- end input files ---
 Initialized empty Git repository in <TMP>/<MASKED>/.git/
 [main (root-commit) <MASKED>] Add files
- 2 files changed, 0 insertions(+), 0 deletions(-)
+ 4 files changed, 0 insertions(+), 0 deletions(-)
  create mode 100644 a/b/target
+ create mode 100644 "a/b/zero\342\200\213width-space"
+ create mode 100644 "a/b/\360\237\230\212"
  create mode 100644 x/y/z
 project root: <TMP>/<MASKED>
 cwd: <TMP>
 scanning root: <TMP>/<MASKED>
 file list:
   test-<HEX>/a/b/target
+  test-<HEX>/a/b/zero​width-space
+  test-<HEX>/a/b/😊
   test-<HEX>/x/y/z
diff --git a/tests/snapshots/semgrep-core/340d1149ba01/name b/tests/snapshots/semgrep-core/340d1149ba01/name
new file mode 100644
index 000000000000..763a3cf668a3
--- /dev/null
+++ b/tests/snapshots/semgrep-core/340d1149ba01/name
@@ -0,0 +1,1 @@
+Osemgrep Scan (e2e) > basic verbose output
diff --git a/tests/snapshots/semgrep-core/84cced48c6d1/stdxxx b/tests/snapshots/semgrep-core/340d1149ba01/stdxxx
similarity index 89%
rename from tests/snapshots/semgrep-core/84cced48c6d1/stdxxx
rename to tests/snapshots/semgrep-core/340d1149ba01/stdxxx
index 4897905c6317..1e4cbeb48ad8 100644
--- a/tests/snapshots/semgrep-core/84cced48c6d1/stdxxx
+++ b/tests/snapshots/semgrep-core/340d1149ba01/stdxxx
@@ -1,4 +1,3 @@
-Snapshot for Test_scan_subcommand.test_basic_verbose_output.(fun)
 --- begin input files ---
 rules.yml
 stupid.py
@@ -20,7 +19,7 @@ Initialized empty Git repository in <TMP>/<MASKED>/.git/
 └─────────────┘
 
 [<MASKED TIMESTAMP>][INFO]: Checking for API token in environment variables
-[<MASKED TIMESTAMP>][INFO]: Environment variable SEMGREP_APP_TOKEN is set but not well-formed, ignoring it
+[<MASKED TIMESTAMP>][INFO]: Found API token in environment variables
 [<MASKED TIMESTAMP>][INFO]: Loading settings from nosettings.yaml
 [<MASKED TIMESTAMP>][INFO]: Settings file 'nosettings.yaml' does not exist or is not a regular file
 [<MASKED TIMESTAMP>][INFO]: No settings file found, using default settings
@@ -30,8 +29,8 @@ Initialized empty Git repository in <TMP>/<MASKED>/.git/
 [32m✔[39m [1mSemgrep OSS[0m
   [32m✔[39m Basic security coverage for first-party code vulnerabilities.
 
-[31m✘[39m [1mSemgrep Code (SAST)[0m
-  [31m✘[39m Find and fix vulnerabilities in the code you write with advanced scanning and expert security rules.
+[32m✔[39m [1mSemgrep Code (SAST)[0m
+  [32m✔[39m Find and fix vulnerabilities in the code you write with advanced scanning and expert security rules.
 
 [31m✘[39m [1mSemgrep Secrets[0m
   [31m✘[39m Detect and validate potential secrets in your code.
@@ -48,8 +47,8 @@ More information: https://semgrep.dev/docs/metrics
 [<MASKED TIMESTAMP>][INFO]: loading local config from rules.yml
 [<MASKED TIMESTAMP>][INFO]: Done loading local config from rules.yml
 [<MASKED TIMESTAMP>][INFO]: Computing the targets
-[<MASKED TIMESTAMP>][INFO]: Running external command: 'git' '-C' '<TMP>/<MASKED>' 'ls-files' '--cached' '<TMP>/<MASKED>/'
-[<MASKED TIMESTAMP>][INFO]: Running external command: 'git' '-C' '<TMP>/<MASKED>' 'ls-files' '--others' '--exclude-standard' '<TMP>/<MASKED>/'
+[<MASKED TIMESTAMP>][INFO]: Running external command: 'git' '-C' '<TMP>/<MASKED>' 'ls-files' '-z' '--cached' '<TMP>/<MASKED>/'
+[<MASKED TIMESTAMP>][INFO]: Running external command: 'git' '-C' '<TMP>/<MASKED>' 'ls-files' '-z' '--others' '--exclude-standard' '<TMP>/<MASKED>/'
 [<MASKED TIMESTAMP>][INFO]: running 1 rules from 1 config rules.yml
 Rules:
 - eqeq-bad
@@ -66,7 +65,7 @@ Rules:
 [<MASKED TIMESTAMP>][INFO]: scan: processing 1 files (skipping 0), with 1 rules (skipping 0 )
 [<MASKED TIMESTAMP>][INFO]: reporting matches if any
 [<MASKED TIMESTAMP>][INFO]: Checking for API token in environment variables
-[<MASKED TIMESTAMP>][INFO]: Environment variable SEMGREP_APP_TOKEN is set but not well-formed, ignoring it
+[<MASKED TIMESTAMP>][INFO]: Found API token in environment variables
 [<MASKED TIMESTAMP>][INFO]: Loading settings from nosettings.yaml
 [<MASKED TIMESTAMP>][INFO]: Settings file found, using settings from file
 
diff --git a/tests/snapshots/semgrep-core/343da82ffbcb/stdout b/tests/snapshots/semgrep-core/343da82ffbcb/stdout
index 54d25d30f586..02034a0ea8b1 100644
--- a/tests/snapshots/semgrep-core/343da82ffbcb/stdout
+++ b/tests/snapshots/semgrep-core/343da82ffbcb/stdout
@@ -1,15 +1,21 @@
 --- begin input files ---
 a/b/target
+a/b/zero​width-space
+a/b/😊
 x/y/z
 --- end input files ---
 Initialized empty Git repository in <TMP>/<MASKED>/.git/
 [main (root-commit) <MASKED>] Add files
- 2 files changed, 0 insertions(+), 0 deletions(-)
+ 4 files changed, 0 insertions(+), 0 deletions(-)
  create mode 100644 a/b/target
+ create mode 100644 "a/b/zero\342\200\213width-space"
+ create mode 100644 "a/b/\360\237\230\212"
  create mode 100644 x/y/z
 project root: <TMP>/<MASKED>
 cwd: <TMP>/<MASKED>
 scanning root: .
 file list:
   a/b/target
+  a/b/zero​width-space
+  a/b/😊
   x/y/z
diff --git a/tests/snapshots/semgrep-core/36408fbb6ab3/stdout b/tests/snapshots/semgrep-core/36408fbb6ab3/stdout
index d3b3ed5a6bf4..4d34564111a5 100644
--- a/tests/snapshots/semgrep-core/36408fbb6ab3/stdout
+++ b/tests/snapshots/semgrep-core/36408fbb6ab3/stdout
@@ -1,14 +1,20 @@
 --- begin input files ---
 a/b/target
+a/b/zero​width-space
+a/b/😊
 x/y/z
 --- end input files ---
 Initialized empty Git repository in <TMP>/<MASKED>/.git/
 [main (root-commit) <MASKED>] Add files
- 2 files changed, 0 insertions(+), 0 deletions(-)
+ 4 files changed, 0 insertions(+), 0 deletions(-)
  create mode 100644 a/b/target
+ create mode 100644 "a/b/zero\342\200\213width-space"
+ create mode 100644 "a/b/\360\237\230\212"
  create mode 100644 x/y/z
 project root: <TMP>/<MASKED>
 cwd: <TMP>/<MASKED>/a
 scanning root: .
 file list:
   b/target
+  b/zero​width-space
+  b/😊
diff --git a/tests/snapshots/semgrep-core/84cced48c6d1/name b/tests/snapshots/semgrep-core/84cced48c6d1/name
deleted file mode 100644
index 1140d8adcff0..000000000000
--- a/tests/snapshots/semgrep-core/84cced48c6d1/name
+++ /dev/null
@@ -1,1 +0,0 @@
-Osemgrep Scan (e2e) > Test_scan_subcommand.test_basic_verbose_output
diff --git a/tests/snapshots/semgrep-core/946b937b8376/stdout b/tests/snapshots/semgrep-core/946b937b8376/stdout
index 5646e8b0f998..7dced80fd240 100644
--- a/tests/snapshots/semgrep-core/946b937b8376/stdout
+++ b/tests/snapshots/semgrep-core/946b937b8376/stdout
@@ -1,15 +1,21 @@
 --- begin input files ---
 a/b/target
+a/b/zero​width-space
+a/b/😊
 x/y/z
 --- end input files ---
 Initialized empty Git repository in <TMP>/<MASKED>/.git/
 [main (root-commit) <MASKED>] Add files
- 2 files changed, 0 insertions(+), 0 deletions(-)
+ 4 files changed, 0 insertions(+), 0 deletions(-)
  create mode 100644 a/b/target
+ create mode 100644 "a/b/zero\342\200\213width-space"
+ create mode 100644 "a/b/\360\237\230\212"
  create mode 100644 x/y/z
 project root: <TMP>/<MASKED>
 cwd: <TMP>
 scanning root: test-<HEX>
 file list:
   test-<HEX>/a/b/target
+  test-<HEX>/a/b/zero​width-space
+  test-<HEX>/a/b/😊
   test-<HEX>/x/y/z
diff --git a/tests/snapshots/semgrep-core/a1eb714b5efd/name b/tests/snapshots/semgrep-core/a1eb714b5efd/name
deleted file mode 100644
index d67f71c73e83..000000000000
--- a/tests/snapshots/semgrep-core/a1eb714b5efd/name
+++ /dev/null
@@ -1,1 +0,0 @@
-Osemgrep Scan (e2e) > Test_scan_subcommand.test_basic_output
diff --git a/tests/snapshots/semgrep-core/bbb375beed7d/stdout b/tests/snapshots/semgrep-core/bbb375beed7d/stdout
index 11e6ffc71c45..17100dfd1ddc 100644
--- a/tests/snapshots/semgrep-core/bbb375beed7d/stdout
+++ b/tests/snapshots/semgrep-core/bbb375beed7d/stdout
@@ -1,14 +1,20 @@
 --- begin input files ---
 a/b/target
+a/b/zero​width-space
+a/b/😊
 x/y/z
 --- end input files ---
 Initialized empty Git repository in <TMP>/<MASKED>/.git/
 [main (root-commit) <MASKED>] Add files
- 2 files changed, 0 insertions(+), 0 deletions(-)
+ 4 files changed, 0 insertions(+), 0 deletions(-)
  create mode 100644 a/b/target
+ create mode 100644 "a/b/zero\342\200\213width-space"
+ create mode 100644 "a/b/\360\237\230\212"
  create mode 100644 x/y/z
 project root: <TMP>/<MASKED>
 cwd: <TMP>/<MASKED>/x
 scanning root: ../a
 file list:
   ../a/b/target
+  ../a/b/zero​width-space
+  ../a/b/😊
diff --git a/tests/snapshots/semgrep-core/bc2f173cbf29/name b/tests/snapshots/semgrep-core/bc2f173cbf29/name
new file mode 100644
index 000000000000..a2e0798457cc
--- /dev/null
+++ b/tests/snapshots/semgrep-core/bc2f173cbf29/name
@@ -0,0 +1,1 @@
+Osemgrep Scan (e2e) > basic output
diff --git a/tests/snapshots/semgrep-core/a1eb714b5efd/stdxxx b/tests/snapshots/semgrep-core/bc2f173cbf29/stdxxx
similarity index 92%
rename from tests/snapshots/semgrep-core/a1eb714b5efd/stdxxx
rename to tests/snapshots/semgrep-core/bc2f173cbf29/stdxxx
index 28708226b797..ede1cdfb0628 100644
--- a/tests/snapshots/semgrep-core/a1eb714b5efd/stdxxx
+++ b/tests/snapshots/semgrep-core/bc2f173cbf29/stdxxx
@@ -1,4 +1,3 @@
-Snapshot for Test_scan_subcommand.test_basic_output.(fun)
 --- begin input files ---
 rules.yml
 stupid.py
@@ -21,8 +20,8 @@ Initialized empty Git repository in <TMP>/<MASKED>/.git/
 [32m✔[39m [1mSemgrep OSS[0m
   [32m✔[39m Basic security coverage for first-party code vulnerabilities.
 
-[31m✘[39m [1mSemgrep Code (SAST)[0m
-  [31m✘[39m Find and fix vulnerabilities in the code you write with advanced scanning and expert security rules.
+[32m✔[39m [1mSemgrep Code (SAST)[0m
+  [32m✔[39m Find and fix vulnerabilities in the code you write with advanced scanning and expert security rules.
 
 [31m✘[39m [1mSemgrep Secrets[0m
   [31m✘[39m Detect and validate potential secrets in your code.
diff --git a/tests/snapshots/semgrep-core/f4ba77a8739a/stdout b/tests/snapshots/semgrep-core/f4ba77a8739a/stdout
index af5c4daa7284..ddfbb27298ec 100644
--- a/tests/snapshots/semgrep-core/f4ba77a8739a/stdout
+++ b/tests/snapshots/semgrep-core/f4ba77a8739a/stdout
@@ -1,14 +1,20 @@
 --- begin input files ---
 a/b/target
+a/b/zero​width-space
+a/b/😊
 x/y/z
 --- end input files ---
 Initialized empty Git repository in <TMP>/<MASKED>/.git/
 [main (root-commit) <MASKED>] Add files
- 2 files changed, 0 insertions(+), 0 deletions(-)
+ 4 files changed, 0 insertions(+), 0 deletions(-)
  create mode 100644 a/b/target
+ create mode 100644 "a/b/zero\342\200\213width-space"
+ create mode 100644 "a/b/\360\237\230\212"
  create mode 100644 x/y/z
 project root: <TMP>/<MASKED>
 cwd: <TMP>/<MASKED>/x
 scanning root: <TMP>/<MASKED>/a
 file list:
   ../a/b/target
+  ../a/b/zero​width-space
+  ../a/b/😊