Skip to content

Commit 107dbb1

Browse files
committed
feat(auth): move browser login to RFC 8628 device flow on www.vulnetix.com
`vulnetix auth login` no longer talks to app.vulnetix.com. The flow is now a proper device authorization grant served by www.vulnetix.com. The old scheme was not a device flow. The CLI invented the six-character code and polled a public endpoint that handed the org ApiKey to whoever presented an unclaimed code inside the five-minute TTL. Nothing bound the CLI that created the code to the client that collected the key; guessing the 36^6 space was the only barrier. The endpoints were also hardcoded constants, so the flow could not be pointed at a test server and had no test coverage at all. Now: - The CLI POSTs /cli/device/authorize before opening a browser and receives a 256-bit device_code plus a short user_code. The device_code is the credential and is never printed; the user_code is only an approval handle. - Polling /cli/device/token honours the server's interval and expires_in, and handles authorization_pending, slow_down (backs off), expired_token and access_denied. A 429 from the rate limiter also backs off. - The browser is opened at verification_uri_complete, so the code is pre-filled and the user confirms rather than transcribes. - VULNETIX_WEB_URL overrides the console base for local verification, mirroring VULNETIX_API_URL. `--verbose` prints the resolved endpoints. Adds cmd/auth_device_test.go — the first test coverage of this code — driving the whole client state machine against an httptest server, and scripts/verify-device-flow.sh, which stands up website + vdb-site + Postgres locally and asserts the proxy path, the JWT gate, the rate limiter, the pending state, single-use redemption, and that only the sha256 of the device_code is persisted. Docs: every `--base-url https://app.vulnetix.com/api` example was wrong. The CLI has never contacted that host. Uploads and `gha` go to api.vdb.vulnetix.com/v1, `vdb`/`package-firewall` to api.vdb.vulnetix.com, and `/api/check` never existed — the reachable probe is /health. The corporate proxy guide now names all three hosts an allowlist must carry. Lint: golangci-lint is clean uncapped. Removes seven dead functions, fixes four ineffectual assignments (one in the login path), two S1017 TrimPrefix cases, an empty branch and a dead append, and checks the errors that errcheck flagged. Adds a lint job to CI with a pinned linter version, and a pre-commit hook (`just hooks`).
1 parent f58ba12 commit 107dbb1

127 files changed

Lines changed: 13104 additions & 1556 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.githooks/pre-commit

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Fast local feedback before a commit. GitHub Actions is the authority; this
4+
# hook exists so you find out in two seconds rather than two minutes.
5+
#
6+
# Enable once per clone: just hooks
7+
# Bypass in an emergency: git commit --no-verify
8+
#
9+
# Only staged Go files are considered, so the cost scales with the diff.
10+
11+
set -uo pipefail
12+
13+
staged_go() { git diff --cached --name-only --diff-filter=ACMR -- '*.go'; }
14+
15+
mapfile -t GO_FILES < <(staged_go)
16+
[[ ${#GO_FILES[@]} -eq 0 ]] && exit 0
17+
18+
fail=0
19+
20+
# --- gofmt -----------------------------------------------------------------
21+
unformatted=$(gofmt -l "${GO_FILES[@]}" 2>/dev/null)
22+
if [[ -n "${unformatted}" ]]; then
23+
echo "pre-commit: these files are not gofmt'd:" >&2
24+
printf ' %s\n' ${unformatted} >&2
25+
echo " fix: gofmt -w ${unformatted}" >&2
26+
fail=1
27+
fi
28+
29+
# --- go vet, scoped to the packages that changed ----------------------------
30+
mapfile -t PKGS < <(printf '%s\n' "${GO_FILES[@]}" | xargs -r -n1 dirname | sort -u | sed 's#^#./#')
31+
if [[ ${#PKGS[@]} -gt 0 ]]; then
32+
if ! go vet "${PKGS[@]}" 2>&1; then
33+
echo "pre-commit: go vet failed" >&2
34+
fail=1
35+
fi
36+
fi
37+
38+
# --- golangci-lint, when available -----------------------------------------
39+
# A missing linter must not block a commit; CI still enforces it.
40+
if command -v golangci-lint >/dev/null 2>&1; then
41+
if ! golangci-lint run "${PKGS[@]}" 2>&1; then
42+
echo "pre-commit: golangci-lint found issues" >&2
43+
fail=1
44+
fi
45+
else
46+
echo "pre-commit: golangci-lint not installed, skipping (CI will run it)" >&2
47+
fi
48+
49+
if [[ ${fail} -ne 0 ]]; then
50+
echo >&2
51+
echo "pre-commit failed. Fix the above, or commit with --no-verify to bypass." >&2
52+
exit 1
53+
fi

.github/workflows/docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
- name: Setup Go
3838
uses: actions/setup-go@v6
3939
with:
40-
go-version: "1.21"
40+
go-version-file: go.mod
4141

4242
- name: Build Hugo site
4343
working-directory: website

.github/workflows/release.yml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ on:
1414
permissions:
1515
contents: write
1616

17+
# Never run two releases for the same version at once: the create-or-update
18+
# step is idempotent but not concurrency-safe against itself.
19+
concurrency:
20+
group: release-${{ inputs.version || github.ref }}
21+
cancel-in-progress: false
22+
1723
env:
1824
# Force JS actions to Node 24 so deprecated Node 20 runs stop firing while
1925
# action versions catch up (silences upload-artifact@v5 et al.).
@@ -22,6 +28,7 @@ env:
2228
jobs:
2329
version:
2430
runs-on: ubuntu-latest
31+
timeout-minutes: 5
2532
outputs:
2633
version: ${{ steps.version.outputs.VERSION }}
2734
steps:
@@ -40,13 +47,14 @@ jobs:
4047
# Linux + Windows targets are cross-compiled from ubuntu using zig cc.
4148
needs: version
4249
runs-on: ubuntu-latest
50+
timeout-minutes: 45
4351
steps:
4452
- uses: actions/checkout@v5
4553
with:
4654
fetch-depth: 0
4755
- uses: actions/setup-go@v6
4856
with:
49-
go-version: '1.25'
57+
go-version-file: go.mod
5058

5159
- name: Configure vdb-sca-match private fetch (not yet in module proxy)
5260
run: |
@@ -99,13 +107,14 @@ jobs:
99107
# CGO (zig cc can't target macOS without the SDK).
100108
needs: version
101109
runs-on: macos-latest
110+
timeout-minutes: 45
102111
steps:
103112
- uses: actions/checkout@v5
104113
with:
105114
fetch-depth: 0
106115
- uses: actions/setup-go@v6
107116
with:
108-
go-version: '1.25'
117+
go-version-file: go.mod
109118

110119
- name: Configure vdb-sca-match private fetch (not yet in module proxy)
111120
run: |
@@ -137,6 +146,7 @@ jobs:
137146
release:
138147
needs: [version, build-cross, build-darwin]
139148
runs-on: ubuntu-latest
149+
timeout-minutes: 20
140150
outputs:
141151
version: ${{ needs.version.outputs.version }}
142152
steps:
@@ -180,7 +190,11 @@ jobs:
180190
test-go-install:
181191
needs: release
182192
runs-on: ubuntu-latest
193+
timeout-minutes: 20
183194
steps:
195+
# This job deliberately does not check out the repo — it proves the
196+
# published module installs from the proxy. go.mod is therefore not on
197+
# disk and go-version-file cannot be used; keep this pin in step with go.mod.
184198
- name: Set up Go
185199
uses: actions/setup-go@v6
186200
with:
@@ -206,6 +220,7 @@ jobs:
206220
update-packages:
207221
needs: release
208222
runs-on: ubuntu-latest
223+
timeout-minutes: 20
209224
steps:
210225
- name: Checkout cli repo
211226
uses: actions/checkout@v5

.github/workflows/test.yml

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,51 @@ env:
1414
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
1515

1616
jobs:
17+
lint:
18+
name: Lint
19+
runs-on: ubuntu-latest
20+
21+
steps:
22+
- name: Checkout code
23+
uses: actions/checkout@v5
24+
25+
- name: Set up Go
26+
uses: actions/setup-go@v6
27+
with:
28+
go-version-file: go.mod
29+
30+
- name: gofmt
31+
run: |
32+
unformatted=$(gofmt -l ./cmd ./pkg ./internal)
33+
if [ -n "$unformatted" ]; then
34+
echo "::error::these files are not gofmt'd:"
35+
echo "$unformatted"
36+
exit 1
37+
fi
38+
shell: bash
39+
40+
- name: go vet
41+
run: go vet ./...
42+
env:
43+
# The bundled tree-sitter grammars need cgo; -w suppresses their
44+
# vendored parser.c warnings (mirrors the justfile).
45+
CGO_ENABLED: "1"
46+
CGO_CFLAGS: "-w"
47+
48+
- name: golangci-lint
49+
uses: golangci/golangci-lint-action@v6
50+
with:
51+
# Pinned: a linter upgrade must be a deliberate commit, not a
52+
# surprise red pipeline on an unrelated PR.
53+
version: v1.64.8
54+
args: --timeout=10m
55+
env:
56+
CGO_ENABLED: "1"
57+
CGO_CFLAGS: "-w"
58+
1759
test:
1860
runs-on: ubuntu-latest
61+
needs: [lint]
1962

2063
steps:
2164
- name: Checkout code
@@ -24,7 +67,7 @@ jobs:
2467
- name: Set up Go
2568
uses: actions/setup-go@v6
2669
with:
27-
go-version: '1.21'
70+
go-version-file: go.mod
2871

2972
- name: Build binaries
3073
run: |

_system.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ graph TB
387387
388388
subgraph External["External Services"]
389389
VDBApi["VDB API<br/>api.vdb.vulnetix.com/v1"]
390-
AppApi["Vulnetix SaaS<br/>app.vulnetix.com/api"]
390+
Console["Vulnetix Console<br/>www.vulnetix.com (device flow)"]
391391
GHAPI["GitHub API<br/>api.github.com"]
392392
Registries["Ecosystem Registries<br/>rubygems.org, hex.pm, hackage,<br/>cran, packagist, pub.dev,<br/>cocoapods, juliahub, deps.dev"]
393393
GHReleases["GitHub Releases<br/>Vulnetix/cli"]
@@ -407,7 +407,8 @@ graph TB
407407
Version --> UpdatePkg
408408
409409
VDBPkg --> VDBApi
410-
UploadPkg --> AppApi
410+
UploadPkg --> VDBApi
411+
AuthPkg --> Console
411412
GHPkg --> GHAPI
412413
LicensePkg --> GHAPI & Registries
413414
UpdatePkg --> GHReleases
@@ -584,14 +585,12 @@ Two methods, resolved by `vdb.Client.DoRequest()`:
584585
| `vdb fix-distributions` | GET | `/fix-distributions` | — | `api.go:327` |
585586
| `vdb purl` | — | Dispatches to one of the above | varies | `vdb.go:749` |
586587
587-
### Upload API (app.vulnetix.com)
588+
### Upload API (api.vdb.vulnetix.com/v1)
588589
589590
| Operation | Method | Path |
590591
|-----------|--------|------|
591-
| Initiate | POST | `/artifact-upload/initiate` |
592-
| Chunk | POST | `/artifact-upload/chunk/{sessionId}/{chunkNumber}` |
593-
| Finalize | POST | `/artifact-upload/finalize/{sessionId}` |
594-
| Verify auth | GET | `/cli/verify` |
592+
| Upload artifact | POST | `/v2/cli.upload` |
593+
| Verify auth | GET | `/uploads/verify` |
595594
596595
### Rate Limit Headers
597596

action.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,8 @@ runs:
7070
shell: bash
7171
run: |
7272
vulnetix auth login \
73-
--method apikey \
73+
--api-key '${{ inputs.api-key }}' \
7474
--org-id '${{ inputs.org-id }}' \
75-
--secret '${{ inputs.api-key }}' \
7675
--store project
7776
7877
- name: Verify Vulnetix Authentication

cmd/aibom.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ func runAIBOM(cmd *cobra.Command, args []string) error {
151151

152152
// Always persist the CycloneDX AIBOM to a file. Default location is
153153
// <path>/.vulnetix/ai-bom.cdx.json; --output-file overrides the path.
154+
warnOutputExtension(outputFile, ".cdx.json")
154155
outFile := outputFile
155156
if outFile == "" {
156157
outFile = filepath.Join(rootPath, ".vulnetix", "ai-bom.cdx.json")

0 commit comments

Comments
 (0)