Repository: semgrep/semgrep (language: python)

Issue:
Lack of exact version pinning of ruamel.yaml library
**Describe the bug**
The `ruamel.yaml` library contains a bug having to do with string values containing colons, ':'

https://sourceforge.net/p/ruamel-yaml/tickets/469/

This bug was introduced in a patch version of ruamel.yaml (0.17.29). However, due to semgrep not pinning the ruamel.yaml version on the patch version (`cli/setup.py` contains the range `ruamel.yaml>=0.16.0,<0.18`), semgrep's behavior in parsing YAML files changed _in the same version_, depending on the moment in time semgrep was installed.

This was probably also the root cause of:
https://github.com/returntocorp/semgrep/issues/7944
and
https://github.com/returntocorp/semgrep-rules/pull/2941
because both seem to be related to the same error, but are workarounds that do not address the actual issue.


**To Reproduce**
Install the current latest version of semgrep:
```
pip install semgrep==1.24.1
```

Take a "bad" rule from https://github.com/returntocorp/semgrep-rules/pull/2941 before it was modified:
```
rules:
- id: python.lang.security.audit.network.bind.avoid-bind-to-all-interfaces
  languages:
  - python
  message: Running `socket.bind` to 0.0.0.0, ::, or empty string could unexpectedly
    expose the server publicly as it binds to all available interfaces. Consider instead
    getting correct address from an environment variable or configuration file.
  metadata:
    category: security
    confidence: HIGH
    cwe:
    - 'CWE-200: Exposure of Sensitive Information to an Unauthorized Actor'
    cwe2021-top25: true
    impact: MEDIUM
    license: Commons Clause License Condition v1.0[LGPL-2.1-only]
    likelihood: HIGH
    owasp:
    - A01:2021 - Broken Access Control
    references:
    - https://owasp.org/Top10/A01_2021-Broken_Access_Control
    semgrep.dev:
      rule:
        origin: community
        rule_id: OrU3og
        url: https://semgrep.dev/playground/r/5PTY5G/python.lang.security.audit.network.bind.avoid-bind-to-all-interfaces
        version_id: 5PTY5G
    shortlink: https://sg.run/rdln
    source: https://semgrep.dev/r/python.lang.security.audit.network.bind.avoid-bind-to-all-interfaces
    subcategory:
    - vuln
    technology:
    - python
  pattern-either:
  - pattern: '$S = socket.socket(...)

      ...

      $S.bind(("0.0.0.0", ...))

      '
  - pattern: '$S = socket.socket(...)

      ...

      $S.bind(("::", ...))

      '
  - pattern: '$S = socket.socket(...)

      ...

      $S.bind(("", ...))

      '
  severity: INFO
```

Validate with semgrep:
```
$ semgrep --config bad-rule.yml --validate
Configuration is invalid - found 1 configuration error(s), and 0 rule(s).
[ERROR] Invalid YAML file bad-rule.yml:
	while parsing a block mapping
	  in "<file>", line 2, column 3
	expected <block end>, but found '<scalar>'
	  in "<file>", line 5, column 47
```

This hits the ruamel.yaml bug. Now, when we force a downgrade of the patch version of ruamel.yaml _right_ before the bug was introduced:
```
pip install ruamel.yaml==0.17.28
```
.. and validate again, it works fine:
```
$ semgrep --config bad-rule.yml --validate
Configuration is valid - found 0 configuration error(s), and 1 rule(s).
```

In our situation, we experienced different behavior overnight due to the way pinning happens on ruamel.yaml's minor range inside semgrep, which caused some headaches trying to figure out what went wrong while using the exact same version of semgrep, only a new installation (we install semgrep during CI/CD runs), but fortunately we now have a good understanding of the issue.

**Expected behavior**
For important dependencies like these, it might make sense to pin them on the exact (patch) version, to avoid changing behavior between semgrep installs of the exact same version.

**Screenshots**
None, but can be made on request.

**What is the priority of the bug to you?**

- [ ] P0: blocking your adoption of Semgrep or workflow
- [x] P1: important to fix or quite annoying
- [ ] P2: regular bug that should get fixed

**Environment**
pip install in virtualenv

**Use case**
No more sudden parsing errors with rules.

Change iter_files interface to parse the AST lazily
This will enable later to skip parsing entire files if we don't find
certain regexps in it

test plan:
make test


Discussion / hints:
This issue is synced in Linear at https://linear.app/semgrep/issue/PA-2842/lack-of-exact-version-pinning-of-ruamelyaml-library. _Note_: this link is for r2c use only and is not accessible publicly.
Can ruamel-yaml >= 0.18.5 be used? This is currently causing conflicts with other packages that require later versions of ruamel-yaml. Version 0.18.6 has been out since 1Q24.


Fix patch:
diff --git a/.github/workflows/.codemapignore b/.github/workflows/.codemapignore
index 33545517c9ce..73ae5a917fa0 100644
--- a/.github/workflows/.codemapignore
+++ b/.github/workflows/.codemapignore
@@ -5,16 +5,15 @@ build-test-osx-x86.yml
 build-test-osx-arm64.yml
 build-test-manylinux-x86.yml
 build-test-manylinux-aarch64.yml
-build-test-javascript.yml
+build-test-windows-x86.yml
 tests.yml
 test-e2e-semgrep-ci.yml
 nightly.yml
-cron-parsing-stats.yml
 check-semgrep-pro-version.yml
+build-test-docker.yml
+push-docker.yml
 start-release.yml
 release.yml
 release-homebrew.yml
 trigger-semgrep-comparison-argo.yml
-build-test-docker.yml
-push-docker.yml
 sync-with-PRO.yml
diff --git a/.github/workflows/README.md b/.github/workflows/README.md
index 90de3db69ef0..8c0bb11872bc 100644
--- a/.github/workflows/README.md
+++ b/.github/workflows/README.md
@@ -5,7 +5,7 @@ See https://docs.github.com/en/actions/learn-github-actions/understanding-github
 for more information on GHA or our Notion page on "Github actions".
 
 Most of those workflows have the 'workflow_dispatch:' directive so you can
-also trigger them manually here: https://github.com/returntocorp/semgrep/actions
+also trigger them manually here: https://github.com/semgrep/semgrep/actions
 
 Note that many workflows are now written using Jsonnet
 (see https://jsonnet.org/learning/tutorial.html for a great intro to Jsonnet)
diff --git a/.github/workflows/libs/actions.libsonnet b/.github/workflows/libs/actions.libsonnet
index 6c935c6edbf9..f72b9d1a9af4 100644
--- a/.github/workflows/libs/actions.libsonnet
+++ b/.github/workflows/libs/actions.libsonnet
@@ -75,16 +75,15 @@
           tar czf artifacts.tgz artifacts
         ||| % path,
   },
-  // TODO: switch to v4 for upload/download below
   upload_artifact_step: function(artifact_name, path='artifacts.tgz') {
-       uses: 'actions/upload-artifact@v3',
+       uses: 'actions/upload-artifact@v4',
        with: {
           path: path,
           name: artifact_name,
         },
   },
   download_artifact_step(artifact_name): {
-      uses: 'actions/download-artifact@v3',
+      uses: 'actions/download-artifact@v4',
       with: {
         name: artifact_name,
       },
diff --git a/.github/workflows/libs/semgrep.libsonnet b/.github/workflows/libs/semgrep.libsonnet
index 6a1ff35c3209..0024339eb414 100644
--- a/.github/workflows/libs/semgrep.libsonnet
+++ b/.github/workflows/libs/semgrep.libsonnet
@@ -362,6 +362,10 @@ local setup_nix_step =
   depot_project_id: 'fhmxj6w9z8',
   opam_switch: opam_switch,
   opam_setup: opam_setup,
+  // coupling: cli/setup.py, the matrix in run-cli-tests.libsonnet,
+  // build-test-manylinux-x86.jsonnet in pro, tests.jsonnet in OSS
+  // TODO? could switch to higher like 3.11
+  default_python_version: '3.9',
   containers: containers,
 
   github_bot: github_bot,
diff --git a/.github/workflows/libs/util.libsonnet b/.github/workflows/libs/util.libsonnet
index a95c94cae7f1..e7232592a28e 100644
--- a/.github/workflows/libs/util.libsonnet
+++ b/.github/workflows/libs/util.libsonnet
@@ -56,7 +56,7 @@
   },
   // workflow_inputs is a JSON object that looks like [{ "name": "name_1", "value": "value_1" }, ...]
   // "value_1" should be a string that can include GHA expression for the value, e.g.,
-  // "returntocorp/semgrep:${{ needs.setup-docker-tag.outputs.docker-tag }}".
+  // "semgrep/semgrep:${{ needs.setup-docker-tag.outputs.docker-tag }}".
   // This will be converted into env vars to avoid accidental script injections
   // and reconstructed as the JSON body in the POST request to Argo.
   trigger_argo_workflow: function(trigger_url, workflow_inputs) {
diff --git a/.github/workflows/push-docker.jsonnet b/.github/workflows/push-docker.jsonnet
index 53aba5ea8b30..29654a4813c6 100644
--- a/.github/workflows/push-docker.jsonnet
+++ b/.github/workflows/push-docker.jsonnet
@@ -52,7 +52,7 @@ local job = {
       uses: 'iarekylew00t/regctl-installer@v1',
     },
     {
-      uses: 'actions/download-artifact@v3',
+      uses: 'actions/download-artifact@v4',
       with: {
         path: '/tmp/artifacts',
       },
diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml
index b9f0a93bb3e1..e10df81da72e 100644
--- a/.github/workflows/push-docker.yml
+++ b/.github/workflows/push-docker.yml
@@ -4,7 +4,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - uses: iarekylew00t/regctl-installer@v1
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           path: /tmp/artifacts
       - env:
diff --git a/.github/workflows/release.jsonnet b/.github/workflows/release.jsonnet
index a8e21a2c877e..f7714aa3d06d 100644
--- a/.github/workflows/release.jsonnet
+++ b/.github/workflows/release.jsonnet
@@ -232,7 +232,7 @@ local park_pypi_packages_job = {
 
 local download_step(name) = {
   name: 'Download %s' % name,
-  uses: 'actions/download-artifact@v3',
+  uses: 'actions/download-artifact@v4',
   with: {
     name: name,
     path: name,
@@ -290,7 +290,7 @@ local create_release_job = {
         GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}',
       },
       run: |||
-        while ! gh release --repo returntocorp/semgrep list -L 5 | grep -q "%s"; do
+        while ! gh release --repo semgrep/semgrep list -L 5 | grep -q "%s"; do
           echo "release not yet ready, sleeping for 5 seconds"
           sleep 5
         done
@@ -302,7 +302,7 @@ local create_release_job = {
       env: {
         GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}',
       },
-      run: 'gh release --repo returntocorp/semgrep edit %s --draft=false' % version,
+      run: 'gh release --repo semgrep/semgrep edit %s --draft=false' % version,
     },
   ],
 } + unless_dry_run;
@@ -327,7 +327,7 @@ local create_release_interfaces_job = {
       env: {
         GITHUB_TOKEN: semgrep.github_bot.token_ref,
       },
-      run: 'gh release --repo returntocorp/semgrep-interfaces upload %s cli/src/semgrep/semgrep_interfaces/rule_schema_v1.yaml' % version,
+      run: 'gh release --repo semgrep/semgrep-interfaces upload %s cli/src/semgrep/semgrep_interfaces/rule_schema_v1.yaml' % version,
     },
     {
       name: 'Publish Release Semgrep Interfaces',
@@ -335,7 +335,7 @@ local create_release_interfaces_job = {
       env: {
         GITHUB_TOKEN: semgrep.github_bot.token_ref,
       },
-      run: 'gh release --repo returntocorp/semgrep-interfaces edit %s --draft=false' % version,
+      run: 'gh release --repo semgrep/semgrep-interfaces edit %s --draft=false' % version,
     },
   ],
 } + unless_dry_run;
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index faf0f1e5a0b3..0da2799392aa 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -65,7 +65,7 @@ jobs:
         id: wait-draft-release
         name: Wait for Draft Release if not Ready
         run: |
-          while ! gh release --repo returntocorp/semgrep list -L 5 | grep -q "${{ steps.get-version.outputs.VERSION }}"; do
+          while ! gh release --repo semgrep/semgrep list -L 5 | grep -q "${{ steps.get-version.outputs.VERSION }}"; do
             echo "release not yet ready, sleeping for 5 seconds"
             sleep 5
           done
@@ -73,7 +73,7 @@ jobs:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         id: publish_release
         name: Publish Release
-        run: gh release --repo returntocorp/semgrep edit ${{ steps.get-version.outputs.VERSION }} --draft=false
+        run: gh release --repo semgrep/semgrep edit ${{ steps.get-version.outputs.VERSION }} --draft=false
   create-release-interfaces:
     if: ${{ ! inputs.dry-run }}
     needs:
@@ -108,12 +108,12 @@ jobs:
           GITHUB_TOKEN: ${{ steps.token.outputs.token }}
         id: upload-semgrep-schema-files
         name: Upload Schema Files
-        run: gh release --repo returntocorp/semgrep-interfaces upload ${{ steps.get-version.outputs.VERSION }} cli/src/semgrep/semgrep_interfaces/rule_schema_v1.yaml
+        run: gh release --repo semgrep/semgrep-interfaces upload ${{ steps.get-version.outputs.VERSION }} cli/src/semgrep/semgrep_interfaces/rule_schema_v1.yaml
       - env:
           GITHUB_TOKEN: ${{ steps.token.outputs.token }}
         id: publish_release_semgrep_interfaces
         name: Publish Release Semgrep Interfaces
-        run: gh release --repo returntocorp/semgrep-interfaces edit ${{ steps.get-version.outputs.VERSION }} --draft=false
+        run: gh release --repo semgrep/semgrep-interfaces edit ${{ steps.get-version.outputs.VERSION }} --draft=false
   park-pypi-packages:
     defaults:
       run:
@@ -188,22 +188,22 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Download manylinux-x86-wheel
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: manylinux-x86-wheel
           path: manylinux-x86-wheel
       - name: Download manylinux-aarch64-wheel
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: manylinux-aarch64-wheel
           path: manylinux-aarch64-wheel
       - name: Download osx-x86-wheel
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: osx-x86-wheel
           path: osx-x86-wheel
       - name: Download osx-arm64-wheel
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: osx-arm64-wheel
           path: osx-arm64-wheel
diff --git a/.github/workflows/semgrep.jsonnet b/.github/workflows/semgrep.jsonnet
index 59a1f379cfcc..66b455873d7c 100644
--- a/.github/workflows/semgrep.jsonnet
+++ b/.github/workflows/semgrep.jsonnet
@@ -1,6 +1,6 @@
 // This workflow dogfoods 'semgrep ci', which includes running rules
 // for Code, Supply Chain, and Secrets set in the Semgrep WebApp.
-// We're also dogfooding the returntocorp/semgrep:canary docker image!
+// We're also dogfooding the semgrep/semgrep:canary docker image!
 // (see https://www.notion.so/semgrep/returntocorp-semgrep-canary-docker-canary).
 
 local actions = import 'libs/actions.libsonnet';
diff --git a/.github/workflows/start-release.jsonnet b/.github/workflows/start-release.jsonnet
index 90c6e91d7e1e..7f9e50f0781d 100644
--- a/.github/workflows/start-release.jsonnet
+++ b/.github/workflows/start-release.jsonnet
@@ -86,7 +86,7 @@ local bump_job(repository) = {
 // Wait PR checks helpers
 // ----------------------------------------------------------------------------
 
-local len_checks = "$(gh pr -R returntocorp/semgrep view %s --json statusCheckRollup --jq '.statusCheckRollup | length')" % pr_number;
+local len_checks = "$(gh pr -R semgrep/semgrep view %s --json statusCheckRollup --jq '.statusCheckRollup | length')" % pr_number;
 
 local wait_pr_checks_to_register(while_cond) = |||
   LEN_CHECKS=%s;
@@ -97,7 +97,7 @@ local wait_pr_checks_to_register(while_cond) = |||
   done
   echo "checks are valid"
   echo ${LEN_CHECKS}
-  gh pr -R returntocorp/semgrep view %s --json statusCheckRollup
+  gh pr -R semgrep/semgrep view %s --json statusCheckRollup
 ||| % [len_checks, while_cond, len_checks, pr_number];
 
 local wait_pr_checks_to_register_step(while_cond) = {
@@ -115,7 +115,7 @@ local wait_pr_checks_to_complete_step = {
     GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}',
   },
   // Wait for PR checks to finish
-  run: 'gh pr -R returntocorp/semgrep checks %s --interval 90 --watch' % pr_number,
+  run: 'gh pr -R semgrep/semgrep checks %s --interval 90 --watch' % pr_number,
 };
 
 local get_current_num_checks_step = {
@@ -365,7 +365,7 @@ local create_draft_release_job = {
         token: semgrep.github_bot.token_ref,
         prerelease: false,
         draft: true,
-        repository: 'returntocorp/semgrep-interfaces',
+        repository: 'semgrep/semgrep-interfaces',
       },
     },
   ],
@@ -446,7 +446,7 @@ local notify_success_job = {
       run: |||
         # POST a webhook to Zapier to allow for public notifications to our users via Twitter
         curl "${{ secrets.ZAPIER_WEBHOOK_URL }}" \
-          -d '{"version":"${VERSION}","changelog_url":"https://github.com/returntocorp/semgrep/releases/tag/v${VERSION}"}'
+          -d '{"version":"${VERSION}","changelog_url":"https://github.com/semgrep/semgrep/releases/tag/v${VERSION}"}'
       |||
     },
     {
diff --git a/.github/workflows/start-release.yml b/.github/workflows/start-release.yml
index f1fedc661d88..397eae39162e 100644
--- a/.github/workflows/start-release.yml
+++ b/.github/workflows/start-release.yml
@@ -252,7 +252,7 @@ jobs:
           draft: true
           name: Release v${{ github.event.inputs.semgrep-version }}
           prerelease: false
-          repository: returntocorp/semgrep-interfaces
+          repository: semgrep/semgrep-interfaces
           tag_name: v${{ github.event.inputs.semgrep-version }}
           token: ${{ steps.token.outputs.token }}
   create-tag:
@@ -350,7 +350,7 @@ jobs:
         run: |
           # POST a webhook to Zapier to allow for public notifications to our users via Twitter
           curl "${{ secrets.ZAPIER_WEBHOOK_URL }}" \
-            -d '{"version":"${VERSION}","changelog_url":"https://github.com/returntocorp/semgrep/releases/tag/v${VERSION}"}'
+            -d '{"version":"${VERSION}","changelog_url":"https://github.com/semgrep/semgrep/releases/tag/v${VERSION}"}'
       - name: Notify Success on Slack
         run: |
           curl --request POST \
@@ -458,26 +458,26 @@ jobs:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         name: Wait for checks to register
         run: |
-          LEN_CHECKS=$(gh pr -R returntocorp/semgrep view "${{ needs.release-setup.outputs.pr-number }}" --json statusCheckRollup --jq '.statusCheckRollup | length');
+          LEN_CHECKS=$(gh pr -R semgrep/semgrep view "${{ needs.release-setup.outputs.pr-number }}" --json statusCheckRollup --jq '.statusCheckRollup | length');
           while [ ${LEN_CHECKS} = "0" ]; do
             echo "No checks available yet"
             sleep 1
-            LEN_CHECKS=$(gh pr -R returntocorp/semgrep view "${{ needs.release-setup.outputs.pr-number }}" --json statusCheckRollup --jq '.statusCheckRollup | length');
+            LEN_CHECKS=$(gh pr -R semgrep/semgrep view "${{ needs.release-setup.outputs.pr-number }}" --json statusCheckRollup --jq '.statusCheckRollup | length');
           done
           echo "checks are valid"
           echo ${LEN_CHECKS}
-          gh pr -R returntocorp/semgrep view "${{ needs.release-setup.outputs.pr-number }}" --json statusCheckRollup
+          gh pr -R semgrep/semgrep view "${{ needs.release-setup.outputs.pr-number }}" --json statusCheckRollup
       - env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         id: wait-checks
         name: Wait for checks to complete
-        run: gh pr -R returntocorp/semgrep checks "${{ needs.release-setup.outputs.pr-number }}" --interval 90 --watch
+        run: gh pr -R semgrep/semgrep checks "${{ needs.release-setup.outputs.pr-number }}" --interval 90 --watch
       - env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         id: num-checks
         name: Get Current Num Checks
         run: |
-          LEN_CHECKS=$(gh pr -R returntocorp/semgrep view "${{ needs.release-setup.outputs.pr-number }}" --json statusCheckRollup --jq '.statusCheckRollup | length');
+          LEN_CHECKS=$(gh pr -R semgrep/semgrep view "${{ needs.release-setup.outputs.pr-number }}" --json statusCheckRollup --jq '.statusCheckRollup | length');
           echo "num-checks=${LEN_CHECKS}" >> $GITHUB_OUTPUT
   wait-for-release-checks:
     if: ${{ ! inputs.dry-run }}
@@ -491,20 +491,20 @@ jobs:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         name: Wait for checks to register
         run: |
-          LEN_CHECKS=$(gh pr -R returntocorp/semgrep view "${{ needs.release-setup.outputs.pr-number }}" --json statusCheckRollup --jq '.statusCheckRollup | length');
+          LEN_CHECKS=$(gh pr -R semgrep/semgrep view "${{ needs.release-setup.outputs.pr-number }}" --json statusCheckRollup --jq '.statusCheckRollup | length');
           while [ ${LEN_CHECKS} = "${{ needs.wait-for-pr-checks.outputs.num-checks }}" ]; do
             echo "No checks available yet"
             sleep 1
-            LEN_CHECKS=$(gh pr -R returntocorp/semgrep view "${{ needs.release-setup.outputs.pr-number }}" --json statusCheckRollup --jq '.statusCheckRollup | length');
+            LEN_CHECKS=$(gh pr -R semgrep/semgrep view "${{ needs.release-setup.outputs.pr-number }}" --json statusCheckRollup --jq '.statusCheckRollup | length');
           done
           echo "checks are valid"
           echo ${LEN_CHECKS}
-          gh pr -R returntocorp/semgrep view "${{ needs.release-setup.outputs.pr-number }}" --json statusCheckRollup
+          gh pr -R semgrep/semgrep view "${{ needs.release-setup.outputs.pr-number }}" --json statusCheckRollup
       - env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         id: wait-checks
         name: Wait for checks to complete
-        run: gh pr -R returntocorp/semgrep checks "${{ needs.release-setup.outputs.pr-number }}" --interval 90 --watch
+        run: gh pr -R semgrep/semgrep checks "${{ needs.release-setup.outputs.pr-number }}" --interval 90 --watch
 name: start-release
 on:
   workflow_dispatch:
diff --git a/.github/workflows/trigger-doc-update.yml b/.github/workflows/trigger-doc-update.yml
index 30d5aff57fb2..861b6e46cf43 100644
--- a/.github/workflows/trigger-doc-update.yml
+++ b/.github/workflows/trigger-doc-update.yml
@@ -23,4 +23,4 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
         run: |
-          gh api repos/returntocorp/semgrep-docs/dispatches -f event_type=new_release --verbose
+          gh api repos/semgrep/semgrep-docs/dispatches -f event_type=new_release --verbose
diff --git a/Dockerfile b/Dockerfile
index 5c8574b127b0..b67a81326451 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -159,7 +159,7 @@ RUN apk upgrade --no-cache && \
 #
 # history: we used to install here various utilities needed by some of our
 # scripts under scripts/. Indeed, those scripts are run from CI jobs using the
-# returntocorp/semgrep docker image as the container because they rely on semgrep
+# semgrep/semgrep docker image as the container because they rely on semgrep
 # or semgrep-core. Those scripts must also perform different
 # tasks that require utilities other than semgrep (e.g., compute parsing
 # statistics and then run 'jq' to filter the JSON). It is convenient to add
@@ -241,7 +241,7 @@ ENV SEMGREP_IN_DOCKER=1 \
     SEMGREP_USER_AGENT_APPEND="Docker"
 
 # The command we tell people to run for testing semgrep in Docker is
-#   docker run --rm -v "${PWD}:/src" returntocorp/semgrep semgrep --config=auto
+#   docker run --rm -v "${PWD}:/src" semgrep/semgrep semgrep --config=auto
 # (see https://semgrep.dev/docs/getting-started/ ), hence this WORKDIR directive
 WORKDIR /src
 
@@ -261,11 +261,9 @@ RUN printf "[safe]\n	directory = /src"  > ~semgrep/.gitconfig && \
 
 # Note that we just use CMD below. Why not using ENTRYPOINT ["semgrep"] ?
 # so that people can simply run
-# `docker run --rm -v "${PWD}:/src" returntocorp/semgrep --help` instead of
-# `docker run --rm -v "${PWD}:/src" returntocorp/semgrep semgrep --help`?
-# (It's even worse now that we've switched company name with
-# `docker run --rm -v "${PWD}:/src" semgrep/semgrep semgrep --help`, we now
-# have three semgrep, hmmm).
+# `docker run --rm -v "${PWD}:/src" semgrep/semgrep --help` instead of
+# `docker run --rm -v "${PWD}:/src" semgrep/semgrep semgrep --help`?
+# (Yes, that's 3 semgrep in a row, hmmm)
 #
 # This is mainly to play well with CI providers like Gitlab. Indeed,
 # gitlab CI sets up all CI jobs by first running other commands in the
@@ -311,7 +309,7 @@ RUN rm -rf /root/.semgrep
 # We can't make this the default in the semgrep-cli stage above because of
 # permissions errors on the mounted volume when using instructions for running
 # semgrep with docker:
-#   `docker run -v "${PWD}:/src" -i returntocorp/semgrep semgrep`
+#   `docker run -v "${PWD}:/src" -i semgrep/semgrep semgrep`
 
 #coupling: the 'nonroot' name is used in release.jsonnet
 FROM semgrep-cli AS nonroot
diff --git a/Makefile b/Makefile
index c09559882f3e..5518a42f1037 100644
--- a/Makefile
+++ b/Makefile
@@ -633,7 +633,7 @@ DOCKER_IMAGE=semgrep/semgrep:develop
 
 # If you get parsing errors while running this command, maybe you have an old
 # cached version of the docker image. You can invalidate the cache with
-#   'docker rmi returntocorp/semgrep:develop`
+#   'docker rmi semgrep/semgrep:develop`
 check_with_docker:
 	docker run --rm -v "${PWD}:/src" $(DOCKER_IMAGE) semgrep $(SEMGREP_ARGS)
 
diff --git a/README.md b/README.md
index 1a95d500f2c7..088ed509e63d 100644
--- a/README.md
+++ b/README.md
@@ -33,8 +33,8 @@
   <a href="https://hub.docker.com/r/semgrep/semgrep">
     <img src="https://img.shields.io/docker/pulls/semgrep/semgrep.svg?style=flat-square" alt="Docker Pulls" />
   </a>
-  <a href="https://hub.docker.com/r/returntocorp/semgrep">
-    <img src="https://img.shields.io/docker/pulls/returntocorp/semgrep.svg?style=flat-square" alt="Docker Pulls (Old)" />
+  <a href="https://hub.docker.com/r/semgrep/semgrep">
+    <img src="https://img.shields.io/docker/pulls/semgrep/semgrep.svg?style=flat-square" alt="Docker Pulls (Old)" />
   </a>
   <a href="https://twitter.com/intent/follow?screen_name=semgrep">
     <img src="https://img.shields.io/twitter/follow/semgrep?label=Follow%20semgrep&style=social&color=blue" alt="Follow @semgrep on Twitter" />
diff --git a/changelog.d/CODE-7871.fixed b/changelog.d/CODE-7871.fixed
new file mode 100644
index 000000000000..7cf7a3bd8163
--- /dev/null
+++ b/changelog.d/CODE-7871.fixed
@@ -0,0 +1,12 @@
+Fixed bug affecting taint tracking through static fields when mixing accesses
+using the class name and using an instance object, e.g.:
+
+    class C {
+        static String s;
+    }
+
+    ...
+
+            C o = new C();
+            C.s = taint;
+            sink(o.s); // finding !
diff --git a/changelog.d/python.changed b/changelog.d/python.changed
new file mode 100644
index 000000000000..348c0fccdd45
--- /dev/null
+++ b/changelog.d/python.changed
@@ -0,0 +1,2 @@
+The minimum Python version for semgrep is now 3.9.
+We are dropping support for Python 3.8
diff --git a/cli/Pipfile.lock b/cli/Pipfile.lock
index 4ec29866fbd3..5ca70f02350c 100644
--- a/cli/Pipfile.lock
+++ b/cli/Pipfile.lock
@@ -31,11 +31,11 @@
         },
         "bracex": {
             "hashes": [
-                "sha256:0725da5045e8d37ea9592ab3614d8b561e22c3c5fde3964699be672e072ab611",
-                "sha256:d2fcf4b606a82ac325471affe1706dd9bbaa3536c91ef86a31f6b766f3dad1d0"
+                "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6",
+                "sha256:13e5732fec27828d6af308628285ad358047cec36801598368cb28bc631dbaf6"
             ],
             "markers": "python_version >= '3.8'",
-            "version": "==2.5"
+            "version": "==2.5.post1"
         },
         "certifi": {
             "hashes": [
@@ -47,99 +47,114 @@
         },
         "charset-normalizer": {
             "hashes": [
-                "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027",
-                "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087",
-                "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786",
-                "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8",
-                "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09",
-                "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185",
-                "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574",
-                "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e",
-                "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519",
-                "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898",
-                "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269",
-                "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3",
-                "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f",
-                "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6",
-                "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8",
-                "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a",
-                "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73",
-                "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc",
-                "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714",
-                "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2",
-                "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc",
-                "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce",
-                "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d",
-                "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e",
-                "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6",
-                "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269",
-                "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96",
-                "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d",
-                "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a",
-                "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4",
-                "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77",
-                "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d",
-                "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0",
-                "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed",
-                "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068",
-                "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac",
-                "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25",
-                "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8",
-                "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab",
-                "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26",
-                "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2",
-                "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db",
-                "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f",
-                "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5",
-                "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99",
-                "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c",
-                "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d",
-                "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811",
-                "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa",
-                "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a",
-                "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03",
-                "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b",
-                "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04",
-                "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c",
-                "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001",
-                "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458",
-                "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389",
-                "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99",
-                "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985",
-                "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537",
-                "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238",
-                "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f",
-                "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d",
-                "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796",
-                "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a",
-                "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143",
-                "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8",
-                "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c",
-                "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5",
-                "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5",
-                "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711",
-                "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4",
-                "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6",
-                "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c",
-                "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7",
-                "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4",
-                "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b",
-                "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae",
-                "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12",
-                "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c",
-                "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae",
-                "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8",
-                "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887",
-                "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b",
-                "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4",
-                "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f",
-                "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5",
-                "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33",
-                "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519",
-                "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"
+                "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621",
+                "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6",
+                "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8",
+                "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912",
+                "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c",
+                "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b",
+                "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d",
+                "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d",
+                "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95",
+                "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e",
+                "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565",
+                "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64",
+                "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab",
+                "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be",
+                "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e",
+                "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907",
+                "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0",
+                "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2",
+                "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62",
+                "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62",
+                "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23",
+                "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc",
+                "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284",
+                "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca",
+                "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455",
+                "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858",
+                "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b",
+                "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594",
+                "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc",
+                "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db",
+                "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b",
+                "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea",
+                "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6",
+                "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920",
+                "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749",
+                "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7",
+                "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd",
+                "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99",
+                "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242",
+                "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee",
+                "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129",
+                "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2",
+                "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51",
+                "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee",
+                "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8",
+                "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b",
+                "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613",
+                "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742",
+                "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe",
+                "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3",
+                "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5",
+                "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631",
+                "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7",
+                "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15",
+                "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c",
+                "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea",
+                "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417",
+                "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250",
+                "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88",
+                "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca",
+                "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa",
+                "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99",
+                "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149",
+                "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41",
+                "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574",
+                "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0",
+                "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f",
+                "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d",
+                "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654",
+                "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3",
+                "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19",
+                "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90",
+                "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578",
+                "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9",
+                "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1",
+                "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51",
+                "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719",
+                "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236",
+                "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a",
+                "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c",
+                "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade",
+                "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944",
+                "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc",
+                "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6",
+                "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6",
+                "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27",
+                "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6",
+                "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2",
+                "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12",
+                "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf",
+                "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114",
+                "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7",
+                "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf",
+                "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d",
+                "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b",
+                "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed",
+                "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03",
+                "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4",
+                "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67",
+                "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365",
+                "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a",
+                "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748",
+                "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b",
+                "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079",
+                "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"
             ],
             "markers": "python_full_version >= '3.7.0'",
-            "version": "==3.3.2"
+            "version": "==3.4.0"
         },
         "click": {
             "hashes": [
@@ -175,11 +190,11 @@
         },
         "deprecated": {
             "hashes": [
-                "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c",
-                "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"
+                "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320",
+                "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d"
             ],
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==1.2.14"
+            "version": "==1.2.15"
         },
         "exceptiongroup": {
             "hashes": [
@@ -191,10 +206,10 @@
         },
         "face": {
             "hashes": [
-                "sha256:344fe31562d0f6f444a45982418f3793d4b14f9abb98ccca1509d22e0a3e7e35",
-                "sha256:d5d692f90bc8f5987b636e47e36384b9bbda499aaf0a77aa0b0bbe834c76923d"
+                "sha256:0e2c17b426fa4639a4e77d1de9580f74a98f4869ba4c7c8c175b810611622cd3",
+                "sha256:611e29a01ac5970f0077f9c577e746d48c082588b411b33a0dd55c4d872949f6"
             ],
-            "version": "==22.0.0"
+            "version": "==24.0.0"
         },
         "glom": {
             "hashes": [
@@ -205,19 +220,19 @@
         },
         "googleapis-common-protos": {
             "hashes": [
-                "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63",
-                "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0"
+                "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c",
+                "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==1.65.0"
+            "version": "==1.66.0"
         },
         "idna": {
             "hashes": [
-                "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac",
-                "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"
+                "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
+                "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==3.8"
+            "version": "==3.10"
         },
         "importlib-metadata": {
             "hashes": [
@@ -330,34 +345,34 @@
         },
         "packaging": {
             "hashes": [
-                "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002",
-                "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"
+                "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759",
+                "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"
             ],
             "markers": "python_version >= '3.8'",
-            "version": "==24.1"
+            "version": "==24.2"
         },
         "peewee": {
             "hashes": [
-                "sha256:cea5592c6f4da1592b7cff8eaf655be6648a1f5857469e30037bf920c03fb8fb"
+                "sha256:6aefc700bd530fc6ac23fa19c9c5b47041751d92985b799169c8e318e97eabaa"
             ],
-            "version": "==3.17.6"
+            "version": "==3.17.7"
         },
         "protobuf": {
             "hashes": [
-                "sha256:051e97ce9fa6067a4546e75cb14f90cf0232dcb3e3d508c448b8d0e4265b61c1",
-                "sha256:0dc4a62cc4052a036ee2204d26fe4d835c62827c855c8a03f29fe6da146b380d",
-                "sha256:3319e073562e2515c6ddc643eb92ce20809f5d8f10fead3332f71c63be6a7040",
-                "sha256:4c8a70fdcb995dcf6c8966cfa3a29101916f7225e9afe3ced4395359955d3835",
-                "sha256:7e372cbbda66a63ebca18f8ffaa6948455dfecc4e9c1029312f6c2edcd86c4e1",
-                "sha256:90bf6fd378494eb698805bbbe7afe6c5d12c8e17fca817a646cd6a1818c696ca",
-                "sha256:ac79a48d6b99dfed2729ccccee547b34a1d3d63289c71cef056653a846a2240f",
-                "sha256:ba3d8504116a921af46499471c63a85260c1a5fc23333154a427a310e015d26d",
-                "sha256:bfbebc1c8e4793cfd58589acfb8a1026be0003e852b9da7db5a4285bde996978",
-                "sha256:db9fd45183e1a67722cafa5c1da3e85c6492a5383f127c86c4c4aa4845867dc4",
-                "sha256:eecd41bfc0e4b1bd3fa7909ed93dd14dd5567b98c941d6c1ad08fdcab3d6884b"
+                "sha256:0aebecb809cae990f8129ada5ca273d9d670b76d9bfc9b1809f0a9c02b7dbf41",
+                "sha256:4be0571adcbe712b282a330c6e89eae24281344429ae95c6d85e79e84780f5ea",
+                "sha256:5e61fd921603f58d2f5acb2806a929b4675f8874ff5f330b7d6f7e2e784bbcd8",
+                "sha256:7a183f592dc80aa7c8da7ad9e55091c4ffc9497b3054452d629bb85fa27c2a45",
+                "sha256:7f8249476b4a9473645db7f8ab42b02fe1488cbe5fb72fddd445e0665afd8584",
+                "sha256:919ad92d9b0310070f8356c24b855c98df2b8bd207ebc1c0c6fcc9ab1e007f3d",
+                "sha256:98d8d8aa50de6a2747efd9cceba361c9034050ecce3e09136f90de37ddba66e1",
+                "sha256:abe32aad8561aa7cc94fc7ba4fdef646e576983edb94a73381b03c53728a626f",
+                "sha256:b0234dd5a03049e4ddd94b93400b67803c823cfc405689688f59b34e0742381a",
+                "sha256:b2fde3d805354df675ea4c7c6338c1aecd254dfc9925e88c6d31a2bcb97eb173",
+                "sha256:fe14e16c22be926d3abfcb500e60cab068baf10b542b8c858fa27e098123e331"
             ],
             "markers": "python_version >= '3.8'",
-            "version": "==4.25.4"
+            "version": "==4.25.5"
         },
         "pygments": {
             "hashes": [
@@ -423,67 +438,58 @@
         },
         "ruamel.yaml": {
             "hashes": [
-                "sha256:6024b986f06765d482b5b07e086cc4b4cd05dd22ddcbc758fa23d54873cf313d",
-                "sha256:b16b6c3816dff0a93dca12acf5e70afd089fa5acb80604afd1ffa8b465b7722c"
+                "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636",
+                "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"
             ],
-            "markers": "python_version >= '3'",
-            "version": "==0.17.40"
+            "markers": "python_version >= '3.7'",
+            "version": "==0.18.6"
         },
         "ruamel.yaml.clib": {
             "hashes": [
-                "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d",
-                "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001",
-                "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462",
-                "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9",
-                "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe",
-                "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b",
-                "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b",
-                "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615",
-                "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62",
-                "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15",
-                "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b",
-                "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1",
-                "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9",
-                "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675",
-                "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899",
-                "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7",
-                "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7",
-                "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312",
-                "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa",
-                "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91",
-                "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b",
-                "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6",
-                "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3",
-                "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334",
-                "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5",
-                "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3",
-                "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe",
-                "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c",
-                "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed",
-                "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337",
-                "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880",
-                "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f",
-                "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d",
-                "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248",
-                "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d",
-                "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf",
-                "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512",
-                "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069",
-                "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb",
-                "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942",
-                "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d",
-                "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31",
-                "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92",
-                "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5",
-                "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28",
-                "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d",
-                "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1",
-                "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2",
-                "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875",
-                "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"
+                "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b",
+                "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4",
+                "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef",
+                "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5",
+                "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632",
+                "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6",
+                "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680",
+                "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf",
+                "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da",
+                "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6",
+                "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a",
+                "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519",
+                "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6",
+                "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f",
+                "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd",
+                "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2",
+                "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52",
+                "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd",
+                "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d",
+                "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c",
+                "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6",
+                "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb",
+                "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969",
+                "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28",
+                "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e",
+                "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45",
+                "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4",
+                "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12",
+                "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31",
+                "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642",
+                "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e",
+                "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285",
+                "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed",
+                "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1",
+                "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7",
+                "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3",
+                "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475",
+                "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5",
+                "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76",
+                "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987",
+                "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"
             ],
             "markers": "python_version < '3.13' and platform_python_implementation == 'CPython'",
-            "version": "==0.2.8"
+            "version": "==0.2.12"
         },
         "semgrep": {
             "editable": true,
@@ -492,19 +498,19 @@
         },
         "setuptools": {
             "hashes": [
-                "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308",
-                "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6"
+                "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6",
+                "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"
             ],
-            "markers": "python_version >= '3.8'",
-            "version": "==74.1.2"
+            "markers": "python_version >= '3.9'",
+            "version": "==75.6.0"
         },
         "tomli": {
             "hashes": [
-                "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
-                "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
+                "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38",
+                "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"
             ],
-            "markers": "python_version >= '3.7'",
-            "version": "==2.0.1"
+            "markers": "python_version >= '3.8'",
+            "version": "==2.0.2"
         },
         "typing-extensions": {
             "hashes": [
@@ -516,11 +522,11 @@
         },
         "urllib3": {
             "hashes": [
-                "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472",
-                "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"
+                "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac",
+                "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"
             ],
             "markers": "python_version >= '3.8'",
-            "version": "==2.2.2"
+            "version": "==2.2.3"
         },
         "wcmatch": {
             "hashes": [
@@ -532,87 +538,82 @@
         },
         "wrapt": {
             "hashes": [
-                "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc",
-                "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81",
-                "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09",
-                "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e",
-                "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca",
-                "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0",
-                "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb",
-                "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487",
-                "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40",
-                "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c",
-                "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060",
-                "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202",
-                "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41",
-                "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9",
-                "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b",
-                "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664",
-                "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d",
-                "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362",
-                "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00",
-                "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc",
-                "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1",
-                "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267",
-                "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956",
-                "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966",
-                "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1",
-                "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228",
-                "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72",
-                "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d",
-                "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292",
-                "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0",
-                "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0",
-                "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36",
-                "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c",
-                "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5",
-                "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f",
-                "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73",
-                "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b",
-                "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2",
-                "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593",
-                "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39",
-                "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389",
-                "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf",
-                "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf",
-                "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89",
-                "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c",
-                "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c",
-                "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f",
-                "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440",
-                "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465",
-                "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136",
-                "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b",
-                "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8",
-                "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3",
-                "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8",
-                "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6",
-                "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e",
-                "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f",
-                "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c",
-                "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e",
-                "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8",
-                "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2",
-                "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020",
-                "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35",
-                "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d",
-                "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3",
-                "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537",
-                "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809",
-                "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d",
-                "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a",
-                "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"
+                "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d",
+                "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301",
+                "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635",
+                "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a",
+                "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed",
+                "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721",
+                "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801",
+                "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b",
+                "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1",
+                "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88",
+                "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8",
+                "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0",
+                "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f",
+                "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578",
+                "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7",
+                "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045",
+                "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada",
+                "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d",
+                "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b",
+                "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a",
+                "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977",
+                "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea",
+                "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346",
+                "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13",
+                "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22",
+                "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339",
+                "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9",
+                "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181",
+                "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c",
+                "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90",
+                "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a",
+                "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489",
+                "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f",
+                "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504",
+                "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea",
+                "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569",
+                "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4",
+                "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce",
+                "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab",
+                "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a",
+                "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f",
+                "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c",
+                "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9",
+                "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf",
+                "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d",
+                "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627",
+                "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d",
+                "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4",
+                "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c",
+                "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d",
+                "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad",
+                "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b",
+                "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33",
+                "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371",
+                "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1",
+                "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393",
+                "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106",
+                "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df",
+                "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379",
+                "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451",
+                "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b",
+                "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575",
+                "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed",
+                "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb",
+                "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"
             ],
-            "markers": "python_version >= '3.6'",
-            "version": "==1.16.0"
+            "markers": "python_version >= '3.8'",
+            "version": "==1.17.0"
         },
         "zipp": {
             "hashes": [
-                "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064",
-                "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"
+                "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4",
+                "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"
             ],
-            "markers": "python_version >= '3.8'",
-            "version": "==3.20.1"
+            "markers": "python_version >= '3.9'",
+            "version": "==3.21.0"
         }
     },
     "develop": {
@@ -650,99 +651,114 @@
         },
         "charset-normalizer": {
             "hashes": [
-                "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027",
-                "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087",
-                "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786",
-                "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8",
-                "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09",
-                "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185",
-                "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574",
-                "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e",
-                "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519",
-                "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898",
-                "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269",
-                "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3",
-                "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f",
-                "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6",
-                "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8",
-                "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a",
-                "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73",
-                "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc",
-                "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714",
-                "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2",
-                "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc",
-                "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce",
-                "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d",
-                "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e",
-                "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6",
-                "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269",
-                "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96",
-                "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d",
-                "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a",
-                "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4",
-                "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77",
-                "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d",
-                "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0",
-                "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed",
-                "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068",
-                "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac",
-                "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25",
-                "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8",
-                "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab",
-                "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26",
-                "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2",
-                "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db",
-                "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f",
-                "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5",
-                "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99",
-                "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c",
-                "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d",
-                "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811",
-                "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa",
-                "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a",
-                "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03",
-                "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b",
-                "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04",
-                "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c",
-                "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001",
-                "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458",
-                "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389",
-                "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99",
-                "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985",
-                "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537",
-                "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238",
-                "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f",
-                "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d",
-                "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796",
-                "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a",
-                "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143",
-                "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8",
-                "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c",
-                "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5",
-                "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5",
-                "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711",
-                "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4",
-                "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6",
-                "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c",
-                "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7",
-                "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4",
-                "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b",
-                "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae",
-                "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12",
-                "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c",
-                "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae",
-                "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8",
-                "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887",
-                "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b",
-                "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4",
-                "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f",
-                "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5",
-                "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33",
-                "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519",
-                "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"
+                "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621",
+                "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6",
+                "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8",
+                "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912",
+                "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c",
+                "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b",
+                "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d",
+                "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d",
+                "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95",
+                "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e",
+                "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565",
+                "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64",
+                "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab",
+                "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be",
+                "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e",
+                "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907",
+                "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0",
+                "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2",
+                "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62",
+                "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62",
+                "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23",
+                "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc",
+                "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284",
+                "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca",
+                "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455",
+                "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858",
+                "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b",
+                "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594",
+                "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc",
+                "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db",
+                "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b",
+                "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea",
+                "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6",
+                "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920",
+                "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749",
+                "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7",
+                "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd",
+                "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99",
+                "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242",
+                "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee",
+                "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129",
+                "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2",
+                "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51",
+                "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee",
+                "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8",
+                "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b",
+                "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613",
+                "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742",
+                "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe",
+                "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3",
+                "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5",
+                "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631",
+                "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7",
+                "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15",
+                "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c",
+                "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea",
+                "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417",
+                "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250",
+                "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88",
+                "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca",
+                "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa",
+                "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99",
+                "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149",
+                "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41",
+                "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574",
+                "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0",
+                "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f",
+                "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d",
+                "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654",
+                "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3",
+                "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19",
+                "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90",
+                "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578",
+                "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9",
+                "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1",
+                "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51",
+                "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719",
+                "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236",
+                "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a",
+                "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c",
+                "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade",
+                "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944",
+                "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc",
+                "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6",
+                "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6",
+                "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27",
+                "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6",
+                "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2",
+                "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12",
+                "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf",
+                "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114",
+                "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7",
+                "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf",
+                "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d",
+                "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b",
+                "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed",
+                "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03",
+                "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4",
+                "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67",
+                "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365",
+                "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a",
+                "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748",
+                "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b",
+                "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079",
+                "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"
             ],
             "markers": "python_full_version >= '3.7.0'",
-            "version": "==3.3.2"
+            "version": "==3.4.0"
         },
         "colorama": {
             "hashes": [
@@ -754,10 +770,10 @@
         },
         "distlib": {
             "hashes": [
-                "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784",
-                "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"
+                "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87",
+                "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"
             ],
-            "version": "==0.3.8"
+            "version": "==0.3.9"
         },
         "docutils": {
             "hashes": [
@@ -777,11 +793,11 @@
         },
         "filelock": {
             "hashes": [
-                "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec",
-                "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609"
+                "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0",
+                "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"
             ],
             "markers": "python_version >= '3.8'",
-            "version": "==3.16.0"
+            "version": "==3.16.1"
         },
         "freezegun": {
             "hashes": [
@@ -793,11 +809,11 @@
         },
         "idna": {
             "hashes": [
-                "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac",
-                "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"
+                "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
+                "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==3.8"
+            "version": "==3.10"
         },
         "importlib-metadata": {
             "hashes": [
@@ -882,19 +898,19 @@
         },
         "packaging": {
             "hashes": [
-                "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002",
-                "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"
+                "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759",
+                "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"
             ],
             "markers": "python_version >= '3.8'",
-            "version": "==24.1"
+            "version": "==24.2"
         },
         "platformdirs": {
             "hashes": [
-                "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c",
-                "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"
+                "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907",
+                "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"
             ],
             "markers": "python_version >= '3.8'",
-            "version": "==4.3.2"
+            "version": "==4.3.6"
         },
         "pluggy": {
             "hashes": [
@@ -922,11 +938,11 @@
         },
         "pyproject-api": {
             "hashes": [
-                "sha256:2dc1654062c2b27733d8fd4cdda672b22fe8741ef1dde8e3a998a9547b071eeb",
-                "sha256:7ebc6cd10710f89f4cf2a2731710a98abce37ebff19427116ff2174c9236a827"
+                "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228",
+                "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496"
             ],
             "markers": "python_version >= '3.8'",
-            "version": "==1.7.1"
+            "version": "==1.8.0"
         },
         "pytest": {
             "hashes": [
@@ -974,12 +990,12 @@
         },
         "pytest-split": {
             "hashes": [
-                "sha256:9e197df601828d76a1ab615158d9c6253ec9f96e46c1d3ea27187aa5ac0ef9de",
-                "sha256:ca52527e4d9024f6ec3aba723527bd276d12096024999b1f5b8445a38da1e81c"
+                "sha256:466096b086a7147bcd423c6e6c2e57fc62af1c5ea2e256b4ed50fc030fc3dddc",
+                "sha256:adf80ba9fef7be89500d571e705b4f963dfa05038edf35e4925817e6b34ea66f"
             ],
             "index": "pypi",
             "markers": "python_version < '4.0' and python_full_version >= '3.8.1'",
-            "version": "==0.9.0"
+            "version": "==0.10.0"
         },
         "pytest-xdist": {
             "hashes": [
@@ -1033,20 +1049,20 @@
         },
         "tomli": {
             "hashes": [
-                "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
-                "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
+                "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38",
+                "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"
             ],
-            "markers": "python_version >= '3.7'",
-            "version": "==2.0.1"
+            "markers": "python_version >= '3.8'",
+            "version": "==2.0.2"
         },
         "tox": {
             "hashes": [
-                "sha256:35d472032ee1f73fe20c3e0e73d7073a4e85075c86ff02c576f9fc7c6a15a578",
-                "sha256:3c0c96bc3a568a5c7e66387a4cfcf8c875b52e09f4d47c9f7a277ec82f1a0b11"
+                "sha256:452bc32bb031f2282881a2118923176445bac783ab97c874b8770ab4c3b76c38",
+                "sha256:86075e00e555df6e82e74cfc333917f91ecb47ffbc868dcafbd2672e332f4a2c"
             ],
             "index": "pypi",
             "markers": "python_version >= '3.8'",
-            "version": "==4.18.1"
+            "version": "==4.23.2"
         },
         "types-colorama": {
             "hashes": [
@@ -1090,30 +1106,30 @@
         },
         "types-python-dateutil": {
             "hashes": [
-                "sha256:27c8cc2d058ccb14946eebcaaa503088f4f6dbc4fb6093d3d456a49aef2753f6",
-                "sha256:9706c3b68284c25adffc47319ecc7947e5bb86b3773f843c73906fd598bc176e"
+                "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d",
+                "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446"
             ],
             "index": "pypi",
             "markers": "python_version >= '3.8'",
-            "version": "==2.9.0.20240906"
+            "version": "==2.9.0.20241003"
         },
         "types-requests": {
             "hashes": [
-                "sha256:1d1e79faeaf9d42def77f3c304893dea17a97cae98168ac69f3cb465516ee8da",
-                "sha256:ff33935f061b5e81ec87997e91050f7b4af4f82027a7a7a9d9aaea04a963fdf8"
+                "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95",
+                "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"
             ],
             "index": "pypi",
             "markers": "python_version >= '3.8'",
-            "version": "==2.32.0.20240907"
+            "version": "==2.32.0.20241016"
         },
         "types-setuptools": {
             "hashes": [
-                "sha256:0abdb082552ca966c1e5fc244e4853adc62971f6cd724fb1d8a3713b580e5a65",
-                "sha256:15b38c8e63ca34f42f6063ff4b1dd662ea20086166d5ad6a102e670a52574120"
+                "sha256:78cb5fef4a6056d2f37114d27da90f4655a306e4e38042d7034a8a880bc3f5dd",
+                "sha256:f9e1ebd17a56f606e16395c4ee4efa1cdc394b9a2a0ee898a624058b4b62ef8f"
             ],
             "index": "pypi",
             "markers": "python_version >= '3.8'",
-            "version": "==74.1.0.20240907"
+            "version": "==75.3.0.20241112"
         },
         "typing-extensions": {
             "hashes": [
@@ -1125,27 +1141,27 @@
         },
         "urllib3": {
             "hashes": [
-                "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472",
-                "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"
+                "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac",
+                "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"
             ],
             "markers": "python_version >= '3.8'",
-            "version": "==2.2.2"
+            "version": "==2.2.3"
         },
         "virtualenv": {
             "hashes": [
-                "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55",
-                "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"
+                "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba",
+                "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"
             ],
-            "markers": "python_version >= '3.7'",
-            "version": "==20.26.4"
+            "markers": "python_version >= '3.8'",
+            "version": "==20.27.1"
         },
         "zipp": {
             "hashes": [
-                "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064",
-                "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"
+                "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4",
+                "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"
             ],
-            "markers": "python_version >= '3.8'",
-            "version": "==3.20.1"
+            "markers": "python_version >= '3.9'",
+            "version": "==3.21.0"
         }
     }
 }
diff --git a/cli/setup.py b/cli/setup.py
index dda7459fa671..d94bff58f9f9 100644
--- a/cli/setup.py
+++ b/cli/setup.py
@@ -33,8 +33,13 @@ def get_tag(self):
             # For more information about python compatibility tags, check out:
             # https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/
 
-            # We support Python 3.8+
-            python = "cp38.cp39.cp310.cp311.py37.py38.py39.py310.py311"
+            # We support Python 3.9+
+            # coupling: if you drop support for some python, you'll probably
+            # have to update 'python_requires' at the end of this file
+            # and a few workflows as show for example in this PR:
+            # https://github.com/semgrep/semgrep-proprietary/pull/2606/files
+            # coupling: semgrep.libsonnet default_python_version
+            python = "cp39.cp310.cp311.py39.py310.py311"
 
             # We don't require a specific Python ABI
             abi = "none"
@@ -123,7 +128,7 @@ def find_executable(env_name, exec_name):
     "peewee~=3.14",
     "requests~=2.22",
     "rich~=13.5.2",
-    "ruamel.yaml>=0.16.0,<0.18",
+    "ruamel.yaml>=0.18.5",
     "tomli~=2.0.1",
     "typing-extensions~=4.2",
     "urllib3~=2.0",
@@ -158,13 +163,12 @@ def find_executable(env_name, exec_name):
         "License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)",
         "Operating System :: MacOS",
         "Operating System :: POSIX :: Linux",
-        "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: 3.11",
         "Topic :: Security",
         "Topic :: Software Development :: Quality Assurance",
     ],
-    python_requires=">=3.8",
+    python_requires=">=3.9",
     zip_safe=False,
 )
diff --git a/cli/src/semdep/package_restrictions.py b/cli/src/semdep/package_restrictions.py
index 0f26948eb915..6f1711caeef4 100644
--- a/cli/src/semdep/package_restrictions.py
+++ b/cli/src/semdep/package_restrictions.py
@@ -2,12 +2,12 @@
 from typing import List
 from typing import Tuple
 
+import semgrep.semgrep_interfaces.semgrep_output_v1 as out
 from semdep.external.packaging.specifiers import InvalidSpecifier  # type: ignore
 from semdep.external.packaging.specifiers import SpecifierSet  # type: ignore
 from semdep.golang_version import compare_golang_specifier
 from semdep.maven_version import compare_maven_specifier
 from semgrep.error import SemgrepError
-from semgrep.semgrep_interfaces.semgrep_output_v1 import DependencyPattern
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Ecosystem
 from semgrep.semgrep_interfaces.semgrep_output_v1 import FoundDependency
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Gomod
@@ -51,9 +51,9 @@ def is_in_range(ecosystem: Ecosystem, range: str, version: str) -> bool:
 
 # compare vulnerable range to version in lockfile
 def dependencies_range_match_any(
-    search_for_ranges: List[DependencyPattern],
+    search_for_ranges: List[out.ScaPattern],
     have_deps: List[FoundDependency],
-) -> Iterator[Tuple[DependencyPattern, FoundDependency]]:
+) -> Iterator[Tuple[out.ScaPattern, FoundDependency]]:
     for have_dep in have_deps:
         for target_range in search_for_ranges:
             if (
diff --git a/cli/src/semgrep/config_resolver.py b/cli/src/semgrep/config_resolver.py
index 97f18a14a174..c913390432fe 100644
--- a/cli/src/semgrep/config_resolver.py
+++ b/cli/src/semgrep/config_resolver.py
@@ -573,7 +573,8 @@ def from_config_list(
 
         for i, config in enumerate(configs):
             try:
-                # Patch config_id to fix https://github.com/returntocorp/semgrep/issues/1912
+                # Patch config_id to fix
+                # https://github.com/semgrep/semgrep/issues/1912
                 resolved_config, config_errors = resolve_config(
                     config, project_url, force_jsonschema=force_jsonschema
                 )
diff --git a/cli/src/semgrep/dependency_aware_rule.py b/cli/src/semgrep/dependency_aware_rule.py
index 2b3b574f5c7e..ade74db7d0a3 100644
--- a/cli/src/semgrep/dependency_aware_rule.py
+++ b/cli/src/semgrep/dependency_aware_rule.py
@@ -18,12 +18,10 @@
 from semgrep.rule import Rule
 from semgrep.rule_match import RuleMatch
 from semgrep.semgrep_interfaces.semgrep_output_v1 import DependencyMatch
-from semgrep.semgrep_interfaces.semgrep_output_v1 import DependencyPattern
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Direct
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Ecosystem
 from semgrep.semgrep_interfaces.semgrep_output_v1 import FoundDependency
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Pypi
-from semgrep.semgrep_interfaces.semgrep_output_v1 import ScaInfo
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Transitive
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Transitivity
 from semgrep.subproject import find_closest_subproject
@@ -36,7 +34,7 @@
 SCA_FINDING_SCHEMA = 20220913
 
 
-def parse_depends_on_yaml(entries: List[Dict[str, str]]) -> Iterator[DependencyPattern]:
+def parse_depends_on_yaml(entries: List[Dict[str, str]]) -> Iterator[out.ScaPattern]:
     """
     Convert the entries in the Yaml to ProjectDependsOnEntry objects that specify
     namespace, package name, and semver ranges
@@ -65,7 +63,7 @@ def parse_depends_on_yaml(entries: List[Dict[str, str]]) -> Iterator[DependencyP
         if ecosystem == Ecosystem(Pypi()):
             package = package.lower()
 
-        yield DependencyPattern(
+        yield out.ScaPattern(
             ecosystem=ecosystem, package=package, semver_range=semver_range
         )
 
@@ -136,7 +134,7 @@ def generate_unreachable_sca_findings(
                         ),
                     ),
                     extra={
-                        "sca_info": ScaInfo(
+                        "sca_info": out.ScaMatch(
                             sca_finding_schema=SCA_FINDING_SCHEMA,
                             reachable=False,
                             reachability_rule=rule.should_run_on_semgrep_core,
@@ -229,7 +227,7 @@ def generate_reachable_sca_findings(
                     # ! deepcopy is necessary here since we might iterate over the
                     # ! same match for multiple dependencies
                     new_match = copy.deepcopy(match)
-                    new_match.extra["sca_info"] = ScaInfo(
+                    new_match.extra["sca_info"] = out.ScaMatch(
                         sca_finding_schema=SCA_FINDING_SCHEMA,
                         reachable=True,
                         reachability_rule=rule.should_run_on_semgrep_core,
diff --git a/cli/src/semgrep/rule_lang.py b/cli/src/semgrep/rule_lang.py
index 79b5579b3022..99f432de0b6f 100644
--- a/cli/src/semgrep/rule_lang.py
+++ b/cli/src/semgrep/rule_lang.py
@@ -387,7 +387,7 @@ def _validation_error_message(error: jsonschema.exceptions.ValidationError) -> s
     return contexts[0].message
 
 
-DUMMY_POSITION = out.Position(line=1, col=0)
+DUMMY_POSITION = out.Position(line=1, col=0, offset=0)
 
 
 def safe_relative_to(a: Path, b: Path) -> Path:
diff --git a/cli/src/semgrep/semgrep_interfaces b/cli/src/semgrep/semgrep_interfaces
index 60809032a2e3..3171f2052e3a 160000
--- a/cli/src/semgrep/semgrep_interfaces
+++ b/cli/src/semgrep/semgrep_interfaces
@@ -1,1 +1,1 @@
-Subproject commit 60809032a2e39742f42910d46b3e5dd305b8b8cf
+Subproject commit 3171f2052e3a2611688f84ca2664891b37e8c154
diff --git a/flake.lock b/flake.lock
index ba13a276f008..f44224c21f10 100644
--- a/flake.lock
+++ b/flake.lock
@@ -52,11 +52,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1732837521,
-        "narHash": "sha256-jNRNr49UiuIwaarqijgdTR2qLPifxsVhlJrKzQ8XUIE=",
+        "lastModified": 1733581040,
+        "narHash": "sha256-Qn3nPMSopRQJgmvHzVqPcE3I03zJyl8cSbgnnltfFDY=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "970e93b9f82e2a0f3675757eb0bfc73297cc6370",
+        "rev": "22c3f2cf41a0e70184334a958e6b124fb0ce3e01",
         "type": "github"
       },
       "original": {
@@ -129,11 +129,11 @@
     "opam-repository_2": {
       "flake": false,
       "locked": {
-        "lastModified": 1733107127,
-        "narHash": "sha256-gYyaYnbLTLJyRYqeGIMJbZ/J2tSxMS+BTzfEU82dhno=",
+        "lastModified": 1733656817,
+        "narHash": "sha256-ddoiIYY6AhWLUxkgtXhPbB7Du0OcHPyR4iEF/Tm8BsY=",
         "owner": "ocaml",
         "repo": "opam-repository",
-        "rev": "5cfbed0a5094c19375b0d6e7b243c13f677ec0d1",
+        "rev": "211ce1c3d5d6eb57dee3ca21cb1e4a16da41d01f",
         "type": "github"
       },
       "original": {
diff --git a/languages/javascript/generic/js_to_generic.ml b/languages/javascript/generic/js_to_generic.ml
index 0afaff434ed7..a35ab88f99b7 100644
--- a/languages/javascript/generic/js_to_generic.ml
+++ b/languages/javascript/generic/js_to_generic.ml
@@ -305,7 +305,7 @@ and expr (x : expr) =
       let e = G.Lambda def |> G.e in
       (* Since the attrs aren't included in the AST, at least update the range
        * to include them. See
-       * https://github.com/returntocorp/semgrep/issues/7353 *)
+       * https://github.com/semgrep/semgrep/issues/7353 *)
       let attrs_any = List_.map (fun attr -> G.At attr) more_attrs in
       H.set_e_range_with_anys (G.Dk (G.FuncDef def) :: attrs_any) e;
       e
diff --git a/libs/ast_generic/AST_generic.ml b/libs/ast_generic/AST_generic.ml
index b154cc5e1bc1..ba0c181f2a05 100644
--- a/libs/ast_generic/AST_generic.ml
+++ b/libs/ast_generic/AST_generic.ml
@@ -2275,7 +2275,8 @@ let empty_id_info ?(hidden = false) ?(case_insensitive = false)
     id_resolved_alternatives = ref [];
     id_type = ref None;
     id_svalue = ref None;
-    id_flags = ref (IdFlags.make ~hidden ~case_insensitive ~final:false);
+    id_flags =
+      ref (IdFlags.make ~hidden ~case_insensitive ~final:false ~static:false);
     id_info_id = id;
   }
 
diff --git a/libs/ast_generic/IdFlags.ml b/libs/ast_generic/IdFlags.ml
index c33f36ab8b62..77ade21056fc 100644
--- a/libs/ast_generic/IdFlags.ml
+++ b/libs/ast_generic/IdFlags.ml
@@ -16,6 +16,7 @@ open Ppx_hash_lib.Std.Hash.Builtin
 let bitmask_HIDDEN = 0x01
 let bitmask_CASE_INSENSITIVE = 0x02
 let bitmask_FINAL = 0x04
+let bitmask_STATIC = 0x08
 
 type t = int [@@deriving show, eq, ord, hash]
 
@@ -29,11 +30,14 @@ let is_case_insensitive, set_case_insensitive =
   make_flag bitmask_CASE_INSENSITIVE
 
 let is_final, set_final = make_flag bitmask_FINAL
+let is_static, set_static = make_flag bitmask_STATIC
+let union x y = Int.logor x y
 let to_int x = x
 
-let make ~hidden ~case_insensitive ~final =
+let make ~hidden ~case_insensitive ~final ~static =
   let flags = empty in
   let flags = if hidden then set_hidden flags else flags in
   let flags = if case_insensitive then set_case_insensitive flags else flags in
   let flags = if final then set_final flags else flags in
+  let flags = if static then set_static flags else flags in
   flags
diff --git a/libs/ast_generic/IdFlags.mli b/libs/ast_generic/IdFlags.mli
index 22e404febe81..e65d759520c5 100644
--- a/libs/ast_generic/IdFlags.mli
+++ b/libs/ast_generic/IdFlags.mli
@@ -4,7 +4,8 @@ type t [@@deriving show, eq, ord, hash]
 val empty : t
 (** No flags set *)
 
-val make : hidden:bool -> case_insensitive:bool -> final:bool -> t
+val make :
+  hidden:bool -> case_insensitive:bool -> final:bool -> static:bool -> t
 
 val is_hidden : t -> bool
 (**
@@ -42,9 +43,20 @@ val set_case_insensitive : t -> t
 
 val is_final : t -> bool
 (** Flag 'is_final' is used within variable definitions/assignments to record
- * whether the variable being defined is "effectively final" (as in Java).
- * This is used by e.g. taint analysis to propagate taint via globals and
- * private class attributes. *)
+    whether the variable being defined is "effectively final" (as in Java).
+    This is used by e.g. taint analysis to propagate taint via globals and
+    private class attributes. *)
 
 val set_final : t -> t
+
+val is_static : t -> bool
+(** Flag 'is_static' indicates that the identifier is "static",
+    e.g. to mark a static class field/variable. *)
+
+val set_static : t -> t
+
+val union : t -> t -> t
+(** Union two sets of flags. *)
+
 val to_int : t -> int
+(** Get the underlying representation of a set of flags. *)
diff --git a/libs/commons/UFile.ml b/libs/commons/UFile.ml
index 8088101c077c..def3742576c4 100644
--- a/libs/commons/UFile.ml
+++ b/libs/commons/UFile.ml
@@ -217,39 +217,72 @@ let filemtime file =
   if !Common.jsoo then failwith "JSOO:filemtime"
   else (UUnix.stat !!file).st_mtime
 
-(* TODO? there's also USys.is_directory? *)
-let is_directory file = (UUnix.stat !!file).st_kind =*= Unix.S_DIR
-let is_file file = (UUnix.stat !!file).st_kind =*= Unix.S_REG
-let is_symlink file = (UUnix.lstat !!file).st_kind =*= Unix.S_LNK
+let is_dir ~follow_symlinks path =
+  let stat = if follow_symlinks then UUnix.stat else UUnix.lstat in
+  match (stat !!path).st_kind with
+  | S_DIR -> true
+  | _ -> false
+  | exception UUnix.Unix_error _ -> false
+
+let is_reg ~follow_symlinks path =
+  let stat = if follow_symlinks then UUnix.stat else UUnix.lstat in
+  match (stat !!path).st_kind with
+  | S_REG -> true
+  | _ -> false
+  | exception UUnix.Unix_error _ -> false
+
+let is_dir_or_reg ~follow_symlinks path =
+  let stat = if follow_symlinks then UUnix.stat else UUnix.lstat in
+  match (stat !!path).st_kind with
+  | S_DIR
+  | S_REG ->
+      true
+  | _ -> false
+  | exception UUnix.Unix_error _ -> false
+
+let is_lnk path =
+  match (UUnix.lstat !!path).st_kind with
+  | S_LNK -> true
+  | _ -> false
+  | exception UUnix.Unix_error _ -> false
+
+let is_lnk_or_reg path =
+  match (UUnix.lstat !!path).st_kind with
+  | S_LNK
+  | S_REG ->
+      true
+  | _ -> false
+  | exception UUnix.Unix_error _ -> false
+
+(* This function isn't very useful but we offer it for completeness. *)
+let is_dir_or_lnk path =
+  match (UUnix.lstat !!path).st_kind with
+  | S_LNK
+  | S_DIR ->
+      true
+  | _ -> false
+  | exception UUnix.Unix_error _ -> false
+
+let is_dir_or_lnk_or_reg path =
+  match (UUnix.lstat !!path).st_kind with
+  | S_DIR
+  | S_LNK
+  | S_REG ->
+      true
+  | _ -> false
+  | exception UUnix.Unix_error _ -> false
 
 let is_executable file =
   let stat = UUnix.stat !!file in
   let perms = stat.st_perm in
   stat.st_kind =*= Unix.S_REG && perms land 0o011 <> 0
 
-let lfile_exists filename =
-  try
-    match (UUnix.lstat !!filename).st_kind with
-    | Unix.S_REG
-    | Unix.S_LNK ->
-        true
-    | _ -> false
-  with
-  | UUnix.Unix_error (Unix.ENOENT, _, _) -> false
-
-(* Helps avoid the `Fatal error: exception Unix_error: No such file or directory stat` *)
-let dir_exists path =
-  try
-    match (UUnix.lstat !!path).st_kind with
-    | S_DIR -> true
-    | _ -> false
-  with
-  | UUnix.Unix_error (Unix.ENOENT, _, _) -> false
-
 let rec make_directories dir =
   try UUnix.mkdir !!dir 0o755 with
   (* The directory already exists *)
-  | UUnix.Unix_error ((EEXIST | EISDIR), _, _) when is_directory dir -> ()
+  | UUnix.Unix_error ((EEXIST | EISDIR), _, _)
+    when is_dir ~follow_symlinks:false dir ->
+      ()
   (* parent doesn't exist *)
   | UUnix.Unix_error (ENOENT, _, _) ->
       let parent = Fpath.parent dir in
diff --git a/libs/commons/UFile.mli b/libs/commons/UFile.mli
index 91844c302f5c..b4addcc8216b 100644
--- a/libs/commons/UFile.mli
+++ b/libs/commons/UFile.mli
@@ -85,63 +85,52 @@ val find_first_match_with_whole_line :
 (*****************************************************************************)
 (* File properties *)
 (*****************************************************************************)
-val is_executable : Fpath.t -> bool
-val filesize : Fpath.t -> int
-val filemtime : Fpath.t -> float
-
-(*
-   TODO: the current interface for checking the existence and kind of files
-   is confusing and raises exceptions that we usually want to ignore.
-   Provide a new interface that is convenient, unambiguous,
-   and covers the most common situations.
 
-   Design guideline: make common tasks easy and uncommon ones possible.
+(* Check if the file is executable by others or by the group.
+   If the file is only executable by the user owning the file ('u'),
+   this function reports it as not executable.
+   For example, the following commands create a file that's executable by its
+   owner (and by root) on which is_executable fails:
 
-   Here are some ideas:
+     echo > foo
+     chmod 700 foo
+     ./foo && echo 'success'
 
-   - input 1: follow symlinks?
-   - input 2: which kind of files we accept: regular file, a folder (dir),
-              or a symlink? (don't care about the exotic kinds)
-   - input 3: is the file readable or writable? -> out of scope
-   - result: bool, turn all Unix_error exceptions into 'false'
-
-     val is_dir : follow_symlinks:bool -> Fpath.t -> bool
-     val is_reg : follow_symlinks:bool -> Fpath.t -> bool
-
-     (* follow_symlinks is implicitly false when one of the allowed file
-        kinds is symlink *)
-     val is_lnk : Fpath.t -> bool
-     val is_lnk_or_reg : Fpath.t -> bool
-     val is_dir_or_lnk_or_reg : Fpath.t -> bool
-
-   For other file kinds (pipes, sockets, ...) or unusual combinations
-   (dir or lnk), use the Unix module directly.
+   TODO: is this intentional? Please explain.
 *)
+val is_executable : Fpath.t -> bool
+val filesize : Fpath.t -> int
+val filemtime : Fpath.t -> float
 
-(* raise Unix_error if the directory does not exist *)
-val is_directory : Fpath.t -> bool
-
-(* Check if a file is a regular file or a symbolic link that references
-   a regular file.
+(*
+   Functions for testing whether a file exists and is of the expected kind,
+   without raising exceptions.
 
-   Raise Unix_error if the file doesn't exist or if the symlink is broken.
-*)
-val is_file : Fpath.t -> bool
+   The goal is to deal with the 3 common file types (dir, reg, lnk)
+   and focus only on files that are usable. If a file is not usable
+   due for example to missing permissions, all these functions will return a
+   negative answer ('false') rather than raising an exception.
+   A design principle is "make common tasks easy and uncommon tasks possible".
+   Here, we're focusing on the former.
 
-(* Check if a file is a symlink. It may be a broken symlink.
+   dir = directory = folder
+   reg = regular files
+   lnk = symbolic link
 
-   Raise Unix_error if the file (symlink) doesn't exist.
-*)
-val is_symlink : Fpath.t -> bool
+   The functions whose name contains 'lnk' never follow symlinks.
 
-(*
-   Check if a file is a regular file or a symlink, possibly a broken symlink.
-   Return false if the file doesn't exist or is of the wrong kind.
+   For more exotic file kinds or for classifying files by kind,
+   use UUnix.stat or UUnix.lstat directly.
 *)
-val lfile_exists : Fpath.t -> bool
-
-(* no raised Unix_error if the directory does not exist *)
-val dir_exists : Fpath.t -> bool
+val is_dir : follow_symlinks:bool -> Fpath.t -> bool
+val is_reg : follow_symlinks:bool -> Fpath.t -> bool
+val is_lnk : Fpath.t -> bool
+val is_dir_or_reg : follow_symlinks:bool -> Fpath.t -> bool
+val is_dir_or_lnk : Fpath.t -> bool
+val is_lnk_or_reg : Fpath.t -> bool
+val is_dir_or_lnk_or_reg : Fpath.t -> bool
+
+(* Turn a file kind into a JSON string node and vice-versa *)
 val file_kind_to_yojson : Unix.file_kind -> Yojson.Safe.t
 val file_kind_of_yojson : Yojson.Safe.t -> (Unix.file_kind, string) result
 
diff --git a/libs/commons/UTmp.mli b/libs/commons/UTmp.mli
index bf48d33970dd..bf4a465f9a16 100644
--- a/libs/commons/UTmp.mli
+++ b/libs/commons/UTmp.mli
@@ -66,7 +66,7 @@ val with_temp_file :
  * rely on the cached lookup table for the previous file with that filename,
  * and chaos ensues.
  *
- * See https://github.com/returntocorp/semgrep/issues/5277 for more info.
+ * See https://github.com/semgrep/semgrep/issues/5277 for more info.
  *)
 val register_temp_file_cleanup_hook : (Fpath.t -> unit) -> unit
 
diff --git a/libs/commons/Uri_.ml b/libs/commons/Uri_.ml
new file mode 100644
index 000000000000..c8d140359a7b
--- /dev/null
+++ b/libs/commons/Uri_.ml
@@ -0,0 +1,28 @@
+(* Yoann Padioleau
+ *
+ * Copyright (C) 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.
+ *)
+
+(*****************************************************************************)
+(* Prelude *)
+(*****************************************************************************)
+(* Extensions to Uri.ml
+ *)
+
+(*****************************************************************************)
+(* API *)
+(*****************************************************************************)
+
+let of_string_opt (str : string) : Uri.t option =
+  let uri = Uri.of_string str in
+  if Uri.equal uri Uri.empty then None else Some uri
diff --git a/libs/commons/Uri_.mli b/libs/commons/Uri_.mli
new file mode 100644
index 000000000000..02638580d2fc
--- /dev/null
+++ b/libs/commons/Uri_.mli
@@ -0,0 +1,4 @@
+(* like Uri.of_string but instead of silently returning an
+ * empty uri in case of error, we return None here.
+ *)
+val of_string_opt : string -> Uri.t option
diff --git a/libs/commons2/common2.ml b/libs/commons2/common2.ml
index fd483a211e61..6fa0dd50d107 100644
--- a/libs/commons2/common2.ml
+++ b/libs/commons2/common2.ml
@@ -2565,7 +2565,7 @@ let sanity_check_files_and_adjust ext files =
            if not (file =~ ".*\\." ^ ext) then (
              pr2 ("warning: seems not a ." ^ ext ^ " file");
              false)
-           else if UFile.is_directory (Fpath.v file) then (
+           else if UFile.is_dir ~follow_symlinks:true (Fpath.v file) then (
              pr2 (spf "warning: %s is a directory" file);
              false)
            else true)
@@ -4966,7 +4966,7 @@ let _ = example
 let inits_of_absolute_dir dir =
   if not (is_absolute dir) then
     failwith (spf "inits_of_absolute_dir: %s is not an absolute path" dir);
-  if not (UFile.is_directory (Fpath.v dir)) then
+  if not (UFile.is_dir ~follow_symlinks:true (Fpath.v dir)) then
     failwith (spf "inits_of_absolute_dir: %s is not a directory" dir);
   let dir = chop_dirsymbol dir in
 
diff --git a/libs/gitignore/Parse_gitignore.ml b/libs/gitignore/Parse_gitignore.ml
index b0a5a5adf396..fee3d9d4413d 100644
--- a/libs/gitignore/Parse_gitignore.ml
+++ b/libs/gitignore/Parse_gitignore.ml
@@ -141,10 +141,8 @@ let rec expand_includes ~orig_semgrepignore_path lines =
         let include_path =
           get_include_path ~orig_semgrepignore_path relative_include_path
         in
-        if
-          Sys.file_exists (Fpath.to_string include_path)
-          && UFile.is_file include_path
-        then include_path |> UFile.read_file |> read_lines_from_string
+        if UFile.is_reg ~follow_symlinks:true include_path then
+          include_path |> UFile.read_file |> read_lines_from_string
         else
           (* ignore silently
              (why: git also ignores .gitignore files that are broken
diff --git a/libs/lib_parsing/Parsing_stat.ml b/libs/lib_parsing/Parsing_stat.ml
index 1c0a7bcda27e..3ba1876bab36 100644
--- a/libs/lib_parsing/Parsing_stat.ml
+++ b/libs/lib_parsing/Parsing_stat.ml
@@ -196,7 +196,7 @@ let regression_information ~ext (xs : Fpath.t list) (newscore : Common2.score) :
       let xs = Fpath_.to_strings xs in
       let dirname_opt =
         match xs with
-        | [ x ] when UFile.is_directory (Fpath.v x) -> Some x
+        | [ x ] when UFile.is_dir ~follow_symlinks:true (Fpath.v x) -> Some x
         | _ -> None
       in
       (* TODO Config_pfff.regression_data_dir *)
diff --git a/libs/process_limits/Memory_limit.mli b/libs/process_limits/Memory_limit.mli
index 94e4987892a2..325db6fc227b 100644
--- a/libs/process_limits/Memory_limit.mli
+++ b/libs/process_limits/Memory_limit.mli
@@ -29,7 +29,7 @@ exception ExceededMemoryLimit of string
    on Linux/x86_64 where we'd get a 'Stack_overflow' exception.
    See:
    - https://discuss.ocaml.org/t/is-there-any-value-in-having-a-maximum-stack-size/8214/10
-   - https://github.com/returntocorp/semgrep/issues/3640
+   - https://github.com/semgrep/semgrep/issues/3640
 
    The limits set by this function call should be lower than the system
    limits so as to get exceptions instead of segfaults.
diff --git a/libs/spacegrep/src/bin/Spacecat_main.ml b/libs/spacegrep/src/bin/Spacecat_main.ml
index 85d7ffbb0298..fc69140df617 100644
--- a/libs/spacegrep/src/bin/Spacecat_main.ml
+++ b/libs/spacegrep/src/bin/Spacecat_main.ml
@@ -64,10 +64,9 @@ let man =
       "Read an arbitrary program from stdin and print the parse tree\n\
       \      to stdout.";
     `S Manpage.s_authors;
-    `P "Martin Jambon <martin@returntocorp.com>";
+    `P "Martin Jambon <martin@semgrep.com>";
     `S Manpage.s_bugs;
-    `P
-      "Check out bug reports at https://github.com/returntocorp/semgrep/issues.";
+    `P "Check out bug reports at https://github.com/semgrep/semgrep/issues.";
     `S Manpage.s_see_also;
     `P "spacegrep(1)";
   ]
diff --git a/libs/spacegrep/src/bin/Spacegrep_main.ml b/libs/spacegrep/src/bin/Spacegrep_main.ml
index eaadd3b3d795..c33fb70c9191 100644
--- a/libs/spacegrep/src/bin/Spacegrep_main.ml
+++ b/libs/spacegrep/src/bin/Spacegrep_main.ml
@@ -487,10 +487,9 @@ let man =
     `S Manpage.s_description;
     `P "Match a pattern against an arbitrary program read from stdin.";
     `S Manpage.s_authors;
-    `P "Martin Jambon <martin@returntocorp.com>";
+    `P "Martin Jambon <martin@semgrep.com>";
     `S Manpage.s_bugs;
-    `P
-      "Check out bug reports at https://github.com/returntocorp/semgrep/issues.";
+    `P "Check out bug reports at https://github.com/semgrep/semgrep/issues.";
     `S Manpage.s_see_also;
     `P "semgrep, spacecat";
   ]
diff --git a/libs/spacegrep/src/lib/Pre_match.ml b/libs/spacegrep/src/lib/Pre_match.ml
index f40140d2ff16..2d3664a28548 100644
--- a/libs/spacegrep/src/lib/Pre_match.ml
+++ b/libs/spacegrep/src/lib/Pre_match.ml
@@ -6,7 +6,7 @@
    optimization that operates directly on the source files rather than the
    AST. It would be better since it turns out parsing is usually more
    expensive than matching, and that would allow skipping the parsing step.
-   See https://github.com/returntocorp/semgrep/issues/3461
+   See https://github.com/semgrep/semgrep/issues/3461
 *)
 
 (*
diff --git a/libs/tracing/Tracing.mli b/libs/tracing/Tracing.mli
index 3a3d6d889fb4..f0466bfcf8a3 100644
--- a/libs/tracing/Tracing.mli
+++ b/libs/tracing/Tracing.mli
@@ -34,6 +34,15 @@ type config = {
 
 type user_data = Trace_core.user_data
 
+(*****************************************************************************)
+(* Constants *)
+(*****************************************************************************)
+module Attributes : sig
+  val version : string
+  val instance_id : string
+  val deployment_environment_name : string
+end
+
 (*****************************************************************************)
 (* Levels *)
 (*****************************************************************************)
@@ -136,20 +145,13 @@ val trace_data_only :
 (*****************************************************************************)
 
 val configure_tracing :
-  ?attrs:(string * user_data) list ->
-  ?env:string ->
-  ?version:string ->
-  string ->
-  Uri.t ->
-  unit
-(** [configure_tracing ~env:"prod" ~version:"v1.0.0" service_name
-    tracing_endpoint] Before instrumenting anything, configure some settings.
-    This should only be run once in a program, because it creates a backend with
-    threads, HTTP connections, etc. when called. [~env] is the environment (e.g.
-    production, staging, development). [~version] is the version of the service.
-    [service_name] is the name of the service. [~attrs] can be used to set
-    additional global attributes, which are tags that will be applied to all
-    outgoing traces/metrics/logs etc.
+  ?attrs:(string * user_data) list -> string -> Uri.t -> unit
+(** [configure_tracing service_name tracing_endpoint] Before instrumenting
+    anything, configure some settings. This should only be run once in a
+    program, because it creates a backend with threads, HTTP connections, etc.
+    when called. [service_name] is the name of the service. [~attrs] can be used
+    to set additional global attributes (such as ["service.version"]), which are
+    tags that will be applied to all outgoing traces/metrics/logs etc.
 
     NOTE: this will set the active trace endpoint to
     whatever is passed. This endpoint will be used when restarting tracing via
diff --git a/libs/tracing/js/Tracing.ml b/libs/tracing/js/Tracing.ml
index 0ce8e0a63a5e..7207fefcc29f 100644
--- a/libs/tracing/js/Tracing.ml
+++ b/libs/tracing/js/Tracing.ml
@@ -36,6 +36,14 @@ type config = {
 }
 [@@deriving show]
 
+(*****************************************************************************)
+(* Constants *)
+(*****************************************************************************)
+module Attributes = struct
+  let version = "version"
+  let instance_id = "instance_id"
+  let deployment_environment_name = "deployment.environment.name"
+end
 (*****************************************************************************)
 (* Levels *)
 (*****************************************************************************)
@@ -94,11 +102,9 @@ let otel_reporter : Logs.reporter = Logs.nop_reporter
 let stop_tracing ~exit_active_spans:_ () = ()
 let restart_tracing () = ()
 
-let configure_tracing ?(attrs = []) ?(env = "") ?(version = "")
-    (_service_name : string) (_endpoint : Uri.t) =
+let configure_tracing ?(attrs = []) (_service_name : string) (_endpoint : Uri.t)
+    =
   ignore attrs;
-  ignore env;
-  ignore version;
   ()
 
 let with_tracing (_fname : string)
diff --git a/libs/tracing/unix/Tracing.ml b/libs/tracing/unix/Tracing.ml
index 2f495145e880..4bf5c2dd2469 100644
--- a/libs/tracing/unix/Tracing.ml
+++ b/libs/tracing/unix/Tracing.ml
@@ -97,6 +97,15 @@ let trace_level_var = "SEMGREP_TRACE_LEVEL"
 let parent_span_id_var = "SEMGREP_TRACE_PARENT_SPAN_ID"
 let parent_trace_id_var = "SEMGREP_TRACE_PARENT_TRACE_ID"
 
+(* Service related attributes *)
+module Attributes = struct
+  open Opentelemetry.Conventions
+
+  let version = Attributes.Service.version
+  let instance_id = Attributes.Service.instance_id
+  let deployment_environment_name = "deployment.environment.name"
+end
+
 (*****************************************************************************)
 (* Helpers *)
 (*****************************************************************************)
@@ -267,6 +276,10 @@ let otel_reporter : Logs.reporter =
   in
   { Logs.report }
 
+(*****************************************************************************)
+(* Metrics *)
+(*****************************************************************************)
+
 (*****************************************************************************)
 (* Span/Event entrypoints *)
 (*****************************************************************************)
@@ -440,15 +453,11 @@ let setup_otel trace_endpoint =
     Opentelemetry_trace.setup ()
 
 (* Set according to README of https://github.com/imandra-ai/ocaml-opentelemetry/ *)
-let configure_tracing ?(attrs : (string * user_data) list = []) ?(env = "prod")
-    ?version service_name trace_endpoint =
+let configure_tracing ?(attrs : (string * user_data) list = []) service_name
+    trace_endpoint =
   Otel.Globals.service_name := service_name;
   Otel.Globals.default_span_kind := Otel.Span.Span_kind_internal;
-  version
-  |> Option.iter (fun version ->
-         add_global_attribute Otel.Conventions.Attributes.Service.version
-           (`String version));
-  add_global_attribute "deployment.environment.name" (`String env);
+  let attrs = attrs @ Otel.GC_metrics.get_runtime_attributes () in
   List.iter
     (fun (key, value) -> Otel.Globals.add_global_attribute key value)
     attrs;
diff --git a/scripts/build-semgrep-proprietary b/scripts/build-semgrep-proprietary
deleted file mode 100755
index cafa9853d777..000000000000
--- a/scripts/build-semgrep-proprietary
+++ /dev/null
@@ -1,58 +0,0 @@
-#! /usr/bin/env bash
-#
-# CI job that checks whether semgrep-proprietary can compile using the
-# current semgrep commit as a submodule.
-#
-set -eu
-
-# Try to not print the GitHub access token since it's a secret.
-access=""
-if [[ -n "${GITHUB_TOKEN+x}" ]]; then
-  echo "Using GITHUB_TOKEN from environment."
-  access="$GITHUB_TOKEN:@"
-fi
-
-semgrep_pro_url="https://${access}github.com/returntocorp/semgrep-proprietary.git"
-semgrep_root=$(git rev-parse --show-toplevel)
-semgrep_commit=$(git rev-parse --short=7 HEAD)
-
-cat <<EOF
-We're attempting to build semgrep-proprietary using the current version
-of semgrep. We're assuming we're in a folder within the semgrep repo.
-
-Here's what we found about semgrep:
-
-semgrep repo root: $semgrep_root
-semgrep commit: $semgrep_commit
-semgrep-proprietary remote URL: <hidden>
-semgrep git status:
-$(cd "$semgrep_root" && git status)
-EOF
-
-# Use a folder where it's safe to delete any existing semgrep-proprietary.
-workdir=$semgrep_root/../tmp-semgrep-pro
-mkdir -p "$workdir"
-(
-  cd "$workdir"
-  rm -rf semgrep-proprietary
-  git clone "$semgrep_pro_url" --depth 1
-  (
-    cd semgrep-proprietary
-    echo "semgrep-proprietary work folder: $(pwd)"
-    echo "Git remote:"
-    git remote -v
-
-    # Fetch all submodules (although we don't need the semgrep submodule)
-    git submodule update --init --recursive --depth 1
-
-    # Replace the semgrep submodule
-    rm -rf semgrep
-    ln -s "$semgrep_root" semgrep
-
-    # Build semgrep-proprietary
-    make
-
-    # Run the tests
-    make test
-  )
-)
diff --git a/scripts/compare.py b/scripts/compare.py
index 707769489a42..fe3ba7f6bb87 100755
--- a/scripts/compare.py
+++ b/scripts/compare.py
@@ -66,7 +66,7 @@ def docker_cmd(use_podman: bool, dir: str, version: str) -> Sequence[str]:
             *(["--security-opt", "label=disable"] if use_podman else []),
             "-v",
             f"{dir}:/src",
-            f"returntocorp/semgrep:{version}",
+            f"semgrep/semgrep:{version}",
             "semgrep",
             "scan",
             "--config",
diff --git a/scripts/release/get_semgrep_runs_zip.py b/scripts/release/get_semgrep_runs_zip.py
deleted file mode 100644
index a4c682d59fcd..000000000000
--- a/scripts/release/get_semgrep_runs_zip.py
+++ /dev/null
@@ -1,73 +0,0 @@
-import subprocess
-from typing import Any
-from typing import Mapping
-
-import click
-import requests
-
-API_ROOT = "https://api.github.com/repos/returntocorp/semgrep-rules"
-ACCEPT = "application/vnd.github.v3+json"
-HEADERS = {"accept": ACCEPT}
-TIMEOUT = 30
-ZIP_LOC = "/tmp/semgrep_runs_output.zip"
-FILE_LOC = "/tmp/semgrep_runs_output.tar.gz"
-
-JsonObject = Mapping[str, Any]
-
-
-def err(content: str, **kwargs: Any) -> None:
-    click.secho(content, err=True, **kwargs)
-
-
-def _gh_get(path: str) -> JsonObject:
-    r = requests.get(f"{API_ROOT}/{path}", headers=HEADERS, timeout=TIMEOUT)
-    r.raise_for_status()
-    return r.json()
-
-
-def _get_last_rules_commit() -> str:
-    res = _gh_get("branches/develop")
-    return res["commit"]["sha"]
-
-
-def _get_action_run() -> JsonObject:
-    res = _gh_get(f"actions/runs?branch=develop&event=push")
-    # Runs are in reverse time order, so this gets the most recent run
-    return next(r for r in res["workflow_runs"] if r["name"] == "semgrep-rules-test")
-
-
-def _get_artifact_url(run_id: int) -> str:
-    res = _gh_get(f"actions/runs/{run_id}/artifacts")
-    return next(a for a in res["artifacts"] if a["name"] == "semgrep_runs")[
-        "archive_download_url"
-    ]
-
-
-def _get_runs_artifact(url: str, access_token: str) -> None:
-    res = requests.get(
-        url, headers={"Authorization": f"bearer {access_token}"}, timeout=30
-    )
-    res.raise_for_status()
-    with open(ZIP_LOC, "wb") as fd:
-        fd.write(res.content)
-
-
-def _unzip_artifact() -> None:
-    subprocess.run(["unzip", ZIP_LOC], cwd="/tmp")
-
-
-@click.command()
-@click.argument("access_token")
-def main(access_token: str):
-    run_id: int = _get_action_run()["id"]
-    url: str = _get_artifact_url(run_id)
-    err(f"Downloading {url}")
-    _get_runs_artifact(url, access_token)
-    err(f"Download successful")
-    err(f"Unzipping archive")
-    _unzip_artifact()
-    err(f"Done; artifact is at {FILE_LOC}")
-
-
-if __name__ == "__main__":
-    main()
diff --git a/scripts/release/towncrier.toml b/scripts/release/towncrier.toml
index 78132c945b93..3bdf1d673318 100644
--- a/scripts/release/towncrier.toml
+++ b/scripts/release/towncrier.toml
@@ -2,7 +2,7 @@
     directory = "../../changelog.d"
     filename = "../../CHANGELOG.md"
     name = "semgrep"
-    title_format = "## [{version}](https://github.com/returntocorp/semgrep/releases/tag/v{version}) - {project_date}"
+    title_format = "## [{version}](https://github.com/semgrep/semgrep/releases/tag/v{version}) - {project_date}"
     start_string = "<!-- insertion point -->\n"
     underlines = ["", ""]
     [[tool.towncrier.type]]
diff --git a/scripts/validate-docker-release.sh b/scripts/validate-docker-release.sh
index 6464f00d0a74..2c4c05434474 100755
--- a/scripts/validate-docker-release.sh
+++ b/scripts/validate-docker-release.sh
@@ -10,7 +10,7 @@ docker_tag="${version/v/}"
 echo "Validating release with docker tag: $docker_tag"
 
 echo "if 1 == 1: pass" \
-    | docker run -i returntocorp/semgrep:"$docker_tag" semgrep -l python -e '$X == $X' - \
+    | docker run -i semgrep/semgrep:"$docker_tag" semgrep -l python -e '$X == $X' - \
     | grep -q "1 == 1"
 
 echo "Docker image OK!"
diff --git a/scripts/validate-wheel.sh b/scripts/validate-wheel.sh
index 202e55c10b52..7e5d76a3d9cc 100755
--- a/scripts/validate-wheel.sh
+++ b/scripts/validate-wheel.sh
@@ -8,12 +8,12 @@ if [ -z "$1" ]; then
     exit 1
 fi
 
-# temporary workaround for https://github.com/returntocorp/semgrep/pull/8336/commits/565ba8443bd1ca4f2cd18fabb6ee61af971c5dda,
+# temporary workaround for https://github.com/semgrep/semgrep/pull/8336/commits/565ba8443bd1ca4f2cd18fabb6ee61af971c5dda,
 # which forces amd64 builds to use the `any` platform compatibility tag and
 # causes the glob that we're calling this script with not match anything
 # this will be fixed in the near future as we factor python wheel building out
 # of the main Dockerfile and update amd64 to use the same approach as arm64
-# (see: https://github.com/returntocorp/semgrep/pull/8371)
+# (see: https://github.com/semgrep/semgrep/pull/8371)
 if [ ! -f "$1" ]; then
     echo "not a file: $1"
     exit 0
diff --git a/semgrep.opam b/semgrep.opam
index 219669baaa62..d023a5e11c1d 100644
--- a/semgrep.opam
+++ b/semgrep.opam
@@ -1,6 +1,6 @@
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
-version: "1.97.0"
+version: "1.98.0"
 synopsis:
   "Like grep but for code: fast and syntax-aware semantic code pattern for many languages"
 description: """
diff --git a/src/core/Core_error.ml b/src/core/Core_error.ml
index 68dda19c3261..8df6f06a5c40 100644
--- a/src/core/Core_error.ml
+++ b/src/core/Core_error.ml
@@ -79,7 +79,7 @@ end)
 
 let please_file_issue_text =
   "An error occurred while invoking the Semgrep engine. Please help us fix \
-   this by creating an issue at https://github.com/returntocorp/semgrep"
+   this by creating an issue at https://github.com/semgrep/semgrep"
 
 let mk_error ?rule_id ?(msg = "") ?(loc : Tok.location option)
     (err : Out.error_type) : t =
diff --git a/src/core/Core_match.ml b/src/core/Core_match.ml
index f4608f4dd91b..ec46151808fd 100644
--- a/src/core/Core_match.ml
+++ b/src/core/Core_match.ml
@@ -16,9 +16,9 @@
 (*****************************************************************************)
 (* Prelude *)
 (*****************************************************************************)
-(* Type to represent a semgrep "match" (a.k.a. a finding).
+(* Type to represent a semgrep "match" (a.k.a. finding).
  *
- * Note that the "core" match are translated at some point in
+ * Note that the "core" matches are translated at some point in
  * Semgrep_output_v1.core_match, then processed in pysemgrep (or osemgrep)
  * and translated again in Semgrep_output_v1.cli_match, and
  * translated even further by pysemgrep (or osemgrep) ci in
@@ -53,6 +53,18 @@ type t = {
    * as separate matches? or better make them equal for dedup purpose?
    *)
   engine_of_match : Engine_kind.engine_of_finding; [@equal fun _a _b -> true]
+  (* metavars for the pattern match *)
+  env : Metavariable.bindings;
+      [@equal
+        fun a b ->
+          List.equal
+            (fun (s1, m1) (s2, m2) ->
+              (* See the comment in Metavariable.mli for location_aware_equal_mvalue,
+                 but basically we would like to consider matches different if they
+                 metavariables bound to the same content, but at different locations.
+              *)
+              s1 = s2 && Metavariable.location_aware_equal_mvalue m1 m2)
+            a b]
   (* location info *)
   path : Target.path;
   (* less: redundant with location? *)
@@ -75,18 +87,6 @@ type t = {
   ast_node : AST_generic.any option;
   (* less: do we need to be lazy? *)
   tokens : Tok.t list Lazy.t; [@equal fun _a _b -> true]
-  (* metavars for the pattern match *)
-  env : Metavariable.bindings;
-      [@equal
-        fun a b ->
-          List.equal
-            (fun (s1, m1) (s2, m2) ->
-              (* See the comment in Metavariable.mli for location_aware_equal_mvalue,
-                 but basically we would like to consider matches different if they
-                 metavariables bound to the same content, but at different locations.
-              *)
-              s1 = s2 && Metavariable.location_aware_equal_mvalue m1 m2)
-            a b]
   (* Lazy since construction involves forcing lazy token lists. *)
   (* We used to have `[@equal fun _a _b -> true]` here, but this causes issues with
      multiple findings to the same sink (but different sources) being removed
@@ -94,7 +94,9 @@ type t = {
      We now rely on equality of taint traces, which in turn relies on equality of `Parse_info.t`.
   *)
   taint_trace : Taint_trace.t Lazy.t option; (* secrets stuff *)
-  (* Indicates whether a postprocessor ran and validated this result. *)
+  (* SCA extra info about a match (e.g., the satisfied version constraint) *)
+  sca_match : SCA_match.t option;
+  (* Secrets. Indicates whether a postprocessor ran and validated this result. *)
   validation_state : Rule.validation_state;
   (* Indicates if the rule default severity should be modified to a different
      severity. Currently this is just used by secrets validators in order to
@@ -109,7 +111,6 @@ type t = {
      the override is applied on top of the default and only changes the fields
      present in the override. *)
   metadata_override : JSON.t option;
-  dependency : SCA_match.kind option;
   (* A field to be populated based on intra-formula `fix` keys.
      This is _prior_ to AST-based autofix and interpolation, which occurs in
      Autofix.ml.
diff --git a/src/core/Core_match.mli b/src/core/Core_match.mli
index 167c7c0d61e3..c67489b2f162 100644
--- a/src/core/Core_match.mli
+++ b/src/core/Core_match.mli
@@ -3,19 +3,20 @@ type t = {
   (* rule (or mini rule) responsible for the pattern match found *)
   rule_id : rule_id;
   engine_of_match : Engine_kind.engine_of_finding;
+  env : Metavariable.bindings;
   (* location info *)
   path : Target.path;
   range_loc : Tok.location * Tok.location;
   ast_node : AST_generic.any option;
   tokens : Tok.t list Lazy.t;
-  env : Metavariable.bindings;
   (* trace *)
   taint_trace : Taint_trace.t Lazy.t option;
-  (* for secrets *)
+  (* for SCA *)
+  sca_match : SCA_match.t option;
+  (* for Secrets *)
   validation_state : Rule.validation_state;
   severity_override : Rule.severity option;
   metadata_override : JSON.t option;
-  dependency : SCA_match.kind option;
   (* A field to be populated based on intra-formula `fix` keys.
      This is _prior_ to AST-based autofix and interpolation, which occurs in
      Autofix.ml.
diff --git a/src/core/SCA_match.ml b/src/core/SCA_match.ml
index fda91aa3e672..3dafe6c6ddce 100644
--- a/src/core/SCA_match.ml
+++ b/src/core/SCA_match.ml
@@ -1,13 +1,18 @@
-type t = SCA_dependency.t * SCA_pattern.t [@@deriving show, eq]
+type t = {
+  (* the actual dependency in the lockfile *)
+  dep : SCA_dependency.t;
+  (* the version constraint on a package and its ecosystem *)
+  pat : SCA_pattern.t;
+  kind : kind;
+}
 
-(* alt: use a record as both constructors have the same type *)
-type kind =
+and kind =
   (* Rule had both code patterns and dependency patterns, got matches on *both*,
    * the Pattern Match is in code, annotated with this dependency match *)
-  | CodeAndLockfileMatch of t
+  | CodeAndLockfileMatch
   (* Rule had dependency patterns, they matched, the Pattern Match is in a
    * lockfile. So the range_loc of the Dependency.t in this dependency_match
    * should be *the same* as the range_loc in the PatternMatch.t
    *)
-  | LockfileOnlyMatch of t
+  | LockfileOnlyMatch
 [@@deriving show, eq]
diff --git a/src/core/Trace_data.ml b/src/core/Trace_data.ml
index 75e24c06481d..303c0b3f1200 100644
--- a/src/core/Trace_data.ml
+++ b/src/core/Trace_data.ml
@@ -12,12 +12,81 @@
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the file
  * LICENSE for more details.
  *)
-
 (*****************************************************************************)
 (* Prelude *)
 (*****************************************************************************)
-(* Helpers to prepare data for Opentelemetry tracing *)
+(* NOTE: [^0] is a footnote *)
+(* Helpers to prepare attributes for Opentelemetry. Attributes[^0] are key-value
+   pairs that are used for sorting and filtering telemetry data, and also for
+   attaching info that may be relevant to the telemetry data. This module helps
+   organize these attributes, and provides some attributes that we always want
+   to set.
+
+   In general, there are two places we can add "attributes" to Opentelemetry
+   data. There are "resource" attributes, where a resource[^1] is basically a
+   service or program (think a rest API, a database, semgrep itself) that is
+   emitting telemetry. Resource attributes usually help categorize and describe
+   what's emitting telemetry. Examples:
+   * Version
+   * Deployment environment (develop, staging, production...)
+   * Name of the deployment (semgrep, semgrep-app...)
+   * Runtime version (OCaml 4.15, OCaml 5.0...)
+   * How the resource was executed (# of jobs, cli flags passed, )
+   * Commonly defined resource attributes: https://opentelemetry.io/docs/specs/semconv/resource/
+
+   Some of these resourcs are handled in a special way by opentelemetry[^2], or
+   by tools that ingest opentelemetry data, like prometheus[^3] and datadog[4].
+   That means we have to be careful what we set as resource attributes, as it
+   can break alarms/monitors, dashboards, or tools as a whole altogether.
+   There's a lot of rules of what attributes are used where, so if you are
+   setting an attribute that's handles in a special way, please review the
+   linked docs. These attributes are set in the ocaml otel sdk by setting the
+   `global attributes` (this is a misnomer and not according to spec, see issue
+   here[^5]). In general these attributes should be information that is immutable
+   once the program starts.
+
+   The other kind of attributes are for any kind of opentelemetry event (traces,
+   logs, metrics), and are used to describe said event, or attach relevant info.
+   Examples:
+   * Line/file a log was recorded
+   * Stacktrace of an error in a trace
+   * Status of a trace (success, error)
+   * Args of a function being traced
+   * Category of metric being recorded (whether a file was scanned succesfully
+     or not)
+
+   Like resource attributes, there are commonly defined resource attributes[^5].
+   These usually don't have any special handling.
+
+   footnotes:
+   [^0] https://opentelemetry.io/docs/specs/otel/common/#attribute
+   [^1] https://opentelemetry.io/docs/specs/otel/resource/sdk/
+   [^2] https://opentelemetry.io/docs/specs/semconv/resource/#attributes-with-special-handling
+   [^3] https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes-1
+   [^4] https://docs.datadoghq.com/opentelemetry/schema_semantics/
+   [^5] https://opentelemetry.io/docs/specs/semconv/
+*)
+
+(*****************************************************************************)
+(* Constants *)
+(*****************************************************************************)
 
+(* Only add Semgrep specific attributes here, the rest should go in Tracing.ml
+   (like ocaml runtime version, if we're in a container etc.) *)
+module Attributes = struct
+  (* Scan related attrs *)
+  let semgrep_managed_scan = "scan.semgrep_managed_scan"
+  let engine = "scan.engine"
+  let repo_name = "scan.repo_name"
+  let jobs = "scan.jobs"
+  let job = "scan.parmap_job"
+  let folder = "scan.folder"
+  let pro_secrets_validators = "scan.pro_secrets_validators"
+  let pro_historical_scanning = "scan.pro_historical_scanning"
+  let pro_deep_intrafile = "scan.pro_deep_intrafile"
+  let pro_deep_interfile = "scan.pro_deep_interfile"
+  let pro_secrets_allowed_origins = "scan.pro_secrets_allowed_origins"
+end
 (*****************************************************************************)
 (* Types *)
 (*****************************************************************************)
@@ -40,24 +109,89 @@ type analysis_flags = {
 let allowed_origins allow_all_origins =
   if allow_all_origins then "all_origins" else "pro_rules_only"
 
-(* Poor man's Git repo detection. Running git repo detection again
-   seems wasteful, but checking two env vars is pretty cheap.
-
-   TODO the more we port of semgrep scan and semgrep ci, the more
-   of this information will already be in OCaml *)
-let repo_name () =
-  match Sys.getenv_opt "SEMGREP_REPO_DISPLAY_NAME" with
-  | Some name -> name
-  | None -> (
-      match Sys.getenv_opt "SEMGREP_REPO_NAME" with
-      | Some name -> name
-      | None -> "<local run>")
+let get_env_vars =
+  (* just get the first env var that is set in a list of env vars *)
+  let get_first_env_var env_vars : string option =
+    try
+      match env_vars |> List_.map Sys.getenv_opt |> List_.filter_some with
+      | hd :: _ -> Some hd
+      | [] -> None
+      (* any Sys.* function can raise Sys_error :( *)
+    with
+    | Sys_error e ->
+        (* We probably want to see this error since it'd be really weird if it
+           happened *)
+        (* nosemgrep *)
+        Logs.err (fun m ->
+            m
+              "System error reading an environment variable for tracing data: \
+               %s"
+              e);
+        None
+  in
+  let map_env_var_to_otel_data name type_ env_vars default :
+      string * Trace_core.user_data =
+    let user_data_val_of_string x =
+      let v_opt =
+        match type_ with
+        | `Int -> int_of_string_opt x |> Option.map (fun x -> `Int x)
+        | `String -> Some (`String x)
+        | `Bool -> Some (`Bool (x = "true" || x = "1" || x = "yes"))
+        | `Float -> float_of_string_opt x |> Option.map (fun x -> `Float x)
+        | `None -> None
+      in
+      Option.value v_opt ~default
+    in
+    let user_data =
+      get_first_env_var env_vars
+      |> Option.fold ~none:default ~some:user_data_val_of_string
+    in
+    (name, user_data)
+  in
+  List_.map (fun (name, type_, env_vars, default) ->
+      map_env_var_to_otel_data name type_ env_vars default)
 
 (* In case we don't have a repo name, report the base folder where
    semgrep was run. We report only the base name to avoid leaking
    user information they may not have expected us to include. *)
 let current_working_folder () = Filename.basename (Sys.getcwd ())
 
+(*****************************************************************************)
+(* Defaults *)
+(*****************************************************************************)
+(* Format:
+   (name, type, env_vars, default_value)
+   where we pick the first env var that is set, and if none are set we use the
+   default value
+*)
+
+(* Resource attributes we always want to try and set from the environment *)
+let default_resource_env_attrs =
+  [
+    (* coupling: semgrep/meta.py, if you change this we may want to change
+       something about job url there, or vice versa *)
+    (* Instance of the semgrep `service` *)
+    ( Tracing.Attributes.instance_id,
+      `String,
+      [ "SEMGREP_JOB_URL"; "CI_JOB_URL" ],
+      `None );
+    (* coupling: semgrep/meta.py, if you change this we may want to change
+       something about job url there, or vice versa *)
+    ( Attributes.semgrep_managed_scan,
+      `Bool,
+      [ "SEMGREP_MANAGED_SCAN" ],
+      `Bool false );
+    (* Poor man's Git repo detection. Running git repo detection again
+       seems wasteful, but checking two env vars is pretty cheap.
+
+       TODO the more we port of semgrep scan and semgrep ci, the more
+       of this information will already be in OCaml *)
+    ( Attributes.repo_name,
+      `String,
+      [ "SEMGREP_REPO_DISPLAY_NAME"; "SEMGREP_REPO_NAME" ],
+      `String "<local run>" );
+  ]
+
 (*****************************************************************************)
 (* Shortcuts for Otel tracing *)
 (*****************************************************************************)
@@ -74,21 +208,34 @@ let no_analysis_features () =
 let data_of_languages (languages : Xlang.t list) =
   languages |> List_.map (fun l -> (Xlang.to_string l, `Bool true))
 
-let get_top_level_data jobs version analysis_flags =
+(* NOTE: If this IS NOT semgrep specific stick it in Tracing.ml *)
+(* WARNING: Let's be careful what we add as a resource attribute. TL;DR; these
+   are used in different ways by the tools that ingest otel data , and certain
+   types of data can have different performance and cost implications for these
+   tools. See module commentary for more info
+*)
+let get_resource_attrs ?(env = "prod") ~engine ~analysis_flags ~jobs () =
   [
-    ("version", `String version);
-    ("jobs", `Int jobs);
-    ("folder", `String (current_working_folder ()));
-    ("repo_name", `String (repo_name ()));
-    ("pro_secrets_validators", `Bool analysis_flags.secrets_validators);
-    ("pro_historical_scanning", `Bool analysis_flags.historical_scan);
-    ("pro_deep_intrafile", `Bool analysis_flags.deep_intra_file);
-    ("pro_deep_interfile", `Bool analysis_flags.deep_inter_file);
+    (* Version of Semgrep *)
+    (Tracing.Attributes.version, `String Version.version);
+    (* Whether we're running in a production, staging, or develop environment
+       (Usually maps to SMS prod,staging,dev2) *)
+    (Tracing.Attributes.deployment_environment_name, `String env);
+    (Attributes.engine, `String engine);
+    (Attributes.jobs, `Int jobs);
+    (Attributes.folder, `String (current_working_folder ()));
+    (Attributes.pro_secrets_validators, `Bool analysis_flags.secrets_validators);
+    (Attributes.pro_historical_scanning, `Bool analysis_flags.historical_scan);
+    (Attributes.pro_deep_intrafile, `Bool analysis_flags.deep_intra_file);
+    (Attributes.pro_deep_interfile, `Bool analysis_flags.deep_inter_file);
+    (* TODO it would be nice if we also got how the process was executed, and
+       with what config/flags *)
   ]
+  @ get_env_vars default_resource_env_attrs
   @
   if analysis_flags.secrets_validators then
     [
-      ( "pro_secrets_allowed_origins",
+      ( Attributes.pro_secrets_allowed_origins,
         `String (allowed_origins analysis_flags.allow_all_origins) );
     ]
   else []
diff --git a/src/core/Trace_data.mli b/src/core/Trace_data.mli
index 963003ff50fe..efe40c2c82cc 100644
--- a/src/core/Trace_data.mli
+++ b/src/core/Trace_data.mli
@@ -16,6 +16,21 @@ type analysis_flags = {
 }
 [@@derving show]
 
+(* constants *)
+module Attributes : sig
+  val semgrep_managed_scan : string
+  val engine : string
+  val repo_name : string
+  val jobs : string
+  val job : string
+  val folder : string
+  val pro_secrets_validators : string
+  val pro_historical_scanning : string
+  val pro_deep_intrafile : string
+  val pro_deep_interfile : string
+  val pro_secrets_allowed_origins : string
+end
+
 (* Helpers *)
 
 val no_analysis_features : unit -> analysis_flags
@@ -24,7 +39,28 @@ val no_analysis_features : unit -> analysis_flags
 val data_of_languages : Xlang.t list -> (string * Tracing.user_data) list
 (** Convenience function to turn a list of interfile languages into otel data *)
 
-val get_top_level_data :
-  int -> string -> analysis_flags -> (string * Tracing.user_data) list
-(** Create the tags for the top level span. These tags make it easy to see
-    the traces we care about *)
+val get_resource_attrs :
+  ?env:string ->
+  engine:string ->
+  analysis_flags:analysis_flags ->
+  jobs:int ->
+  unit ->
+  (string * Tracing.user_data) list
+(** [get_resource_data ~engine:"oss" ~env:"prod" ~analysis_flags () ] creates
+    tags for the resource we report traces to. This is essentially info about
+    the "service" itself, that is immutable once the service/program starts.
+    This data is usually useful for grouping large sets of
+    logs/traces/errors/metrics and discovering or investigating other macro
+    trends about Semgrep. Example: Service Version, OCaml runtime version,
+    telemetry sdk version. See module commentary for more info
+
+    Other data besides what's passed in as flags to this function may be
+    gathered from the environment such as Semgrep's version number.
+
+    [engine] is the engine we are using, e.g. "oss" or "pro"
+
+    [env] is the environment we are working in ("prod","dev2" etc.). Defaults to
+    "prod"
+
+    [analysis_flags] see {!analysis_flags}
+  *)
diff --git a/src/core_cli/Core_CLI.ml b/src/core_cli/Core_CLI.ml
index f9545d9fdc6d..70b2c82cae4e 100644
--- a/src/core_cli/Core_CLI.ml
+++ b/src/core_cli/Core_CLI.ml
@@ -834,7 +834,8 @@ let main_exn (caps : Cap.all_caps) (argv : string array) : unit =
           let target_source : Core_scan_config.target_source =
             match (!target_file, !lang, roots) with
             | Some file, None, [] -> Target_file file
-            | None, Some lang, [ file ] when UFile.is_file file ->
+            | None, Some lang, [ file ]
+              when UFile.is_reg ~follow_symlinks:true file ->
                 Targets [ Target.mk_target lang file ]
             | _ ->
                 (* alt: use the file targeting in targets_of_config_DEPRECATED
@@ -858,17 +859,17 @@ let main_exn (caps : Cap.all_caps) (argv : string array) : unit =
           match config.tracing with
           | None -> run caps config
           | Some tracing ->
-              let trace_data =
-                Trace_data.get_top_level_data config.ncores Version.version
-                  (Trace_data.no_analysis_features ())
+              let resource_attrs =
+                (* Let's make sure all traces/logs/metrics etc. are tagged as
+                   coming from the OSS invocation *)
+                Trace_data.get_resource_attrs ?env:tracing.env ~engine:"oss"
+                  ~analysis_flags:(Trace_data.no_analysis_features ())
+                  ~jobs:config.ncores ()
               in
-              Tracing.configure_tracing
-              (* Let's make sure all traces/logs/metrics etc. are tagged as coming from the pro invocation *)
-                ~attrs:[ ("engine", `String "oss") ]
-                ?env:tracing.env ~version:Version.version "semgrep-core"
+              Tracing.configure_tracing ~attrs:resource_attrs "semgrep-core"
                 tracing.endpoint;
-              Tracing.with_tracing "Core_command.semgrep_core_dispatch"
-                trace_data (fun span_id ->
+              Tracing.with_tracing "Core_command.semgrep_core_dispatch" []
+                (fun span_id ->
                   let tracing =
                     { tracing with top_level_span = Some span_id }
                   in
diff --git a/src/core_scan/Core_scan.ml b/src/core_scan/Core_scan.ml
index e2b4644a47f4..6fc899f72856 100644
--- a/src/core_scan/Core_scan.ml
+++ b/src/core_scan/Core_scan.ml
@@ -724,8 +724,8 @@ let rules_for_target ~analyzer ~products ~origin ~respect_rule_paths rules =
 
 let lockfile_xtarget_resolve (manifest : Manifest.t option)
     (lockfile : Lockfile.t) : Lockfile_xtarget.t =
-  Lockfile_xtarget.resolve Parse_lockfile.parse_manifest
-    Parse_lockfile.parse_lockfile lockfile manifest
+  Lockfile_xtarget.resolve Parse_lockfile.parse_manifest Parse_lockfile.parse
+    lockfile manifest
 
 let rules_for_lockfile_kind ~lockfile_kind rules =
   rules
@@ -840,7 +840,7 @@ let mk_target_handler (caps : < Cap.time_limit >) (config : Core_scan_config.t)
               caps;
             }
       in
-      let matches =
+      let matches : Core_result.matches_single_file =
         (* !!Calling Match_rules!! Calling the matching engine!! *)
         Match_rules.check ~match_hook ~timeout ~dependency_match_table xconf
           rules xtarget
diff --git a/src/core_scan/Parmap_targets.ml b/src/core_scan/Parmap_targets.ml
index 9b2d58871c89..2d286afd9192 100644
--- a/src/core_scan/Parmap_targets.ml
+++ b/src/core_scan/Parmap_targets.ml
@@ -86,7 +86,7 @@ let core_error_of_path_exc (internal_path : Fpath.t) (e : Exception.t) :
 let init job =
   (* Set a global attribute to the job number so we know when we look at
      traces/logs/metrics which job it came from! *)
-  Tracing.add_global_attribute "parmap.job" (`Int job);
+  Tracing.add_global_attribute Trace_data.Attributes.job (`Int job);
   (* Restart tracing as it is paused before forking below in both
      map_targets___* funcs *)
   (* NOTE: this only restarts tracing in the child *)
diff --git a/src/engine/Match_SCA_mode.ml b/src/engine/Match_SCA_mode.ml
index d72f86bc2943..cd920b837162 100644
--- a/src/engine/Match_SCA_mode.ml
+++ b/src/engine/Match_SCA_mode.ml
@@ -1,8 +1,40 @@
+(* Matthew McQuaid
+ *
+ * Copyright (C) 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.
+ *)
+
 module R = Rule
 module Out = Semgrep_output_v1_t
+module Log = Log_engine.Log
+
+(*****************************************************************************)
+(* Prelude *)
+(*****************************************************************************)
+(* SCA matching code for both parity rules (lockfile only findings)
+ * and reachable rules.
+ *)
+
+(*****************************************************************************)
+(* Types *)
+(*****************************************************************************)
 
 (* TODO? could move to SCA_match.ml *)
 type dependency_match_table = (Rule_ID.t, SCA_match.t list) Hashtbl.t
+
+(*****************************************************************************)
+(* Helpers *)
+(*****************************************************************************)
+
 type cmp = [ `EQ | `GT | `LT ]
 
 let compare_version_core (c1 : SCA_version.core) (c2 : SCA_version.core) =
@@ -33,15 +65,22 @@ let check_constraint SCA_pattern.{ version; op } v' =
 let match_dependency_pattern (deps : SCA_dependency.t list)
     (pat : SCA_pattern.t) : SCA_match.t list =
   deps
-  |> List_.filter_map @@ fun (dep : SCA_dependency.t) ->
-     if
-       String.equal dep.package_name pat.package_name
-       && pat.version_constraints |> fun (SCA_pattern.SCA_And cs) ->
-          List.for_all
-            (fun constr -> check_constraint constr dep.package_version)
-            cs
-     then Some (dep, pat)
-     else None
+  |> List_.filter_map (fun (dep : SCA_dependency.t) ->
+         if
+           String.equal dep.package_name pat.package_name
+           && pat.version_constraints |> fun (SCA_pattern.SCA_And cs) ->
+              cs
+              |> List.for_all (fun constr ->
+                     let res = check_constraint constr dep.package_version in
+                     Log.debug (fun m ->
+                         m "match for %s on %s = %b (package = %s)"
+                           (SCA_pattern.show_version_constraint constr)
+                           (SCA_version.show dep.package_version)
+                           res dep.package_name);
+                     res)
+           (* the kind can be adjusted later in annotate_pattern_match *)
+         then Some SCA_match.{ dep; pat; kind = LockfileOnlyMatch }
+         else None)
 
 (* Return the set of dependency/pattern pairs that matched *)
 let match_dependency_formula :
@@ -58,7 +97,13 @@ let match_dependencies lockfile_target rule =
 let match_all_dependencies lockfile_target =
   List_.map (fun rule -> (rule, match_dependencies lockfile_target rule))
 
-let check_rule rule (xtarget : Lockfile_xtarget.t) dependency_formula =
+(*****************************************************************************)
+(* Entry points *)
+(*****************************************************************************)
+
+let check_rule (rule : Rule.t) (xtarget : Lockfile_xtarget.t)
+    (dependency_formula : Rule.sca_dependency_formula) :
+    Core_profiling.rule_profiling Core_result.match_result =
   let _, parse_time =
     Common.with_time (fun () -> Lazy.force xtarget.lazy_dependencies)
   in
@@ -66,9 +111,11 @@ let check_rule rule (xtarget : Lockfile_xtarget.t) dependency_formula =
     Common.with_time (fun () ->
         match_dependency_formula xtarget dependency_formula)
   in
+  Log.info (fun m -> m "found %d lockfile matches" (List.length matches));
   let matches =
     matches
-    |> List_.map (fun ((dep, pat) : SCA_match.t) ->
+    |> List_.map (fun (sca_match : SCA_match.t) ->
+           let dep = sca_match.dep in
            Core_match.
              {
                rule_id =
@@ -95,7 +142,7 @@ let check_rule rule (xtarget : Lockfile_xtarget.t) dependency_formula =
                validation_state = `No_validator;
                severity_override = None;
                metadata_override = None;
-               dependency = Some (SCA_match.LockfileOnlyMatch (dep, pat));
+               sca_match = Some sca_match;
                fix_text = None;
                facts = [];
              })
@@ -107,31 +154,33 @@ let check_rule rule (xtarget : Lockfile_xtarget.t) dependency_formula =
       rule_id = fst rule.R.id;
     }
 
-let annotate_pattern_match dep_matches pm =
+let annotate_pattern_match (dep_matches : SCA_match.t list option)
+    (pm : Core_match.t) : Core_match.t list =
   match dep_matches with
   | None -> [ pm ]
   | Some dep_matches ->
-      (* If there are two, transitive copies of a library, and no direct copies, and it's used in code, we produce TWO reachable matches *)
+      (* If there are two, transitive copies of a library, and no direct copies,
+       * and it's used in code, we produce TWO reachable matches *)
       dep_matches
-      |> List_.filter_map (fun dm ->
+      |> List_.filter_map (fun (dm : SCA_match.t) ->
              (* TODO: Make this not quadratic
                 If the match is on a transitive dep and there's also a match on
-                a direct copy of the dep, then do not include it, only use the direct one
-                this is what the python code does
+                a direct copy of the dep, then do not include it, only use the
+                direct one this is what the python code does
              *)
              if
                SCA_dependency.(
-                 Out.equal_transitivity (fst dm).transitivity `Transitive)
+                 Out.equal_transitivity dm.dep.transitivity `Transitive)
                && dep_matches
-                  |> List.exists (fun (dep, _) ->
+                  |> List.exists (fun ({ dep; _ } : SCA_match.t) ->
                          SCA_dependency.(
                            Out.equal_transitivity dep.transitivity `Direct
-                           && String.equal dep.package_name
-                                (fst dm).package_name))
+                           && String.equal dep.package_name dm.dep.package_name))
              then None
              else
                Some
                  {
                    pm with
-                   Core_match.dependency = Some (CodeAndLockfileMatch dm);
+                   Core_match.sca_match =
+                     Some { dm with kind = SCA_match.CodeAndLockfileMatch };
                  })
diff --git a/src/engine/Match_SCA_mode.mli b/src/engine/Match_SCA_mode.mli
index 50de8298482c..88e7bedb87c6 100644
--- a/src/engine/Match_SCA_mode.mli
+++ b/src/engine/Match_SCA_mode.mli
@@ -14,7 +14,7 @@
    dependencies between matches, which would impede parallelism.
 *)
 val check_rule :
-  Rule.rule ->
+  Rule.t ->
   Lockfile_xtarget.t ->
   Rule.sca_dependency_formula ->
   Core_profiling.rule_profiling Core_result.match_result
diff --git a/src/engine/Match_rules.ml b/src/engine/Match_rules.ml
index bacf1ff5e697..7624929d7fb1 100644
--- a/src/engine/Match_rules.ml
+++ b/src/engine/Match_rules.ml
@@ -103,7 +103,9 @@ let group_rules xconf rules xtarget =
            | _ when not relevant_rule -> Right3 r
            | `Taint _ as mode -> Left3 { r with mode }
            | (`Extract _ | `Search _) as mode -> Middle3 { r with mode }
-           | `SCA _ -> failwith "SCA rule not available in core."
+           | `SCA _ ->
+               (* alt: failwith "SCA rule not available in core." *)
+               Right3 r
            | `Steps _ ->
                Log.warn (fun m ->
                    m "Step rule not handled: %s" (Rule.show_rule r));
diff --git a/src/engine/Match_search_mode.ml b/src/engine/Match_search_mode.ml
index ca91e3f85424..628f28dfc171 100644
--- a/src/engine/Match_search_mode.ml
+++ b/src/engine/Match_search_mode.ml
@@ -428,7 +428,7 @@ let apply_focus_on_ranges (env : env) (focus_mvars_list : R.focus_mv_list list)
                validation_state = `No_validator;
                severity_override = None;
                metadata_override = None;
-               dependency = None;
+               sca_match = None;
                fix_text = None;
                facts = [];
              })
@@ -879,7 +879,7 @@ and evaluate_formula env opt_context
       *  - bind closer metavariable-regexp with the relevant pattern
       *  - propagate metavariables when intersecting ranges
       *  - distribute filter_range in intersect_range?
-      * See https://github.com/returntocorp/semgrep/issues/2664
+      * See https://github.com/semgrep/semgrep/issues/2664
   *)
   let ranges, filter_expls =
     fold_with_expls
diff --git a/src/engine/Range_with_metavars.ml b/src/engine/Range_with_metavars.ml
index 16e2da74fb32..16473430a02d 100644
--- a/src/engine/Range_with_metavars.ml
+++ b/src/engine/Range_with_metavars.ml
@@ -109,7 +109,7 @@ let inside_compatible x y =
  *
  * alt: we could force the user to first And the metavariable-regex
  * with the pattern-inside, to have the right scope.
- * See https://github.com/returntocorp/semgrep/issues/2664
+ * See https://github.com/semgrep/semgrep/issues/2664
  * alt: we could do the rewriting ourselves, detecting that the
  * metavariable-regex has the wrong scope.
  *
diff --git a/src/engine/Xpattern_matcher.ml b/src/engine/Xpattern_matcher.ml
index 627a898d9efd..980b1968915c 100644
--- a/src/engine/Xpattern_matcher.ml
+++ b/src/engine/Xpattern_matcher.ml
@@ -97,7 +97,7 @@ let (matches_of_matcher :
                               validation_state = `No_validator;
                               severity_override = None;
                               metadata_override = None;
-                              dependency = None;
+                              sca_match = None;
                               fix_text = None;
                               facts = [];
                             })))
diff --git a/src/il/IL_helpers.ml b/src/il/IL_helpers.ml
index f0f151cf02a9..4555984b4b0f 100644
--- a/src/il/IL_helpers.ml
+++ b/src/il/IL_helpers.ml
@@ -94,6 +94,19 @@ let is_pro_resolved_global name =
   | None ->
       false
 
+(* HACK: Because we don't have a "Class" type, classes have themselves as types. *)
+let is_class_name (name : name) =
+  match (!(name.id_info.id_resolved), !(name.id_info.id_type)) with
+  | Some resolved1, Some { t = TyN (Id (_, { id_resolved; _ })); _ } -> (
+      match !id_resolved with
+      | None -> false
+      | Some resolved2 ->
+          (* If 'name' has type 'name' then we assume it's a class. *)
+          AST_generic.equal_resolved_name resolved1 resolved2)
+  | _, None
+  | _, Some _ ->
+      false
+
 (***********************************************)
 (* L-values *)
 (***********************************************)
diff --git a/src/il/IL_helpers.mli b/src/il/IL_helpers.mli
index 9f234150e59c..cc0c8791cc24 100644
--- a/src/il/IL_helpers.mli
+++ b/src/il/IL_helpers.mli
@@ -1,6 +1,7 @@
 val is_pro_resolved_global : IL.name -> bool
 (** Test whether a name is global and has been resolved by Pro-naming. *)
 
+val is_class_name : IL.name -> bool
 val exp_of_arg : IL.exp IL.argument -> IL.exp
 
 (** Lvalue/Rvalue helpers working on the IL *)
diff --git a/src/matching/Generic_vs_generic.ml b/src/matching/Generic_vs_generic.ml
index 337c56bc717a..b74eee243de2 100644
--- a/src/matching/Generic_vs_generic.ml
+++ b/src/matching/Generic_vs_generic.ml
@@ -2093,8 +2093,8 @@ and m_type_ a b =
    *
    * See the following for context about why this is done here, and not with
    * desugaring earlier in the pipeline:
-   * - https://github.com/returntocorp/semgrep/pull/5540
-   * - https://github.com/returntocorp/semgrep/pull/4682
+   * - https://github.com/semgrep/semgrep/pull/5540
+   * - https://github.com/semgrep/semgrep/pull/4682
    *)
   | G.TyN a1, B.TyExpr { e = N b1; _ } ->
       m_name a1 b1
@@ -3653,7 +3653,7 @@ and m_directive a b =
  * `require` nodes with the `ImportFrom` but instead added the `ImportFrom`.
  *
  * Having two places where the same symbol was defined complicated downstream
- * analysis. See https://github.com/returntocorp/semgrep/pull/6532 for some
+ * analysis. See https://github.com/semgrep/semgrep/pull/6532 for some
  * of the issues that it caused.
  *
  * So, in order to simplify naming and maintain the existing matching behavior,
diff --git a/src/matching/Match_patterns.ml b/src/matching/Match_patterns.ml
index a5416012fbfa..532efbafb637 100644
--- a/src/matching/Match_patterns.ml
+++ b/src/matching/Match_patterns.ml
@@ -186,10 +186,9 @@ let match_rules_and_recurse
                         validation_state = `No_validator;
                         severity_override = None;
                         metadata_override = None;
-                        dependency = None;
-                        (* NOTE: the global rule fix and fix_regexp in rule_id will be
-                           applied _later_, in `Autofix.ml`
-                        *)
+                        sca_match = None;
+                        (* NOTE: the global rule fix and fix_regexp in rule_id
+                         * will be applied _later_, in `Autofix.ml` *)
                         fix_text = None;
                         facts = [];
                       }
@@ -448,7 +447,7 @@ let check ~hook ?(has_as_metavariable = false) ?mvar_context
                                 validation_state = `No_validator;
                                 severity_override = None;
                                 metadata_override = None;
-                                dependency = None;
+                                sca_match = None;
                                 fix_text = None;
                                 facts = x.facts;
                               }
@@ -516,7 +515,7 @@ let check ~hook ?(has_as_metavariable = false) ?mvar_context
                                   validation_state = `No_validator;
                                   severity_override = None;
                                   metadata_override = None;
-                                  dependency = None;
+                                  sca_match = None;
                                   fix_text = None;
                                   facts;
                                 }
@@ -571,7 +570,7 @@ let check ~hook ?(has_as_metavariable = false) ?mvar_context
                                     validation_state = `No_validator;
                                     severity_override = None;
                                     metadata_override = None;
-                                    dependency = None;
+                                    sca_match = None;
                                     fix_text = None;
                                     facts = [];
                                   }
@@ -671,7 +670,7 @@ let check ~hook ?(has_as_metavariable = false) ?mvar_context
                                     validation_state = `No_validator;
                                     severity_override = None;
                                     metadata_override = None;
-                                    dependency = None;
+                                    sca_match = None;
                                     fix_text = None;
                                     facts = [];
                                   }
diff --git a/src/naming/Naming_AST.ml b/src/naming/Naming_AST.ml
index 476fea629362..62f367e041d5 100644
--- a/src/naming/Naming_AST.ml
+++ b/src/naming/Naming_AST.ml
@@ -675,7 +675,7 @@ let resolve lang prog =
              * Function-name resolution is useful for interprocedural analysis,
              * feature that was requested by JS/TS users, see:
              *
-             *     https://github.com/returntocorp/semgrep/issues/2787.
+             *     https://github.com/semgrep/semgrep/issues/2787.
              *)
             | Lang.Js
             | Lang.Ts ->
diff --git a/src/osemgrep/cli_ci/Ci_subcommand.ml b/src/osemgrep/cli_ci/Ci_subcommand.ml
index e4e10fdc5979..b98922e5a660 100644
--- a/src/osemgrep/cli_ci/Ci_subcommand.ml
+++ b/src/osemgrep/cli_ci/Ci_subcommand.ml
@@ -30,7 +30,7 @@ module Out = Semgrep_output_v1_j
    to look at the latest errors.
 
    As an example, here is a workflow that failed in the past:
-   https://github.com/returntocorp/semgrep/actions/runs/6599573075/job/17928762827
+   https://github.com/semgrep/semgrep/actions/runs/6599573075/job/17928762827
    Looking at the job log, we can see a problem when connecting to
    the https://semgrep.dev/api/agent/scans/14253285/complete endpoint.
    Then in Sentry you can paste this 'url: <URL>' in the query and search
@@ -218,6 +218,14 @@ let scan_metadata () : Out.scan_metadata =
          Logs.debug (fun m -> m "SMS scan id: %s" scan_id));
   res
 
+(*****************************************************************************)
+(* Project config *)
+(*****************************************************************************)
+(* TODO: read the .semgrepconfig.yml in the repo *)
+let project_config () : Out.ci_config_from_repo option =
+  (* alt: Out.{ version = "v1"; tags = None; } *)
+  None
+
 (*****************************************************************************)
 (* Scan config *)
 (*****************************************************************************)
@@ -292,13 +300,30 @@ let scan_config_and_rules_from_deployment ~dry_run
       m "  Reporting start of scan for %a"
         Fmt.(styled `Bold string)
         deployment_config.name);
-  let scan_meta : Out.scan_metadata = scan_metadata () in
+  let scan_metadata : Out.scan_metadata = scan_metadata () in
+  let project_config : Out.ci_config_from_repo option = project_config () in
+
+  (* TODO: deprecated from 1.43 *)
+  (* less: should concatenate with raw_json project_config *)
+  let meta =
+    (* ugly: would be good for ATDgen to generate also a json_of_xxx *)
+    prj_meta |> Out.string_of_project_metadata |> Yojson.Basic.from_string
+  in
+  let request : Out.scan_request =
+    {
+      meta = Some meta;
+      project_metadata = prj_meta;
+      scan_metadata;
+      project_config;
+    }
+  in
+
   (* TODO:
       metadata_dict["is_sca_scan"] = supply_chain
       proj_config = ProjectConfig.load_all()
       metadata_dict = {**metadata_dict, **proj_config.to_dict()}
   *)
-  match Semgrep_App.start_scan ~dry_run caps prj_meta scan_meta with
+  match Semgrep_App.start_scan ~dry_run caps request with
   | Error msg ->
       Logs.err (fun m -> m "Could not start scan %s" msg);
       Error.exit_code_exn (Exit_code.fatal ~__LOC__)
diff --git a/src/osemgrep/cli_ci/Test_ci_subcommand.ml b/src/osemgrep/cli_ci/Test_ci_subcommand.ml
index 3c7d2765546a..77994ea99300 100644
--- a/src/osemgrep/cli_ci/Test_ci_subcommand.ml
+++ b/src/osemgrep/cli_ci/Test_ci_subcommand.ml
@@ -96,10 +96,8 @@ let test_sms_scan_id (caps : Ci_subcommand.caps) =
                           in
                           (* similar to Unit_ci.ml sms_scan_id assert *)
                           match scan with
-                          | {
-                           scan_metadata = Some { sms_scan_id = Some str; _ };
-                           _;
-                          } ->
+                          | { scan_metadata = { sms_scan_id = Some str; _ }; _ }
+                            ->
                               Alcotest.(check string)
                                 "checking sms_scan_id" sms_scan_id str
                           | _ -> failwith (spf "wrong scan request: %s" s))))))
diff --git a/src/osemgrep/cli_install_ci/Install_ci_CLI.ml b/src/osemgrep/cli_install_ci/Install_ci_CLI.ml
index 811598eae33f..eb243ce67a59 100644
--- a/src/osemgrep/cli_install_ci/Install_ci_CLI.ml
+++ b/src/osemgrep/cli_install_ci/Install_ci_CLI.ml
@@ -94,7 +94,8 @@ let cmdline_term =
     let repo =
       match repo_arg with
       | "." -> Dir (Fpath.v ".")
-      | _ when UFile.dir_exists (Fpath.v repo_arg) -> Dir (Fpath.v repo_arg)
+      | _ when UFile.is_dir ~follow_symlinks:true (Fpath.v repo_arg) ->
+          Dir (Fpath.v repo_arg)
       | _ ->
           let owner, repo =
             match String.split_on_char '/' repo_arg with
diff --git a/src/osemgrep/cli_install_ci/Install_ci_subcommand.ml b/src/osemgrep/cli_install_ci/Install_ci_subcommand.ml
index 17b3a4ae6657..eae9a3182735 100644
--- a/src/osemgrep/cli_install_ci/Install_ci_subcommand.ml
+++ b/src/osemgrep/cli_install_ci/Install_ci_subcommand.ml
@@ -68,7 +68,7 @@ jobs:
         env:
             SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
         container:
-            image: returntocorp/semgrep
+            image: semgrep/semgrep
         steps:
             - uses: actions/checkout@v3
             - run: semgrep ci
@@ -336,7 +336,7 @@ let git_commit (caps : < Cap.exec >) : unit =
  *)
 let semgrep_workflow_exists (caps : < Cap.exec >) ~repo : bool =
   let dir, cmd =
-    if UFile.dir_exists repo then
+    if UFile.is_dir ~follow_symlinks:true repo then
       ( Fpath.to_dir_path repo,
         (Cmd.Name "gh", [ "workflow"; "view"; "semgrep.yml" ]) )
     else
@@ -359,7 +359,7 @@ let semgrep_workflow_exists (caps : < Cap.exec >) ~repo : bool =
    and then return the path to the cloned repo.
 *)
 let prep_repo (caps : < caps ; .. >) (repo : Fpath.t) : Fpath.t =
-  if UFile.dir_exists repo then repo
+  if UFile.is_dir ~follow_symlinks:true repo then repo
   else
     let tmp_dir =
       CapTmp.get_temp_dir_name caps#tmp
diff --git a/src/osemgrep/core_runner/Core_runner.ml b/src/osemgrep/core_runner/Core_runner.ml
index d0884cabf950..763a40c066c5 100644
--- a/src/osemgrep/core_runner/Core_runner.ml
+++ b/src/osemgrep/core_runner/Core_runner.ml
@@ -1,5 +1,6 @@
 open Common
 module Env = Semgrep_envvars
+module Out = Semgrep_output_v1_t
 
 (*************************************************************************)
 (* Prelude *)
@@ -282,6 +283,55 @@ let targets_for_files_and_rules (files : Fpath.t list) (rules : Rule.t list) :
   let lang_jobs = split_jobs_by_language conf rules files in
   lang_jobs |> List.concat_map targets_of_lang_job
 
+(*************************************************************************)
+(* SCA targeting *)
+(*************************************************************************)
+
+(* TODO: port subproject_matchers.py
+ * TODO(matthew): We'll ultimately need to do something different, because
+ * right now if a lockfile is ignored by .semgrepignore, it should still
+ * be possible to produce reachable findings that reference that lockfile,
+ * though we won't produce any lockfile-only findings for it
+ *)
+let find_lockfiles (targets : Fpath.t list) : Lockfile.t list =
+  targets
+  |> List_.filter_map (fun (target : Fpath.t) ->
+         let res =
+           match Fpath.basename target with
+           | "package-lock.json" ->
+               Some (Lockfile.mk_lockfile Out.NpmPackageLockJson target)
+           | _else_ -> None
+         in
+         res
+         |> Option.iter (fun (lockfile : Lockfile.t) ->
+                Logs.debug (fun m ->
+                    m "found lockfile %s" (Lockfile.show lockfile)));
+         res)
+
+(* TODO: take just Target.regular instead
+ * TODO: port subproject_matchers.py and more
+ * TODO: we should use the path in the lockfile and the path in the
+ *  target to associate the most precise lockfile.
+ * TODO: the code below is wrong; it's just a v0 to get a demo working;
+ * it will just attach the very first lockfile we found to every single
+ * JS target.
+ *)
+let add_possibly_lockfile_to_regular_target (lockfiles : Lockfile.t list)
+    (targets : Target.t list) : Target.t list =
+  lockfiles
+  |> List.fold_left
+       (fun acc (lockfile : Lockfile.t) ->
+         acc
+         |> List_.map (fun (target : Target.t) ->
+                match target with
+                | Lockfile _ -> target
+                | Regular
+                    ({ analyzer = Xlang.L (Lang.Js, _); lockfile = None; _ } as
+                     regular) ->
+                    Regular { regular with lockfile = Some lockfile }
+                | Regular _ -> target))
+       targets
+
 (*************************************************************************)
 (* Input/output adapters to Core_scan input/output *)
 (*************************************************************************)
@@ -371,9 +421,9 @@ let mk_core_run_for_osemgrep (core_scan_func : Core_scan.func) : func =
        work or findings.
        TODO: do this even earlier? *)
     let valid_rules =
-      List_.deduplicate_gen
-        ~get_key:(fun r -> Rule_ID.to_string (fst r.Rule.id))
-        valid_rules
+      valid_rules
+      |> List_.deduplicate_gen ~get_key:(fun r ->
+             Rule_ID.to_string (fst r.Rule.id))
     in
     let rule_errors : Core_error.t list =
       invalid_rules |> List_.map Core_error.error_of_invalid_rule
@@ -393,7 +443,14 @@ let mk_core_run_for_osemgrep (core_scan_func : Core_scan.func) : func =
     report_status_and_add_metrics_languages
       ~respect_gitignore:targeting_conf.respect_gitignore lang_jobs valid_rules
       targets;
-    let targets, applicable_rules = targets_and_rules_of_lang_jobs lang_jobs in
+    let code_targets, applicable_rules =
+      targets_and_rules_of_lang_jobs lang_jobs
+    in
+    let lockfiles = find_lockfiles targets in
+    let final_targets =
+      add_possibly_lockfile_to_regular_target lockfiles code_targets
+      @ (lockfiles |> List_.map (fun x -> Target.Lockfile x))
+    in
     Logs.debug (fun m ->
         m "core runner: %i applicable rules of %i valid rules, %i invalid rules"
           (List.length applicable_rules)
@@ -402,7 +459,7 @@ let mk_core_run_for_osemgrep (core_scan_func : Core_scan.func) : func =
     let config =
       {
         config with
-        target_source = Targets targets;
+        target_source = Targets final_targets;
         rule_source = Rules applicable_rules;
       }
     in
diff --git a/src/osemgrep/language_server/Unit_LS.ml b/src/osemgrep/language_server/Unit_LS.ml
index 116e033b2bd5..ce27455acaeb 100644
--- a/src/osemgrep/language_server/Unit_LS.ml
+++ b/src/osemgrep/language_server/Unit_LS.ml
@@ -57,15 +57,16 @@ let mock_run_results (files : string list) : Core_runner.result =
       {
         message = Some "test";
         metavars = [];
-        dataflow_trace = None;
+        severity = None;
+        metadata = None;
         fix = None;
         is_ignored = false;
         engine_kind = `OSS;
+        dataflow_trace = None;
+        sca_match = None;
         validation_state = Some `No_validator;
         historical_info = None;
         extra_extra = None;
-        severity = None;
-        metadata = None;
       }
     in
     let (m : Out.core_match) =
diff --git a/src/osemgrep/networking/Semgrep_App.ml b/src/osemgrep/networking/Semgrep_App.ml
index 8530ddbd75d5..bb5abdd0dab4 100644
--- a/src/osemgrep/networking/Semgrep_App.ml
+++ b/src/osemgrep/networking/Semgrep_App.ml
@@ -138,7 +138,12 @@ let extract_errors (data : string) : string list =
 let extract_block_override (data : string) : (app_block_override, string) result
     =
   match Out.ci_scan_complete_response_of_string data with
-  | { success = _; app_block_override; app_block_reason } as response ->
+  | {
+      success = _;
+      app_block_override;
+      app_block_reason;
+      app_blocking_match_based_ids = _TODO;
+    } as response ->
       Logs.debug (fun m ->
           m "complete response = %s"
             (Out.show_ci_scan_complete_response response));
@@ -188,9 +193,8 @@ let get_deployment_from_token token =
 (* Step1 : start scan *)
 (*****************************************************************************)
 
-(* TODO: pass project_config *)
-let start_scan_async ~dry_run caps (prj_meta : Project_metadata.t)
-    (scan_meta : Out.scan_metadata) : (scan_id, string) result Lwt.t =
+let start_scan_async ~dry_run caps (request : Out.scan_request) :
+    (scan_id, string) result Lwt.t =
   if dry_run then (
     Logs.app (fun m -> m "Would have sent POST request to create scan");
     Lwt.return_ok "")
@@ -207,21 +211,6 @@ let start_scan_async ~dry_run caps (prj_meta : Project_metadata.t)
       ]
     in
     let url = Uri.with_path !Semgrep_envvars.v.semgrep_url start_scan_route in
-    (* deprecated from 1.43 *)
-    (* TODO: should concatenate with raw_json project_config *)
-    let meta =
-      (* ugly: would be good for ATDgen to generate also a json_of_xxx *)
-      prj_meta |> Out.string_of_project_metadata |> Yojson.Basic.from_string
-    in
-    let request : Out.scan_request =
-      {
-        meta;
-        scan_metadata = Some scan_meta;
-        project_metadata = Some prj_meta;
-        (* TODO *)
-        project_config = None;
-      }
-    in
     let body = Out.string_of_scan_request request in
     let pretty_body =
       body |> Yojson.Basic.from_string |> Yojson.Basic.pretty_to_string
@@ -245,8 +234,8 @@ Please make sure they have been set correctly.
         Lwt.return_error msg
     | Error e -> Lwt.return_error (spf "Failed to start scan: %s" e)
 
-let start_scan ~dry_run caps prj_meta scan_meta =
-  Lwt_platform.run (start_scan_async ~dry_run caps prj_meta scan_meta)
+let start_scan ~dry_run caps request =
+  Lwt_platform.run (start_scan_async ~dry_run caps request)
 
 (*****************************************************************************)
 (* Step2 : fetch scan config (version 2) *)
diff --git a/src/osemgrep/networking/Semgrep_App.mli b/src/osemgrep/networking/Semgrep_App.mli
index 4a2f0e20a708..60f87064156d 100644
--- a/src/osemgrep/networking/Semgrep_App.mli
+++ b/src/osemgrep/networking/Semgrep_App.mli
@@ -35,10 +35,9 @@ type scan_id = string
 val start_scan :
   dry_run:bool ->
   < Cap.network ; Auth.cap_token ; .. > ->
-  Project_metadata.t ->
-  Semgrep_output_v1_t.scan_metadata ->
+  Semgrep_output_v1_t.scan_request ->
   (scan_id, string) result
-(** [start_scan ~dry_run ~token url prj] informs the Semgrep App that a scan
+(** [start_scan ~dry_run caps req] informs the Semgrep App that a scan
     is about to be started, and returns the scan id from the server. If
     [dry_run] is [true], the empty string will be returned ([Ok ""]). *)
 
@@ -119,8 +118,6 @@ val fetch_scan_config_async :
   repository:string ->
   < Cap.network ; Auth.cap_token ; .. > ->
   (Semgrep_output_v1_t.scan_config, string) result Lwt.t
-(** [fetch_scan_config_async ~token ~sca ~dry_run ~full_scan repo] returns a
-     promise of the rules for the provided configuration. *)
 
 val report_failure_async :
   dry_run:bool ->
@@ -132,12 +129,8 @@ val report_failure_async :
 val start_scan_async :
   dry_run:bool ->
   < Cap.network ; Auth.cap_token ; .. > ->
-  Project_metadata.t ->
-  Semgrep_output_v1_t.scan_metadata ->
+  Semgrep_output_v1_t.scan_request ->
   (scan_id, string) result Lwt.t
-(** [start_scan_async ~dry_run ~token url prj] informs the Semgrep App that a
-    scan is about to be started, and returns the scan id from the server. If
-    [dry_run] is [true], the empty string will be returned ([Ok ""]). *)
 
 val upload_findings_async :
   dry_run:bool ->
@@ -146,8 +139,6 @@ val upload_findings_async :
   complete:Semgrep_output_v1_t.ci_scan_complete ->
   < Cap.network ; Auth.cap_token ; .. > ->
   (app_block_override, string) result Lwt.t
-(** [upload_findings_async ~dry_run ~token ~scan_id ~results ~complete]
-    reports the findings to Semgrep App. *)
 
 val upload_rule_to_registry_async :
   < Cap.network ; Auth.cap_token ; .. > ->
diff --git a/src/osemgrep/reporting/Cli_json_output.ml b/src/osemgrep/reporting/Cli_json_output.ml
index d23129deaedc..0648a2194ef7 100644
--- a/src/osemgrep/reporting/Cli_json_output.ml
+++ b/src/osemgrep/reporting/Cli_json_output.ml
@@ -273,6 +273,7 @@ let cli_match_of_core_match ~fixed_lines fixed_env (hrules : Rule.hrules)
        fix;
        is_ignored;
        dataflow_trace;
+       sca_match;
      };
   } ->
       let rule =
@@ -327,15 +328,13 @@ let cli_match_of_core_match ~fixed_lines fixed_env (hrules : Rule.hrules)
             metadata;
             fix;
             is_ignored = Some is_ignored;
-            (* TODO: extra fields *)
             fingerprint =
               Semgrep_hashing_functions.match_based_id_partial rule rule_id
                 metavars !!path;
-            sca_info = None;
+            sca_info = sca_match;
             fixed_lines;
             dataflow_trace;
-            (* It's optional in the CLI output, but not in the core match results!
-             *)
+            (* It's optional in the CLI output, but not in core match results!*)
             engine_kind = Some engine_kind;
             validation_state;
             historical_info;
diff --git a/src/osemgrep/reporting/Gitlab_output.ml b/src/osemgrep/reporting/Gitlab_output.ml
index 045fac38375c..9e64e21f0bc7 100644
--- a/src/osemgrep/reporting/Gitlab_output.ml
+++ b/src/osemgrep/reporting/Gitlab_output.ml
@@ -8,7 +8,7 @@ module J = JSON
 (* Output findings compatible with GitLab SAST JSON format.
 
    - Written based on:
-     https://github.com/returntocorp/semgrep-action/blob/678eff1a4269ed04b76631771688c8be860ec4e9/src/semgrep_agent/findings.py#L137-L165
+     https://github.com/semgrep/semgrep-action/blob/678eff1a4269ed04b76631771688c8be860ec4e9/src/semgrep_agent/findings.py#L137-L165
    - Docs:
      https://docs.gitlab.com/ee/user/application_security/sast/#reports-json-format
    - Schema:
diff --git a/src/osemgrep/reporting/Semgrep_hashing_functions.ml b/src/osemgrep/reporting/Semgrep_hashing_functions.ml
index 3afd3a915e50..2a525eaf230e 100644
--- a/src/osemgrep/reporting/Semgrep_hashing_functions.ml
+++ b/src/osemgrep/reporting/Semgrep_hashing_functions.ml
@@ -197,7 +197,7 @@ let match_formula_interpolated_str (rule : Rule.t) metavars : string =
  * quirks.
  *
  * The way match based ID is calculated on the python side is as follows:
- * (see https://github.com/returntocorp/semgrep/blob/2d34ce584d16c4e954349690a5f12fae877a94d6/cli/src/semgrep/rule.py#L289-L334)
+ * (see https://github.com/semgrep/semgrep/blob/2d34ce584d16c4e954349690a5f12fae877a94d6/cli/src/semgrep/rule.py#L289-L334)
  * 1. Sort all top level keys (i.e pattern, patterns etc.) alphabetically
  * 2. For each key: DFS the tree and find all pattern values (i.e. the rhs of
  *    pattern: <THING>)
diff --git a/src/parsing/Parse_SCA_version.ml b/src/parsing/Parse_SCA_version.ml
new file mode 100644
index 000000000000..a7cc428d3006
--- /dev/null
+++ b/src/parsing/Parse_SCA_version.ml
@@ -0,0 +1,104 @@
+(* Matthew McQuaid, Yoann Padioleau
+ *
+ * Copyright (C) 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.
+ *)
+open Common
+
+(*****************************************************************************)
+(* Prelude *)
+(*****************************************************************************)
+(* Parse a package version or version constraint
+ *
+ * TODO: port parts of cli/src/semdep/external/packaging/specifiers.py
+ *)
+
+(*****************************************************************************)
+(* Error management *)
+(*****************************************************************************)
+
+(* alt: use Result.t *)
+exception Error of string
+
+(*****************************************************************************)
+(* Helpers *)
+(*****************************************************************************)
+
+(*****************************************************************************)
+(* Entrypoints *)
+(*****************************************************************************)
+
+(* TODO: port part of specifiers.py
+ * TODO: pass more context (e.g., a Tok.t) for better Logs.warn or error
+ *)
+let parse (str : string) : SCA_version.t =
+  (* just enough to parse some toy package-lock.json and enough
+   * to parse the version part of the version constraints in our SCA rules
+   *)
+  match str with
+  (* "1.2.3" *)
+  | _ when str =~ "^\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)$" ->
+      let s1, s2, s3 = Common.matched3 str in
+      SCA_version.V
+        {
+          major = int_of_string s1;
+          minor = int_of_string s2;
+          incrementals = [ int_of_string s3 ];
+        }
+  (* "40.0" *)
+  | _ when str =~ "^\\([0-9]+\\)\\.\\([0-9]+\\)$" ->
+      let s1, s2 = Common.matched2 str in
+      SCA_version.V
+        {
+          major = int_of_string s1;
+          minor = int_of_string s2;
+          incrementals = [];
+        }
+  (* "40" *)
+  | _ when str =~ "^\\([0-9]+\\)$" ->
+      let s1 = Common.matched1 str in
+      SCA_version.V { major = int_of_string s1; minor = 0; incrementals = [] }
+  (* alt: raise (Error (spf "wrong version format for %s" str)) in *)
+  | _ ->
+      (* nosemgrep: no-logs-in-library *)
+      Logs.warn (fun m -> m "unrecognized version format for %s" str);
+      SCA_version.Other str
+
+(* TODO: port part of specifiers.py *)
+let parse_constraints (s : string) : SCA_pattern.version_constraints =
+  let error () = raise (Error (spf "wrong constraint format for %s" s)) in
+  (* just enough to parse some toy package-lock.json *)
+  (* similar to what we did for ruleid: annots in cli_test/Test_annotation.ml *)
+  (* start from " > 1.0.2, < 1.05 " *)
+  let s = String.trim s in
+  let xs = Str.split_delim (Str.regexp "[ \t]*,[ \t]*") s in
+  SCA_pattern.SCA_And
+    (xs
+    |> List_.map (fun s ->
+           (* "> 1.0.2" *)
+           let s = String.trim s in
+           if s =~ "^\\([=<>]+\\)[ \t]*\\([^ ]+\\)$" then
+             let op, ver = Common.matched2 s in
+             let op : SCA_pattern.sca_operator =
+               match op with
+               | "="
+               | "==" ->
+                   Eq
+               | ">=" -> Gte
+               | "<=" -> Lte
+               | ">" -> Gt
+               | "<" -> Lt
+               | _ -> error ()
+             in
+             let version : SCA_version.t = parse ver in
+             SCA_pattern.{ op; version }
+           else error ()))
diff --git a/src/parsing/Parse_SCA_version.mli b/src/parsing/Parse_SCA_version.mli
new file mode 100644
index 000000000000..bf8cdf408a87
--- /dev/null
+++ b/src/parsing/Parse_SCA_version.mli
@@ -0,0 +1,7 @@
+exception Error of string
+
+(* may raise Error *)
+val parse : string -> SCA_version.t
+
+(* used in Parse_rule.ml, may also raise Error *)
+val parse_constraints : string -> SCA_pattern.version_constraints
diff --git a/src/parsing/Parse_lockfile.ml b/src/parsing/Parse_lockfile.ml
index 01cd8be58003..c87237f15b57 100644
--- a/src/parsing/Parse_lockfile.ml
+++ b/src/parsing/Parse_lockfile.ml
@@ -1,13 +1,98 @@
-let parse_lockfile :
-    Lockfile.kind ->
-    Lockfile_xtarget.manifest option ->
-    Fpath.t ->
-    SCA_dependency.t list = function
-  (* TODO: add parsers, guard behind semgrep-pro  *)
+(* Matthew McQuaid, Yoann Padioleau
+ *
+ * Copyright (C) 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.
+ *)
+open Fpath_.Operators
+module Log = Log_parsing.Log
+module J = JSON
+
+(*****************************************************************************)
+(* Prelude *)
+(*****************************************************************************)
+(* Parsing a lockfile or manifest file.
+ *
+ * TODO: port lots of code from cli/src/semdep/parsers/
+ *)
+
+(*****************************************************************************)
+(* Error management *)
+(*****************************************************************************)
+
+exception UnsupportedFormat of string
+exception WrongFormat of string
+
+(*****************************************************************************)
+(* Helpers *)
+(*****************************************************************************)
+
+(* TODO: reuse recent matthew's parser in src/sca/ in semgrep-pro *)
+let parse_npm_package_json (file : Fpath.t) : SCA_dependency.t list =
+  (* TODO we should use our own json parser to get the location
+   * information.
+   *)
+  let loc1 = Tok.first_loc_of_file file in
+  let tok1 = Tok.first_tok_of_file file in
+  let loc = (loc1, loc1) in
+  let toks = [ tok1 ] in
+
+  UChan.with_open_in file (fun chan ->
+      let json = JSON.json_of_chan chan in
+      match json with
+      (* just enough to parse some toy package-lock.json *)
+      | J.Object
+          [
+            ("requires", _);
+            ("lockfileVersion", J.Int 1);
+            ("dependencies", J.Object deps);
+          ] ->
+          deps
+          |> List_.map (fun (package_name, json) ->
+                 match json with
+                 | J.Object
+                     [
+                       ("version", J.String ver);
+                       ("resolved", J.String url);
+                       ("integrity", J.String _checksum);
+                     ] ->
+                     SCA_dependency.
+                       {
+                         package_name;
+                         package_version = Parse_SCA_version.parse ver;
+                         package_version_string = ver;
+                         ecosystem = `Npm;
+                         transitivity = `Unknown;
+                         url = Uri_.of_string_opt url;
+                         loc;
+                         toks;
+                       }
+                 | _ -> raise (WrongFormat "package-lock.json"))
+      | _ -> raise (WrongFormat "package-lock.json"))
+
+(*****************************************************************************)
+(* Entry points *)
+(*****************************************************************************)
+
+(* TODO: add parsers, guard behind semgrep-pro  *)
+let parse (kind : Lockfile.kind)
+    (_manifest_optTODO : Lockfile_xtarget.manifest option) (file : Fpath.t) :
+    SCA_dependency.t list =
+  Log.debug (fun m ->
+      m "parsing lockfile %s with kind %s" !!file (Lockfile.show_kind kind));
+  match kind with
+  | NpmPackageLockJson -> parse_npm_package_json file
   | PipRequirementsTxt
   | PoetryLock
   | PipfileLock
-  | NpmPackageLockJson
   | YarnLock
   | PnpmLock
   | GemfileLock
@@ -22,7 +107,7 @@ let parse_lockfile :
   | MixLock
   | UvLock
   | ConanLock ->
-      fun _ _ -> []
+      raise (UnsupportedFormat (Lockfile.show_kind kind))
 
 let parse_manifest :
     Manifest.kind -> Fpath.t -> SCA_dependency.manifest_dependency list =
@@ -44,5 +129,6 @@ let parse_manifest :
   | `Pipfile
   | `PyprojectToml
   | `ConanFilePy
-  | `ConanFileTxt ->
+  | `ConanFileTxt
+  | `Csproj ->
       fun _ -> []
diff --git a/src/parsing/Parse_lockfile.mli b/src/parsing/Parse_lockfile.mli
new file mode 100644
index 000000000000..427046cb6dfc
--- /dev/null
+++ b/src/parsing/Parse_lockfile.mli
@@ -0,0 +1,12 @@
+exception WrongFormat of string
+exception UnsupportedFormat of string
+
+(* may raise WrongFormat or UnsupportedFormat *)
+val parse :
+  Lockfile.kind ->
+  Lockfile_xtarget.manifest option ->
+  Fpath.t ->
+  SCA_dependency.t list
+
+val parse_manifest :
+  Manifest.kind -> Fpath.t -> SCA_dependency.manifest_dependency list
diff --git a/src/parsing/Parse_rule.ml b/src/parsing/Parse_rule.ml
index 852633a25351..f47de6ae90e9 100644
--- a/src/parsing/Parse_rule.ml
+++ b/src/parsing/Parse_rule.ml
@@ -825,12 +825,13 @@ let parse_dependency_pattern key env value :
   let/ rd = parse_dict env key value in
   let/ ecosystem = take_key rd env parse_ecosystem "namespace" in
   let/ package_name = take_key rd env parse_string "package" in
+  let/ version_str = take_key rd env parse_string "version" in
   let/ version_constraints =
-    (* TODO: version parser *)
-    take_key rd env parse_string "version"
-    |> Result.map (fun _ ->
-           SCA_pattern.SCA_And
-             [ { version = SCA_version.Other "not implemented"; op = Eq } ])
+    try Ok (Parse_SCA_version.parse_constraints version_str) with
+    | Parse_SCA_version.Error error_str ->
+        error_at_key env.id key
+          (spf "bad version constraint format for %s, error = %s" version_str
+             error_str)
   in
   Ok SCA_pattern.{ ecosystem; package_name; version_constraints }
 
@@ -1224,7 +1225,7 @@ let parse_fake_xpattern xlang str =
 (* Useful for tests *)
 (*****************************************************************************)
 
-let parse file =
+let parse (file : Fpath.t) : (Rule.rules, Rule_error.t) result =
   let/ xs, _skipped = parse_file ~error_recovery:false file in
   (* The skipped rules include Apex rules and other rules that are always
      skippable. *)
diff --git a/src/parsing/Parse_rule.mli b/src/parsing/Parse_rule.mli
index e79b049e77ac..e931b82cba99 100644
--- a/src/parsing/Parse_rule.mli
+++ b/src/parsing/Parse_rule.mli
@@ -14,7 +14,7 @@
 val parse_and_filter_invalid_rules :
   ?rewrite_rule_ids:(Rule_ID.t -> Rule_ID.t) ->
   Fpath.t ->
-  (Rule_error.rules_and_invalid, Rule_error.t) Result.t
+  (Rule_error.rules_and_invalid, Rule_error.t) result
 
 (* This is used for parsing -e/-f extended patterns in Run_semgrep.ml
  * and now also in osemgrep Config_resolver.ml.
@@ -22,17 +22,16 @@ val parse_and_filter_invalid_rules :
  * Error (Rule.InvalidRegexp _) for regexp errors.
  *)
 val parse_xpattern :
-  Xlang.t -> string Rule.wrap -> (Xpattern.t, Rule_error.t) Result.t
+  Xlang.t -> string Rule.wrap -> (Xpattern.t, Rule_error.t) result
 
-val parse_fake_xpattern :
-  Xlang.t -> string -> (Xpattern.t, Rule_error.t) Result.t
+val parse_fake_xpattern : Xlang.t -> string -> (Xpattern.t, Rule_error.t) result
 
 (* This should be used mostly in testing code. Otherwise you should
  * use parse_and_filter_invalid_rules.
  * This function may raise (Rule.Err ....) or Assert_failure (when
  * there are invalid rules).
  *)
-val parse : Fpath.t -> (Rule.rules, Rule_error.t) Result.t
+val parse : Fpath.t -> (Rule.rules, Rule_error.t) result
 
 (* Internals, used by osemgrep to setup a ojsonnet import hook.
  * The filename parameter is just used in case of missing 'rules:'
@@ -43,4 +42,4 @@ val parse_generic_ast :
   ?rewrite_rule_ids:(Rule_ID.t -> Rule_ID.t) ->
   Fpath.t ->
   AST_generic.program ->
-  (Rule_error.rules_and_invalid, Rule_error.t) Result.t
+  (Rule_error.rules_and_invalid, Rule_error.t) result
diff --git a/src/parsing/Unit_parsing.ml b/src/parsing/Unit_parsing.ml
index bea4c29fc6e6..a553a69e949e 100644
--- a/src/parsing/Unit_parsing.ml
+++ b/src/parsing/Unit_parsing.ml
@@ -181,7 +181,14 @@ let parsing_rules_tests () =
      in
      tests |> Fpath_.of_strings
      |> List_.map (fun file ->
-            t (Fpath.basename file) (fun () -> Parse_rule.parse file |> ignore)))
+            t (Fpath.basename file) (fun () ->
+                let res = Parse_rule.parse file in
+                match res with
+                | Ok _ -> ()
+                | Error err ->
+                    failwith
+                      (spf "error %s while parsing %s" (Rule_error.show err)
+                         !!file))))
 
 let parsing_rules_with_atd_tests () =
   let dir = tests_path / "rules_v2" in
diff --git a/src/parsing/dune b/src/parsing/dune
index df78d8f53fb2..152cc015a853 100644
--- a/src/parsing/dune
+++ b/src/parsing/dune
@@ -14,6 +14,7 @@
    lib_parsing
    process_limits
    spacegrep
+   uri ; for Parse_lockfile.ml
 
    pfff-lang_GENERIC-naming
 
diff --git a/src/parsing_languages/Parse_pattern2.ml b/src/parsing_languages/Parse_pattern2.ml
index 17b6cabf1fbb..eb3bdf9b6210 100644
--- a/src/parsing_languages/Parse_pattern2.ml
+++ b/src/parsing_languages/Parse_pattern2.ml
@@ -30,7 +30,7 @@ let parse_pattern options lang str =
   (* coupling: update the files semgrep/js/languages/<lang>/Parser.ml
      when updating this function.
      TODO: Share the logic of which parser to try for each language to
-     remove this coupling. https://github.com/returntocorp/semgrep/issues/8331
+     remove this coupling. https://github.com/semgrep/semgrep/issues/8331
   *)
   match lang with
   (* use adhoc parser (neither menhir nor tree-sitter) *)
diff --git a/src/reporting/Core_json_output.ml b/src/reporting/Core_json_output.ml
index b7b0cf54c6fd..96651be2e7ec 100644
--- a/src/reporting/Core_json_output.ml
+++ b/src/reporting/Core_json_output.ml
@@ -377,11 +377,12 @@ let unsafe_match_to_match
         fix =
           Option.map (fun edit -> edit.Textedit.replacement_text) autofix_edit;
         is_ignored;
-        (* TODO *)
         engine_kind = x.engine_of_match;
         validation_state = Some x.validation_state;
         historical_info;
         extra_extra = None;
+        (* TODO: use pm.sca_match! *)
+        sca_match = None;
       };
   }
 
diff --git a/src/reporting/Unit_core_json_output.ml b/src/reporting/Unit_core_json_output.ml
index 2fc49d1cb450..cddcf8275790 100644
--- a/src/reporting/Unit_core_json_output.ml
+++ b/src/reporting/Unit_core_json_output.ml
@@ -41,9 +41,10 @@ let make_core_match ?(check_id = "fake-rule-id") ?annotated_rule_id
         severity = None;
         metavars = [];
         fix = None;
-        dataflow_trace = None;
         engine_kind = `OSS;
+        dataflow_trace = None;
         is_ignored = false;
+        sca_match = None;
         validation_state = None;
         historical_info = None;
         extra_extra = None;
diff --git a/src/rule/Rule.ml b/src/rule/Rule.ml
index 49b347c2712d..7305fd83e4b5 100644
--- a/src/rule/Rule.ml
+++ b/src/rule/Rule.ml
@@ -103,7 +103,7 @@ and formula_kind =
    * The same is true for pattern-not and pattern-not-inside
    * (see tests/rules/negation_exact.yaml)
    * todo: try to remove this at some point, but difficult. See
-   * https://github.com/returntocorp/semgrep/issues/1218
+   * https://github.com/semgrep/semgrep/issues/1218
    *)
   | Inside of tok * formula
   (* alt: Could do this under a `where` (call it something like `also`).
diff --git a/src/rule/SCA_pattern.ml b/src/rule/SCA_pattern.ml
index 33d23cd4fa5f..3c4c79121b55 100644
--- a/src/rule/SCA_pattern.ml
+++ b/src/rule/SCA_pattern.ml
@@ -1,6 +1,7 @@
 module Out = Semgrep_output_v1_t
 
-type sca_operator = Eq | Gte | Lte | Gt | Lt [@@deriving show, eq]
+type sca_operator = Eq | Gte | Lte | Gt | Lt
+[@@deriving show { with_path = false }, eq]
 
 (* Something like (>= 2.0.0) or (== 5.1.7) *)
 type version_constraint = { op : sca_operator; version : SCA_version.t }
@@ -25,4 +26,4 @@ type t = {
  * union.
  *)
 and version_constraints = SCA_And of version_constraint list
-[@@deriving show, eq]
+[@@deriving show { with_path = false }, eq]
diff --git a/src/rule/SCA_version.ml b/src/rule/SCA_version.ml
index 782796ab070f..49ac873d3a49 100644
--- a/src/rule/SCA_version.ml
+++ b/src/rule/SCA_version.ml
@@ -1,4 +1,8 @@
-(* The "core of a version": a dot separated list of numbers, like 4.1.6.2.7 *)
+(* The "core of a version": a dot separated list of numbers, like 4.1.6.2.7
+ * alt: we could inline it in V of { ... } which is allowed in modern OCaml
+ * but Match_SCA_mode.ml has also functions operating just on this type
+ * so simpler to have a separate type with a proper name.
+ *)
 type core = { major : int; minor : int; incrementals : int list }
 [@@deriving show, eq]
 
@@ -7,4 +11,4 @@ type t =
   | V of core
   (* Versions are sometimes listed as arbitrary strings, like a github URL *)
   | Other of string
-[@@deriving show, eq]
+[@@deriving show { with_path = false }, eq]
diff --git a/src/sca/Manifest.ml b/src/sca/Manifest.ml
index f6400a65f30c..d299ffb182af 100644
--- a/src/sca/Manifest.ml
+++ b/src/sca/Manifest.ml
@@ -44,21 +44,25 @@ let mk_manifest (kind : kind) (path : Fpath.t) : t = { path; kind }
 
 let kind_to_ecosystem_opt : kind -> Semgrep_output_v1_t.ecosystem option =
   function
-  | `RequirementsIn -> Some `Pypi
+  | `RequirementsIn
+  | `Pipfile
+  | `PyprojectToml ->
+      Some `Pypi
   | `PackageJson -> Some `Npm
   | `Gemfile -> Some `Gem
   | `GoMod -> Some `Gomod
   | `CargoToml -> Some `Cargo
-  | `PomXml -> Some `Maven
-  | `BuildGradle -> Some `Maven
-  | `SettingsGradle -> Some `Maven
+  | `PomXml
+  | `BuildGradle
+  | `SettingsGradle ->
+      Some `Maven
   | `ComposerJson -> Some `Composer
-  | `NugetManifestJson -> Some `Nuget
+  | `NugetManifestJson
+  | `Csproj ->
+      Some `Nuget
   | `PubspecYaml -> Some `Pub
   | `PackageSwift -> Some `SwiftPM
   | `MixExs -> Some `Mix
-  | `Pipfile -> Some `Pypi
-  | `PyprojectToml -> Some `Pypi
   | `ConanFilePy
   | `ConanFileTxt ->
       None
diff --git a/src/tainting/Dataflow_tainting.ml b/src/tainting/Dataflow_tainting.ml
index 7bcb86870d15..f5e17445cabe 100644
--- a/src/tainting/Dataflow_tainting.ml
+++ b/src/tainting/Dataflow_tainting.ml
@@ -677,87 +677,19 @@ let check_orig_if_sink env ?filter_sinks orig taints shape =
   let effects = effects_of_tainted_sinks env taints sinks in
   record_effects env effects
 
-let fix_poly_taint_with_field env lval xtaint =
-  let type_of_il_offset il_offset =
-    match il_offset.IL.o with
-    | Dot n -> !(n.id_info.id_type)
-    | Index _ -> None
-  in
-  let add_offset_to_lval o ({ offset; _ } as lval : T.lval) =
-    if
-      (* If the offset we are trying to take is already in the
-           list of offsets, don't append it! This is so we don't
-           never-endingly loop the dataflow and make it think the
-           Arg taint is never-endingly changing.
-
-           For instance, this code example would previously loop,
-           if `x` started with an `Arg` taint:
-           while (true) { x = x.getX(); }
-      *)
-      (not (List.mem o offset))
-      && (* For perf reasons we don't allow offsets to get too long.
-          * Otherwise in a long chain of function calls where each
-          * function adds some offset, we could end up a very large
-          * amount of polymorphic taint.
-          * This actually happened with rule
-          * semgrep.perf.rules.express-fs-filename from the Pro
-          * benchmarks, and file
-          * WebGoat/src/main/resources/webgoat/static/js/libs/ace.js.
-          *
-          * TODO: This is way less likely to happen if we had better
-          *   type info and we used it to remove taint, e.g. if Boolean
-          *   and integer expressions didn't propagate taint. *)
-      List.length offset < Limits_semgrep.taint_MAX_POLY_OFFSET
-    then { lval with offset = lval.offset @ [ o ] }
-    else lval
-  in
-  (* TODO: Aren't we missing here C# and Go ? *)
-  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
-    | `Clean
-    | `None ->
-        xtaint
-    | `Tainted taints -> (
-        match lval.rev_offset with
-        | o :: _ -> (
-            match (type_of_il_offset o, T.offset_of_IL o) with
-            | Some { t = TyFun _; _ }, _ ->
-                (* We have an l-value like `o.f` where `f` has a function type,
-                 * so it's a method call, we return nothing here. We cannot just
-                 * return `xtaint`, which is the taint of `o` in the environment;
-                 * whether that taint propagates or not is determined in
-                 * 'check_tainted_instr'/'Call'. Otherwise, if `o` had taint var
-                 * 'o@i', the call `o.getX()` would have taints '{o@i, o@i.x}'
-                 * when it should only have taints '{o@i.x}'. *)
-                `None
-            | _, Oany ->
-                (* Cannot handle this offset. *)
-                xtaint
-            | __any__, ((Ofld _ | Ostr _ | Oint _) as o) ->
-                (* Not a method call (to the best of our knowledge) or
-                 * an unresolved Java `getX` method. *)
-                let taints' =
-                  taints
-                  |> Taints.map (fun taint ->
-                         match taint.orig with
-                         | Var lval ->
-                             let lval' = add_offset_to_lval o lval in
-                             { taint with orig = Var lval' }
-                         | Shape_var lval ->
-                             let lval' = add_offset_to_lval o lval in
-                             { taint with orig = Shape_var lval' }
-                         | Src _
-                         | Control ->
-                             taint)
-                in
-                `Tainted taints')
-        | [] -> xtaint)
-  else xtaint
+let fix_poly_taint_with_field lval xtaint =
+  match xtaint with
+  | `Sanitized
+  | `Clean
+  | `None ->
+      xtaint
+  | `Tainted taints -> (
+      match lval.rev_offset with
+      | o :: _ ->
+          let o = T.offset_of_IL o in
+          let taints = Shape.fix_poly_taint_with_offset [ o ] taints in
+          `Tainted taints
+      | [] -> xtaint)
 
 (*****************************************************************************)
 (* Tainted *)
@@ -1139,7 +1071,7 @@ and check_tainted_lval_aux env (lval : IL.lval) :
                      * where `obj.y` is tainted but `obj.x` is not tainted, we will not
                      * produce a finding.
                   *)
-                  fix_poly_taint_with_field env lval sub_xtaint
+                  fix_poly_taint_with_field lval sub_xtaint
             in
             (xtaint', shape)
       in
@@ -1450,12 +1382,8 @@ let check_function_call env fun_exp args
           m ~tags:sigs_tag "Call to %s : %s"
             (Display_IL.string_of_exp fun_exp)
             (Signature.show fun_sig));
-      let check_lval lval =
-        let taints, shape, _sub, _lval_env = check_tainted_lval env lval in
-        (taints, shape)
-      in
       let* call_effects =
-        Sig_inst.instantiate_function_signature env.lval_env ~check_lval fun_sig
+        Sig_inst.instantiate_function_signature env.lval_env fun_sig
           ~callee:fun_exp ~args:(Some args) args_taints
       in
       Some
@@ -1487,8 +1415,8 @@ let check_function_call env fun_exp args
                    (taints_acc, shape_acc, lval_env |> Lval_env.add lval taints))
              (Taints.empty, Bot, env.lval_env))
   | None ->
-      Log.debug (fun m ->
-          m ~tags:sigs_tag "Call to %s : NO SIGNATURE !!!!"
+      Log.info (fun m ->
+          m "No taint signature found for `%s'"
             (Display_IL.string_of_exp fun_exp));
       None
 
diff --git a/src/tainting/Shape_and_sig.ml b/src/tainting/Shape_and_sig.ml
index 8e589b72bf30..658581ac3af8 100644
--- a/src/tainting/Shape_and_sig.ml
+++ b/src/tainting/Shape_and_sig.ml
@@ -82,58 +82,60 @@ module rec Shape : sig
     | Bot  (** _|_, don't know or don't care *)
     | Obj of obj
         (** An "object" or struct-like thing.
-          *
-          * Tuples or lists are also represented by 'Obj' shapes! We just treat
-          * constant indexes as if they were fields, and use 'Oany' to capture the
-          * non-constant indexes.
+
+           Tuples or lists are also represented by 'Obj' shapes! We just treat
+           constant indexes as if they were fields, and use 'Oany' to capture the
+           non-constant indexes.
           *)
     | Arg of Taint.arg
         (** Represents the yet-unknown shape of a function/method parameter. It is
-          * a polymorphic shape variable that is meant to be instantiated at call
-          * site. Before adding 'Arg' we assumed parameters had shape 'Bot', and
-          * 'Arg' still acts like 'Bot' in some places. *)
+            a polymorphic shape variable that is meant to be instantiated at call
+            site. Before adding 'Arg' we assumed parameters had shape 'Bot', and
+            'Arg' still acts like 'Bot' in some places.
+
+            TODO: Generalize to 'Taint.lval', e.g. `function test(o) { return o.x }`. *)
     | Fun of Signature.t
         (** Function shapes. These enable Semgrep to handle HOFs. *)
 
   and cell =
     | Cell of Xtaint.t * shape
         (** A cell or "reference" represents the "storage" of a value, like
-          * a variable in C.
-          *
-          * A cell may be explicitly tainted ('`Tainted'), not explicitly tainted
-          * ('`None' / "0"),  or explicitly clean ('`Clean' / "C").
-          *
-          * A cell that is not explicitly tainted inherits any taints from "parent"
-          * refs. A cell that is explicitly clean it is clean regardless.
-          *
-          * For example, given a variable `x` and the following statements:
-          *
-          *     x.a := "taint";
-          *     x.a.u := "clean";
-          *
-          * We could assign the following shape to `x`:
-          *
-          *     Cell(`None, Obj {
-          *             .a -> Cell({"taint"}, Obj {
-          *                     .u -> Cell(`Clean, _|_)
-          *                     })
-          *             })
-          *
-          * We have that `x` itself has no taint directly assigned to it, but `x.a` is
-          * tainted (by the string `"taint"`). Other fields like `x.b` are not tainted.
-          * When it comes to `x.a`, we have that `x.a.u` has been explicitly marked clean,
-          * so `x.a.u` will be considered clean despite `x.a` being tainted. Any other field
-          * of `x.a` such as `x.a.v` will inherit the same taint as `x.a`.
-          *
-          * INVARIANT(cell): To keep shapes minimal:
-          *   1. If the xtaint is '`None', then the shape is not 'Bot' and we can reach
-          *      another 'cell' whose xtaint is either '`Tainted' or '`Clean'.
-          *   2. If the xtaint is '`Clean', then the shape is 'Bot'.
-          *      (If we add aliasing we may need to revisit this, and instead just mark
-          *       every reachable 'cell' as clean too.)
-          *
-          * TODO: We can attach "region ids" to refs and assign taints to regions rather than
-          *   to refs directly, then we can have alias analysis.
+            a variable in C.
+
+            A cell may be explicitly tainted ('`Tainted'), not explicitly tainted
+            ('`None' / "0"),  or explicitly clean ('`Clean' / "C").
+
+            A cell that is not explicitly tainted inherits any taints from "parent"
+            refs. A cell that is explicitly clean it is clean regardless.
+
+            For example, given a variable `x` and the following statements:
+
+                x.a := "taint";
+                x.a.u := "clean";
+
+            We could assign the following shape to `x`:
+
+                Cell(`None, Obj {
+                        .a -> Cell({"taint"}, Obj {
+                                .u -> Cell(`Clean, _|_)
+                                })
+                        })
+
+            We have that `x` itself has no taint directly assigned to it, but `x.a` is
+            tainted (by the string `"taint"`). Other fields like `x.b` are not tainted.
+            When it comes to `x.a`, we have that `x.a.u` has been explicitly marked clean,
+            so `x.a.u` will be considered clean despite `x.a` being tainted. Any other field
+            of `x.a` such as `x.a.v` will inherit the same taint as `x.a`.
+
+            INVARIANT(cell): To keep shapes minimal:
+              1. If the xtaint is '`None', then the shape is not 'Bot' and we can reach
+                 another 'cell' whose xtaint is either '`Tainted' or '`Clean'.
+              2. If the xtaint is '`Clean', then the shape is 'Bot'.
+                 (If we add aliasing we may need to revisit this, and instead just mark
+                  every reachable 'cell' as clean too.)
+
+            TODO: We can attach "region ids" to refs and assign taints to regions rather than
+              to refs directly, then we can have alias analysis.
           *)
 
   and obj = cell Fields.t
