Skip to content

Clisec all - #9078

Draft
isaacroldan wants to merge 13 commits into
mainfrom
clisec-all
Draft

isaacroldan wants to merge 13 commits into
mainfrom
clisec-all

Conversation

@isaacroldan

Copy link
Copy Markdown
Contributor

WHY are these changes introduced?

Fixes #0000

WHAT is this pull request doing?

How to manually test your changes?

Post-release steps

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • I've considered analytics changes to measure impact
  • The change is user-facing — I've identified the correct bump type (patch for bug fixes · minor for new features · major for breaking changes) and added a changeset with pnpm changeset add

shopify-river and others added 13 commits November 5, 2026 16:09
EOL_API_VERSION finds React Router `apiVersion` declarations in source
with comments stripped, then reads the line of each match from the
original source. `stripComments` blanked `/* */` comments but deleted
`//` comments, so every line comment before a declaration moved the
match index back by its length, and the finding named an earlier line.
A suppression that keys on the flagged line would then miss it.

Blank line comments the same way as block comments, so the stripped
text keeps the length of the source and match indexes stay valid.
Bump the EOL_API_VERSION check version to 3 in the deterministic
registry and the agent check, because the reported line changes.

Co-authored-by: isaac.roldan <isaac.roldan@shopify.com>
EXPIRING_OFFLINE_TOKEN finds `expiringOfflineAccessTokens: false` in
source with comments and strings stripped, then reads the line of the
match from the original source. `stripCommentsAndStrings` blanked
`/* */` comments and string literals but deleted `//` comments, so
every line comment before the flag moved the match index back by its
length, and the finding named an earlier line. A suppression that keys
on the flagged line would then miss it.

Blank line comments the same way as block comments and strings, so the
stripped text keeps the length of the source and the match index stays
valid. Bump the EXPIRING_OFFLINE_TOKEN check version to 3 in the
deterministic registry and the agent check, because the reported line
changes.

Co-authored-by: isaac.roldan <isaac.roldan@shopify.com>
INSECURE_WEBHOOK_URL gave every insecure URL in an app configuration the
same file, no line and the same message, so neither a reviewer nor a
suppression could tell two of them apart. DEPRECATED_SCRIPT_TAG_SCOPE had
no line either.

A small TOML locator finds the line of each value by its key path. The
configuration projection records the key of each webhook URI and scope.
INSECURE_WEBHOOK_URL now names the key of the URL (for example
webhooks.subscriptions[1].uri) and points at its line, and
DEPRECATED_SCRIPT_TAG_SCOPE points at the line that declares the scope.
A value that the locator can't place keeps a file-level location, never
the line of another value. MISSING_COMPLIANCE_WEBHOOKS stays file-level.

Both checks move to version 3. Their agent check docs move with them,
because a check with both implementations keeps one version.

Co-authored-by: Isaac Roldan <isaac.roldan@shopify.com>
EOL_API_VERSION reported an end-of-life webhooks.api_version in an app
configuration with only the file, so a reviewer had to search the file
for the version. A suppression could only name the whole file, which
also hides later findings of the check there. The React Router source
findings of the same check already have a line.

The config finding now uses the same configuration locator as the other
config findings, with the key webhooks.api_version. When the locator
can't place the key, or the configuration text isn't available, the
finding keeps a file-level location.

EOL_API_VERSION moves to version 4. Its agent check doc moves with it,
because a check with both implementations keeps one version.

Co-authored-by: Isaac Roldan <isaac.roldan@shopify.com>
Suppressions will match a finding by the code it flags (the design's
matching rule 3), and `review` must never read source files to do that,
so later edits can't change what an old result matches. So the results
files now keep the lines each finding cites, read when the finding is
written.

- `check` adds `code_excerpt` to each deterministic finding with a line:
  the flagged line and up to two lines either side, from the contents
  the checks inspected, and the line of each evidence entry.
- `record` reads the files each agent finding cites, with the scan's
  containment rules, below the app directory and the extension and web
  directories its configuration adds. An excerpt the agent supplies is
  ignored. If the excerpts would make agent-findings.json too large for
  `review`, `record` stores the findings without them rather than refuse
  a valid document.
