openqodex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +35 -0
- package/README.md +160 -0
- package/demo/baseline/.github/workflows/ci.yml +18 -0
- package/demo/baseline/Dockerfile +12 -0
- package/demo/baseline/app/__init__.py +0 -0
- package/demo/baseline/app/server.py +37 -0
- package/demo/baseline/package-lock.json +21 -0
- package/demo/baseline/package.json +9 -0
- package/demo/baseline/requirements.txt +1 -0
- package/demo/baseline/scripts/deploy.sh +13 -0
- package/demo/expected.json +126 -0
- package/demo/planted/.github/workflows/ci.yml +20 -0
- package/demo/planted/Dockerfile +11 -0
- package/demo/planted/app/config.py +3 -0
- package/demo/planted/app/search.py +17 -0
- package/demo/planted/app/server.py +40 -0
- package/demo/planted/package-lock.json +21 -0
- package/demo/planted/package.json +9 -0
- package/demo/planted/scripts/deploy.sh +13 -0
- package/dist/bin.js +42546 -0
- package/docs/agents.md +121 -0
- package/docs/cli.md +161 -0
- package/docs/config.md +165 -0
- package/docs/custom-scanners.md +164 -0
- package/docs/faq.md +54 -0
- package/docs/github-action.md +73 -0
- package/docs/index.md +29 -0
- package/docs/llms.txt +13 -0
- package/docs/quickstart.md +82 -0
- package/docs/scanners.md +148 -0
- package/docs/security.md +74 -0
- package/docs/telemetry.md +9 -0
- package/lenses/a11y-icon-only-button-no-aria-label.md +51 -0
- package/lenses/array-iteration-missing-key-prop.md +31 -0
- package/lenses/async-await-in-loop-n-plus-one.md +42 -0
- package/lenses/async-click-double-fire-race.md +59 -0
- package/lenses/async-floating-promise.md +40 -0
- package/lenses/async-promise-all-swallows-errors.md +42 -0
- package/lenses/async-unhandled-rejection-in-handler.md +42 -0
- package/lenses/auth-missing-on-state-change-route.md +60 -0
- package/lenses/auth-role-from-user-input.md +49 -0
- package/lenses/auth-timing-attack-password-compare.md +59 -0
- package/lenses/cookie-missing-secure-httponly.md +46 -0
- package/lenses/cors-wildcard-with-credentials.md +47 -0
- package/lenses/crypto-jwt-verify-without-algo-allowlist.md +41 -0
- package/lenses/crypto-math-random-for-tokens.md +53 -0
- package/lenses/crypto-md5-sha1-for-secrets.md +60 -0
- package/lenses/env-vars-read-at-module-top.md +42 -0
- package/lenses/eval-on-user-input.md +51 -0
- package/lenses/fetch-without-timeout.md +41 -0
- package/lenses/id-enumeration-sequential.md +49 -0
- package/lenses/interactive-state-decoupled-from-output.md +67 -0
- package/lenses/json-parse-no-try-catch.md +41 -0
- package/lenses/missing-rate-limit-on-auth.md +49 -0
- package/lenses/oauth-scope-wider-than-use.md +79 -0
- package/lenses/object-spread-clobber.md +44 -0
- package/lenses/open-redirect-from-untrusted-host.md +55 -0
- package/lenses/orm-drizzle-on-conflict-clobber.md +47 -0
- package/lenses/path-traversal-in-fs-access.md +59 -0
- package/lenses/pii-in-url-or-log.md +48 -0
- package/lenses/race-check-then-act.md +49 -0
- package/lenses/react-dangerously-set-inner-html.md +32 -0
- package/lenses/react-fetch-in-effect-without-abort.md +31 -0
- package/lenses/react-stale-closure-in-callback.md +28 -0
- package/lenses/react-state-set-in-render.md +33 -0
- package/lenses/react-use-effect-missing-cleanup.md +29 -0
- package/lenses/react-use-effect-missing-deps.md +30 -0
- package/lenses/regexp-from-user-input.md +43 -0
- package/lenses/return-shape-contract-break.md +71 -0
- package/lenses/secrets-logged-in-error-path.md +60 -0
- package/lenses/sql-migration-references-later-object.md +47 -0
- package/lenses/sql-string-concatenation.md +63 -0
- package/lenses/ssrf-server-side-fetch.md +69 -0
- package/lenses/supabase-comment-on-function-unqualified.md +37 -0
- package/lenses/supabase-function-default-public-execute.md +50 -0
- package/lenses/supabase-security-definer-no-search-path.md +34 -0
- package/lenses/supabase-single-500-on-no-match.md +65 -0
- package/lenses/upsert-state-column.md +57 -0
- package/lenses/url-not-encoded-for-user-id.md +78 -0
- package/lenses/use-state-default-not-functional.md +30 -0
- package/package.json +57 -0
- package/skills/openqodex/SKILL.md +141 -0
- package/templates/README.md +67 -0
- package/templates/claude-code/settings-hook.json +16 -0
- package/templates/cline/openqodex.md +9 -0
- package/templates/codex/AGENTS-section.md +8 -0
- package/templates/codex/hooks.json +16 -0
- package/templates/cursor/openqodex.mdc +15 -0
- package/toolchain.json +345 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# GitHub Action
|
|
2
|
+
|
|
3
|
+
The OpenQodex Action runs `openqodex scan` on a pull request's change. It uploads the findings to GitHub code scanning as SARIF. No model is involved: the Action runs the scanners only.
|
|
4
|
+
|
|
5
|
+
## Example workflow
|
|
6
|
+
|
|
7
|
+
Save this as `.github/workflows/openqodex.yml`:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
name: OpenQodex
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
pull_request:
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
security-events: write
|
|
18
|
+
actions: read
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
scan:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v7
|
|
25
|
+
with:
|
|
26
|
+
fetch-depth: 0
|
|
27
|
+
- uses: openqodex/openqodex@v0
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- `fetch-depth: 0` fetches the full history. The scan needs the pull request's base commit, which a shallow checkout does not have.
|
|
31
|
+
- `security-events: write` lets the Action upload SARIF to code scanning.
|
|
32
|
+
- `actions: read` is read by the SARIF upload in a private repository.
|
|
33
|
+
- `contents: read` lets the job check out the code.
|
|
34
|
+
|
|
35
|
+
## Inputs
|
|
36
|
+
|
|
37
|
+
- `version`: the `openqodex` version to run. The default is the version the Action was released with.
|
|
38
|
+
- `upload-sarif`: `true` or `false`. The default is `true`. Set it to `false` to skip the code scanning upload.
|
|
39
|
+
|
|
40
|
+
## What it does
|
|
41
|
+
|
|
42
|
+
1. Sets up Node 22.
|
|
43
|
+
2. Restores `~/.openqodex/tools` from the Actions cache, keyed on the runner and the `openqodex` version.
|
|
44
|
+
3. Runs `npx -y openqodex@<version> doctor --install`, which installs every scanner and waits.
|
|
45
|
+
4. Runs `npx -y openqodex@<version> scan --base <pull request base commit> --format sarif`. The SARIF goes to a new folder under the runner's temporary folder, never into the checkout.
|
|
46
|
+
5. Uploads that SARIF to code scanning, when `upload-sarif` is `true` and the scan wrote a report.
|
|
47
|
+
6. Fails the job when the scan exited 1 or 2.
|
|
48
|
+
|
|
49
|
+
The scan exits 1 only when `.openqodex.yaml` sets `review.block_on_severity` and a finding on a changed line meets it. Without that key, the job never fails on findings. A scan that fails for its own reasons exits 2, and the job fails too.
|
|
50
|
+
|
|
51
|
+
## Config
|
|
52
|
+
|
|
53
|
+
The Action reads `.openqodex.yaml` from the repository, like every other command. Custom scanners need an approval stored on the machine that runs them. The runner has none, so the Action lists custom scanners as `untrusted` and skips them.
|
|
54
|
+
|
|
55
|
+
## Pre-commit
|
|
56
|
+
|
|
57
|
+
The repository also ships a pre-commit hook for the pre-push stage. Add this to `.pre-commit-config.yaml`:
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
repos:
|
|
61
|
+
- repo: https://github.com/openqodex/openqodex
|
|
62
|
+
rev: v0.1.0
|
|
63
|
+
hooks:
|
|
64
|
+
- id: openqodex-scan
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then install the pre-push hook:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
pre-commit install --hook-type pre-push
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The hook runs `npx -y openqodex@<version> scan` on the commits not yet pushed plus the working tree. It stops the push only when the scan exits 1. A scan that fails for its own reasons never stops the push. The hook needs Node 22, npx and `sh` on your machine.
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# OpenQodex docs
|
|
2
|
+
|
|
3
|
+
OpenQodex is open source code review that runs inside your coding agent, before you push.
|
|
4
|
+
|
|
5
|
+
These pages ship inside the npm package. `npx openqodex guide <topic>` prints one in your terminal, offline. The topic is the file name without `.md`.
|
|
6
|
+
|
|
7
|
+
## Words used in these pages
|
|
8
|
+
|
|
9
|
+
- Change: the commits not yet pushed plus everything uncommitted, untracked files included.
|
|
10
|
+
- Changed lines: the lines the change adds or edits. Context lines around them do not count.
|
|
11
|
+
- Scanner: a program that checks code without a model, such as gitleaks or semgrep.
|
|
12
|
+
- Finding: one problem at one place in the code, with a severity.
|
|
13
|
+
- Candidate: a scanner finding on a changed line, waiting for the agent to verify it.
|
|
14
|
+
- Brief: the text `openqodex review --agent` prints for the agent to review from.
|
|
15
|
+
- Report: the result of a scan or a review, written as `report.md`, `report.json` and `report.sarif`.
|
|
16
|
+
- Verdict: `passed` or `blocked`.
|
|
17
|
+
|
|
18
|
+
## Pages
|
|
19
|
+
|
|
20
|
+
- `quickstart`: install OpenQodex and run the first review.
|
|
21
|
+
- `cli`: every command, flag and exit code.
|
|
22
|
+
- `config`: every key of `.openqodex.yaml`.
|
|
23
|
+
- `scanners`: the thirteen built-in scanners.
|
|
24
|
+
- `custom-scanners`: add any scanner by its GitHub link.
|
|
25
|
+
- `agents`: what `init` writes for each coding agent.
|
|
26
|
+
- `github-action`: run the scan on pull requests.
|
|
27
|
+
- `security`: what runs, what is sent where, and where files go.
|
|
28
|
+
- `telemetry`: there is none.
|
|
29
|
+
- `faq`: short answers.
|
package/docs/llms.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# OpenQodex docs
|
|
2
|
+
|
|
3
|
+
- [Agents](agents.md)
|
|
4
|
+
- [Commands](cli.md)
|
|
5
|
+
- [Configuration](config.md)
|
|
6
|
+
- [Custom scanners](custom-scanners.md)
|
|
7
|
+
- [FAQ](faq.md)
|
|
8
|
+
- [GitHub Action](github-action.md)
|
|
9
|
+
- [OpenQodex docs](index.md)
|
|
10
|
+
- [Quickstart](quickstart.md)
|
|
11
|
+
- [Scanners](scanners.md)
|
|
12
|
+
- [Security](security.md)
|
|
13
|
+
- [Telemetry](telemetry.md)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Quickstart
|
|
2
|
+
|
|
3
|
+
## Paste this prompt into your agent
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Install the OpenQodex skill with `npx skills add openqodex/openqodex`.
|
|
7
|
+
Then review my current change with openqodex and tell me the verdict and the findings.
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The agent installs the skill, runs the review and tells you the result. The steps below do the same by hand.
|
|
11
|
+
|
|
12
|
+
## Before you start
|
|
13
|
+
|
|
14
|
+
- Node 22 or newer, and git.
|
|
15
|
+
- macOS or Linux. On Windows, use WSL.
|
|
16
|
+
- A git repository with a change in it.
|
|
17
|
+
|
|
18
|
+
## 1. Install into your agent
|
|
19
|
+
|
|
20
|
+
Run this in your own terminal, not inside the agent:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
npx openqodex init
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`init` finds Claude Code, Cursor, Codex CLI and Cline on your machine. It prints each file it will write, then asks once. `--yes` skips the question. `agents` lists every file for each agent.
|
|
27
|
+
|
|
28
|
+
`init` also starts the scanner downloads that your repo needs, in the background. Running it outside the agent matters: some agents run commands in a sandbox that cannot download.
|
|
29
|
+
|
|
30
|
+
Codex only: open Codex, run `/hooks` and trust the OpenQodex hook. Codex runs a new hook only after you trust it.
|
|
31
|
+
|
|
32
|
+
## 2. Ask for a review
|
|
33
|
+
|
|
34
|
+
Say to your agent:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
review my change with openqodex
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The agent runs `openqodex review --agent`. That command works out the change, runs the scanners and prints a brief. The agent verifies each scanner finding, reviews the change itself, and writes its findings to a file. Then it runs `openqodex review --finalize`, which checks those findings without a model and writes the report.
|
|
41
|
+
|
|
42
|
+
## 3. Read the report
|
|
43
|
+
|
|
44
|
+
The agent tells you the verdict and the most serious findings. The full report is in `.openqodex/reviews/<time>-<id>/report.md` in your repo. `.openqodex/` ignores itself in git, so `git status` does not change.
|
|
45
|
+
|
|
46
|
+
The verdict is `passed` unless `.openqodex.yaml` sets `review.block_on_severity` and a finding meets it. With no config, OpenQodex warns and never blocks.
|
|
47
|
+
|
|
48
|
+
## Try it on the demo repo
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
npx openqodex demo /tmp/openqodex-demo
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`demo` builds a small repo with planted bugs: a secret, a SQL injection, a bad Dockerfile, a vulnerable lockfile, a shell bug and a workflow injection. It scans the change and prints the report. Then open the folder in your agent and ask for a review.
|
|
55
|
+
|
|
56
|
+
## Without an agent
|
|
57
|
+
|
|
58
|
+
`openqodex scan` runs the scanners on the change and prints the report:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
npx openqodex scan
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
It is the same check the git hook, the pre-commit hook and the GitHub Action run.
|
|
65
|
+
|
|
66
|
+
## First run
|
|
67
|
+
|
|
68
|
+
Scanners download on first use into `~/.openqodex/tools/`. Only the scanners your change needs download. A scanner still installing after 45 seconds keeps going in the background. The report lists it as installing. It joins the next run.
|
|
69
|
+
|
|
70
|
+
One measured first run: an Apple Silicon Mac, an empty tool folder, a line of 2 MB per second. The first `demo` printed its report in under a minute. That report held the scanners that had finished installing and listed the rest as installing. The next `scan` included all eight scanners the demo needs. They take about 700 MB of disk.
|
|
71
|
+
|
|
72
|
+
To download every scanner now:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
npx openqodex doctor --install
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Next
|
|
79
|
+
|
|
80
|
+
- `config`: block pushes at a severity, exclude paths, switch scanners off.
|
|
81
|
+
- `custom-scanners`: add any scanner by its GitHub link.
|
|
82
|
+
- `security`: what runs and what is sent where.
|
package/docs/scanners.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Scanners
|
|
2
|
+
|
|
3
|
+
OpenQodex has thirteen built-in scanners. Each one runs only when the change holds a file it reads. Every downloaded scanner is pinned to one version. OpenQodex never uses a copy of a built-in scanner from your `PATH`.
|
|
4
|
+
|
|
5
|
+
Pinned versions do not make findings identical on every machine. semgrep fetches its registry rule packs at run time, and OpenQodex does not pin their version.
|
|
6
|
+
|
|
7
|
+
Every scanner reads the whole changed file. OpenQodex keeps only the findings on changed lines.
|
|
8
|
+
|
|
9
|
+
## Where scanners come from
|
|
10
|
+
|
|
11
|
+
Scanners download on first use into `~/.openqodex/tools/<scanner>/<version>/`. `OPENQODEX_HOME` moves that folder.
|
|
12
|
+
|
|
13
|
+
- GitHub release files are checked against the sha256 pinned in the package before they are unpacked.
|
|
14
|
+
- semgrep and bandit install from PyPI through uv, into one Python 3.11 that OpenQodex manages. uv comes from your `PATH` when present. Otherwise OpenQodex downloads a pinned uv.
|
|
15
|
+
- oxlint installs from npm, with the npm that ships beside your Node. Package install scripts are switched off.
|
|
16
|
+
- brakeman and rubocop install from RubyGems with your Ruby's `gem` command.
|
|
17
|
+
|
|
18
|
+
A scanner install that takes longer than 45 seconds keeps going in the background. The report lists that scanner as installing. The scanner joins the next run. `openqodex doctor --install` installs every scanner and waits.
|
|
19
|
+
|
|
20
|
+
Installed scanners take more disk than their downloads. The eight scanners the demo needs take about 700 MB of disk on an Apple Silicon Mac. semgrep with its Python takes about 440 MB of that.
|
|
21
|
+
|
|
22
|
+
OpenQodex does not install Ruby or Go. Without them, the report lists the scanners that need them as not installed, with the reason.
|
|
23
|
+
|
|
24
|
+
## Status in the report
|
|
25
|
+
|
|
26
|
+
The report lists every selected scanner with one status. A scanner left out with `--only` or `--skip` is not listed.
|
|
27
|
+
|
|
28
|
+
- `ran`: it ran.
|
|
29
|
+
- `no_matching_files`: the change holds no file it reads.
|
|
30
|
+
- `installing`: it is downloading for the first time. It joins the next run.
|
|
31
|
+
- `not_installed`: it could not be installed here. The reason says why.
|
|
32
|
+
- `failed`: it ran and broke. The reason holds its error.
|
|
33
|
+
- `disabled`: `scanners.disable` names it, or `--offline` skipped it.
|
|
34
|
+
- `untrusted`: a custom scanner you have not approved.
|
|
35
|
+
|
|
36
|
+
A scanner problem never changes the exit code.
|
|
37
|
+
|
|
38
|
+
## semgrep
|
|
39
|
+
|
|
40
|
+
- Version: 1.94.0.
|
|
41
|
+
- Runs when: any file changed.
|
|
42
|
+
- Needs: Python 3.11, which OpenQodex downloads through uv. About 86 MB with uv and bandit, measured on Apple Silicon.
|
|
43
|
+
- Rules: the registry packs `p/default`, `p/security-audit` and `p/secrets`.
|
|
44
|
+
- Sends: semgrep fetches those rule packs from the Semgrep registry on each run. It runs with its metrics switched off. The rules are never bundled in the OpenQodex package.
|
|
45
|
+
- `--offline` skips it. The report lists it as disabled.
|
|
46
|
+
|
|
47
|
+
## gitleaks
|
|
48
|
+
|
|
49
|
+
- Version: 8.21.2.
|
|
50
|
+
- Runs when: any file changed.
|
|
51
|
+
- Needs: nothing. 2.9 MB on Apple Silicon, 3.0 MB on Linux x64.
|
|
52
|
+
- Writes: links the changed files into a temporary folder outside the repo and scans that folder. It reads the repo's `.gitleaks.toml` or `gitleaks.toml` when present.
|
|
53
|
+
- gitleaks writes its raw report to a temporary file outside the repo. That file holds the matched secrets. OpenQodex deletes it when the run ends.
|
|
54
|
+
- Sends: nothing.
|
|
55
|
+
- Secrets it finds are redacted from the brief, every report file and the terminal. No file OpenQodex keeps holds the secret.
|
|
56
|
+
|
|
57
|
+
## bandit
|
|
58
|
+
|
|
59
|
+
- Version: 1.9.4.
|
|
60
|
+
- Runs when: a `.py` or `.pyi` file changed.
|
|
61
|
+
- Needs: the same Python as semgrep.
|
|
62
|
+
- Sends: nothing.
|
|
63
|
+
|
|
64
|
+
## ruff
|
|
65
|
+
|
|
66
|
+
- Version: 0.8.4.
|
|
67
|
+
- Runs when: a `.py` or `.pyi` file changed.
|
|
68
|
+
- Needs: nothing. 9.9 MB on Apple Silicon, 11.2 MB on Linux x64.
|
|
69
|
+
- Reads the repo's own ruff settings. It runs with fixes and its cache switched off, so it changes no file.
|
|
70
|
+
- Sends: nothing.
|
|
71
|
+
|
|
72
|
+
## oxlint
|
|
73
|
+
|
|
74
|
+
- Version: 1.71.0.
|
|
75
|
+
- Runs when: a `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`, `.mts` or `.cts` file changed.
|
|
76
|
+
- Needs: npm, which ships with Node. About 7.3 MB on Apple Silicon, 8.2 MB on Linux x64.
|
|
77
|
+
- Uses OpenQodex's own settings. A config file in the repo is not loaded.
|
|
78
|
+
- Sends: nothing.
|
|
79
|
+
|
|
80
|
+
## osv-scanner
|
|
81
|
+
|
|
82
|
+
- Version: 1.9.2.
|
|
83
|
+
- Runs when: one of these lockfiles changed: `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `Cargo.lock`, `go.mod`, `go.sum`, `requirements.txt`, `Pipfile.lock`, `poetry.lock`, `Gemfile.lock`, `composer.lock`, `pom.xml`, `gradle.lockfile`, `pubspec.lock`, `mix.lock`, `conan.lock`.
|
|
84
|
+
- Needs: network access to osv.dev. 31.8 MB on Apple Silicon, 32.1 MB on Linux x64.
|
|
85
|
+
- Sends: the names and versions of the dependencies in those lockfiles, to osv.dev. It never sends code.
|
|
86
|
+
- `--offline` skips it. The report lists it as disabled.
|
|
87
|
+
|
|
88
|
+
## actionlint
|
|
89
|
+
|
|
90
|
+
- Version: 1.7.7.
|
|
91
|
+
- Runs when: a `.yml` or `.yaml` file under `.github/workflows/` changed.
|
|
92
|
+
- Needs: nothing. 2.0 MB on Apple Silicon, 2.1 MB on Linux x64.
|
|
93
|
+
- Sends: nothing.
|
|
94
|
+
|
|
95
|
+
## hadolint
|
|
96
|
+
|
|
97
|
+
- Version: 2.15.1.
|
|
98
|
+
- Runs when: a Dockerfile changed: `Dockerfile`, `Dockerfile.<name>` or `<name>.dockerfile`.
|
|
99
|
+
- Needs: nothing. 102.6 MB on Apple Silicon, 55.7 MB on Linux x64.
|
|
100
|
+
- Sends: nothing.
|
|
101
|
+
|
|
102
|
+
## shellcheck
|
|
103
|
+
|
|
104
|
+
- Version: 0.10.0.
|
|
105
|
+
- Runs when: a `.sh` or `.bash` file changed.
|
|
106
|
+
- Needs: `xz` to unpack the download. 7.2 MB on Apple Silicon, 2.4 MB on Linux x64.
|
|
107
|
+
- Sends: nothing.
|
|
108
|
+
|
|
109
|
+
## golangci-lint
|
|
110
|
+
|
|
111
|
+
- Version: 2.12.2. Its name in `.openqodex.yaml` is `golangci`.
|
|
112
|
+
- Runs when: a `.go` file changed. It checks the packages that hold the changed files.
|
|
113
|
+
- Needs: Go on your `PATH`. 14.4 MB on Apple Silicon, 15.0 MB on Linux x64.
|
|
114
|
+
- Uses OpenQodex's own settings, with gosec switched on. A `.golangci.yml` in the repo is not loaded. It never rewrites `go.mod` or `go.sum`.
|
|
115
|
+
- golangci-lint 2.12.2 is built with Go 1.26. With a newer Go on your PATH it cannot check the code. The report then lists golangci as failed, with the reason.
|
|
116
|
+
- Runs with the Go module proxy off. The modules the repo needs must already be in your Go module cache. Nothing is downloaded.
|
|
117
|
+
- Sends: nothing.
|
|
118
|
+
|
|
119
|
+
## brakeman
|
|
120
|
+
|
|
121
|
+
- Version: 6.2.1.
|
|
122
|
+
- Runs when: a Ruby or Rails file changed, and the repo has a `Gemfile` and an `app/` folder. The files are `.rb`, `.rake`, `.gemspec`, `.erb`, `.haml`, `.slim`, `Gemfile`, `Rakefile` and `config.ru`.
|
|
123
|
+
- Needs: Ruby 2.7 or newer. It installs from RubyGems.
|
|
124
|
+
- Uses OpenQodex's own settings. The repo's brakeman config is not loaded.
|
|
125
|
+
- Sends: nothing.
|
|
126
|
+
- Licence: the Brakeman Public Use License, which is not an open source licence. OpenQodex does not bundle brakeman. It downloads brakeman at run time onto your machine. Read the licence before you use it, or switch it off with `scanners.disable: [brakeman]`.
|
|
127
|
+
|
|
128
|
+
## rubocop
|
|
129
|
+
|
|
130
|
+
- Version: 1.69.2, with rubocop-rails 2.28.0 and rubocop-performance 1.23.0.
|
|
131
|
+
- Runs when: a `.rb`, `.rake` or `.gemspec` file, a `Gemfile` or a `Rakefile` changed.
|
|
132
|
+
- Needs: Ruby 2.7 or newer. It installs from RubyGems.
|
|
133
|
+
- Uses OpenQodex's own settings. A `.rubocop.yml` in the repo is not loaded, because it can load Ruby code.
|
|
134
|
+
- Sends: nothing.
|
|
135
|
+
|
|
136
|
+
## sqllint
|
|
137
|
+
|
|
138
|
+
- Version: part of OpenQodex.
|
|
139
|
+
- Runs when: a `.sql` file changed.
|
|
140
|
+
- Needs: nothing. It runs inside OpenQodex and downloads nothing.
|
|
141
|
+
- Checks Postgres migrations for common mistakes, such as a privileged function left callable by every role.
|
|
142
|
+
- Sends: nothing.
|
|
143
|
+
|
|
144
|
+
## Choosing scanners
|
|
145
|
+
|
|
146
|
+
- `scanners.disable` in `.openqodex.yaml` switches built-in scanners off.
|
|
147
|
+
- `--only` and `--skip` on `scan` and `review` pick scanners for one run.
|
|
148
|
+
- `custom-scanners` explains how to add any other scanner.
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
This page says what OpenQodex runs, what it sends where, and what it writes. Report a vulnerability through GitHub's private vulnerability reporting on the openqodex/openqodex repository. Never open a public issue for one.
|
|
4
|
+
|
|
5
|
+
## What runs
|
|
6
|
+
|
|
7
|
+
- The `openqodex` CLI, on your Node.
|
|
8
|
+
- The built-in scanners that fit the change, from `~/.openqodex/tools/`. Each is pinned to one version. A built-in scanner is never taken from your `PATH`.
|
|
9
|
+
- Custom scanners from `.openqodex.yaml` that you approved with `openqodex trust`.
|
|
10
|
+
- git, from your `PATH`.
|
|
11
|
+
|
|
12
|
+
OpenQodex starts every program with an argument list, never through a shell. Scanners get a small set of environment variables: `PATH`, `HOME`, `TMPDIR`, `LANG`, the `LC_` variables, the proxy variables, and what the scanner itself needs. Your other variables, such as API keys, are not passed on.
|
|
13
|
+
|
|
14
|
+
A repository can hold config files that make a scanner run code or rewrite files. OpenQodex does not load such files for oxlint, golangci-lint, brakeman and rubocop. It uses its own settings for them. ruff and gitleaks read the repository's own settings for rules only. ruff runs with fixes switched off.
|
|
15
|
+
|
|
16
|
+
## The trust step
|
|
17
|
+
|
|
18
|
+
A custom scanner is an arbitrary command. It runs on your machine with your permissions. The config file that names it comes from whatever repository you cloned. So nothing installs or runs a custom scanner until you approve that exact entry:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
npx openqodex trust
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`trust` downloads the release asset to a quarantine folder before it asks. Nothing is installed or run before your yes. `scan` and `review` never download a custom scanner. `trust` prints the version, the asset, its sha256, the program and the run line, then asks yes or no. The approval covers that repository and that entry only. An edited entry needs a new approval. `scan` and `review` skip an unapproved entry and list it as `untrusted`.
|
|
25
|
+
|
|
26
|
+
The stored sha256 is checked against the project's checksum file when the project publishes one. Otherwise it is the hash of your first download. `custom-scanners` explains the difference.
|
|
27
|
+
|
|
28
|
+
Agents that follow the OpenQodex skill are told never to run `openqodex trust` without asking you.
|
|
29
|
+
|
|
30
|
+
## What is sent where
|
|
31
|
+
|
|
32
|
+
OpenQodex and the built-in scanners send no code anywhere. The review runs on the model your agent already uses, which sees what the agent reads. A custom scanner you approved does whatever its own command does.
|
|
33
|
+
|
|
34
|
+
OpenQodex and the built-in scanners use the network for these things only:
|
|
35
|
+
|
|
36
|
+
- Scanner downloads on first use. GitHub release files are checked against sha256 sums pinned in the package. semgrep and bandit come from PyPI through uv, with a Python 3.11 that uv downloads. oxlint comes from npm. brakeman and rubocop come from RubyGems. These package installs are pinned by version.
|
|
37
|
+
- Semgrep rule packs. semgrep fetches `p/default`, `p/security-audit` and `p/secrets` from the Semgrep registry on each run. Its metrics are off. The rules are never bundled in the package.
|
|
38
|
+
- The dependency check. When the change holds a lockfile, osv-scanner sends the names and versions of the dependencies in it to osv.dev. It never sends code.
|
|
39
|
+
- Custom scanners. `openqodex trust` reads the release from the GitHub API and downloads the asset. After approval, a custom scanner does whatever its own command does.
|
|
40
|
+
|
|
41
|
+
golangci-lint runs with the Go module proxy off, so it downloads no modules.
|
|
42
|
+
|
|
43
|
+
`--offline` skips osv-scanner and semgrep, which the report lists as disabled. It also turns scanner downloads off.
|
|
44
|
+
|
|
45
|
+
OpenQodex sends no telemetry. See `telemetry`.
|
|
46
|
+
|
|
47
|
+
## Secrets
|
|
48
|
+
|
|
49
|
+
When gitleaks finds a secret in the change, OpenQodex removes it from the brief, every report file and the terminal. It keeps the length and sha256 of each secret, to redact any text the agent quotes.
|
|
50
|
+
|
|
51
|
+
gitleaks writes its raw report to a temporary file outside the repository. That file holds the matched secrets. OpenQodex deletes it when the run ends. No file OpenQodex keeps holds the secret.
|
|
52
|
+
|
|
53
|
+
A secret is redacted only when a scanner matched it. When gitleaks did not run, the brief shows the change as it is.
|
|
54
|
+
|
|
55
|
+
## Where files are written
|
|
56
|
+
|
|
57
|
+
In your home folder, under `~/.openqodex/` (`OPENQODEX_HOME` moves it):
|
|
58
|
+
|
|
59
|
+
- `tools/<scanner>/<version>/`: the scanners.
|
|
60
|
+
- `tools/uv-python/`: the Python 3.11 for semgrep and bandit.
|
|
61
|
+
- `cache/`: the download caches for uv and npm.
|
|
62
|
+
- `runtime/<version>/` and `bin/openqodex`: the copy of the package and the launcher that the hooks call, written by `init`.
|
|
63
|
+
- `install.json`: what `init` and `hook install` wrote, so an uninstall removes only that.
|
|
64
|
+
- `trust.json`: your approvals of custom scanners.
|
|
65
|
+
|
|
66
|
+
In the repository, under `.openqodex/` only:
|
|
67
|
+
|
|
68
|
+
- `.gitignore`, holding `*`, so the folder ignores itself and `git status` does not change.
|
|
69
|
+
- `reviews/<time>-<id>/`: one folder per run, holding the brief, the scan result, the agent's findings and the reports. OpenQodex keeps the newest 20.
|
|
70
|
+
- `latest.json`: points at the newest run.
|
|
71
|
+
|
|
72
|
+
The agent settings and skill files `init` writes are listed in `agents`.
|
|
73
|
+
|
|
74
|
+
The change itself is worked out without writing inside `.git`. OpenQodex uses a temporary copy of the index and a temporary object folder.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Telemetry
|
|
2
|
+
|
|
3
|
+
OpenQodex sends no telemetry.
|
|
4
|
+
|
|
5
|
+
It collects no usage data, no crash reports and no identifiers. It has no switch to turn telemetry on, because there is none to turn on.
|
|
6
|
+
|
|
7
|
+
semgrep runs with its own metrics switched off.
|
|
8
|
+
|
|
9
|
+
The network traffic OpenQodex does cause is listed in `security`: scanner downloads, Semgrep rule packs, the osv.dev dependency lookup, and custom scanner downloads you approve.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: a11y-icon-only-button-no-aria-label
|
|
3
|
+
description: Icon-only button / clickable that relies on `title` for description without an aria-label
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.tsx"
|
|
7
|
+
- "*.jsx"
|
|
8
|
+
- "*.vue"
|
|
9
|
+
- "*.svelte"
|
|
10
|
+
- "*.astro"
|
|
11
|
+
- "**/*.tsx"
|
|
12
|
+
- "**/*.jsx"
|
|
13
|
+
- "**/*.vue"
|
|
14
|
+
- "**/*.svelte"
|
|
15
|
+
- "**/*.astro"
|
|
16
|
+
hunk_regex: "<button\\b|<a\\b|role=[\"']button[\"']|onClick=\\{"
|
|
17
|
+
confidence_floor: 0.75
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
A clickable element renders only an icon (an `<svg>`, an emoji, a
|
|
21
|
+
single character from a font-icon set, an `<img>` whose `alt` is
|
|
22
|
+
empty or omitted) and depends on `title` to convey what it does.
|
|
23
|
+
`title` is hover-only and is NOT reliably announced by screen
|
|
24
|
+
readers, so the control is opaque to assistive-tech users: they
|
|
25
|
+
hear "button" with no context.
|
|
26
|
+
|
|
27
|
+
Flag when an interactive element added in this diff:
|
|
28
|
+
- has no readable text node as a child (only `<svg>`, an icon
|
|
29
|
+
component like `<ChevronIcon />`, `<img alt="">`, a single
|
|
30
|
+
glyph, or pure children-of-children that are themselves icons),
|
|
31
|
+
- has no `aria-label`, `aria-labelledby`, OR a visually-hidden
|
|
32
|
+
text label (`<span class="sr-only">…</span>`, `<VisuallyHidden>`,
|
|
33
|
+
Tailwind's `sr-only` utility), AND
|
|
34
|
+
- the element is `<button>`, `<a>`, `[role="button"]`, or has an
|
|
35
|
+
`onClick`/`onPointerDown`/`onKeyDown` handler.
|
|
36
|
+
|
|
37
|
+
The `title` attribute alone is NOT a fix; flag those too.
|
|
38
|
+
|
|
39
|
+
Suppress when:
|
|
40
|
+
- the button already has `aria-label`, `aria-labelledby`, or a
|
|
41
|
+
visually-hidden text child (`sr-only`, `visually-hidden`,
|
|
42
|
+
`<VisuallyHidden>`, `screen-reader-text`),
|
|
43
|
+
- the icon itself carries a label (`<svg aria-label="…">` or an
|
|
44
|
+
icon component whose own implementation injects an aria-label),
|
|
45
|
+
- the project already has a wrapping component like `<IconButton
|
|
46
|
+
label="…" />` that injects the aria-label,
|
|
47
|
+
- the element has visible text alongside the icon.
|
|
48
|
+
|
|
49
|
+
Severity: `minor` by default. Bump to `major` for primary CTAs
|
|
50
|
+
(submit / save / delete) where the missing label blocks the user's
|
|
51
|
+
intent, not just degrades it.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: array-iteration-missing-key-prop
|
|
3
|
+
description: .map() rendering JSX without a stable key prop (or using index as key for mutable lists)
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.tsx"
|
|
7
|
+
- "*.jsx"
|
|
8
|
+
- "**/*.tsx"
|
|
9
|
+
- "**/*.jsx"
|
|
10
|
+
hunk_regex: "\\.map\\s*\\("
|
|
11
|
+
confidence_floor: 0.7
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
JSX rendered inside `.map(...)` either has no `key` prop or uses
|
|
15
|
+
the array index as a key. Without a stable key React can't reorder
|
|
16
|
+
children correctly: form inputs lose their internal state on
|
|
17
|
+
re-order, animations replay, and components mounted in the loop
|
|
18
|
+
double-fire effects when items shift positions.
|
|
19
|
+
|
|
20
|
+
Flag when:
|
|
21
|
+
- the `.map((item) => <Component .../>)` returns JSX with no `key`
|
|
22
|
+
- the `key` is `index` (or any expression that depends only on the
|
|
23
|
+
loop index) AND the underlying list can reorder / insert / remove
|
|
24
|
+
|
|
25
|
+
Suppress when:
|
|
26
|
+
- a stable id field is used (`key={item.id}` / `key={item.uuid}`)
|
|
27
|
+
- the list is provably static for the component's lifetime
|
|
28
|
+
(read-only constants, sorted-once display lists)
|
|
29
|
+
- the index IS the natural key (immutable, append-only)
|
|
30
|
+
- the rendered children are stateless / sideeffect-free and won't
|
|
31
|
+
notice a reorder
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: async-await-in-loop-n-plus-one
|
|
3
|
+
description: await inside a for / map / forEach, N round-trips per row
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "**/*.ts"
|
|
13
|
+
- "**/*.tsx"
|
|
14
|
+
- "**/*.js"
|
|
15
|
+
- "**/*.jsx"
|
|
16
|
+
- "**/*.mjs"
|
|
17
|
+
- "**/*.cjs"
|
|
18
|
+
hunk_regex: "for\\s*\\(|\\.(forEach|map|reduce)\\s*\\("
|
|
19
|
+
confidence_floor: 0.7
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
A `for` loop / `for-of` / `.map(async ...)` / `.forEach(async ...)`
|
|
23
|
+
body contains an `await` against a network or database client.
|
|
24
|
+
Sequential awaits multiply latency by row count (100 rows × 30ms
|
|
25
|
+
= 3s) and turn a list view, batch import, or N-of-M lookup into a
|
|
26
|
+
classic N+1.
|
|
27
|
+
|
|
28
|
+
Flag when:
|
|
29
|
+
- the loop iterates over a list of identifiers / records and the
|
|
30
|
+
body awaits a fetch / db query / API call **per iteration**
|
|
31
|
+
- the result is collected into an array (a `Promise.all` over the
|
|
32
|
+
iterable would parallelize cleanly)
|
|
33
|
+
|
|
34
|
+
Suppress when:
|
|
35
|
+
- ordering matters AND the next iteration's input depends on the
|
|
36
|
+
previous iteration's result (rate-limited APIs, paged cursors,
|
|
37
|
+
workflows that build on prior steps)
|
|
38
|
+
- there's an explicit per-iteration delay / rate-limit
|
|
39
|
+
- the loop iterates a known-small fixed set (≤ 3) where
|
|
40
|
+
parallelization wouldn't change anything material
|
|
41
|
+
- a `.map(async ...)` is followed by `await Promise.all(...)`;
|
|
42
|
+
that IS the parallel pattern
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: async-click-double-fire-race
|
|
3
|
+
description: Async onClick handler with no in-flight guard / disabled toggle, double-clicks fire duplicate requests
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.tsx"
|
|
7
|
+
- "*.jsx"
|
|
8
|
+
- "*.vue"
|
|
9
|
+
- "*.svelte"
|
|
10
|
+
- "**/*.tsx"
|
|
11
|
+
- "**/*.jsx"
|
|
12
|
+
- "**/*.vue"
|
|
13
|
+
- "**/*.svelte"
|
|
14
|
+
hunk_regex: "onClick=|@click=|on:click=|onPress=|onPointerDown="
|
|
15
|
+
confidence_floor: 0.75
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
A button / link / clickable triggers an async operation
|
|
19
|
+
(`fetch`, `mutate`, an awaited handler) without any guard against
|
|
20
|
+
re-firing while the first request is still in flight. A rapid
|
|
21
|
+
second click queues a duplicate request: a duplicate insert, a
|
|
22
|
+
double-charge, an interleaved state update where the late response
|
|
23
|
+
clobbers the early one. Touchpad users and slow networks hit this
|
|
24
|
+
constantly.
|
|
25
|
+
|
|
26
|
+
Flag when a handler added in this diff:
|
|
27
|
+
- is `async` OR returns a Promise / calls `await ...` / calls
|
|
28
|
+
`fetch(...)` / calls a project-specific async helper (mutate,
|
|
29
|
+
a fetch wrapper, etc.),
|
|
30
|
+
- AND has no `isLoading` / `inFlight` / `pending` boolean checked
|
|
31
|
+
at the top (early return when true),
|
|
32
|
+
- AND has no `disabled={isLoading}` (or equivalent class swap) on
|
|
33
|
+
the bound element,
|
|
34
|
+
- AND has no library-level dedup (TanStack Query's `mutate` with
|
|
35
|
+
the same key is OK; React Query's `useMutation` running through
|
|
36
|
+
its own queue is OK; a hand-rolled `setX([...])` is NOT).
|
|
37
|
+
|
|
38
|
+
Examples that should fire:
|
|
39
|
+
- `onClick={async () => { const r = await fetch(...); setState(r); }}`
|
|
40
|
+
with no disabled / loading guard
|
|
41
|
+
- an expand/collapse toggle that calls `fetchTeamMembers(id)`
|
|
42
|
+
but doesn't track `loadingTeams.has(id)`
|
|
43
|
+
- `<button onClick={async () => { await save(); navigate(...) }}>`
|
|
44
|
+
where save can take seconds
|
|
45
|
+
|
|
46
|
+
Suppress when:
|
|
47
|
+
- the handler is wrapped by `useMutation` / `useAction` /
|
|
48
|
+
`useTransition` / similar library primitive that owns the
|
|
49
|
+
in-flight tracking,
|
|
50
|
+
- the bound element has `disabled={isLoading}` /
|
|
51
|
+
`aria-disabled={isLoading}` / a CSS class swap that prevents
|
|
52
|
+
pointer events,
|
|
53
|
+
- the handler is genuinely idempotent (PATCH against a known
|
|
54
|
+
resource state, GET-only, etc.); call out the idempotency
|
|
55
|
+
guarantee when you suppress.
|
|
56
|
+
|
|
57
|
+
Severity: `minor` for read-only / idempotent paths, `major` for
|
|
58
|
+
writes / mutations / billable actions (the duplicate insert and
|
|
59
|
+
the double-charge cases).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: async-floating-promise
|
|
3
|
+
description: An async function is called without await / .catch, silent rejection
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "**/*.ts"
|
|
13
|
+
- "**/*.tsx"
|
|
14
|
+
- "**/*.js"
|
|
15
|
+
- "**/*.jsx"
|
|
16
|
+
- "**/*.mjs"
|
|
17
|
+
- "**/*.cjs"
|
|
18
|
+
hunk_regex: "\\basync\\b|\\.then\\("
|
|
19
|
+
confidence_floor: 0.7
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
An async function is invoked from synchronous code (or from another
|
|
23
|
+
async fn) without `await`, without `.then(...).catch(...)`, and
|
|
24
|
+
without `void`. The returned promise floats: a rejection becomes an
|
|
25
|
+
unhandled rejection (process warning, eventual crash on newer Node
|
|
26
|
+
versions, swallowed entirely in browsers) and the caller has no
|
|
27
|
+
ordering guarantee with subsequent statements.
|
|
28
|
+
|
|
29
|
+
Flag a call to an `async fn` (or a fn that demonstrably returns a
|
|
30
|
+
promise: `.then` chains, fetch wrappers, db client calls) whose
|
|
31
|
+
return value is **discarded**: not assigned, not returned, not
|
|
32
|
+
awaited, not chained with `.catch`, and not preceded by the explicit
|
|
33
|
+
`void` keyword.
|
|
34
|
+
|
|
35
|
+
Suppress when:
|
|
36
|
+
- the call is intentionally fire-and-forget AND prefixed with `void`
|
|
37
|
+
(the documented "I know" marker)
|
|
38
|
+
- the outer scope wraps in `Promise.all([...])` / `Promise.allSettled`
|
|
39
|
+
- the call is on a logger / telemetry / metrics method where dropped
|
|
40
|
+
rejections are explicitly acceptable
|