Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/ci-go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- '**.go'
- go.*
- Makefile
- spec/**
run-name: CI Go by @${{ github.actor }} ${{ github.sha }}
jobs:
build-test:
Expand All @@ -31,5 +32,19 @@ jobs:
with:
version: ${{ steps.extract_version.outputs.golangci_lint_version }}
args: ./cmd/... ./internal/...
- name: generated code matches the spec
run: |
make generate-api
git status --porcelain internal/api internal/commands spec
test -z "$(git status --porcelain internal/api internal/commands spec)"
- name: command surface keeps every command in the base branch
env:
BASE_REF: ${{ github.base_ref }}
run: |
git fetch --depth=1 origin "$BASE_REF"
git show FETCH_HEAD:spec/mcp-tools.tsv > "$RUNNER_TEMP/base-tools.tsv" || : > "$RUNNER_TEMP/base-tools.tsv"
if git show FETCH_HEAD:spec/command-surface.tsv > "$RUNNER_TEMP/base-surface.tsv"; then
go run ./scripts/commandsurface --base "$RUNNER_TEMP/base-surface.tsv" --base-tools "$RUNNER_TEMP/base-tools.tsv"
fi
- name: test
run: make unit
140 changes: 140 additions & 0 deletions .github/workflows/spec-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Opens a pull request when the OpenAPI pin in the Makefile is behind the bundle
# published in the monorepo. It only pushes a branch and opens a pull request:
# it never merges, and release.yml runs only on a push to main.
#
# Each token has one repository and the least permission. The steps that run Go
# from this repository have no token in their environment.
#
# Create the spec-sync environment with a main-only deployment branch policy before
# you add the app key to it as an environment secret. GitHub creates a missing
# environment with no policy when a job names it, so without this step a modified
# workflow on another branch could read the key.
name: Spec Drift
on:
schedule:
- cron: '0 20 * * 1'
workflow_dispatch:
run-name: Spec Drift by @${{ github.actor }}
permissions:
contents: read
concurrency:
group: spec-drift
cancel-in-progress: false
env:
SPEC_REPO: nullify-platform/nullify
SPEC_PATH: public-docs/.gitbook/assets/api/nullify-openapi-bundle.yaml
BRANCH: automation/spec-sync
jobs:
open-sync-pr:
if: github.repository == 'Nullify-Platform/cli'
runs-on: ubuntu-24.04
environment: spec-sync
steps:
- name: Require the app key
env:
APP_KEY: ${{ secrets.SPEC_SYNC_GITHUB_APP_KEY }}
run: |
if [ -z "$APP_KEY" ]; then
echo "::error::Secret SPEC_SYNC_GITHUB_APP_KEY is missing from the spec-sync environment. Add the private key of GitHub App 359904 as an environment secret."
exit 1
fi
- name: Mint a token that reads only the monorepo
id: read-token
uses: actions/create-github-app-token@v3
with:
app-id: 359904
private-key: ${{ secrets.SPEC_SYNC_GITHUB_APP_KEY }}
owner: Nullify-Platform
repositories: nullify
permission-contents: read
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Compare the pin with the newest bundle commit
id: drift
env:
GH_TOKEN: ${{ steps.read-token.outputs.token }}
OWN_TOKEN: ${{ github.token }}
run: |
latest=$(gh api "repos/$SPEC_REPO/commits?path=$SPEC_PATH&sha=main&per_page=1" --jq '.[0].sha')
pinned=$(sed -n 's/^SPEC_REF ?= //p' Makefile)
proposed=$(GH_TOKEN="$OWN_TOKEN" gh api "repos/$GITHUB_REPOSITORY/contents/Makefile?ref=$BRANCH" -H "Accept: application/vnd.github.raw" 2>/dev/null | sed -n 's/^SPEC_REF ?= //p' || true)
test -n "$latest"
echo "latest=$latest" >> "$GITHUB_OUTPUT"
echo "pinned=$pinned" >> "$GITHUB_OUTPUT"
if [ "$latest" = "$pinned" ] || [ "$latest" = "$proposed" ]; then
echo "drifted=false" >> "$GITHUB_OUTPUT"
else
echo "drifted=true" >> "$GITHUB_OUTPUT"
fi
- name: Download the newest bundle
if: steps.drift.outputs.drifted == 'true'
env:
GH_TOKEN: ${{ steps.read-token.outputs.token }}
LATEST: ${{ steps.drift.outputs.latest }}
run: |
sed -i "s/^SPEC_REF ?= .*/SPEC_REF ?= $LATEST/" Makefile
make fetch-spec
- name: Regenerate with no token in the environment
if: steps.drift.outputs.drifted == 'true'
id: regen
run: |
make generate-api
if make command-surface > surface.log 2>&1; then
echo "surface_ok=true" >> "$GITHUB_OUTPUT"
else
echo "surface_ok=false" >> "$GITHUB_OUTPUT"
fi
- name: Mint a token that writes only to this repository
if: steps.drift.outputs.drifted == 'true'
id: write-token
uses: actions/create-github-app-token@v3
with:
app-id: 359904
private-key: ${{ secrets.SPEC_SYNC_GITHUB_APP_KEY }}
owner: Nullify-Platform
repositories: cli
permission-contents: write
permission-pull-requests: write
- name: Open or update the pull request
if: steps.drift.outputs.drifted == 'true'
env:
GH_TOKEN: ${{ steps.write-token.outputs.token }}
LATEST: ${{ steps.drift.outputs.latest }}
PINNED: ${{ steps.drift.outputs.pinned }}
SURFACE_OK: ${{ steps.regen.outputs.surface_ok }}
run: |
{
echo "The published OpenAPI bundle moved from \`$PINNED\` to \`$LATEST\`."
echo
if [ "$SURFACE_OK" = "true" ]; then
echo "No command or MCP tool disappeared. New commands and any spec changes are in the diff."
else
echo "A command, flag or MCP tool disappeared. Record each one in \`internal/commands/surface_decisions.go\` or \`mcp.Decisions\`, and run \`make command-surface\`:"
echo
echo '```'
cat surface.log
echo '```'
fi
echo
echo "\`AllowlistTypeUnrecorded\` is on the \`bughunt\`, \`sca_dependencies\`, \`sca_containers\` and \`secrets_sensitive_data\` rows in \`internal/findings/types.go\`. If this bundle includes monorepo #17588, remove it from the SCA and sensitive-data rows. Keep it on \`bughunt\` unless the bundle adds an allowlist type input for bug hunt."
echo
echo "This pull request needs a person to review and merge it. A merge to main releases to customers."
} > "$RUNNER_TEMP/body.md"
rm -f surface.log

git config user.name "nullify-spec-sync[bot]"
git config user.email "nullify-spec-sync[bot]@users.noreply.github.com"
git switch -C "$BRANCH"
git add -A
git commit -m "feat(api): sync the OpenAPI spec to ${LATEST:0:12}"
git -c credential.helper= -c 'credential.helper=!gh auth git-credential' push --force origin "$BRANCH"

if [ "$(gh pr list --head "$BRANCH" --state open --json number --jq length)" = "0" ]; then
gh pr create --draft --head "$BRANCH" --title "feat(api): sync the OpenAPI spec" --body-file "$RUNNER_TEMP/body.md"
else
gh pr edit "$BRANCH" --body-file "$RUNNER_TEMP/body.md"
fi
97 changes: 97 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Changelog

## Unreleased

### Changed

- `nullify fix` now follows the run. It used to send the request, read the diff and exit 0 without waiting for the run. CI callers now wait up to 50 minutes by default, because a run can take 45. `--timeout` sets the wait. A failed run, or a run still going at the timeout, exits 1 and prints how to check it. The run continues on the server. `--no-wait` gives the fast return: it starts the run, prints its state and exits 0. If the token expires during the wait, the command refreshes it and continues. If it cannot refresh it, the command prints `run_id`, `state` and `check_with`, and exits 3.
- `nullify mcp serve` and `nullify fix` honour `NULLIFY_HTTP_TIMEOUT`.
- `nullify ci gate` is unchanged.
- `nullify fix`: any API failure on the start request now exits 3, including 403 and 409. It exited 1 before.
- The MCP search with `limit: 0` is now an error.
- `nullify api admin delete-mapping` and `update-mapping` now print the fields of the published response only, because the published API now describes these responses. Other fields are dropped.
- For a search, `sca` means dependencies and containers. For a verb on one finding, `sca` still means the dependency route. The MCP tool `type` parameters accept the same names as the CLI.
- `get_findings_for_repo` returns `total`, `returned` and `truncated` per section, and says that cloud and bug hunt findings have no repository instead of an empty list.
- `nullify_fix_finding` now starts the run and returns at once, with a non-error result. It no longer waits and no longer takes `create_pr`. Use `nullify_get_fix_status` and `nullify_get_fix_diff`.

### Added

- `nullify api` now follows the API published on 2 November 2026 (it was pinned to 26 May). This adds about 530 commands, including `nullify api cspm` allowlist, unallowlist, archive, unarchive, mark and unmark false positive, patch, triage and ticket delete, and a new `nullify api dast` group.
- `nullify findings` subcommands: `list`, `get`, `triage`, `allowlist`, `unallowlist`, `archive`, `unarchive`, `mark-false-positive`, `unmark-false-positive`, `create-ticket`, `delete-ticket`, `feedback` and `get-feedback`.
- `nullify findings --type` accepts `scpm`. `--from` and `--to` set time bounds. The output adds `returned` and `truncated`. Each row is the JSON that the server sent, so it holds only the fields of that row and any field that the CLI does not know.
- MCP tools in the default set: `nullify_submit_finding_feedback`, `nullify_get_finding_feedback`. `nullify_allowlist_finding`, `nullify_unallowlist_finding` and `nullify_get_finding_triage` work for CSPM, and `nullify_unallowlist_finding` and `nullify_create_ticket` work for bug hunt.
- MCP tools in the new `findings-write` set and in `all`, not in the default set: `nullify_archive_finding`, `nullify_unarchive_finding`, `nullify_mark_false_positive`, `nullify_unmark_false_positive`, `nullify_delete_ticket`.
- `nullify findings fix-status` and `fix-diff`, with the MCP tools `nullify_get_fix_status` and `nullify_get_fix_diff`.
- MCP `list_action_items`. Every MCP tool now has read-only or destructive annotations.

### Known limits

- `nullify fix` exits 1 when a run succeeded but its diff cannot be read, even when it prints a new pull request. The exit is the same as before.
- `nullify findings unallowlist --reason` is required for every type, but the CSPM route does not record it. CSPM returns a `warning` field, and the CLI also writes the warning to stderr.
- `nullify findings` and the MCP search tools use the unified search. It returns triaged findings only, and a repository filter matches the exact stored name. With no `--type` inside a repository, cloud and bug hunt findings are excluded.
- `--allowlist-type` and the MCP `allowlist_type` are required for every finding type, but the server may not record them for `bughunt`, `sca_dependencies`, `sca_containers` and `secrets_sensitive_data`. These types return a `warning` field, and the CLI also writes the warning to stderr. The reason is recorded. Bug hunt has no such input. SCA and sensitive-data handlers ignore the type until monorepo #17588 deploys: remove `AllowlistTypeUnrecorded` from these rows when the spec moves past that pull request.


### Fixed

- `nullify fix --create-pr` stops when the server rescues an abandoned pull-request step and moves the run to `cached`. It exits 1 and says that no new pull request was opened. It used to wait for the whole timeout and then say that the run was still going.
- `nullify findings` and the MCP search reject a `--from` or `--to` that is not RFC 3339, plain dates included. The server ignored such a value, so the result was not filtered and the command exited 0.
- `nullify findings --type secrets` and `--type bughunt` sent filter values that the API rejects, and exited 3 with a 400.
- The MCP `type=scpm` search sent `Platform`, which the API accepts but which matches no finding, so it always returned nothing. `--type Platform` now means `scpm`. `--type Unknown` is rejected, because it also matches no finding.
- `nullify findings --type cspm` no longer filters by the repository of the working directory. Cloud findings have no repository, so that result was always empty.
- `nullify findings` rejects a `--limit` of zero or less, an unknown `--type` and an unknown `--status`. Each of these returned a wrong result before. Text after `nullify findings` that is not a subcommand is now an error, so a mistyped verb does not list findings.
- `nullify fix --create-pr` called a route that the API removed. The fix run opens the pull request itself, so the command now waits for that pull request and reports it. `nullify fix` also waits for the fix to end before it reads the diff, and accepts every type with autofix.
- `nullify_fix_finding` no longer calls the removed diff routes for CSPM, SCPM and pentest.
- `nullify fix` reported success for a failed fix. A failed state, a run still going at the timeout, and `--create-pr` with no new pull request now exit non-zero. The output adds `state` and `run_id`. `--create-pr` reports only a pull request that this run opened, never an older one.
- `nullify findings` could list sensitive-data secrets but every verb used the credential routes. `--type secrets_sensitive_data` uses `/secrets/sensitivedata/findings/...`, and each verb accepts the `type` value of a listed row. `--type secrets` still lists both.
- Unallowlisting a secret sends the `unallowlistType` that the API requires, with the value that the dashboard sends.
- MCP allowlist and unallowlist reject an empty reason or an invalid allowlist type before they call the API.

### Moved

These commands keep working. Each prints a deprecation notice on stderr and calls the new route.