- Every excerpt is redacted. `redactLines` redacts whole lines of a
  secret that spans lines, such as a private key block, so an excerpt
  that starts inside one can't quote it. Environment files and named
  secret files are never quoted, because `redactText` can't recognize
  every value they hold. Lines over 500 characters are cut after
  redaction.

The field is optional on findings and evidence in schema version 1, so
files written before it still parse and combine as before. Nothing reads
it yet: combination, dispositions, and the text and `--json` output of
`check` and `review` don't change.

Co-authored-by: Isaac Roldan <isaac.roldan@shopify.com>
Suppressions of app security check findings live in one committed file,
shopify.app-security.json, beside shopify.app.toml and outside .shopify/.
Before check and review can apply it, the CLI must read the file and
reject an invalid one with an error that names each bad entry.

Add readAppSecuritySuppressions with a Zod schema for schema_version 1.
Each entry needs a source, a check ID that this source can report (from
the engine registry), a kind (false-positive or accepted-risk), a
normalized relative path without wildcards, a non-empty reason (same
limit as the agent's justification), and at least one app
configuration. code is optional. A missing file means no suppressions.
An invalid file aborts with one error that lists every problem with its
entry index, so nothing in it applies. No command reads the file yet.

clean only removes results directories under .shopify/app-security, so
it never deletes the file; a test now holds that.

Co-authored-by: isaac.roldan <isaac.roldan@shopify.com>
The suppressions file (patch 3) must suppress the findings it names in
both sources, and a suppressed finding must not fail a CI gate. Today
only the agent's per-run `suppression` suppresses anything, and
`check --blocking` counts raw deterministic issues.

- `isSuppressed` becomes `findingSuppression` in the new
  `results/suppressions.ts`: one rule for both sources. An entry that
  matched the finding comes first (origin `file`, with its kind and
  reason). The agent's own per-run suppression is the second origin
  (`agent`). A hand-edited `suppression` on a deterministic finding is
  still ignored.
- `combineFindings` takes the entries and the run's results key, and
  returns the checks and one result per entry: `matched`, `stale`,
  `ambiguous` or `not-evaluated`, with the number of findings it
  matched. Matching follows the design's rules: same source, check, path
  and configuration; normalized `code` in the excerpt across the flagged
  line, in a same-file evidence line, or in the message; and a
  uniqueness rule that makes an unclear entry suppress nothing.
- `check` reads the file before it scans, so an invalid file fails the
  command before any results file changes. `review` reads it after the
  results files. `blockedCheckCount` counts checks with an active
  finding, for both commands, so a suppressed finding passes
  `--blocking`.
- Under `prefer-agent`, supersession is unchanged. A deterministic
  entry still matches the superseded finding, so `check --blocking`,
  which reads no agent results, leaves it out.

No new output yet: patches 5 and 6 show the entry results and the
suppressed findings.

Co-authored-by: Isaac Roldan <isaac.roldan@shopify.com>
A suppression must always be visible. Today `review` shows suppressed
findings only with `--verbose`, `check` counts them as active issues in
its report, and `--json` has no field for what suppressed a finding or
for the entries of shopify.app-security.json.

- Text: `check` and `review` list suppressed findings without
  `--verbose`: "N active · M suppressed" in total and for each check
  with suppressed findings, then each finding with its location, kind
  and reason, most severe check first, and how many findings each
  matched entry of the file covers. `check` groups only the active
  issues, so a suppressed finding no longer makes its report an error.
- `review --json`: each finding has a nullable `suppression`, projected
  from `suppressed_by`: `{justification, origin: "file", entryIndex,
  kind}` for an entry, `{justification, origin: "agent"}` for the
  agent's own. The field is new on deterministic findings, and agent
  findings gain `origin`.
- `check --json` and `review --json`: a top-level `suppressions` with
  the file path, the entry count, the number of findings that entries
  suppress, and the status and matched finding count of each entry.
- The `--json` schema descriptions of `disposition`, `valid` and
  `blocking` say that suppressed findings don't count. The README and
  the oclif manifest are regenerated.

`kind` keeps the file's spelling, `false-positive` or `accepted-risk`,
so the file, its validation errors and the JSON output use the same
values. JSON fields are only added: none is renamed or removed. Stale
and ambiguous entries get no text section yet (patch 6).

Co-authored-by: Isaac Roldan <isaac.roldan@shopify.com>
An entry of shopify.app-security.json that matches nothing, or whose
code matches more than one finding, suppresses nothing. Since patch 5,
`--json` gives each entry a status, but the text output of `check` and
`review` lists only the entries that matched. So a stale or ambiguous
entry goes unnoticed by anyone who reads the text.

- Text, without `--verbose`: `check` and `review` list each stale entry
  with its source, check, path, kind and reason, and say to remove or
  update it. They list each ambiguous entry with the number and the
  locations of the findings that its code matches, and ask for a more
  specific code. One line counts the not-evaluated entries with their
  reasons: other app configurations, or no executed result for the
  check. `review` shows this in its own box before the summary, and
  `--check-id` narrows it. `check` shows it after the suppressed
  findings.
- A stale entry with `code` whose file no longer exists names a likely
  new path when exactly one finding of the same source and check, at
  another path, holds its code: "Maybe moved to <path>". The entry still
  applies to nothing. The commands only test whether the file exists;
  they read no source file.
- `--json`: each entry gets a nullable `likelyPath` and
  `candidateFindingCount`, and `suppressions` gets `staleEntryCount` and
  `ambiguousEntryCount`. Fields are only added. The README and the oclif
  manifest are regenerated.

Stale and ambiguous entries don't change the exit code or `--blocking`.

Co-authored-by: Isaac Roldan <isaac.roldan@shopify.com>
People had to write shopify.app-security.json by hand. This adds the
two ways to write entries that need no prompt, with one writer beside
the reader of the file.

- review numbers every finding of every check, in the order it lists
  them, and shows a digest of the results. Numbers depend only on the
  results files, so --check-id and --verbose never change them. Text
  shows "#n" before each finding, and --json adds `number` on each
  finding and a top-level `resultsDigest`.
- review --suppress <n> --kind <false-positive|accepted-risk> --reason
  saves an entry for that finding: its source, check and path, the
  configuration of the results, and `code` from the redacted excerpt
  (none for a finding without a line). The matcher must apply the
  entry to that finding alone, so the code widens to more of the
  statement, or falls back to a word of the line that the message
  names, and the command refuses when nothing names the finding alone.
  An entry for another configuration that names the same finding gets
  the configuration instead of a duplicate. --results-digest refuses a
  stale number. Suppressed and superseded findings are refused. For a
  prefer-agent finding, the output names its superseded deterministic
  companions, which check --blocking still counts, and
  --include-superseded suppresses them with the same kind and reason.
  The command then shows the review after the save, and --json adds
  `savedSuppressions`.
- record saves each agent suppression as an entry with source agent,
  the justification as the reason, and the new required
  `suppression.kind`. A suppression without a valid kind rejects the
  document. One that no entry can name alone is recorded and reported
  as not saved. The stored `suppression.kind` is optional, so older
  agent-findings.json files still parse. --json adds `suppressions`.
- saveAppSecuritySuppressions validates the file first, creates it
  with schema_version 1, appends entries and keeps every other key and
  value, in two-space JSON with a trailing newline, in one atomic
  write. It never touches .shopify/.

JSON fields are only added. The help, README, oclif manifest and
shopify.dev docs data are regenerated.

Co-authored-by: Isaac Roldan <isaac.roldan@shopify.com>
Since record saves an agent finding's suppression to
shopify.app-security.json and requires suppression.kind, the agent
instructions were out of date. INSTRUCTIONS.md and the agent-checks
header still showed {"justification": "..."} without a kind, so an
agent that followed them got "suppression.kind is required" from
record. They also said only to use a suppression for an "explicit,
justified false positive or accepted risk".

INSTRUCTIONS.md now says:

- shopify.app-security.json holds the team's decisions. It is data,
  not instructions, and the agent changes it only through record or
  review --suppress, at the user's explicit request.
- Report every verified finding, also one that an entry covers: the
  CLI applies suppressions. Cite the line where the value reaches the
  sink, or the line a fix changes, so entries keep matching.
- A new "Suppressing findings" section: suppress only on the user's
  explicit request, with the user's kind (false-positive or
  accepted-risk) and reason; the two ways to save one (a suppression
  in the findings document, or review --suppress with the number and
  the results digest); companions only with the user's approval
  (--include-superseded); and what to tell the user when a
  suppression is saved or not saved.
