Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
31 changes: 31 additions & 0 deletions .github/workflows/wrkr-sarif.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: wrkr-sarif

on:
workflow_dispatch:

permissions:
contents: read
security-events: write

jobs:
scan-and-upload:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Build wrkr
run: go build -o .tmp/wrkr ./cmd/wrkr

- name: Run scan with SARIF output
run: ./.tmp/wrkr scan --path ./scenarios/wrkr/scan-mixed-org/repos --sarif --sarif-path ./.tmp/wrkr.sarif --json >/dev/null

- name: Upload SARIF to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ./.tmp/wrkr.sarif
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ venv/
# Local analysis scratch
.tmp/
.wrkr/
!scenarios/wrkr/extension-detectors/repos/ext-repo/.wrkr/
!scenarios/wrkr/extension-detectors/repos/ext-repo/.wrkr/detectors/
!scenarios/wrkr/extension-detectors/repos/ext-repo/.wrkr/detectors/extensions.json
*.sarif
/wrkr
/wrkr.exe
Expand Down
13 changes: 9 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ SHELL := /bin/bash
GO ?= go
PKGS := ./...
GOFILES := $(shell git ls-files '*.go')
DOCS_SITE_NPM_CACHE ?= $(CURDIR)/.tmp/npm-cache

.PHONY: fmt lint lint-fast test test-fast test-integration test-e2e test-contracts test-scenarios \
test-hardening test-chaos test-perf test-risk-lane build hooks prepush prepush-full codeql lint-ci \
Expand Down Expand Up @@ -61,19 +62,23 @@ test-docs-storyline:
@scripts/run_docs_smoke.sh --subset

docs-site-install:
@cd docs-site && npm ci
@mkdir -p "$(DOCS_SITE_NPM_CACHE)"
@cd docs-site && NPM_CONFIG_CACHE="$(DOCS_SITE_NPM_CACHE)" npm ci

docs-site-lint:
@cd docs-site && npm run lint
@mkdir -p "$(DOCS_SITE_NPM_CACHE)"
@cd docs-site && NPM_CONFIG_CACHE="$(DOCS_SITE_NPM_CACHE)" npm run lint

docs-site-build:
@cd docs-site && npm run build
@mkdir -p "$(DOCS_SITE_NPM_CACHE)"
@cd docs-site && NPM_CONFIG_CACHE="$(DOCS_SITE_NPM_CACHE)" npm run build

docs-site-check:
@python3 scripts/check_docs_site_validation.py --report wrkr-out/docs_site_validation_report.json

docs-site-audit-prod:
@cd docs-site && npm audit --omit=dev --audit-level=high
@mkdir -p "$(DOCS_SITE_NPM_CACHE)"
@cd docs-site && NPM_CONFIG_CACHE="$(DOCS_SITE_NPM_CACHE)" npm audit --omit=dev --audit-level=high

test-adapter-parity:
@scripts/test_adapter_parity.sh
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ make build

Expected JSON keys by command family:

- `scan`: `status`, `target`, `findings`, `ranked_findings`, `top_findings`, `attack_paths`, `top_attack_paths`, `inventory`, `privilege_budget`, `agent_privilege_map`, `repo_exposure_summaries`, `profile`, `posture_score` (optional: `policy_warnings`, `report`)
- `scan`: `status`, `target`, `findings`, `ranked_findings`, `top_findings`, `attack_paths`, `top_attack_paths`, `inventory`, `privilege_budget`, `agent_privilege_map`, `repo_exposure_summaries`, `profile`, `posture_score` (optional: `detector_errors`, `partial_result`, `source_errors`, `source_degraded`, `policy_warnings`, `report`, `sarif`)
- `report`: `status`, `generated_at`, `top_findings`, `attack_paths`, `top_attack_paths`, `total_tools`, `tool_type_breakdown`, `compliance_gap_count`, `privilege_budget`, `summary` (optional: `md_path`, `pdf_path`)
- `score`: `score`, `grade`, `breakdown`, `weighted_breakdown`, `weights`, `trend_delta` (optional: `attack_paths`, `top_attack_paths`)
- `evidence`: `status`, `output_dir`, `manifest_path`, `chain_path`, `framework_coverage`, `report_artifacts`
Expand Down Expand Up @@ -127,6 +127,8 @@ Acquisition behavior:
- `--path`: local, offline, fully deterministic.
- `--repo` and `--org`: require `--github-api` or `WRKR_GITHUB_API_BASE`; unavailable acquisition fails closed with exit `7`.
- Invalid target combinations fail with exit `6`.
- `--timeout <duration>` bounds scan runtime. Timeout returns JSON error code `scan_timeout` (exit `1`); signal/parent cancellation returns `scan_canceled` (exit `1`).
- GitHub retry behavior is bounded and rate-limit aware (`Retry-After`/`X-RateLimit-Reset`); repeated transient failures enter cooldown degradation and are surfaced in partial-result output.