| Old command | New command |
|-------------|-------------|
| `nullify api asset-graph list-reachability` | `nullify api context list-reachability` |
| `nullify api asset-graph list-search` | `nullify api context list-search` |
| `nullify api asset-graph list-subgraph` | `nullify api context list-subgraph` |
| `nullify api asset-graph list-summary` | `nullify api context list-asset-graph-summary` |
| `nullify api infrastructure list-graphs` | `nullify api context list-graphs` |
| `nullify api infrastructure list-info` | `nullify api context list-info` |
| `nullify api infrastructure list-summary` | `nullify api context list-infrastructure-graphs-summary` |
| `nullify api infrastructure get-graphs` | `nullify api context get-graphs` |
| `nullify api infrastructure get-download` | `nullify api context get-download` |
| `nullify api infrastructure get-graphs-versions` | `nullify api context get-infrastructure-graphs-versions` |
| `nullify api infrastructure get-graphs-versions-2` | `nullify api context get-infrastructure-graphs-versions-2` |

### Removed

The API no longer serves these routes, so these commands could only return 404. None of the routes is in the service specs of the deployed API.

| Command | Replacement |
|---------|-------------|
| `nullify api admin create-initialize`, `create-uninitialize` | `nullify api admin update-repositories-scan-scope` |
| `nullify api context list-cloud-accounts` | `nullify api context list-asset-inventory` |
| `nullify api context list-cloud-resources`, `list-resources` | `nullify api context list-asset-inventory` |
| `nullify api context list-public-facing` | `nullify api context list-public` |
| `nullify api cspm get-diff` | `nullify api cspm get-state` |
| `nullify api scpm get-diff` | `nullify api scpm get-state` |
| `nullify pentest get-pentest-findings-autofix-cache-diff` | `nullify api dast get-state` |
| `nullify api scpm create-cache-by-id`, `create-create_pr-by-id` | `nullify api scpm create-fix-by-id` |
| `nullify api scpm create-retriage` | `nullify api orchestrator create-retriage`, with a new body |
| `nullify api scpm list-dependencies` | `nullify api scpm create-dependencies-analyze`, `create-batch` |
| `nullify api sca get-sbom` | `nullify sbom get`, `nullify api context get-sboms-repository-latest` |
| `nullify api orchestrator create-complete` | None: the route exists only in a local stack |
| `nullify api manager list-credits`, `create-credits` | None: the credits system is gone |
| `nullify api manager list-customer-questions`, `get-customer-questions`, `delete-customer-questions`, `delete-customer-questions-2`, `delete-questions`, `patch-answers` | `nullify api manager list-action-items` |
| `nullify api manager list-escalations`, `patch-escalations` | `nullify api manager list-action-items` |

The MCP tool `list_escalations` is removed. Its route `GET /manager/escalations` is gone from prod (#5596), so the tool only returned errors. Use `list_action_items`, which returns a different schema, so it is not an alias. `spec/mcp-tools.tsv` now locks the MCP tool names, annotations and tool sets.

### Deprecated flags

The API no longer accepts these filters. The flags stay accepted, have no effect and print a notice.

- `nullify api cspm list-scans --region`, `--scan-type`
- `nullify api asset-graph list-reachability --account-id`, `list-subgraph --account-id`
15 changes: 12 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build clean lint lint-go lint-docker fetch-spec generate-api unit cov
.PHONY: build clean lint lint-go lint-docker fetch-spec generate-api command-surface unit cov

# set the version as the latest commit sha if it's not already defined
ifndef VERSION
Expand Down Expand Up @@ -37,8 +37,9 @@ lint-docker:
# OpenAPI bundle sourcing. The spec is published in the (private) nullify
# monorepo; we pin a commit and vendor the bundle into spec/ so generation is
# reproducible and offline. To update: bump SPEC_REF, run `make fetch-spec`,
# then `make generate-api`, and commit both spec/ and the regenerated code.
SPEC_REF ?= 7b34970bbbeeabf665cd71fa0eb0e07eaac534a3
# `make generate-api` and `make command-surface`, and commit spec/ and the
# regenerated code. The spec-drift workflow does this on a schedule.
SPEC_REF ?= 1b9c3dd0cf497030de8646ae1b8e1f407d25e04a
SPEC_REPO ?= nullify-platform/nullify
SPEC_PATH_IN_REPO ?= public-docs/.gitbook/assets/api/nullify-openapi-bundle.yaml
SPEC_LOCAL ?= spec/nullify-openapi-bundle.yaml
Expand All @@ -54,6 +55,14 @@ fetch-spec:
generate-api:
go run ./scripts/generate --spec $(SPEC_LOCAL) --output internal/api --cmd-output internal/commands

# command-surface checks the command tree against spec/command-surface.tsv and the
# MCP tools against spec/mcp-tools.tsv, and rewrites both snapshots. It fails on a
# command, flag or tool that disappeared unless internal/commands/surface_decisions.go
# or mcp.Decisions records the decision. A tool that changes its annotation or
# tool sets needs a decision too.
command-surface:
go run ./scripts/commandsurface --write

unit:
go test -v -skip TestIntegration ./...

Expand Down
Loading