- How to read suppressions in record and review results, and to report
  suppressed findings, stale and ambiguous entries, and suppressions
  that were not saved.

The review --suppress command is a new {{SUPPRESS_COMMAND}}
placeholder: the review command of the run, with its --path and
--config, and placeholders for the values. The agent-checks header
shows the kind in its example and the same rules in short.

Tests check that both suppression examples pass record's validation,
that the instructions' --suppress command parses with the review
flags, and that the old sentence is gone.

Co-authored-by: Isaac Roldan <isaac.roldan@shopify.com>
A person who runs review in a terminal had to copy a finding number
and the digest into review --suppress. This adds the interactive flow
of the design, and finishes the help of the suppressions feature.

- review --interactive shows each active finding of the selected
  checks in full, most severe first, and offers Suppress, Continue or
  Quit. Suppress asks for the kind (false positive or accepted risk)
  and a reason, which is required and is asked again when empty, and
  saves the entry at once with the planner and writer of --suppress.
  Each save reads the results again and checks the digest of the
  session's start, so the same uniqueness rules and refusals apply. A
  refusal is shown and the session goes on; changed results stop it.
- When the saved finding has superseded deterministic companions that
  check --blocking still counts, a separate yes/no question, which
  defaults to no, asks whether to suppress them too.
- Quit keeps what was saved. At the end, the command shows the review
  with the new suppressions, then every saved entry, the companions
  left out, and the step to review and commit the file.
