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
116 changes: 110 additions & 6 deletions cmd/attach-open-score/main.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/attach-dev/attach-open-score/internal/fixtures"
"github.com/attach-dev/attach-open-score/pkg/schema"
"github.com/attach-dev/attach-open-score/pkg/score"
)

func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
}

func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
if err := runE(args, stdin, stdout, stderr); err != nil {
fmt.Fprintln(stderr, err)
return 1
}
return 0
}

func runE(args []string, stdin io.Reader, stdout, stderr io.Writer) error {
if idx, subcommand := firstPositional(args); subcommand == "score" {
return runScore(args[idx+1:], stdin, stdout, stderr)
}
return runFixtureValidation(args, stdout, stderr)
}

func run(args []string) error {
func runFixtureValidation(args []string, stdout, stderr io.Writer) error {
flags := flag.NewFlagSet("attach-open-score", flag.ContinueOnError)
flags.SetOutput(os.Stderr)
flags.SetOutput(stderr)
root := flags.String("root", ".", "repository root containing fixtures/v0")
if err := flags.Parse(args); err != nil {
return err
Expand All @@ -36,7 +53,94 @@ func run(args []string) error {
if err != nil {
path = report.Path
}
fmt.Printf("valid %s %s\n", path, report.Decision)
fmt.Fprintf(stdout, "valid %s %s\n", path, report.Decision)
}
return nil
}

func runScore(args []string, stdin io.Reader, stdout, stderr io.Writer) error {
flags := flag.NewFlagSet("attach-open-score score", flag.ContinueOnError)
flags.SetOutput(stderr)
input := flags.String("input", "", "JSON request path, or - for stdin")
profile := flags.String("policy-profile", "default", "policy profile: default, local-dev-default, ci-strict, or audit-only")
if err := flags.Parse(args); err != nil {
return err
}
if flags.NArg() > 0 {
return fmt.Errorf("score does not accept positional arguments: %v", flags.Args())
}
if *input == "" {
return fmt.Errorf("score requires --input <path>, or --input - for stdin")
}

data, err := readScoreInput(*input, stdin)
if err != nil {
return err
}

var request schema.Request
if err := json.Unmarshal(data, &request); err != nil {
return fmt.Errorf("invalid JSON request: %w", err)
}

engineProfile := *profile
if engineProfile == "default" {
engineProfile = ""
}
engine, err := score.NewEngine(score.Options{PolicyProfile: engineProfile})
if err != nil {
return err
}

verdict, err := engine.Evaluate(request)
if err != nil {
return err
}

encoded, err := json.MarshalIndent(verdict, "", " ")
if err != nil {
return err
}
if _, err := stdout.Write(encoded); err != nil {
return err
}
_, err = fmt.Fprintln(stdout)
return err
}

func readScoreInput(path string, stdin io.Reader) ([]byte, error) {
if path == "-" {
return io.ReadAll(stdin)
}
return os.ReadFile(path)
}

func firstPositional(args []string) (int, string) {
for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "--" {
if i+1 < len(args) {
return i + 1, args[i+1]
}
return -1, ""
}
if strings.HasPrefix(arg, "-") && arg != "-" {
if flagConsumesValue(arg) && !strings.Contains(arg, "=") && i+1 < len(args) {
i++
}
continue
}
return i, arg
}
return -1, ""
}

func flagConsumesValue(arg string) bool {
name := strings.TrimLeft(arg, "-")
switch name {
case "root", "input", "policy-profile":
return true
default:
return false
}
}
228 changes: 228 additions & 0 deletions cmd/attach-open-score/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package main

import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"

"github.com/attach-dev/attach-open-score/pkg/reasons"
"github.com/attach-dev/attach-open-score/pkg/schema"
)

func TestScoreCommandHappyPaths(t *testing.T) {
cases := []struct {
name string
request schema.Request
want schema.Decision
}{
{
name: "allow",
request: testScoreRequest(reasons.NoKnownVulnerabilities, "INFO", schema.DecisionEffectNone),
want: schema.DecisionAllow,
},
{
name: "ask",
request: testScoreRequest(reasons.InstallScriptPresent, "MEDIUM", schema.DecisionEffectAsk),
want: schema.DecisionAsk,
},
{
name: "deny",
request: testScoreRequest(reasons.KnownVulnerabilityCritical, "CRITICAL", schema.DecisionEffectDeny),
want: schema.DecisionDeny,
},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
path := writeRequestFile(t, tt.request)
code, stdout, stderr := runCommand(t, []string{"score", "--input", path}, "")
if code != 0 {
t.Fatalf("run exited %d, stderr: %s", code, stderr)
}
if stderr != "" {
t.Fatalf("stderr = %q, want empty", stderr)
}
if !strings.HasSuffix(stdout, "\n") {
t.Fatalf("stdout missing final newline: %q", stdout)
}
if !strings.Contains(stdout, "\n \"schema_version\"") {
t.Fatalf("stdout is not pretty-printed JSON: %s", stdout)
}

var verdict schema.Verdict
if err := json.Unmarshal([]byte(stdout), &verdict); err != nil {
t.Fatalf("stdout did not contain a JSON verdict: %v\n%s", err, stdout)
}
if verdict.Decision != tt.want {
t.Fatalf("decision = %s, want %s", verdict.Decision, tt.want)
}
})
}
}

func TestScoreCommandReadsStdin(t *testing.T) {
input := mustMarshalRequest(t, testScoreRequest(reasons.NoKnownVulnerabilities, "INFO", schema.DecisionEffectNone))

code, stdout, stderr := runCommand(t, []string{"score", "--input", "-"}, string(input))
if code != 0 {
t.Fatalf("run exited %d, stderr: %s", code, stderr)
}

var verdict schema.Verdict
if err := json.Unmarshal([]byte(stdout), &verdict); err != nil {
t.Fatalf("stdout did not contain a JSON verdict: %v\n%s", err, stdout)
}
if verdict.Decision != schema.DecisionAllow {
t.Fatalf("decision = %s, want ALLOW", verdict.Decision)
}
}

func TestScoreCommandPolicyProfile(t *testing.T) {
path := writeRequestFile(t, testScoreRequest(reasons.InstallScriptPresent, "MEDIUM", schema.DecisionEffectAsk))

code, stdout, stderr := runCommand(t, []string{"score", "--input", path, "--policy-profile", "ci-strict"}, "")
if code != 0 {
t.Fatalf("run exited %d, stderr: %s", code, stderr)
}

var verdict schema.Verdict
if err := json.Unmarshal([]byte(stdout), &verdict); err != nil {
t.Fatalf("stdout did not contain a JSON verdict: %v\n%s", err, stdout)
}
if verdict.PolicyProfile != "ci-strict" {
t.Fatalf("policy_profile = %q, want ci-strict", verdict.PolicyProfile)
}
if verdict.Decision != schema.DecisionDeny {
t.Fatalf("decision = %s, want DENY for ci-strict ASK evidence", verdict.Decision)
}
}

func TestScoreCommandMissingFile(t *testing.T) {
missing := filepath.Join(t.TempDir(), "missing.json")

code, stdout, stderr := runCommand(t, []string{"score", "--input", missing}, "")
if code != 1 {
t.Fatalf("run exited %d, want 1", code)
}
if stdout != "" {
t.Fatalf("stdout = %q, want empty", stdout)
}
if !strings.Contains(stderr, "missing.json") {
t.Fatalf("stderr = %q, want missing file path", stderr)
}
}

func TestScoreCommandMalformedJSON(t *testing.T) {
path := filepath.Join(t.TempDir(), "bad.json")
if err := os.WriteFile(path, []byte(`{"package":`), 0o600); err != nil {
t.Fatalf("write malformed JSON: %v", err)
}

code, stdout, stderr := runCommand(t, []string{"score", "--input", path}, "")
if code != 1 {
t.Fatalf("run exited %d, want 1", code)
}
if stdout != "" {
t.Fatalf("stdout = %q, want empty", stdout)
}
if !strings.Contains(stderr, "invalid JSON request") {
t.Fatalf("stderr = %q, want bad JSON error", stderr)
}
}

func TestScoreCommandEngineValidationError(t *testing.T) {
path := writeRequestFile(t, schema.Request{
Package: schema.PackageIdentity{
Name: "synthetic-package",
PURL: "pkg:npm/synthetic-package@1.0.0",
Resolved: true,
Version: "1.0.0",
},
})

code, stdout, stderr := runCommand(t, []string{"score", "--input", path}, "")
if code != 1 {
t.Fatalf("run exited %d, want 1", code)
}
if stdout != "" {
t.Fatalf("stdout = %q, want empty", stdout)
}
if !strings.Contains(stderr, "package ecosystem is required") {
t.Fatalf("stderr = %q, want engine validation error", stderr)
}
}

func TestDefaultCommandStillValidatesFixtures(t *testing.T) {
code, stdout, stderr := runCommand(t, []string{"--root", filepath.Join("..", "..")}, "")
if code != 0 {
t.Fatalf("run exited %d, stderr: %s", code, stderr)
}
if !strings.Contains(stdout, "valid fixtures/v0/allow-clean-synthetic.json ALLOW") {
t.Fatalf("stdout = %q, want fixture validation output", stdout)
}
}

func runCommand(t *testing.T, args []string, stdin string) (int, string, string) {
t.Helper()
var stdout bytes.Buffer
var stderr bytes.Buffer
code := run(args, strings.NewReader(stdin), &stdout, &stderr)
return code, stdout.String(), stderr.String()
}

func writeRequestFile(t *testing.T, request schema.Request) string {
t.Helper()
path := filepath.Join(t.TempDir(), "request.json")
if err := os.WriteFile(path, mustMarshalRequest(t, request), 0o600); err != nil {
t.Fatalf("write request: %v", err)
}
return path
}

func mustMarshalRequest(t *testing.T, request schema.Request) []byte {
t.Helper()
data, err := json.Marshal(request)
if err != nil {
t.Fatalf("marshal request: %v", err)
}
return data
}

func testScoreRequest(code, severity string, effect schema.DecisionEffect) schema.Request {
sourceRef := schema.SourceRef{
ID: "synthetic-source",
Source: "synthetic-fixture",
SourceID: "synthetic-source",
URL: "https://example.invalid/attach-open-score/synthetic-source",
RetrievedAt: "2026-05-06T11:50:00Z",
TTLSeconds: 86400,
LicenseOrTermsURL: "https://example.invalid/terms",
Attribution: "Synthetic fixture data for Attach Open Score tests.",
AttributionRequired: false,
Redistribution: "allowed",
PublicDisplay: "allowed",
}
return schema.Request{
Package: schema.PackageIdentity{
Ecosystem: "npm",
Name: "synthetic-package",
Version: "1.0.0",
PURL: "pkg:npm/synthetic-package@1.0.0",
RequestedSpec: "^1.0.0",
Resolved: true,
},
Evidence: []schema.Evidence{{
Reason: schema.Reason{
Code: code,
Severity: severity,
DecisionEffect: effect,
Message: "Synthetic evidence for CLI scorer tests.",
SourceRefIDs: []string{sourceRef.ID},
},
SourceRef: &sourceRef,
}},
}
}
11 changes: 11 additions & 0 deletions pkg/schema/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ type SourceRef struct {
PublicDisplay string `json:"public_display"`
}

type Request struct {
Package PackageIdentity `json:"package"`
Evidence []Evidence `json:"evidence,omitempty"`
Mode string `json:"mode,omitempty"`
}

type Evidence struct {
Reason Reason `json:"reason"`
SourceRef *SourceRef `json:"source_ref,omitempty"`
}

type Verdict struct {
SchemaVersion string `json:"schema_version"`
PolicyProfile string `json:"policy_profile,omitempty"`
Expand Down
Loading