diff --git a/src/tainting/Sig_inst.ml b/src/tainting/Sig_inst.ml
index aef2caa343aa..f29c489414e0 100644
--- a/src/tainting/Sig_inst.ml
+++ b/src/tainting/Sig_inst.ml
@@ -495,29 +495,6 @@ let lval_of_sig_lval (fun_exp : IL.exp) fparams args_exps (sig_lval : T.lval) :
               { arg_lval with rev_offset = rev_offset @ arg_lval.rev_offset }
             in
             Some (lval, obj)
-        | RecordOrDict fields, [ Ofld o ] -> (
-            (* JS: The argument of a function call may be a record expression such as
-             * `{x="tainted", y="safe"}`, if 'sig_lval' refers to the `x` field then
-             * we want to resolve it to `"tainted"`. *)
-            match
-              fields
-              |> List.find_opt (function
-                   (* The 'o' is the offset that 'sig_lval' is referring to, here
-                    * we look for a `fld=lval` field in the record object such that
-                    * 'fld' has the same name as 'o'. *)
-                   | Field (fld, _) -> IL.compare_name fld o =|= 0
-                   | Entry _
-                   | Spread _ ->
-                       false)
-            with
-            | Some (Field (_, { e = Fetch ({ base = Var obj; _ } as lval); _ }))
-              ->
-                (* Actual argument is of the form {..., fld=lval, ...} and the offset is 'fld',
-                 * we return 'lval'. *)
-                Some (lval, obj)
-            | Some _
-            | None ->
-                None)
         | __else__ -> None)
   in
   Some (lval, snd obj.ident)
@@ -628,15 +605,10 @@ let taints_of_lval lval_env fparams (fun_exp : IL.exp) args_taints lval :
         let* (Cell (xtaints, shape)) = Lval_env.find_var lval_env var in
         Some (Xtaint.to_taints xtaints, shape)
   in
-  match (base_shape, offset) with
-  | base_shape, [] -> Some (base_taints, base_shape)
-  | Bot, _ :: _ -> None
-  | base_shape, _ :: _ ->
-      let* (Cell (xtaints, shape)) = Shape.find_in_shape offset base_shape in
-      Some (Xtaint.to_taints xtaints, shape)
+  Shape.find_in_shape_poly ~taints:base_taints offset base_shape
 
 (* What is the taint denoted by 'sig_lval' ? *)
-let taints_of_sig_lval lval_env ~check_lval fparams fun_exp args_exps
+let taints_of_sig_lval lval_env fparams fun_exp args_exps
     (args_taints : (Taints.t * shape) IL.argument list) (sig_lval : T.lval) =
   match taints_of_lval lval_env fparams fun_exp args_taints sig_lval with
   | Some (taints, shape) -> Some (taints, shape)
@@ -657,7 +629,11 @@ let taints_of_sig_lval lval_env ~check_lval fparams fun_exp args_exps
           let* lval, _obj =
             lval_of_sig_lval fun_exp fparams args_exps sig_lval
           in
-          let lval_taints, shape = check_lval lval in
+          let lval_taints, shape =
+            match Lval_env.find_lval_poly lval_env lval with
+            | None -> (Taints.empty, Bot)
+            | Some (taints, shape) -> (taints, shape)
+          in
           let lval_taints =
             lval_taints
             |> fix_lval_taints_if_global_or_a_field_of_this_class fun_exp
@@ -672,8 +648,8 @@ let taints_of_sig_lval lval_env ~check_lval fparams fun_exp args_exps
    2) Are there any effects that occur within the function due to taints being
       input into the function body, from the calling context?
 *)
-let rec instantiate_function_signature lval_env ~check_lval
-    (taint_sig : Signature.t) ~callee ~(args : _ option)
+let rec instantiate_function_signature lval_env (taint_sig : Signature.t)
+    ~callee ~(args : _ option)
     (args_taints : (Taints.t * shape) IL.argument list) : call_effects option =
   let lval_to_taints lval =
     (* This function simply produces the corresponding taints to the
@@ -687,8 +663,7 @@ let rec instantiate_function_signature lval_env ~check_lval
        So we will isolate this as a specific step to be applied as necessary.
     *)
     let opt_taints_shape =
-      taints_of_sig_lval lval_env ~check_lval taint_sig.params callee args
-        args_taints lval
+      taints_of_sig_lval lval_env taint_sig.params callee args args_taints lval
     in
     Log.debug (fun m ->
         m ~tags:sigs_tag "- Instantiating %s: %s -> %s"
@@ -882,8 +857,8 @@ let rec instantiate_function_signature lval_env ~check_lval
               (Effect.show_args_taints fun_args_taints)
               (Effect.show_args_taints args_taints));
         match
-          instantiate_function_signature lval_env ~check_lval fun_sig
-            ~callee:fun_exp ~args:None args_taints
+          instantiate_function_signature lval_env fun_sig ~callee:fun_exp
+            ~args:None args_taints
         with
         | Some call_effects -> call_effects
         | None ->
diff --git a/src/tainting/Sig_inst.mli b/src/tainting/Sig_inst.mli
index 96e8fbd6ca85..3f4adaefcbf2 100644
--- a/src/tainting/Sig_inst.mli
+++ b/src/tainting/Sig_inst.mli
@@ -12,10 +12,6 @@ type call_effects = call_effect list
 
 val instantiate_function_signature :
   Taint_lval_env.t ->
-  check_lval:(IL.lval -> Taint.Taint_set.t * Shape_and_sig.Shape.shape) ->
-  (* TODO: 'check_lval' is just a way to avoid a recursive dependency with
-   *   'Dataflow_tainting'. We should not need this when all field-sensitive
-   *   taint tracking happens through shapes. *)
   Shape_and_sig.Signature.t ->
   callee:IL.exp ->
   args:IL.exp IL.argument list option (** actual arguments *) ->
diff --git a/src/tainting/Taint.ml b/src/tainting/Taint.ml
index 2c1b757f4774..96cefef6bfa6 100644
--- a/src/tainting/Taint.ml
+++ b/src/tainting/Taint.ml
@@ -163,9 +163,10 @@ let show_offset offset =
   | Ostr s -> Printf.sprintf "[%s]" s
   | Oany -> "[*]"
 
-let show_lval { base; offset = os } =
-  show_base base
-  ^ if os <> [] then os |> List_.map show_offset |> String.concat "" else ""
+let show_offset_list offset =
+  offset |> List_.map show_offset |> String.concat ""
+
+let show_lval { base; offset } = show_base base ^ show_offset_list offset
 
 let offset_of_IL (o : IL.offset) =
   match !hook_offset_of_IL with
diff --git a/src/tainting/Taint.mli b/src/tainting/Taint.mli
index 4b5637e1dda6..df0fcc1d5b5d 100644
--- a/src/tainting/Taint.mli
+++ b/src/tainting/Taint.mli
@@ -53,6 +53,7 @@ type offset =
 
 val compare_offset : offset -> offset -> int
 val show_offset : offset -> string
+val show_offset_list : offset list -> string
 val offset_of_IL : IL.offset -> offset
 val offset_of_rev_IL_offset : rev_offset:IL.offset list -> offset list
 
@@ -64,7 +65,12 @@ val rev_IL_offset_of_offset : offset list -> IL.offset list option
   * (if there is no `Oany`).
   *)
 
-type lval = { base : base; offset : offset list }
+type lval = {
+  base : base;
+  offset : offset list;
+      (** An offset `.a.b.c` is encoded as `[.a; .b; .c]`, note the difference
+          wrt 'IL.offset', this offset list is **not** reversed! *)
+}
 (** A restriction of 'IL.lval', the l-values that are in the scope of a
  * function/method, and on which we can track taint:
  *
diff --git a/src/tainting/Taint_lval_env.ml b/src/tainting/Taint_lval_env.ml
index d34ced7d2325..94eac2566182 100644
--- a/src/tainting/Taint_lval_env.ml
+++ b/src/tainting/Taint_lval_env.ml
@@ -105,19 +105,6 @@ let union le1 le2 =
 
 let union_list ?(default = empty) les = List.fold_left union default les
 
-(* HACK: Because we don't have a "Class" type, classes have themselves as types. *)
-let is_class_name (name : IL.name) =
-  match (!(name.id_info.id_resolved), !(name.id_info.id_type)) with
-  | Some resolved1, Some { t = TyN (Id (_, { id_resolved; _ })); _ } -> (
-      match !id_resolved with
-      | None -> false
-      | Some resolved2 ->
-          (* If 'name' has type 'name' then we assume it's a class. *)
-          AST_generic.equal_resolved_name resolved1 resolved2)
-  | _, None
-  | _, Some _ ->
-      false
-
 (* Reduces an l-value into the form x.a_1. ... . a_N, the resulting l-value may
  * not represent the exact same object as the original l-value, but an
  * overapproximation. For example, the normalized l-value of `x[i]` will be `x`,
@@ -132,9 +119,14 @@ let normalize_lval lval =
         Some (x, rev_offset)
     | Var name -> (
         match rev_offset with
-        (* static class field, `C.x`, we normalize it to just `x` since `x` is
-         * a unique global *)
-        | [ { o = IL.Dot var; _ } ] when is_class_name name -> Some (var, [])
+        (* Static class field, `C.x`, we normalize it to just `x` since `x` is
+           a unique global.
+
+           TODO: C.x.y ? *)
+        | [ { o = IL.Dot var; _ } ]
+          when H.is_class_name name || IdFlags.is_static !(var.id_info.id_flags)
+          ->
+            Some (var, [])
         | __else__ -> Some (name, rev_offset))
     (* explicit dereference of `this` e.g. `this->x` *)
     | Mem { e = Fetch { base = VarSpecial (This, _); rev_offset = [] }; _ }
@@ -254,7 +246,16 @@ let find_var { tainted; _ } var = NameMap.find_opt var tainted
 let find_lval { tainted; _ } lval =
   let* var, offsets = normalize_lval lval in
   let* var_ref = NameMap.find_opt var tainted in
-  Shape.find_in_cell offsets var_ref
+  match Shape.find_in_cell offsets var_ref with
+  | `Clean
+  | `Not_found _ ->
+      None
+  | `Found cell -> Some cell
+
+let find_lval_poly { tainted; _ } lval =
+  let* var, offsets = normalize_lval lval in
+  let* var_ref = NameMap.find_opt var tainted in
+  Shape.find_in_cell_poly offsets var_ref
 
 let find_lval_xtaint env lval =
   match find_lval env lval with
diff --git a/src/tainting/Taint_lval_env.mli b/src/tainting/Taint_lval_env.mli
index f3ffb21f3898..6b7e21da3e74 100644
--- a/src/tainting/Taint_lval_env.mli
+++ b/src/tainting/Taint_lval_env.mli
@@ -65,6 +65,9 @@ val find_var : env -> IL.name -> cell option
 val find_lval : env -> IL.lval -> cell option
 (** Find the 'cell' of an l-value. *)
 
+val find_lval_poly : env -> IL.lval -> (Taint.taints * shape) option
+(** Find the 'cell' of an l-value. *)
+
 val find_lval_xtaint : env -> IL.lval -> Xtaint.t
 (** Look up an l-value on the environemnt and return whether it's tainted, clean,
     or we hold no info about it. It does not check sub-lvalues, e.g. if we record
diff --git a/src/tainting/Taint_rule_inst.ml b/src/tainting/Taint_rule_inst.ml
index fe8206f7e8b4..c963020cef8c 100644
--- a/src/tainting/Taint_rule_inst.ml
+++ b/src/tainting/Taint_rule_inst.ml
@@ -46,7 +46,7 @@ type spec_predicates = {
        *
        * Propagators allow to specify how taint propagates through side effects.
        *
-       * Note that we tried to solve this with a hack in returntocorp/semgrep#5150
+       * Note that we tried to solve this with a hack in semgrep/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`.
        *
diff --git a/src/tainting/Taint_shape.ml b/src/tainting/Taint_shape.ml
index 4237355e00e9..8ebe8074adff 100644
--- a/src/tainting/Taint_shape.ml
+++ b/src/tainting/Taint_shape.ml
@@ -65,6 +65,82 @@ let taints_and_shape_are_relevant taints shape =
        * by INVARIANT(cell) it contains some taint or has field marked clean. *)
       true
 
+(* TODO: This should fix shapes too. *)
+let fix_poly_taint_with_offset offset taints =
+  let type_of_offset o =
+    match o with
+    | T.Ofld n -> !(n.id_info.id_type)
+    | _ -> None
+  in
+  let add_offset_to_lval o ({ offset; _ } as orig_lval : T.lval) =
+    let extended_lval = { orig_lval with offset = orig_lval.offset @ [ o ] } in
+    if
+      (* If the offset we are trying to take is already in the
+           list of offsets, don't append it! This is so we don't
+           never-endingly loop the dataflow and make it think the
+           Arg taint is never-endingly changing.
+
+           For instance, this code example would previously loop,
+           if `x` started with an `Arg` taint:
+           while (true) { x = x.getX(); }
+      *)
+      (not (List.mem o offset))
+      && (* For perf reasons we don't allow offsets to get too long.
+          * Otherwise in a long chain of function calls where each
+          * function adds some offset, we could end up a very large
+          * amount of polymorphic taint.
+          * This actually happened with rule
+          * semgrep.perf.rules.express-fs-filename from the Pro
+          * benchmarks, and file
+          * WebGoat/src/main/resources/webgoat/static/js/libs/ace.js.
+          *
+          * TODO: This is way less likely to happen if we had better
+          *   type info and we used it to remove taint, e.g. if Boolean
+          *   and integer expressions didn't propagate taint. *)
+      List.length offset < Limits_semgrep.taint_MAX_POLY_OFFSET
+    then extended_lval
+    else (
+      Log.warn (fun m ->
+          m "Taint_lval_env.fix_poly_taint_with_offset: %s is too long"
+            (T.show_lval extended_lval));
+      orig_lval)
+  in
+  offset
+  |> List.fold_left
+       (fun taints o ->
+         match (type_of_offset o, o) with
+         | Some { t = TyFun _; _ }, _ ->
+             (* We have an l-value like `o.f` where `f` has a function type,
+              * so it's a method call, we return nothing here. We cannot just
+              * return `xtaint`, which is the taint of `o` in the environment;
+              * whether that taint propagates or not is determined in
+              * 'check_tainted_instr'/'Call'. Otherwise, if `o` had taint var
+              * 'o@i', the call `o.getX()` would have taints '{o@i, o@i.x}'
+              * when it should only have taints '{o@i.x}'. *)
+             Taints.empty
+         | _, Oany ->
+             (* Cannot handle this offset. *)
+             taints
+         | __any__, ((Ofld _ | Ostr _ | Oint _) as o) ->
+             (* Not a method call (to the best of our knowledge) or
+              * an unresolved Java `getX` method. *)
+             let taints' =
+               taints
+               |> Taints.map (fun taint ->
+                      match taint.orig with
+                      | Var lval ->
+                          let lval' = add_offset_to_lval o lval in
+                          { taint with orig = Var lval' }
+                      | Shape_var lval ->
+                          let lval' = add_offset_to_lval o lval in
+                          { taint with orig = Shape_var lval' }
+                      | Src _
+                      | Control ->
+                          taint)
+             in
+             taints')
+       taints
+
 (*********************************************************)
 (* Unification (merging shapes) *)
 (*********************************************************)
@@ -241,54 +317,90 @@ let gather_all_taints_in_shape = gather_all_taints_in_shape_acc Taints.empty
 (* Find an offset *)
 (*********************************************************)
 
-let rec find_in_cell offset cell =
-  let (Cell (_xtaint, shape)) = cell in
+let rec find_in_cell_w_carry ~taints offset cell =
+  let (Cell (xtaint, shape)) = cell in
   match offset with
-  | [] -> Some cell
-  | _ :: _ -> find_in_shape offset shape
-
-and find_in_shape offset shape =
+  | [] -> `Found cell
+  | _ :: _ -> (
+      match xtaint with
+      | `Clean ->
+          if shape <> Bot then
+            Log.err (fun m ->
+                m "BUG: Taint_shape.find_in_cell: INVARIANT(cell).2 is broken");
+          `Clean
+      | `None -> find_in_shape_w_carry ~taints offset shape
+      | `Tainted taints -> find_in_shape_w_carry ~taints offset shape)
+
+and find_in_shape_w_carry ~taints offset shape =
+  let not_found = `Not_found (taints, shape, offset) in
   match shape with
   (* offset <> [] *)
-  | Bot -> None
-  | Obj obj -> find_in_obj offset obj
+  | Bot -> not_found
+  | Obj obj -> find_in_obj_w_carry ~taints offset obj
   | Arg _ ->
       (* TODO: Here we should "refine" the arg shape, it should be an Obj shape. *)
       Log.debug (fun m ->
           m "Could not find offset %s in polymorphic shape %s"
             (debug_offset offset) (show_shape shape));
-      None
+      not_found
   | Fun _ ->
       (* This is an error, we just don't want to crash here. *)
       Log.err (fun m ->
           m "Could not find offset %s in function shape %s"
             (debug_offset offset) (show_shape shape));
-      None
+      not_found
 
-and find_in_obj (offset : T.offset list) obj =
+and find_in_obj_w_carry ~taints (offset : T.offset list) obj =
+  let not_found = `Not_found (taints, Obj obj, offset) in
   (* offset <> [] *)
   match offset with
   | [] ->
-      Log.err (fun m -> m "fix_xtaint_obj: Impossible happened: empty offset");
-      None
+      Log.err (fun m -> m "BUG: Taint_shape.fix_xtaint_obj: empty offset");
+      not_found
   | o :: offset -> (
       match o with
-      | Oany (* arbitrary index [*] *) ->
+      | Oany (* arbitrary index [*] *) -> (
           (* consider all fields/indexes *)
-          Fields.fold
-            (fun _ cell acc ->
-              match (acc, find_in_cell offset cell) with
-              | None, None -> None
-              | Some cell, None
-              | None, Some cell ->
-                  Some cell
-              | Some cell1, Some cell2 -> Some (unify_cell cell1 cell2))
-            obj None
+          match
+            Fields.fold
+              (fun _ cell acc ->
+                match (acc, find_in_cell_w_carry ~taints offset cell) with
+                | None, (`Not_found _ | `Clean) -> None
+                | Some cell, (`Not_found _ | `Clean)
+                | None, `Found cell ->
+                    Some cell
+                | Some cell1, `Found cell2 -> Some (unify_cell cell1 cell2))
+              obj None
+          with
+          | None -> not_found
+          | Some cell -> `Found cell)
       | Ofld _
       | Oint _
-      | Ostr _ ->
-          let* o_cell = Fields.find_opt o obj in
-          find_in_cell offset o_cell)
+      | Ostr _ -> (
+          match Fields.find_opt o obj with
+          | None -> not_found
+          | Some o_cell -> find_in_cell_w_carry ~taints offset o_cell))
+
+let find_in_cell offset cell =
+  find_in_cell_w_carry ~taints:Taints.empty offset cell
+
+let option_of_find_result res =
+  match res with
+  | `Clean -> None
+  | `Not_found (taints, _shape, offset) ->
+      (* TODO: Fix _shape too. *)
+      let taints = fix_poly_taint_with_offset offset taints in
+      Some (taints, Bot)
+  | `Found (Cell (xtaint, shape)) -> Some (Xtaint.to_taints xtaint, shape)
+
+let find_in_cell_poly offset cell =
+  find_in_cell offset cell |> option_of_find_result
+
+let find_in_shape_poly ~taints offset shape =
+  match offset with
+  | [] -> Some (taints, shape)
+  | _ :: _ ->
+      find_in_shape_w_carry ~taints offset shape |> option_of_find_result
 
 (*********************************************************)
 (* Update the xtaint and shape of an offset *)
diff --git a/src/tainting/Taint_shape.mli b/src/tainting/Taint_shape.mli
index f82f0e135a16..a2586441602f 100644
--- a/src/tainting/Taint_shape.mli
+++ b/src/tainting/Taint_shape.mli
@@ -6,6 +6,13 @@ val taints_and_shape_are_relevant : Taint.taints -> shape -> bool
 (** [true] iff the union of [taints] and [gather_all_taints_in_shape shape]
  * is non-empty, or if [shape] contains a cleaned offset. *)
 
+val fix_poly_taint_with_offset :
+  Taint.offset list -> Taint.taints -> Taint.taints
+(** Fix taints with an offset. It just attaches the offset to each polymorphic
+    taint variable (see 'Taint.Var') in the set.
+
+    FEATURE(field-sensitivity) *)
+
 val tuple_like_obj : (Taint.taints * shape) list -> shape
 (** Constructs a 0-indexed tuple-like 'obj' from a list of pairs, taints and shape,
  * for each element in the tuple.  *)
@@ -33,8 +40,41 @@ val gather_all_taints_in_cell : cell -> Taint.taints
 val gather_all_taints_in_shape : shape -> Taint.taints
 (** Gather and union all taints reachable through a shape. *)
 
-val find_in_cell : Taint.offset list -> cell -> cell option
-val find_in_shape : Taint.offset list -> shape -> cell option
+val find_in_cell :
+  Taint.offset list ->
+  cell ->
+  [ `Found of cell
+  | `Clean
+  | `Not_found of Taint.taints * shape * Taint.offset list ]
+(** Find an offset in a cell.
+
+    This is a somewhat "low-level" version.
+
+    If the offset could not be found in the cell, then it returns `Not_found
+    with the base taints and shape for the offset prefix that was found, and
+    the offset suffix that was not. *)
+
+val find_in_cell_poly :
+  Taint.offset list -> cell -> (Taint.taints * shape) option
+(** Find an offset in a cell, BUT if the full offset cannot be found, then
+    it returns the taints of the offset prefix that was found; and if those
+    taints are polymorphic, then it adds to them the remaining offset.
+
+    For example, if `x` is tainted but `x.a` is not being tracked, it just
+    assigns to `x.a` the same taints as `x`. If `x` had polymorphic taint
+    (see 'Taint.Var'), then it would attach the offset `.a` to it.
+
+    TODO: We need to fix polymorphic shapes too instad of just returning 'Bot'.
+
+    FEATURE(field-sensitivity) *)
+
+val find_in_shape_poly :
+  taints:Taint.taints ->
+  Taint.offset list ->
+  shape ->
+  (Taint.taints * shape) option
+(** Like 'find_in_cell_poly' but to find an offset in a shape, the 'taints'
+    are the "base taints" in case the offset cannot be found. *)
 
 val update_offset_in_cell :
   f:(Xtaint.t -> shape -> Xtaint.t * shape) ->
diff --git a/src/target/Target.ml b/src/target/Target.ml
index ff56f2eb6dac..336bb87b5255 100644
--- a/src/target/Target.ml
+++ b/src/target/Target.ml
@@ -101,7 +101,7 @@ let mk_regular ?lockfile analyzer products (origin : Origin.t) : regular =
 let mk_target (xlang : Xlang.t) (file : Fpath.t) : t =
   let all = Product.all in
   (* TODO: should do the check in the other mk_xxx ? *)
-  assert (UFile.is_file file);
+  assert (UFile.is_reg ~follow_symlinks:true file);
   Regular (mk_regular xlang all (Origin.File file))
 
 (*****************************************************************************)
diff --git a/src/targeting/Find_targets.ml b/src/targeting/Find_targets.ml
index 75ddc76fea08..6e4fea580486 100644
--- a/src/targeting/Find_targets.ml
+++ b/src/targeting/Find_targets.ml
@@ -585,7 +585,7 @@ let group_scanning_roots_by_files_and_projects (conf : conf)
   let dir_scanning_roots, file_scanning_roots =
     List.partition
       (fun x ->
-        UFile.dir_exists
+        UFile.is_dir ~follow_symlinks:true
           (Fpath.append (Fpath.v (Sys.getcwd ())) (Scanning_root.to_fpath x)))
       scanning_roots
   in
diff --git a/stats/autofix-printing-stats/projects.txt b/stats/autofix-printing-stats/projects.txt
index 1c3b208b0190..2aacfe62814a 100644
--- a/stats/autofix-printing-stats/projects.txt
+++ b/stats/autofix-printing-stats/projects.txt
@@ -1,1 +1,1 @@
-https://github.com/returntocorp/semgrep-rules.git
+https://github.com/semgrep/semgrep-rules.git
diff --git a/stats/parsing-stats/lang/ocaml/projects.txt b/stats/parsing-stats/lang/ocaml/projects.txt
index 384d531af459..4776361081b8 100644
--- a/stats/parsing-stats/lang/ocaml/projects.txt
+++ b/stats/parsing-stats/lang/ocaml/projects.txt
@@ -2,8 +2,7 @@
 # one per line.
 
 # R2C'S OCaml projects
-https://github.com/returntocorp/semgrep
-https://github.com/returntocorp/pfff
+https://github.com/semgrep/semgrep
 
 # Projects with more than 1000 stars on GitHub.
 # (we could add more, we're looking for large amounts of handwritten
diff --git a/tools/otarzan/README.md b/tools/otarzan/README.md
index a59e5bfc45ab..e51d06eab3a5 100644
--- a/tools/otarzan/README.md
+++ b/tools/otarzan/README.md
@@ -13,7 +13,7 @@ boilerplate that a human can start from and would like to further edit
 (e.g., for AST_to_IL.ml in semgrep, for AST_to_SAST.ml in semgrep-pro).
 Otarzan is such a tool.
 
-See https://github.com/returntocorp/semgrep/issues/6440 for more
+See https://github.com/semgrep/semgrep/issues/6440 for more
 context on this work.
 
 alternatives:


Test patch:
diff --git a/.github/workflows/build-test-core-x86.yml b/.github/workflows/build-test-core-x86.yml
index 22cccc2a6822..4f466e7b1038 100644
--- a/.github/workflows/build-test-core-x86.yml
+++ b/.github/workflows/build-test-core-x86.yml
@@ -32,7 +32,7 @@ jobs:
           mkdir artifacts
           cp bin/semgrep-core artifacts/
           tar czf artifacts.tgz artifacts
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
           name: semgrep-core-x86-artifact
           path: artifacts.tgz
diff --git a/.github/workflows/build-test-docker.jsonnet b/.github/workflows/build-test-docker.jsonnet
index 4353ffa2cd1a..6277dea7f23d 100644
--- a/.github/workflows/build-test-docker.jsonnet
+++ b/.github/workflows/build-test-docker.jsonnet
@@ -1,7 +1,7 @@
 // Build and validate our (multi-arch) semgrep docker image defined in our
 // Dockerfile and save it as a GHA artifact.
 //
-// Note that the actual push to https://hub.docker.com/r/returntocorp/semgrep
+// Note that the actual push to https://hub.docker.com/r/semgrep/semgrep
 // is done in the push-docker.jsonnet workflow, not here.
 
 local gha = import 'libs/gha.libsonnet';
@@ -31,7 +31,7 @@ local inputs(default) = {
     },
     'repository-name': {
       type: 'string',
-      description: 'The repository/name of the docker image to push, e.g., returntocorp/semgrep',
+      description: 'The repository/name of the docker image to push, e.g., semgrep/semgrep',
       required: true,
 
     } + if default then {default: 'returntocorp/semgrep'} else {},
@@ -127,7 +127,7 @@ local job = {
       run: './scripts/validate-docker-build.sh ${{ steps.build-image.outputs.imageid }} linux/${{ matrix.architecture }}',
     },
     {
-      uses: 'actions/upload-artifact@v3',
+      uses: 'actions/upload-artifact@v4',
       with: {
         name: '${{ inputs.artifact-name }}-arch-${{ matrix.architecture }}',
         path: '/tmp/image.tar',
diff --git a/.github/workflows/build-test-docker.yml b/.github/workflows/build-test-docker.yml
index 6fd414fa67e3..8135fd9ee4d3 100644
--- a/.github/workflows/build-test-docker.yml
+++ b/.github/workflows/build-test-docker.yml
@@ -38,7 +38,7 @@ jobs:
       - if: ${{ inputs.enable-tests }}
         name: Test Image
         run: ./scripts/validate-docker-build.sh ${{ steps.build-image.outputs.imageid }} linux/${{ matrix.architecture }}
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
           name: ${{ inputs.artifact-name }}-arch-${{ matrix.architecture }}
           path: /tmp/image.tar
@@ -72,7 +72,7 @@ on:
         required: true
         type: string
       repository-name:
-        description: The repository/name of the docker image to push, e.g., returntocorp/semgrep
+        description: The repository/name of the docker image to push, e.g., semgrep/semgrep
         required: true
         type: string
       target:
@@ -104,7 +104,7 @@ on:
         type: string
       repository-name:
         default: returntocorp/semgrep
-        description: The repository/name of the docker image to push, e.g., returntocorp/semgrep
+        description: The repository/name of the docker image to push, e.g., semgrep/semgrep
         required: true
         type: string
       target:
diff --git a/.github/workflows/build-test-manylinux-aarch64.jsonnet b/.github/workflows/build-test-manylinux-aarch64.jsonnet
index 3a3893eda1c0..2b84f8068922 100644
--- a/.github/workflows/build-test-manylinux-aarch64.jsonnet
+++ b/.github/workflows/build-test-manylinux-aarch64.jsonnet
@@ -62,7 +62,7 @@ local build_wheels_job = {
       |||,
     },
     {
-      uses: 'actions/upload-artifact@v3',
+      uses: 'actions/upload-artifact@v4',
       with: {
         path: 'semgrep/cli/dist.zip',
         name: wheel_name,
diff --git a/.github/workflows/build-test-manylinux-aarch64.yml b/.github/workflows/build-test-manylinux-aarch64.yml
index 65e02a60a5cd..022fddb45892 100644
--- a/.github/workflows/build-test-manylinux-aarch64.yml
+++ b/.github/workflows/build-test-manylinux-aarch64.yml
@@ -39,7 +39,7 @@ jobs:
           #
           # we ended up hitting an edge case where the final ~100kb doesn't always get written to disk, which results in a corrupted zip file.
           # our theory is that the pipe between the container and the host is being closed prematurely.
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
           name: manylinux-aarch64-wheel
           path: semgrep/cli/dist.zip
diff --git a/.github/workflows/build-test-manylinux-x86.jsonnet b/.github/workflows/build-test-manylinux-x86.jsonnet
index 9d4d931977aa..a42ee2105465 100644
--- a/.github/workflows/build-test-manylinux-x86.jsonnet
+++ b/.github/workflows/build-test-manylinux-x86.jsonnet
@@ -38,12 +38,14 @@ local build_wheels_job = {
   container: manylinux_container,
   steps: [
     actions.checkout_with_submodules(),
+    // TODO: use semgrep.default_python_version instead of hardcoding 3.9 below
+    // coupling: if you modify the python version, update the cp39-cp39 further below
     {
       run: |||
         yum update -y
-        yum install -y zip python3-pip python3.8
+        yum install -y zip python3-pip python3.9
         alternatives --remove-all python3
-        alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1
+        alternatives --install /usr/bin/python3 python3 /usr/bin/python3.9 1
         alternatives --auto python3
       |||,
     },
@@ -56,7 +58,7 @@ local build_wheels_job = {
       |||,
     },
     {
-      uses: 'actions/upload-artifact@v3',
+      uses: 'actions/upload-artifact@v4',
       with: {
         name: wheel_name,
         path: 'cli/dist.zip',
@@ -80,21 +82,21 @@ local test_wheels_job = {
     // platform compatibility tag
     {
       name: 'install package',
-      run: '/opt/python/cp38-cp38/bin/pip install dist/*.whl',
+      run: '/opt/python/cp39-cp39/bin/pip install dist/*.whl',
     },
     // TODO? could reuse build-test-osx-x86.test_semgrep_steps
     // only diff is PATH adjustments
     {
       name: 'test package',
       run: |||
-        export PATH=/opt/python/cp38-cp38/bin:$PATH
+        export PATH=/opt/python/cp39-cp39/bin:$PATH
         semgrep --version
       |||,
     },
     {
       name: 'e2e semgrep-core test',
       run: |||
-        export PATH=/opt/python/cp38-cp38/bin:$PATH
+        export PATH=/opt/python/cp39-cp39/bin:$PATH
         echo '1 == 1' | semgrep -l python -e '$X == $X' -
       |||,
     },
@@ -114,7 +116,7 @@ local test_wheels_venv_job = {
     },
     {
       name: 'create venv',
-      run: '/opt/python/cp38-cp38/bin/python3 -m venv env',
+      run: '/opt/python/cp39-cp39/bin/python3 -m venv env',
     },
     // *.whl is fine here because we're building one wheel with the "any"
     // platform compatibility tag
diff --git a/.github/workflows/build-test-manylinux-x86.yml b/.github/workflows/build-test-manylinux-x86.yml
index a3843eda7735..b5be09a83bbe 100644
--- a/.github/workflows/build-test-manylinux-x86.yml
+++ b/.github/workflows/build-test-manylinux-x86.yml
@@ -9,18 +9,18 @@ jobs:
           submodules: true
       - run: |
           yum update -y
-          yum install -y zip python3-pip python3.8
+          yum install -y zip python3-pip python3.9
           alternatives --remove-all python3
-          alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1
+          alternatives --install /usr/bin/python3 python3 /usr/bin/python3.9 1
           alternatives --auto python3
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: semgrep-core-x86-artifact
       - run: |
           tar xf artifacts.tgz
           cp artifacts/semgrep-core cli/src/semgrep/bin
           ./scripts/build-wheels.sh
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
           name: manylinux-x86-wheel
           path: cli/dist.zip
@@ -30,19 +30,19 @@ jobs:
       - build-wheels
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: manylinux-x86-wheel
       - run: unzip dist.zip
       - name: install package
-        run: /opt/python/cp38-cp38/bin/pip install dist/*.whl
+        run: /opt/python/cp39-cp39/bin/pip install dist/*.whl
       - name: test package
         run: |
-          export PATH=/opt/python/cp38-cp38/bin:$PATH
+          export PATH=/opt/python/cp39-cp39/bin:$PATH
           semgrep --version
       - name: e2e semgrep-core test
         run: |
-          export PATH=/opt/python/cp38-cp38/bin:$PATH
+          export PATH=/opt/python/cp39-cp39/bin:$PATH
           echo '1 == 1' | semgrep -l python -e '$X == $X' -
   test-wheels-venv:
     container: quay.io/pypa/manylinux_2_28_x86_64
@@ -50,12 +50,12 @@ jobs:
       - build-wheels
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: manylinux-x86-wheel
       - run: unzip dist.zip
       - name: create venv
-        run: /opt/python/cp38-cp38/bin/python3 -m venv env
+        run: /opt/python/cp39-cp39/bin/python3 -m venv env
       - name: install package
         run: env/bin/pip install dist/*.whl
       - name: test package
@@ -69,7 +69,7 @@ jobs:
       - build-wheels
     runs-on: windows-latest
     steps:
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: manylinux-x86-wheel
       - run: unzip dist.zip
diff --git a/.github/workflows/build-test-osx-arm64.jsonnet b/.github/workflows/build-test-osx-arm64.jsonnet
index 88bfc9895f82..e14238fa6cbc 100644
--- a/.github/workflows/build-test-osx-arm64.jsonnet
+++ b/.github/workflows/build-test-osx-arm64.jsonnet
@@ -96,7 +96,7 @@ local build_wheels_job = {
       |||,
     },
     {
-      uses: 'actions/upload-artifact@v3',
+      uses: 'actions/upload-artifact@v4',
       with: {
         path: 'cli/dist.zip',
         name: wheel_name,
diff --git a/.github/workflows/build-test-osx-arm64.yml b/.github/workflows/build-test-osx-arm64.yml
index f1d642359308..4ea7ec5d92c7 100644
--- a/.github/workflows/build-test-osx-arm64.yml
+++ b/.github/workflows/build-test-osx-arm64.yml
@@ -35,7 +35,7 @@ jobs:
           mkdir artifacts
           cp ./bin/semgrep-core artifacts/
           tar czf artifacts.tgz artifacts
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
           name: semgrep-osx-arm64-${{ github.sha }}
           path: artifacts.tgz
@@ -61,14 +61,14 @@ jobs:
       - uses: actions/checkout@v3
         with:
           submodules: true
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: semgrep-osx-arm64-${{ github.sha }}
       - run: |
           tar xvfz artifacts.tgz
           cp artifacts/semgrep-core cli/src/semgrep/bin
           ./scripts/build-wheels.sh --plat-name macosx_11_0_arm64
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
           name: osx-arm64-wheel
           path: cli/dist.zip
@@ -89,7 +89,7 @@ jobs:
       - uses: actions/setup-python@v4
         with:
           python-version: "3.11"
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: osx-arm64-wheel
       - run: unzip dist.zip
diff --git a/.github/workflows/build-test-osx-x86.jsonnet b/.github/workflows/build-test-osx-x86.jsonnet
index 7d39c8af8a81..9cbfdf599895 100644
--- a/.github/workflows/build-test-osx-x86.jsonnet
+++ b/.github/workflows/build-test-osx-x86.jsonnet
@@ -92,7 +92,7 @@ local build_wheels_job = {
       |||,
     },
     {
-      uses: 'actions/upload-artifact@v3',
+      uses: 'actions/upload-artifact@v4',
       with: {
         path: 'cli/dist.zip',
         name: wheel_name,
diff --git a/.github/workflows/build-test-osx-x86.yml b/.github/workflows/build-test-osx-x86.yml
index 1ae42852e433..5ab0e466219e 100644
--- a/.github/workflows/build-test-osx-x86.yml
+++ b/.github/workflows/build-test-osx-x86.yml
@@ -23,7 +23,7 @@ jobs:
           mkdir artifacts
           cp ./bin/semgrep-core artifacts/
           tar czf artifacts.tgz artifacts
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
           name: semgrep-osx-${{ github.sha }}
           path: artifacts.tgz
@@ -35,14 +35,14 @@ jobs:
       - uses: actions/checkout@v3
         with:
           submodules: true
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: semgrep-osx-${{ github.sha }}
       - run: |
           tar xvfz artifacts.tgz
           cp artifacts/semgrep-core cli/src/semgrep/bin
           ./scripts/build-wheels.sh --plat-name macosx_10_14_x86_64
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
           name: osx-x86-wheel
           path: cli/dist.zip
@@ -51,7 +51,7 @@ jobs:
       - build-wheels
     runs-on: macos-13
     steps:
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: osx-x86-wheel
       - run: unzip dist.zip
diff --git a/.github/workflows/build-test-windows-x86.jsonnet b/.github/workflows/build-test-windows-x86.jsonnet
index 2b7c894ac1ac..e3a5164694f8 100644
--- a/.github/workflows/build-test-windows-x86.jsonnet
+++ b/.github/workflows/build-test-windows-x86.jsonnet
@@ -188,7 +188,7 @@ local build_wheels_job = {
       |||,
     },
     {
-      uses: 'actions/upload-artifact@v3',
+      uses: 'actions/upload-artifact@v4',
       with: {
         path: 'cli/dist.tgz',
         name: wheel_name,
diff --git a/.github/workflows/build-test-windows-x86.yml b/.github/workflows/build-test-windows-x86.yml
index 703c266bb5b1..9499652b29d4 100644
--- a/.github/workflows/build-test-windows-x86.yml
+++ b/.github/workflows/build-test-windows-x86.yml
@@ -101,7 +101,7 @@ jobs:
           cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libintl-8.dll artifacts/
 
           tar czvf artifacts.tgz artifacts
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
           name: semgrep-core-and-dependent-libs-w64-artifact-${{ github.sha }}
           path: artifacts.tgz
@@ -116,7 +116,7 @@ jobs:
       - uses: actions/checkout@v3
         with:
           submodules: true
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: semgrep-core-and-dependent-libs-w64-artifact-${{ github.sha }}
       - env:
@@ -125,7 +125,7 @@ jobs:
           tar xvfz artifacts.tgz
           cp artifacts/* cli/src/semgrep/bin
           ./scripts/build-wheels.sh --plat-name win_amd64
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
           name: windows-x86-wheel
           path: cli/dist.tgz
@@ -137,7 +137,7 @@ jobs:
       - build-wheels
     runs-on: windows-latest
     steps:
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: windows-x86-wheel
       - run: tar xzvf dist.tgz
diff --git a/.github/workflows/test-e2e-semgrep-ci.jsonnet b/.github/workflows/test-e2e-semgrep-ci.jsonnet
index 7b0acf7a3787..c6f81de50adb 100644
--- a/.github/workflows/test-e2e-semgrep-ci.jsonnet
+++ b/.github/workflows/test-e2e-semgrep-ci.jsonnet
@@ -1,5 +1,5 @@
 // Daily cron to check end-to-end (e2e) that the 'semgrep ci' subcommand,
-// run from a returntocorp/semgrep:develop docker image, works correctly.
+// run from a semgrep/semgrep:develop docker image, works correctly.
 // This is very important because 'semgrep ci' and our docker images
 // are the main things the users of our Semgrep WebApp are using in their CIs.
 // This cron also double checks that "fail-open" is working as expected, that is
@@ -77,7 +77,7 @@ local semgrep_ci_job = {
   },
   needs: 'get-inputs',
   container: {
-    image: 'returntocorp/semgrep:' + docker_tag,
+    image: 'semgrep/semgrep:' + docker_tag,
   },
   steps: [
     actions.checkout(),
@@ -100,7 +100,7 @@ local semgrep_ci_fail_open_job = {
   },
   needs: 'get-inputs',
   container: {
-    image: 'returntocorp/semgrep:' + docker_tag,
+    image: 'semgrep/semgrep:' + docker_tag,
   },
   steps: [
     actions.checkout(),
@@ -134,7 +134,7 @@ local semgrep_ci_fail_open_blocking_findings_job = {
   },
   needs: 'get-inputs',
   container: {
-    image: 'returntocorp/semgrep:' + docker_tag,
+    image: 'semgrep/semgrep:' + docker_tag,
   },
   steps: [
     actions.checkout(),
@@ -183,7 +183,7 @@ local semgrep_ci_fail_open_blocking_findings_job = {
 // PR check on another repo
 // ----------------------------------------------------------------------------
 
-// The two jobs below work together and with the returntocorp/e2e repo
+// The two jobs below work together and with the semgrep/e2e repo
 // to check that 'semgrep ci' reports only new findings on PRs.
 // This 'e2e' repo contains:
 //  - a file-under-test.py with a '10 == 10' that
@@ -196,7 +196,7 @@ local semgrep_ci_fail_open_blocking_findings_job = {
 // Because semgrep ci reports only new findings, the goal of the jobs below is to
 // make sure no finding are reported since we don't modify file-under-test.py
 
-// just open a PR on the returntocorp/e2e repo
+// just open a PR on the semgrep/e2e repo
 local semgrep_ci_on_pr_job = {
   'runs-on': 'ubuntu-22.04',
   needs: 'get-inputs',
@@ -207,7 +207,7 @@ local semgrep_ci_on_pr_job = {
     {
       uses: 'actions/checkout@v3',
       with: {
-        repository: 'returntocorp/e2e',
+        repository: 'semgrep/e2e',
         ref: '${{ github.event.repository.default_branch }}',
         token: semgrep.github_bot.token_ref,
       },
@@ -240,7 +240,7 @@ local semgrep_ci_on_pr_job = {
 
 
 // TODO: factorize with start-release.jsonnet
-local len_checks = "$(gh pr -R returntocorp/e2e view %s --json statusCheckRollup --jq '.statusCheckRollup | length')" % pr_number;
+local len_checks = "$(gh pr -R semgrep/e2e view %s --json statusCheckRollup --jq '.statusCheckRollup | length')" % pr_number;
 
 // TODO: factorize with start-release.jsonnet
 local wait_for_checks_job = {
@@ -261,7 +261,7 @@ local wait_for_checks_job = {
         done
         echo "checks are valid"
         echo ${LEN_CHECKS}
-        gh pr -R returntocorp/e2e view %s --json statusCheckRollup
+        gh pr -R semgrep/e2e view %s --json statusCheckRollup
       ||| % [len_checks, len_checks, pr_number],
     },
     {
@@ -269,7 +269,7 @@ local wait_for_checks_job = {
       env: {
         GITHUB_TOKEN: semgrep.github_bot.token_ref,
       },
-      run: 'gh pr -R returntocorp/e2e checks %s --interval 30 --watch' % pr_number,
+      run: 'gh pr -R semgrep/e2e checks %s --interval 30 --watch' % pr_number,
     },
   ],
 };
@@ -298,7 +298,7 @@ local wait_for_checks_job = {
     'wait-for-checks': wait_for_checks_job,
     'notify-failure':
        semgrep.slack.notify_failure_job
-        ("The End to end semgrep ci workflow failed with docker tag %s. The PR in `returntocorp/e2e` that had the failure was %s. See https://github.com/semgrep/semgrep/actions/workflows/test-e2e-semgrep-ci.yml for more info" % [docker_tag, pr_number])
+        ("The End to end semgrep ci workflow failed with docker tag %s. The PR in `semgrep/e2e` that had the failure was %s. See https://github.com/semgrep/semgrep/actions/workflows/test-e2e-semgrep-ci.yml for more info" % [docker_tag, pr_number])
        + { needs: [
           'semgrep-ci',
           'semgrep-ci-on-pr',
diff --git a/.github/workflows/test-e2e-semgrep-ci.yml b/.github/workflows/test-e2e-semgrep-ci.yml
index 7a8627c286c1..23b97e6b43b9 100644
--- a/.github/workflows/test-e2e-semgrep-ci.yml
+++ b/.github/workflows/test-e2e-semgrep-ci.yml
@@ -29,11 +29,11 @@ jobs:
            --url  ${{ secrets.NOTIFICATIONS_URL }} \
            --header 'content-type: application/json' \
            --data "{
-             \"text\": \"The End to end semgrep ci workflow failed with docker tag ${{ needs.get-inputs.outputs.docker_tag }}. The PR in `returntocorp/e2e` that had the failure was ${{ needs.semgrep-ci-on-pr.outputs.pr-number }}. See https://github.com/semgrep/semgrep/actions/workflows/test-e2e-semgrep-ci.yml for more info\"
+             \"text\": \"The End to end semgrep ci workflow failed with docker tag ${{ needs.get-inputs.outputs.docker_tag }}. The PR in `semgrep/e2e` that had the failure was ${{ needs.semgrep-ci-on-pr.outputs.pr-number }}. See https://github.com/semgrep/semgrep/actions/workflows/test-e2e-semgrep-ci.yml for more info\"
            }"
   semgrep-ci:
     container:
-      image: returntocorp/semgrep:${{ needs.get-inputs.outputs.docker_tag }}
+      image: semgrep/semgrep:${{ needs.get-inputs.outputs.docker_tag }}
     env:
       SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_E2E_APP_TOKEN }}
     needs: get-inputs
@@ -43,7 +43,7 @@ jobs:
       - run: semgrep ci
   semgrep-ci-fail-open:
     container:
-      image: returntocorp/semgrep:${{ needs.get-inputs.outputs.docker_tag }}
+      image: semgrep/semgrep:${{ needs.get-inputs.outputs.docker_tag }}
     env:
       SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_E2E_APP_TOKEN }}
       SEMGREP_APP_URL: https://staging.semgrep.dev
@@ -59,7 +59,7 @@ jobs:
       - run: semgrep ci --suppress-errors
   semgrep-ci-fail-open-blocking-findings:
     container:
-      image: returntocorp/semgrep:${{ needs.get-inputs.outputs.docker_tag }}
+      image: semgrep/semgrep:${{ needs.get-inputs.outputs.docker_tag }}
     env:
       SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_E2E_APP_TOKEN }}
       SEMGREP_USER_AGENT_APPEND: semgrep-ci-e2e
@@ -108,7 +108,7 @@ jobs:
       - uses: actions/checkout@v3
         with:
           ref: ${{ github.event.repository.default_branch }}
-          repository: returntocorp/e2e
+          repository: semgrep/e2e
           token: ${{ steps.token.outputs.token }}
       - name: Prepare the PR
         run: |
@@ -153,19 +153,19 @@ jobs:
           GITHUB_TOKEN: ${{ steps.token.outputs.token }}
         name: Wait for checks to register
         run: |
-          LEN_CHECKS=$(gh pr -R returntocorp/e2e view ${{ needs.semgrep-ci-on-pr.outputs.pr-number }} --json statusCheckRollup --jq '.statusCheckRollup | length');
+          LEN_CHECKS=$(gh pr -R semgrep/e2e view ${{ needs.semgrep-ci-on-pr.outputs.pr-number }} --json statusCheckRollup --jq '.statusCheckRollup | length');
           while [ ${LEN_CHECKS} = "0" ]; do
             echo "No checks available yet"
             sleep 30
-            LEN_CHECKS=$(gh pr -R returntocorp/e2e view ${{ needs.semgrep-ci-on-pr.outputs.pr-number }} --json statusCheckRollup --jq '.statusCheckRollup | length');
+            LEN_CHECKS=$(gh pr -R semgrep/e2e view ${{ needs.semgrep-ci-on-pr.outputs.pr-number }} --json statusCheckRollup --jq '.statusCheckRollup | length');
           done
           echo "checks are valid"
           echo ${LEN_CHECKS}
-          gh pr -R returntocorp/e2e view ${{ needs.semgrep-ci-on-pr.outputs.pr-number }} --json statusCheckRollup
+          gh pr -R semgrep/e2e view ${{ needs.semgrep-ci-on-pr.outputs.pr-number }} --json statusCheckRollup
       - env:
           GITHUB_TOKEN: ${{ steps.token.outputs.token }}
         name: Wait for checks to complete
-        run: gh pr -R returntocorp/e2e checks ${{ needs.semgrep-ci-on-pr.outputs.pr-number }} --interval 30 --watch
+        run: gh pr -R semgrep/e2e checks ${{ needs.semgrep-ci-on-pr.outputs.pr-number }} --interval 30 --watch
 name: test-e2e-semgrep-ci
 on:
   schedule:
diff --git a/.github/workflows/tests.jsonnet b/.github/workflows/tests.jsonnet
index 5015a579dba4..b1f3410e4cac 100644
--- a/.github/workflows/tests.jsonnet
+++ b/.github/workflows/tests.jsonnet
@@ -206,7 +206,6 @@ local test_cli_job = {
   strategy: {
     matrix: {
       python: [
-        '3.8',
         '3.9',
         '3.10',
         '3.11',
@@ -316,7 +315,7 @@ local test_qa_job = {
 local bench_prepare_steps = [
   actions.checkout(),
   fetch_submodules_step,
-  actions.setup_python_step('3.8'),
+  actions.setup_python_step(semgrep.default_python_version),
   actions.pipenv_install_step,
   download_x86_artifacts,
   install_x86_artifacts,
@@ -377,14 +376,14 @@ local trigger_semgrep_comparison_argo = {
 // In the jobs below, we set certain docker "tags".
 // To make a comparison to git:
 // - docker image == git repository
-//   example: returntocorp/semgrep
+//   example: semgrep/semgrep
 // - docker digest == git commit
 //   example: sha256:98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3
 // - docker tag == git ref
 //   example: :latest, :canary, 1.2.3, pr-4434
 //
 // You can see those tags in use here:
-// https://hub.docker.com/r/returntocorp/semgrep/tags
+// https://hub.docker.com/r/semgrep/semgrep/tags
 //
 // Example of docker tags:
 // - # tag image with full version (ex. "1.2.3")
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 9b4d96f8a4e0..ce607c2a8ebc 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -11,9 +11,9 @@ jobs:
       - uses: actions/setup-python@v4
         with:
           cache: pipenv
-          python-version: "3.8"
+          python-version: "3.9"
       - run: pip install pipenv==2024.0.1
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: semgrep-core-x86-artifact
       - name: Install artifacts
@@ -132,7 +132,7 @@ jobs:
       - name: Install Python dependencies
         run: pipenv install --dev
         working-directory: cli
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: semgrep-core-x86-artifact
       - name: Install artifacts
@@ -192,7 +192,6 @@ jobs:
     strategy:
       matrix:
         python:
-          - "3.8"
           - "3.9"
           - "3.10"
           - "3.11"
@@ -243,7 +242,7 @@ jobs:
           cache: pipenv
           python-version: "3.11"
       - run: pip install pipenv==2024.0.1
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: semgrep-core-x86-artifact
       - name: Install artifacts
diff --git a/cli/tests/default/e2e/snapshots/test_metavariable_pattern/test1/results.json b/cli/tests/default/e2e/snapshots/test_metavariable_pattern/test1/results.json
index 7bf0d4e30995..9b8a3ba5c185 100644
--- a/cli/tests/default/e2e/snapshots/test_metavariable_pattern/test1/results.json
+++ b/cli/tests/default/e2e/snapshots/test_metavariable_pattern/test1/results.json
@@ -3,7 +3,7 @@
     {
       "code": 2,
       "level": "warn",
-      "message": "Internal matching error when running rules.metavariable-pattern.yaml.github-actions.security.curl-eval.curl-eval on targets/metavariable-pattern/test1.yml:\n An error occurred while invoking the Semgrep engine. Please help us fix this by creating an issue at https://github.com/returntocorp/semgrep\n\nrule rules.metavariable-pattern.yaml.github-actions.security.curl-eval.curl-eval: metavariable-pattern failed when parsing $SHELL's content as Bash: if ${{ steps.pss.outcome=='failure' }}; then FAILED=pss; fi\nif ${{ steps.soc.outcome=='failure' }}; then FAILED=soc; fi\nif ${{ steps.pushsync-chunks-1.outcome=='failure' }}; then FAILED=pushsync-chunks-1; fi\nif ${{ steps.pushsync-chunks-2.outcome=='failure' }}; then FAILED=pushsync-chunks-2; fi\nif ${{ steps.retrieval.outcome=='failure' }}; then FAILED=retrieval; fi\nif ${{ steps.manifest.outcome=='failure' }}; then FAILED=manifest; fi\nif ${{ steps.content-availability.outcome=='failure' }}; then FAILED=content-availability; fi\ncurl -sSf -X POST -H \"Content-Type: application/json\" -d \"{\\\"text\\\": \\\"**${RUN_TYPE}** Test Error\\nBranch: \\`${{ github.head_ref }}\\`\\nUser: @${{ github.event.pull_request.user.login }}\\nDebugging artifacts: [click](https://$BUCKET_NAME.$AWS_ENDPOINT/artifacts_$VERTAG.tar.gz)\\nStep failed: \\`${FAILED}\\`\\\"}\" https://foobar.test.org/hooks/${{ secrets.TUNSHELL_KEY }}\n",
+      "message": "Internal matching error when running rules.metavariable-pattern.yaml.github-actions.security.curl-eval.curl-eval on targets/metavariable-pattern/test1.yml:\n An error occurred while invoking the Semgrep engine. Please help us fix this by creating an issue at https://github.com/semgrep/semgrep\n\nrule rules.metavariable-pattern.yaml.github-actions.security.curl-eval.curl-eval: metavariable-pattern failed when parsing $SHELL's content as Bash: if ${{ steps.pss.outcome=='failure' }}; then FAILED=pss; fi\nif ${{ steps.soc.outcome=='failure' }}; then FAILED=soc; fi\nif ${{ steps.pushsync-chunks-1.outcome=='failure' }}; then FAILED=pushsync-chunks-1; fi\nif ${{ steps.pushsync-chunks-2.outcome=='failure' }}; then FAILED=pushsync-chunks-2; fi\nif ${{ steps.retrieval.outcome=='failure' }}; then FAILED=retrieval; fi\nif ${{ steps.manifest.outcome=='failure' }}; then FAILED=manifest; fi\nif ${{ steps.content-availability.outcome=='failure' }}; then FAILED=content-availability; fi\ncurl -sSf -X POST -H \"Content-Type: application/json\" -d \"{\\\"text\\\": \\\"**${RUN_TYPE}** Test Error\\nBranch: \\`${{ github.head_ref }}\\`\\nUser: @${{ github.event.pull_request.user.login }}\\nDebugging artifacts: [click](https://$BUCKET_NAME.$AWS_ENDPOINT/artifacts_$VERTAG.tar.gz)\\nStep failed: \\`${FAILED}\\`\\\"}\" https://foobar.test.org/hooks/${{ secrets.TUNSHELL_KEY }}\n",
       "path": "targets/metavariable-pattern/test1.yml",
       "rule_id": "rules.metavariable-pattern.yaml.github-actions.security.curl-eval.curl-eval",
       "type": "Internal matching error"
diff --git a/cli/tests/default/unit/test_match_hashes.py b/cli/tests/default/unit/test_match_hashes.py
index 10f9a0619fb1..4e0bedaaccf7 100644
--- a/cli/tests/default/unit/test_match_hashes.py
+++ b/cli/tests/default/unit/test_match_hashes.py
@@ -10,7 +10,6 @@
 from semgrep.rule_match import RuleMatch
 from semgrep.rule_match import RuleMatches
 from semgrep.semgrep_interfaces.semgrep_output_v1 import DependencyMatch
-from semgrep.semgrep_interfaces.semgrep_output_v1 import DependencyPattern
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Ecosystem
 from semgrep.semgrep_interfaces.semgrep_output_v1 import FoundDependency
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Pypi
@@ -142,12 +141,12 @@ def get_lockfile_only_rule_match(
             ),
         ),
         extra={
-            "sca_info": out.ScaInfo(
+            "sca_info": out.ScaMatch(
                 reachable=False,
                 reachability_rule=False,
                 sca_finding_schema=SCA_FINDING_SCHEMA,
                 dependency_match=DependencyMatch(
-                    dependency_pattern=DependencyPattern(
+                    dependency_pattern=out.ScaPattern(
                         ecosystem=Ecosystem(Pypi()),
                         package="foo",
                         semver_range=">=1.0.0",
diff --git a/cli/tests/default/unit/test_rule_match.py b/cli/tests/default/unit/test_rule_match.py
index 3bb154d06f4b..b2a1a6a06969 100644
--- a/cli/tests/default/unit/test_rule_match.py
+++ b/cli/tests/default/unit/test_rule_match.py
@@ -403,7 +403,7 @@ def foo():
 def test_rule_match_to_app_finding(snapshot, mocker):
     mocker.patch.object(RuleMatch, "get_lines", lambda self: "foo()")
     dependency_match = out.DependencyMatch(
-        dependency_pattern=out.DependencyPattern(
+        dependency_pattern=out.ScaPattern(
             ecosystem=out.Ecosystem(out.Pypi()),
             package="awscli",
             semver_range="== 1.11.82",
@@ -438,7 +438,7 @@ def test_rule_match_to_app_finding(snapshot, mocker):
             ),
         ),
         extra={
-            "sca_info": out.ScaInfo(
+            "sca_info": out.ScaMatch(
                 sca_finding_schema=SCA_FINDING_SCHEMA,
                 reachable=True,
                 reachability_rule=True,
@@ -496,7 +496,7 @@ def test_rule_match_to_app_finding_historical_info(snapshot, mocker):
 
 def create_sca_rule_match(sca_kind, reachable_in_code, transitivity):
     dependency_match = out.DependencyMatch(
-        dependency_pattern=out.DependencyPattern(
+        dependency_pattern=out.ScaPattern(
             ecosystem=out.Ecosystem(out.Pypi()),
             package="awscli",
             semver_range="== 1.11.82",
@@ -532,7 +532,7 @@ def create_sca_rule_match(sca_kind, reachable_in_code, transitivity):
             ),
         ),
         extra={
-            "sca_info": out.ScaInfo(
+            "sca_info": out.ScaMatch(
                 sca_finding_schema=SCA_FINDING_SCHEMA,
                 reachable=reachable_in_code,
                 reachability_rule=sca_kind == "reachable",
diff --git a/libs/commons/tests/Unit_File.ml b/libs/commons/tests/Unit_File.ml
index 8b55d3bbd78c..3f3bb40be22e 100644
--- a/libs/commons/tests/Unit_File.ml
+++ b/libs/commons/tests/Unit_File.ml
@@ -18,6 +18,190 @@ let with_string str f =
 (* The tests *)
 (*****************************************************************************)
 
+let with_test_files f =
+  let open Testutil_files in
+  with_tempfiles ~chdir:true ~verbose:true
+    [
+      dir "dir" [];
+      file "reg";
+      symlink "broken-symlink" "missing";
+      symlink "reg-link" "reg";
+      symlink "reg-link2" "reg-link";
+      symlink "dir-link" "dir";
+    ]
+    f
+
+let test_is_dir () =
+  with_test_files (fun _cwd ->
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir ~follow_symlinks:true (Fpath.v "dir"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir ~follow_symlinks:false (Fpath.v "dir"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir ~follow_symlinks:true (Fpath.v "dir-link"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir ~follow_symlinks:false (Fpath.v "dir-link"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir ~follow_symlinks:true (Fpath.v "reg"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir ~follow_symlinks:false (Fpath.v "reg"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir ~follow_symlinks:true (Fpath.v "broken-symlink"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir ~follow_symlinks:false (Fpath.v "broken-symlink"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir ~follow_symlinks:true (Fpath.v "missing"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir ~follow_symlinks:false (Fpath.v "missing")))
+
+let test_is_reg () =
+  with_test_files (fun _cwd ->
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_reg ~follow_symlinks:true (Fpath.v "dir"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_reg ~follow_symlinks:false (Fpath.v "dir"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_reg ~follow_symlinks:true (Fpath.v "reg"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_reg ~follow_symlinks:false (Fpath.v "reg"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_reg ~follow_symlinks:true (Fpath.v "reg-link"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_reg ~follow_symlinks:false (Fpath.v "reg-link"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_reg ~follow_symlinks:true (Fpath.v "reg-link2"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_reg ~follow_symlinks:false (Fpath.v "reg-link2"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_reg ~follow_symlinks:true (Fpath.v "broken-symlink"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_reg ~follow_symlinks:false (Fpath.v "broken-symlink"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_reg ~follow_symlinks:true (Fpath.v "missing"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_reg ~follow_symlinks:false (Fpath.v "missing")))
+
+let test_is_dir_or_reg () =
+  with_test_files (fun _cwd ->
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir_or_reg ~follow_symlinks:true (Fpath.v "dir"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir_or_reg ~follow_symlinks:false (Fpath.v "dir"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir ~follow_symlinks:true (Fpath.v "dir-link"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir ~follow_symlinks:false (Fpath.v "dir-link"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir_or_reg ~follow_symlinks:true (Fpath.v "reg"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir_or_reg ~follow_symlinks:false (Fpath.v "reg"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir_or_reg ~follow_symlinks:true (Fpath.v "reg-link"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir_or_reg ~follow_symlinks:false (Fpath.v "reg-link"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir_or_reg ~follow_symlinks:true (Fpath.v "reg-link2"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir_or_reg ~follow_symlinks:false (Fpath.v "reg-link2"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir_or_reg ~follow_symlinks:true (Fpath.v "broken-symlink"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir_or_reg ~follow_symlinks:false (Fpath.v "broken-symlink"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir_or_reg ~follow_symlinks:true (Fpath.v "missing"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir_or_reg ~follow_symlinks:false (Fpath.v "missing")))
+
+let test_is_lnk () =
+  with_test_files (fun _cwd ->
+      Alcotest.(check bool) "" false (UFile.is_lnk (Fpath.v "dir"));
+      Alcotest.(check bool) "" false (UFile.is_lnk (Fpath.v "reg"));
+      Alcotest.(check bool) "" true (UFile.is_lnk (Fpath.v "dir-link"));
+      Alcotest.(check bool) "" true (UFile.is_lnk (Fpath.v "reg-link"));
+      Alcotest.(check bool) "" true (UFile.is_lnk (Fpath.v "reg-link2"));
+      Alcotest.(check bool) "" true (UFile.is_lnk (Fpath.v "broken-symlink"));
+      Alcotest.(check bool) "" false (UFile.is_lnk (Fpath.v "missing")))
+
+let test_is_dir_or_lnk () =
+  with_test_files (fun _cwd ->
+      Alcotest.(check bool) "" true (UFile.is_dir_or_lnk (Fpath.v "dir"));
+      Alcotest.(check bool) "" false (UFile.is_dir_or_lnk (Fpath.v "reg"));
+      Alcotest.(check bool) "" true (UFile.is_dir_or_lnk (Fpath.v "dir-link"));
+      Alcotest.(check bool) "" true (UFile.is_dir_or_lnk (Fpath.v "reg-link"));
+      Alcotest.(check bool) "" true (UFile.is_dir_or_lnk (Fpath.v "reg-link2"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir_or_lnk (Fpath.v "broken-symlink"));
+      Alcotest.(check bool) "" false (UFile.is_dir_or_lnk (Fpath.v "missing")))
+
+let test_is_lnk_or_reg () =
+  with_test_files (fun _cwd ->
+      Alcotest.(check bool) "" false (UFile.is_lnk_or_reg (Fpath.v "dir"));
+      Alcotest.(check bool) "" true (UFile.is_lnk_or_reg (Fpath.v "reg"));
+      Alcotest.(check bool) "" true (UFile.is_lnk_or_reg (Fpath.v "dir-link"));
+      Alcotest.(check bool) "" true (UFile.is_lnk_or_reg (Fpath.v "reg-link"));
+      Alcotest.(check bool) "" true (UFile.is_lnk_or_reg (Fpath.v "reg-link2"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_lnk_or_reg (Fpath.v "broken-symlink"));
+      Alcotest.(check bool) "" false (UFile.is_lnk_or_reg (Fpath.v "missing")))
+
+let test_is_dir_or_lnk_or_reg () =
+  with_test_files (fun _cwd ->
+      Alcotest.(check bool) "" true (UFile.is_dir_or_lnk_or_reg (Fpath.v "dir"));
+      Alcotest.(check bool) "" true (UFile.is_dir_or_lnk_or_reg (Fpath.v "reg"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir_or_lnk_or_reg (Fpath.v "dir-link"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir_or_lnk_or_reg (Fpath.v "reg-link"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir_or_lnk_or_reg (Fpath.v "reg-link2"));
+      Alcotest.(check bool)
+        "" true
+        (UFile.is_dir_or_lnk_or_reg (Fpath.v "broken-symlink"));
+      Alcotest.(check bool)
+        "" false
+        (UFile.is_dir_or_lnk_or_reg (Fpath.v "missing")))
+
 (*****************************************************************************)
 (* Entry point *)
 (*****************************************************************************)
@@ -56,4 +240,11 @@ let tests =
               let str2 = Common.input_text_line chan in
               Alcotest.(check (list string))
                 __LOC__ [ "foo"; "bar" ] [ str1; str2 ]));
+      t "is_dir" test_is_dir;
+      t "is_reg" test_is_reg;
+      t "is_dir_or_reg" test_is_dir_or_reg;
+      t "is_lnk" test_is_lnk;
+      t "is_dir_or_lnk" test_is_dir_or_lnk;
+      t "is_lnk_or_reg" test_is_lnk_or_reg;
+      t "is_dir_or_lnk_or_reg" test_is_dir_or_lnk_or_reg;
     ]
diff --git a/scripts/run-perf-tests-old.sh b/scripts/run-perf-tests-old.sh
deleted file mode 100755
index c3ccc1121510..000000000000
--- a/scripts/run-perf-tests-old.sh
+++ /dev/null
@@ -1,48 +0,0 @@
-#! /usr/bin/env bash
-
-set -e
-# set -x
-
-HYPERFINE="hyperfine -m 2 --warmup 0 "
-
-A_VERSION="0.4.8"
-B_VERSION="0.4.9"
-
-
-test_test_suite() {
-    rm -rf /tmp/semgrep-rules && git clone https://github.com/returntocorp/semgrep-rules /tmp/semgrep-rules
-    ls -al /tmp/semgrep-rules
-    CMDA="${SGREP_A} --dangerously-allow-arbitrary-code-execution-from-rules --strict --test --test-ignore-todo /tmp/semgrep-rules"
-    CMDB="${SGREP_B} --dangerously-allow-arbitrary-code-execution-from-rules --strict --test --test-ignore-todo /tmp/semgrep-rules"
-    $CMDA
-    $CMDB
-    $HYPERFINE --export-markdown testsuite.md "${CMDA}" "${CMDB}"
-}
-
-test_sample_repos() {
-    rm -rf /tmp/sample && git clone --depth=1 https://github.com/apache/airflow /tmp/sample/
-    cd /tmp/sample
-    SGREP_A="docker run --rm -v ${PWD}:/src returntocorp/semgrep:${A_VERSION}"
-    SGREP_B="docker run --rm -v ${PWD}:/src returntocorp/semgrep:${B_VERSION}"
-
-    CMDA="${SGREP_A} --config=r2c --dangerously-allow-arbitrary-code-execution-from-rules --strict"
-    CMDB="${SGREP_B} --config=r2c --dangerously-allow-arbitrary-code-execution-from-rules --strict"
-    #$CMDA
-    #$CMDB
-    $HYPERFINE --export-markdown "${THIS_DIR}/testsuite.md" "${CMDA}" "${CMDB}"
-    cat "${THIS_DIR}/testsuite.md"
-    cd -
-
-}
-
-echo "-----------------------"
-echo "starting perf tests"
-
-THIS_DIR="$(dirname "$(realpath "$0")")";
-cd "${THIS_DIR}"
-
-#test_test_suite
-test_sample_repos
-
-echo "-----------------------"
-echo "all per tests completed"
diff --git a/src/engine/tests/Test_utils.ml b/src/engine/tests/Test_utils.ml
index ff4298326965..57a113f09ad7 100644
--- a/src/engine/tests/Test_utils.ml
+++ b/src/engine/tests/Test_utils.ml
@@ -33,7 +33,7 @@ let related_file_of_target ?polyglot_pattern_path ~ext file =
  * also exercise autofix.
  *
  * Semgrep's `--test` flag can also test autofix
- * (https://github.com/returntocorp/semgrep/pull/5190), but it has the same
+ * (https://github.com/semgrep/semgrep/pull/5190), but it has the same
  * problems as the existing autofix e2e tests for these purposes. *)
 let compare_fixes ?polyglot_pattern_path ~file matches =
   let expected_fixed_text =
diff --git a/src/osemgrep/core/Rule_tests.ml b/src/osemgrep/core/Rule_tests.ml
index 4593098c6704..6e4a927d51a1 100644
--- a/src/osemgrep/core/Rule_tests.ml
+++ b/src/osemgrep/core/Rule_tests.ml
@@ -91,7 +91,7 @@ let relatively_eq parent_target target parent_config config =
   | _ -> false
 
 let get_config_filenames original_config =
-  if UFile.is_file original_config then [ original_config ]
+  if UFile.is_reg ~follow_symlinks:true original_config then [ original_config ]
   else
     let configs = Common2.glob (Common.spf "%s/**" !!original_config) in
     configs
@@ -136,11 +136,13 @@ let get_config_filenames original_config =
 *)
 
 let get_config_test_filenames ~original_config ~configs ~original_target =
-  if UFile.is_file original_config && UFile.is_file original_target then
-    [ (original_config, [ original_target ]) ]
+  if
+    UFile.is_reg ~follow_symlinks:true original_config
+    && UFile.is_reg ~follow_symlinks:true original_target
+  then [ (original_config, [ original_target ]) ]
   else
     let targets =
-      (if UFile.is_file original_target then
+      (if UFile.is_reg ~follow_symlinks:true original_target then
          Common2.glob (Common.spf "%s/**" !!(Fpath.parent original_target))
        else Common2.glob (Common.spf "%s/**" !!original_target))
       |> List_.map Fpath.v
diff --git a/stats/parsing-stats/test-parsing/lang/javascript/projects.txt b/stats/parsing-stats/test-parsing/lang/javascript/projects.txt
index 57e93a03eca8..597a02b04b6a 100644
--- a/stats/parsing-stats/test-parsing/lang/javascript/projects.txt
+++ b/stats/parsing-stats/test-parsing/lang/javascript/projects.txt
@@ -1,2 +1,2 @@
-https://github.com/returntocorp/semgrep-go.git
-https://github.com/returntocorp/semgrep-ruby.git
+https://github.com/semgrep/semgrep-go.git
+https://github.com/semgrep/semgrep-ruby.git
diff --git a/stats/parsing-stats/test-parsing/lang/ocaml/projects.txt b/stats/parsing-stats/test-parsing/lang/ocaml/projects.txt
index 57e93a03eca8..597a02b04b6a 100644
--- a/stats/parsing-stats/test-parsing/lang/ocaml/projects.txt
+++ b/stats/parsing-stats/test-parsing/lang/ocaml/projects.txt
@@ -1,2 +1,2 @@
-https://github.com/returntocorp/semgrep-go.git
-https://github.com/returntocorp/semgrep-ruby.git
+https://github.com/semgrep/semgrep-go.git
+https://github.com/semgrep/semgrep-ruby.git
diff --git a/tests/rule_formats/sca_version_no_space.yaml b/tests/rule_formats/sca_version_no_space.yaml
new file mode 100644
index 000000000000..b276f375ffed
--- /dev/null
+++ b/tests/rule_formats/sca_version_no_space.yaml
@@ -0,0 +1,15 @@
+rules:
+- id: some-rule
+  message: >-
+    This package contains malicious code. Remove it immediately.
+  severity: ERROR
+  r2c-internal-project-depends-on:
+    depends-on-either:
+    - namespace: npm
+      package: portal-shell
+      # we accept the lack of space after the operator and we also
+      # accept version like just 0 (instead of 0.1.2 or even 0.1)
+      version: '>=0'
+  languages:
+  - js
+  - ts