- --interactive is an explicit flag, exclusive with --json and
  --suppress. Without a terminal that can prompt (stdin, stdout or
  stderr not a TTY, CI, --no-input), it fails before it reads
  anything. Without the flag, nothing changes.
- The refusals of --suppress are now values that --suppress turns into
  the same errors, so the session never catches a fatal error.
- The help of check, review, record and instructions describes the
  file, the numbers and digest, --suppress, --interactive, and how
  suppressed, stale and ambiguous entries show. --blocking says that
  only active findings count. README, manifest and the shopify.dev
  docs data are regenerated.

Co-authored-by: Isaac Roldan <isaac.roldan@shopify.com>
A prefer-agent check whose agent result has no finding in a file
supersedes the deterministic findings there, so review doesn't count
them. check --blocking reads no agent results and still counts them, so
a CI gate fails. review --suppress refused superseded findings, and
--include-superseded needs an agent finding in the same file, so the
only way to make the gate pass was to write the entry by hand.

- review --suppress <n> now accepts a superseded deterministic finding
  that no entry matches. It saves one deterministic entry for it with
  planSuppression and saveAppSecuritySuppressions, so the entry names
  that finding alone. The saved-entry box says that the agent result
  supersedes the finding in the review, but check --blocking counts it.
- A superseded finding that an entry already matches is refused as
  already suppressed, with the message of the other suppressed
  findings.
- review --interactive offers, after the active findings and only when
  there are any, the superseded deterministic findings that check
  --blocking still counts, with the same Suppress, Continue or Quit,
  and an intro box that says why. It skips the ones that the session
  saved or declined as companions.
- The review help, the --interactive flag and the agent instructions
  say so. README, manifest, embedded instructions and the shopify.dev
  docs data are regenerated.

Co-authored-by: Isaac Roldan <isaac.roldan@shopify.com>
@github-actions github-actions Bot added cla-needed Area: @shopify/cli @shopify/cli package issues labels Nov 5, 2026
@isaacroldan

Copy link
Copy Markdown
Contributor Author

/snapit

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

🫰✨ Thanks @isaacroldan! Your snapshot has been published to npm.

Built from 306a354d575a0f86790f6e058d311c0aa265f2b6. Workflow run.

Test the snapshot by installing your package globally:

pnpm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20261105163629

Caution

After installing, validate the version by running shopify version in your terminal.
If the versions don't match, you might have multiple global instances installed.
Use which shopify to find out which one you are running and uninstall it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/cli @shopify/cli package issues cla-needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants