Skip to content
Merged
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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Check formatting
run: test -z "$(gofmt -l .)"
- name: Test
run: go test ./...
- name: Race tests
run: go test -race ./...
- name: Vet
run: go vet ./...

docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build container
run: docker build --tag wolfee:ci .
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@
# - cdxgen : cataloguing for project-path / --bom / --reachable.
# - atom / govulncheck : --reachable call-graph (degrade gracefully).

FROM golang:1.22-bookworm AS go-builder
FROM golang:1.25-bookworm AS go-builder
WORKDIR /src
COPY . .
ARG VERSION=dev
ARG COMMIT=unknown
RUN CGO_ENABLED=0 go build \
-trimpath \
-ldflags "-s -w \
-X sca-go/cli/internal/cli.Version=${VERSION} \
-X sca-go/cli/internal/cli.Commit=${COMMIT}" \
-X github.com/shinigamikiko/wolfee-cli/internal/cli.Version=${VERSION} \
-X github.com/shinigamikiko/wolfee-cli/internal/cli.Commit=${COMMIT}" \
-o /out/wolfee \
./cmd/wolfee

Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
BUILT ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)

LDFLAGS := -s -w \
-X sca-go/cli/internal/cli.Version=$(VERSION) \
-X sca-go/cli/internal/cli.Commit=$(COMMIT) \
-X sca-go/cli/internal/cli.Built=$(BUILT)
-X github.com/shinigamikiko/wolfee-cli/internal/cli.Version=$(VERSION) \
-X github.com/shinigamikiko/wolfee-cli/internal/cli.Commit=$(COMMIT) \
-X github.com/shinigamikiko/wolfee-cli/internal/cli.Built=$(BUILT)

.PHONY: build test test-race vet lint fmt tidy clean docker install tools help

Expand Down
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,14 @@ Feeds are cached to disk (TTL + conditional GET). Zero pre-warming needed.
### Install

```bash
# From source (requires Go 1.21+)
# Install the latest tagged CLI (requires Go 1.25+)
go install github.com/shinigamikiko/wolfee-cli/cmd/wolfee@latest

# Or build locally
# Or build from source
git clone https://github.com/shinigamikiko/wolfee-cli
cd wolfee-cli
make build # → ./bin/wolfee
export PATH="$PWD/bin:$PATH"
make tools # → ./bin/trivy + ./bin/govulncheck
```

Expand All @@ -127,17 +128,61 @@ docker run --rm wolfee --image nginx:latest
| `--reachable` (Go) | `govulncheck` on PATH (or `--govulncheck-bin <path>`) |
| `--reachable` (JS / Python / Java / PHP) | `atom` + `atom-parsetools` on PATH (or `--atom-bin <path>`) |

`--image` needs `trivy`. Project and reachability scans need `cdxgen`.
Use `make docker` when you want all runtime tools bundled in one image.
Upload is best-effort by default; add `--upload-required` in CI when a
successful server upload is part of the job contract.

---

## Usage

### Fix plan

The `fix-plan` format first lists all findings, then groups actionable updates.
Each vulnerable package and advisory stays visible on its own compact line,
followed by the dependency paths that brought it in:

```text
VULNERABILITIES

PACKAGE CVE SEV FIX
qs@6.11.0 CVE-2022-24999 HIGH 6.11.2
path-to-regexp@0.1.7 CVE-2024-45296 HIGH 0.1.8

REMEDIATION PLAN

1. Upgrade express 4.18.2 -> 4.21.2
Fixes: 3 vulnerabilities
PACKAGE CVE SEV FIX
qs@6.11.0 CVE-2022-24999 HIGH 6.11.2
path-to-regexp@0.1.7 CVE-2024-45296 HIGH 0.1.8

Dependency paths:
path: app -> api -> express -> qs
path: app -> api -> express -> path-to-regexp
... more paths; use --format json for full list
```

Use `--format json` to keep the normal report and receive the same grouped
data under the top-level `fixPlan` field.

The dependency graph is always taken from cdxgen. If an exact parent upgrade
cannot be resolved by the optional remediation stage, the plan falls back to
the fixed version reported by OSV instead of parsing Composer or lockfiles.

### Scan a container image

```bash
wolfee --image nginx:1.27
wolfee --image my-app:latest --platform linux/arm64
wolfee --image my-app:latest --save-sbom my-app.cdx.json

# Group transitive vulnerabilities by the direct dependency to upgrade
wolfee scan ./my-app --format fix-plan
# JSON reports include the same grouped data under "fixPlan"
wolfee scan ./my-app --format json --output report.json

# Tell base-image packages apart from your own (see below)
wolfee --image my-app:latest --scout

Expand Down
2 changes: 1 addition & 1 deletion cmd/listplatforms/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package main
import (
"context"
"fmt"
"github.com/shinigamikiko/wolfee-cli/internal/trivydb"
"net/http"
"os"
"sca-go/cli/internal/trivydb"
)

func main() {
Expand Down
2 changes: 1 addition & 1 deletion cmd/wolfee/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"os/signal"
"syscall"

"sca-go/cli/internal/cli"
"github.com/shinigamikiko/wolfee-cli/internal/cli"
)

type exitCoder interface{ ExitCode() int }
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module sca-go/cli
module github.com/shinigamikiko/wolfee-cli

go 1.25.0

Expand Down
13 changes: 7 additions & 6 deletions internal/cdxgen/cdxgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"path/filepath"
"strings"

"sca-go/cli/internal/output"
"github.com/shinigamikiko/wolfee-cli/internal/output"
)

type Options struct {
Expand Down Expand Up @@ -167,17 +167,18 @@ func GenerateImageSBOM(ctx context.Context, o Options) ([]byte, error) {
}

if o.SaveTo != "" {

if err := saveCopy(o.SaveTo, bom); err != nil && o.Logger != nil {
o.Logger.Warn("could not save SBOM to %s: %v", o.SaveTo, err)
if err := saveCopy(o.SaveTo, bom); err != nil {
return nil, fmt.Errorf("cdxgen: save SBOM: %w", err)
}
}
return bom, nil
}

func saveCopy(dst string, data []byte) error {
if dir := filepath.Dir(dst); dir != "" {
_ = os.MkdirAll(dir, 0o755)
if err := os.MkdirAll(dir, 0o700); err != nil {
return err
}
}
return os.WriteFile(dst, data, 0o644)
return os.WriteFile(dst, data, 0o600)
}
2 changes: 1 addition & 1 deletion internal/cli/failon.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package cli

import (
"sca-go/cli/internal/sbomscan"
"github.com/shinigamikiko/wolfee-cli/internal/sbomscan"
"strings"
)

Expand Down
27 changes: 18 additions & 9 deletions internal/cli/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (
"path/filepath"
"strings"

"sca-go/cli/internal/cdxgen"
"sca-go/cli/internal/output"
"sca-go/cli/internal/reachability"
"sca-go/cli/internal/sbomscan"
"sca-go/cli/internal/upload"
"github.com/shinigamikiko/wolfee-cli/internal/cdxgen"
"github.com/shinigamikiko/wolfee-cli/internal/output"
"github.com/shinigamikiko/wolfee-cli/internal/reachability"
"github.com/shinigamikiko/wolfee-cli/internal/sbomscan"
"github.com/shinigamikiko/wolfee-cli/internal/upload"
)

type scanOpts struct {
Expand Down Expand Up @@ -42,9 +42,10 @@ type scanOpts struct {
quiet bool
debug bool

server string
token string
project string
server string
token string
project string
uploadRequired bool

reachable string
govulncheckBin string
Expand Down Expand Up @@ -213,6 +214,9 @@ func runScan(ctx context.Context, args []string) error {

if len(payload) == 0 {
logger.Warn("server upload skipped: nothing to upload for this input mode")
if o.uploadRequired {
return errors.New("upload required: nothing to upload for this input mode")
}
} else {
logger.Step(fmt.Sprintf("Uploading %s to %s (project=%s)", uploading, o.server, o.project))
if err := upload.SendBOM(ctx, upload.Params{
Expand All @@ -223,6 +227,9 @@ func runScan(ctx context.Context, args []string) error {
Logger: logger,
}); err != nil {
logger.Warn("upload failed: %v", err)
if o.uploadRequired {
return fmt.Errorf("upload required: %w", err)
}
}
}
}
Expand Down Expand Up @@ -288,11 +295,13 @@ func writeReport(o *scanOpts, report *sbomscan.Report) error {
renderer = output.JSON{}
case "sarif":
renderer = output.SARIF{}
case "fix-plan":
renderer = output.FixPlan{NoColor: o.outFile != ""}
default:
renderer = output.Table{NoColor: o.outFile != ""}
}
if o.outFile != "" {
f, err := os.Create(o.outFile)
f, err := os.OpenFile(o.outFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
}
Expand Down
13 changes: 10 additions & 3 deletions internal/cli/scan_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ SBOM SAVE (filesystem + image modes):
a second "trivy image --format cyclonedx" run)

OUTPUT:
--format FMT table | json | sarif (default: table)
--format FMT table | json | sarif | fix-plan (default: table)
--output PATH Write report to file instead of stdout
--fail-on LEVEL Exit non-zero if a finding >= LEVEL exists: none|low|medium|high|critical
(default: none - never fail on findings)
Expand All @@ -80,6 +80,7 @@ SERVER UPLOAD (optional - fire-and-forget alongside local output):
--server URL Wolfee server URL (e.g. https://wolfee.example.com)
--token TOKEN API token (or pass via WOLFEE_TOKEN env var)
--project NAME Project name or UUID (auto-create if missing)
--upload-required Fail the scan when server upload fails

SCAN:
--concurrency N Parallel OSV queries (default: 16)
Expand All @@ -95,6 +96,8 @@ EXAMPLES:
wolfee scan --image my-app:latest --compare ./src
wolfee scan --image my-app:latest --fail-on high
wolfee scan --image my-app:latest --format sarif --output report.sarif
wolfee scan ./my-app --format fix-plan
wolfee scan ./my-app --format json
wolfee scan --bom existing.cdx.json --format sarif --output report.sarif
wolfee scan --purl pkg:npm/ngx-bootstrap@20.0.4
wolfee scan --reachable ./my-go-service --fail-on high
Expand Down Expand Up @@ -135,6 +138,7 @@ func parseScanFlags(args []string) (*scanOpts, error) {
fs.StringVar(&o.server, "server", "", "Wolfee server URL")
fs.StringVar(&o.token, "token", "", "API token (or WOLFEE_TOKEN env)")
fs.StringVar(&o.project, "project", "", "project name/UUID")
fs.BoolVar(&o.uploadRequired, "upload-required", false, "fail when server upload fails")
fs.IntVar(&o.concurrency, "concurrency", 0, "parallel scans (0 = auto)")
fs.BoolVar(&o.trivyDBSkip, "trivy-db-skip", false, "disable Trivy DB stage")
fs.StringVar(&o.trivyDBMirror, "trivy-db-mirror", "", "custom OCI registry host for trivy-db (default: ghcr.io)")
Expand Down Expand Up @@ -183,9 +187,9 @@ func (o *scanOpts) validate() error {
return errors.New("--image, --bom, --purl and a project path are mutually exclusive")
}
switch strings.ToLower(o.format) {
case "", "table", "json", "sarif":
case "", "table", "json", "sarif", "fix-plan":
default:
return fmt.Errorf("unsupported --format %q (table|json|sarif)", o.format)
return fmt.Errorf("unsupported --format %q (table|json|sarif|fix-plan)", o.format)
}
switch strings.ToLower(o.failOn) {
case "", "none", "low", "medium", "high", "critical":
Expand All @@ -195,6 +199,9 @@ func (o *scanOpts) validate() error {
if o.server != "" && o.project == "" {
return errors.New("--server requires --project")
}
if o.uploadRequired && o.server == "" {
return errors.New("--upload-required requires --server")
}
if o.scout && o.image == "" {
return errors.New("--scout only applies to --image mode (it attributes image layers base-vs-app)")
}
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"strings"
"testing"

"sca-go/cli/internal/onlinescan"
"sca-go/cli/internal/sbomscan"
"github.com/shinigamikiko/wolfee-cli/internal/onlinescan"
"github.com/shinigamikiko/wolfee-cli/internal/sbomscan"
)

func TestScanOpts_ValidateRequiresOneInput(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"fmt"
"runtime"

"sca-go/cli/internal/sbomscan"
"github.com/shinigamikiko/wolfee-cli/internal/sbomscan"
)

var (
Expand Down
2 changes: 1 addition & 1 deletion internal/onlinescan/dla.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"sync"
"time"

"sca-go/cli/internal/onlinescan/feedcache"
"github.com/shinigamikiko/wolfee-cli/internal/onlinescan/feedcache"
)

const (
Expand Down
2 changes: 1 addition & 1 deletion internal/onlinescan/enrich.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"fmt"
"time"

"sca-go/cli/internal/onlinescan/feedcache"
"github.com/shinigamikiko/wolfee-cli/internal/onlinescan/feedcache"
)

func Enrich(ctx context.Context, results []*ComponentResult, o Options) error {
Expand Down
2 changes: 1 addition & 1 deletion internal/onlinescan/kev.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"sync"
"time"

"sca-go/cli/internal/onlinescan/feedcache"
"github.com/shinigamikiko/wolfee-cli/internal/onlinescan/feedcache"
)

const kevURL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
Expand Down
4 changes: 2 additions & 2 deletions internal/onlinescan/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
"strings"
"time"

"sca-go/cli/internal/onlinescan/feedcache"
"sca-go/cli/internal/trivydb"
"github.com/shinigamikiko/wolfee-cli/internal/onlinescan/feedcache"
"github.com/shinigamikiko/wolfee-cli/internal/trivydb"
)

type Component struct {
Expand Down
Loading
Loading