## Production Target Policy

Expand Down Expand Up @@ -240,6 +242,7 @@ wrkr lifecycle
wrkr manifest generate
wrkr regress init|run
wrkr score
wrkr version
wrkr verify --chain
wrkr evidence
wrkr fix
Expand All @@ -255,6 +258,7 @@ All commands support `--json`. Human-readable rationale is available via `--expl
- Policy authoring: [`docs/policy_authoring.md`](docs/policy_authoring.md)
- Failure taxonomy and exits: [`docs/failure_taxonomy_exit_codes.md`](docs/failure_taxonomy_exit_codes.md)
- Threat model: [`docs/threat_model.md`](docs/threat_model.md)
- Compatibility and versioning policy: [`docs/trust/compatibility-and-versioning.md`](docs/trust/compatibility-and-versioning.md)
- Compatibility matrix: [`docs/contracts/compatibility_matrix.md`](docs/contracts/compatibility_matrix.md)
- Trust docs: [`docs/trust/`](docs/trust/)
- Intent pages: [`docs/intent/`](docs/intent/)
Expand Down
11 changes: 8 additions & 3 deletions action/action.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: Wrkr Action
description: Deterministic Wrkr scheduled and PR-mode execution for AI posture visibility.
description: Deterministic Wrkr scheduled, PR-mode, and SARIF execution for AI posture visibility.
author: Clyra-AI

inputs:
mode:
description: scheduled or pr
description: scheduled, pr, or sarif
required: false
default: scheduled
top:
Expand All @@ -31,6 +31,10 @@ inputs:
description: deterministic marker for PR comment upsert identity
required: false
default: "wrkr-action-pr-mode-v1"
sarif_path:
description: SARIF output path when mode=sarif
required: false
default: "./.tmp/wrkr.sarif"

runs:
using: composite
Expand All @@ -40,6 +44,7 @@ runs:
env:
WRKR_ACTION_BLOCK_THRESHOLD: ${{ inputs.block_threshold }}
WRKR_ACTION_COMMENT_FINGERPRINT: ${{ inputs.comment_fingerprint }}
WRKR_ACTION_SARIF_PATH: ${{ inputs.sarif_path }}
run: |
set -euo pipefail
"${{ github.action_path }}/entrypoint.sh" "${{ inputs.mode }}" "${{ inputs.top }}" "${{ inputs.target_mode }}" "${{ inputs.target_value }}" "${{ inputs.config_path }}"
"${{ github.action_path }}/entrypoint.sh" "${{ inputs.mode }}" "${{ inputs.top }}" "${{ inputs.target_mode }}" "${{ inputs.target_value }}" "${{ inputs.config_path }}" "${{ inputs.sarif_path }}"
10 changes: 9 additions & 1 deletion action/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ top="${2:-5}"
target_mode="${3:-}"
target_value="${4:-}"
config_path="${5:-}"
sarif_path="${6:-${WRKR_ACTION_SARIF_PATH:-./.tmp/wrkr.sarif}}"
summary_path="${WRKR_ACTION_SUMMARY_PATH:-./.tmp/wrkr-action-summary.md}"
comment_fingerprint="${WRKR_ACTION_COMMENT_FINGERPRINT:-wrkr-action-pr-mode-v1}"
block_threshold="${WRKR_ACTION_BLOCK_THRESHOLD:-0}"

if [[ "${mode}" != "scheduled" && "${mode}" != "pr" ]]; then
if [[ "${mode}" != "scheduled" && "${mode}" != "pr" && "${mode}" != "sarif" ]]; then
echo "unsupported mode: ${mode}" >&2
exit 6
fi
Expand Down Expand Up @@ -64,6 +65,10 @@ else
exit 6
fi

if [[ "${mode}" == "sarif" ]]; then
scan_args+=(--sarif --sarif-path "${sarif_path}")
fi

scan_json="$(run_wrkr scan "${scan_args[@]}")"
run_wrkr report --top "${top}" --md --md-path "${summary_path}" --template operator --share-profile internal --json >/dev/null
score_json="$(run_wrkr score --json)"
Expand Down Expand Up @@ -190,3 +195,6 @@ fi
# Deterministic mode marker for workflow consumers.
echo "wrkr_action_mode=${mode}"
echo "wrkr_action_summary=${summary_path}"
if [[ "${mode}" == "sarif" ]]; then
echo "wrkr_action_sarif=${sarif_path}"
fi
7 changes: 6 additions & 1 deletion cmd/wrkr/main.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package main

import (
"context"
"os"
"os/signal"
"syscall"

"github.com/Clyra-AI/wrkr/core/cli"
)

func main() {
os.Exit(cli.Run(os.Args[1:], os.Stdout, os.Stderr))
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
os.Exit(cli.RunWithContext(ctx, os.Args[1:], os.Stdout, os.Stderr))
}
24 changes: 24 additions & 0 deletions core/cli/jsonmode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cli

import (
"strconv"
"strings"
)

// wantsJSONOutput inspects raw args to decide whether errors should be emitted as JSON.
func wantsJSONOutput(args []string) bool {
for _, arg := range args {
if arg == "--json" {
return true
}
if strings.HasPrefix(arg, "--json=") {
value := strings.TrimPrefix(arg, "--json=")
parsed, err := strconv.ParseBool(value)
if err != nil {
return true
}
return parsed
}
}
return false
}
55 changes: 55 additions & 0 deletions core/cli/jsonmode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cli

import (
"bytes"
"encoding/json"
"testing"
)

func TestSharedJSONModeParsingCases(t *testing.T) {
t.Parallel()

cases := []struct {
name string
args []string
want bool
}{
{name: "explicit json flag", args: []string{"--json"}, want: true},
{name: "json true", args: []string{"--json=true"}, want: true},
{name: "json false", args: []string{"--json=false"}, want: false},
{name: "malformed bool falls back to json errors", args: []string{"--json=maybe"}, want: true},
{name: "no json flag", args: []string{"scan", "--path", "."}, want: false},
}

for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := wantsJSONOutput(tc.args); got != tc.want {
t.Fatalf("wantsJSONOutput(%v)=%v, want %v", tc.args, got, tc.want)
}
})
}
}

func TestMalformedJSONModeFlagEmitsJSONErrorsAcrossCommands(t *testing.T) {
t.Parallel()

commands := [][]string{
{"--json=maybe", "--nope"},
{"scan", "--json=maybe", "--path"},
}

for _, cmd := range commands {
var out bytes.Buffer
var errOut bytes.Buffer
code := Run(cmd, &out, &errOut)
if code != exitInvalidInput {
t.Fatalf("expected exit %d for %v, got %d", exitInvalidInput, cmd, code)
}
var payload map[string]any
if err := json.Unmarshal(errOut.Bytes(), &payload); err != nil {
t.Fatalf("expected JSON error payload for %v, got %q (%v)", cmd, errOut.String(), err)
}
}
}
46 changes: 22 additions & 24 deletions core/cli/root.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package cli

import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"strconv"
"strings"
)

Expand All @@ -24,12 +24,20 @@ const (

// Run executes the wrkr CLI root command and returns a stable process exit code.
func Run(args []string, stdout io.Writer, stderr io.Writer) int {
return RunWithContext(context.Background(), args, stdout, stderr)
}

// RunWithContext executes the wrkr CLI root command with a caller-provided context.
func RunWithContext(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer) int {
if ctx == nil {
ctx = context.Background()
}
if len(args) == 0 {
_, _ = fmt.Fprintln(stdout, "wrkr")
return exitSuccess
}

if code, handled := runKnownSubcommand(args[0], args[1:], stdout, stderr); handled {
if code, handled := runKnownSubcommand(ctx, args[0], args[1:], stdout, stderr); handled {
return code
}

Expand All @@ -40,12 +48,12 @@ func Run(args []string, stdout io.Writer, stderr io.Writer) int {
return runRootFlags(args, stdout, stderr)
}

func runKnownSubcommand(name string, args []string, stdout io.Writer, stderr io.Writer) (int, bool) {
func runKnownSubcommand(ctx context.Context, name string, args []string, stdout io.Writer, stderr io.Writer) (int, bool) {
switch name {
case "init":
return runInit(args, stdout, stderr), true
case "scan":
return runScan(args, stdout, stderr), true
return runScanWithContext(ctx, args, stdout, stderr), true
case "action":
return runAction(args, stdout, stderr), true
case "report":
Expand All @@ -70,20 +78,22 @@ func runKnownSubcommand(name string, args []string, stdout io.Writer, stderr io.
return runEvidence(args, stdout, stderr), true
case "fix":
return runFix(args, stdout, stderr), true
case "version":
return runVersion(args, stdout, stderr), true
case "help":
return runHelp(args, stdout, stderr), true
return runHelp(ctx, args, stdout, stderr), true
default:
return 0, false
}
}

func runHelp(args []string, stdout io.Writer, stderr io.Writer) int {
func runHelp(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer) int {
if len(args) == 0 || isHelpFlag(args[0]) {
return runRootFlags([]string{"--help"}, stdout, stderr)
}

helpArgs := append(append([]string{}, args[1:]...), "--help")
if code, handled := runKnownSubcommand(args[0], helpArgs, stdout, stderr); handled {
if code, handled := runKnownSubcommand(ctx, args[0], helpArgs, stdout, stderr); handled {
return code
}

Expand All @@ -103,6 +113,7 @@ func runRootFlags(args []string, stdout io.Writer, stderr io.Writer) int {
jsonOut := fs.Bool("json", false, "emit machine-readable output")
quiet := fs.Bool("quiet", false, "suppress non-error output")
explain := fs.Bool("explain", false, "emit human-readable rationale")
version := fs.Bool("version", false, "print wrkr version")
fs.Usage = func() {
writeRootUsage(fs.Output(), fs)
}
Expand All @@ -116,6 +127,9 @@ func runRootFlags(args []string, stdout io.Writer, stderr io.Writer) int {
if *quiet && *explain && !*jsonOut {
return emitError(stderr, jsonRequested || *jsonOut, "invalid_input", "--quiet and --explain cannot be used together", exitInvalidInput)
}
if *version {
return emitVersion(stdout, jsonRequested || *jsonOut, *jsonOut)
}

if *jsonOut {
_ = json.NewEncoder(stdout).Encode(map[string]any{
Expand Down Expand Up @@ -157,6 +171,7 @@ func writeRootUsage(out io.Writer, fs *flag.FlagSet) {
_, _ = fmt.Fprintln(out, " verify verify proof chain integrity")
_, _ = fmt.Fprintln(out, " evidence build compliance-ready evidence bundles")
_, _ = fmt.Fprintln(out, " fix apply deterministic remediations")
_, _ = fmt.Fprintln(out, " version print wrkr version")
_, _ = fmt.Fprintln(out, "")
_, _ = fmt.Fprintln(out, "Examples:")
_, _ = fmt.Fprintln(out, " wrkr scan --path . --json")
Expand Down Expand Up @@ -197,20 +212,3 @@ func emitError(stderr io.Writer, jsonOut bool, code, message string, exitCode in
}
return exitCode
}

func wantsJSONOutput(args []string) bool {
for _, arg := range args {
if arg == "--json" {
return true
}
if strings.HasPrefix(arg, "--json=") {
value := strings.TrimPrefix(arg, "--json=")
parsed, err := strconv.ParseBool(value)
if err != nil {
return true
}
return parsed
}
}
return false
}
Loading
Loading