diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b80f9a7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: ci + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + go: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - run: go vet ./... + - run: go run honnef.co/go/tools/cmd/staticcheck@2026.2.2 ./... + - run: go test ./... + + proto: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: bufbuild/buf-action@v1 + with: + setup_only: true + version: 1.75.0 + - run: buf lint + - name: buf breaking + run: | + if git cat-file -e origin/main:buf.lock 2>/dev/null; then + buf breaking --against '.git#branch=origin/main' + else + echo "main has no buf module yet; nothing to compare against" + fi + - name: generated code is current + run: | + make generate + git add -A gen web/src/gen openapi schema + git diff --cached --exit-code -- gen web/src/gen openapi schema + + web: + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + # package.json pins exact versions; no lockfile is committed yet. + - run: npm install --no-package-lock + - run: npx tsc --noEmit + - run: npx vitest run diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c132d4f --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.PHONY: generate lint breaking test web-test + +# Requires buf (https://buf.build): regenerates gen/, web/src/gen/, +# openapi/ and schema/ from proto/touchstone/v1/scan.proto, using the +# googleapis version pinned in buf.lock (`buf dep update` to bump it). +# The jsonschema plugin names its output after the message, so the +# published ScanReport schema is moved to its stable, documented name. +generate: + buf generate + mv schema/touchstone.v1.ScanReport.jsonschema.bundle.json schema/scan-report.v1.json + +lint: + buf lint + +breaking: + buf breaking --against '.git#branch=main' + +test: + go vet ./... + staticcheck ./... + go test ./... + +web-test: + cd web && npm install --no-package-lock && npx tsc --noEmit && npx vitest run diff --git a/README.md b/README.md index a3ffc18..4d9e7e9 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,16 @@ labels every finding with how sure it is: would send, plus well-known discovery paths such as `/.well-known/security.txt`. It never sends exploit or CVE-test payloads. -**Status:** planning. Nothing is implemented yet. See the PRD at -[`docs/product/touchstone.md`](docs/product/touchstone.md). +**Status:** S0 (contract first). `go run ./cmd/touchstone scan https://example.com` +prints a canned `ScanReport` from a stub service; it validates against +[`schema/scan-report.v1.json`](schema/scan-report.v1.json). The API contract +lives in [`proto/touchstone/v1/scan.proto`](proto/touchstone/v1/scan.proto); +run `make generate` (requires [buf](https://buf.build)) to regenerate the Go, +TypeScript, OpenAPI and JSON Schema outputs under `gen/`, `web/src/gen/`, +`openapi/` and `schema/`. Generated code is committed and CI fails if it is +stale. `make test` and `make web-test` run the Go and web suites. See the design at +[`docs/architecture/touchstone.md`](docs/architecture/touchstone.md) and +the PRD at [`docs/product/touchstone.md`](docs/product/touchstone.md). ## License diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..424bd7a --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,41 @@ +# Regenerate with `make generate` (wraps `buf generate`). Output under +# gen/, web/src/gen/, openapi/ and schema/ is committed and never hand +# edited; CI fails if `buf generate` would change it +# (.github/workflows/ci.yml, job "proto"). Plugin versions are pinned so +# that check only fails on a real contract change; bump them together +# with the matching Go and npm runtime versions. +version: v2 +# Wipe each output directory first so a removed message can't leave a +# stale file behind for the CI diff check to miss. +clean: true +plugins: + - remote: buf.build/protocolbuffers/go:v1.36.12 + out: gen/go + opt: paths=source_relative + - remote: buf.build/grpc/go:v1.6.2 + out: gen/go + opt: paths=source_relative + - remote: buf.build/grpc-ecosystem/gateway:v2.32.0 + out: gen/go + opt: paths=source_relative + - remote: buf.build/grpc-ecosystem/openapiv2:v2.32.0 + out: openapi + opt: + - output_format=json + - allow_merge=true + - merge_file_name=touchstone_v1 + # include_imports also generates google/api/*_pb.ts, which scan_pb.ts + # imports for the http annotation (well-known types come from + # @bufbuild/protobuf/wkt instead). + - remote: buf.build/bufbuild/es:v2.16.0 + out: web/src/gen + include_imports: true + opt: target=ts + # Only ScanReport and the messages it references get a schema; + # `make generate` renames ScanReport's bundle (which inlines the rest) + # to schema/scan-report.v1.json, the published schema. + - remote: buf.build/bufbuild/protoschema-jsonschema:v0.6.0 + out: schema + opt: target=json-bundle + types: + - touchstone.v1.ScanReport diff --git a/buf.lock b/buf.lock new file mode 100644 index 0000000..9de75cf --- /dev/null +++ b/buf.lock @@ -0,0 +1,6 @@ +# Generated by buf. DO NOT EDIT. +version: v2 +deps: + - name: buf.build/googleapis/googleapis + commit: 8ac61b9e502343289c8c42b17aa5b245 + digest: b5:0318e73c2dec49c5742f0c119642a5cb7fcd9e2d5200a2b478bc1efd93f7aaa06d5431f5e1e5713095101fc990b026a806fa429af80432ec8d1ceefbe07bb817 diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..1e49db2 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,16 @@ +version: v2 +modules: + - path: proto +deps: + - buf.build/googleapis/googleapis +lint: + use: + - STANDARD + except: + # The design's Contracts section names the RPC response ScanReport, + # not ScanResponse, so this repo's rpc intentionally breaks the + # standard-name convention. + - RPC_RESPONSE_STANDARD_NAME +breaking: + use: + - FILE diff --git a/cmd/touchstone/main.go b/cmd/touchstone/main.go new file mode 100644 index 0000000..97c3cd0 --- /dev/null +++ b/cmd/touchstone/main.go @@ -0,0 +1,41 @@ +// Command touchstone scans a vendor URL and prints an evidence-graded +// ScanReport as protojson. In slice S0 the scan is served by the stub +// ScanService, so the report is canned. +package main + +import ( + "context" + "errors" + "fmt" + "io" + "os" + + touchstonev1 "github.com/FootprintAI/touchstone/gen/go/touchstone/v1" + "github.com/FootprintAI/touchstone/internal/service" + "google.golang.org/protobuf/encoding/protojson" +) + +const usage = "usage: touchstone scan " + +func main() { + if err := run(context.Background(), os.Args[1:], os.Stdout, &service.Stub{}); err != nil { + fmt.Fprintln(os.Stderr, "touchstone:", err) + os.Exit(2) + } +} + +func run(ctx context.Context, args []string, out io.Writer, svc touchstonev1.ScanServiceServer) error { + if len(args) != 2 || args[0] != "scan" { + return errors.New(usage) + } + report, err := svc.Scan(ctx, &touchstonev1.ScanRequest{Url: args[1]}) + if err != nil { + return err + } + b, err := protojson.MarshalOptions{Multiline: true, Indent: " "}.Marshal(report) + if err != nil { + return err + } + _, err = fmt.Fprintln(out, string(b)) + return err +} diff --git a/cmd/touchstone/main_test.go b/cmd/touchstone/main_test.go new file mode 100644 index 0000000..dfe88e3 --- /dev/null +++ b/cmd/touchstone/main_test.go @@ -0,0 +1,94 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "flag" + "os" + "path/filepath" + "testing" + "time" + + touchstonev1 "github.com/FootprintAI/touchstone/gen/go/touchstone/v1" + "github.com/FootprintAI/touchstone/internal/service" + "github.com/santhosh-tekuri/jsonschema/v6" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +var update = flag.Bool("update", false, "rewrite testdata/report.golden.json") + +var ( + schemaPath = filepath.Join("..", "..", "schema", "scan-report.v1.json") + goldenPath = filepath.Join("..", "..", "testdata", "report.golden.json") +) + +func fixedStub() *service.Stub { + return &service.Stub{Now: func() time.Time { return time.Date(2026, 10, 22, 0, 0, 0, 0, time.UTC) }} +} + +func TestScanPrintsReportValidAgainstSchema(t *testing.T) { + var out bytes.Buffer + if err := run(context.Background(), []string{"scan", "https://example.com"}, &out, fixedStub()); err != nil { + t.Fatalf("run: %v", err) + } + + c := jsonschema.NewCompiler() + sch, err := c.Compile(schemaPath) + if err != nil { + t.Fatalf("compile %s: %v", schemaPath, err) + } + doc, err := jsonschema.UnmarshalJSON(bytes.NewReader(out.Bytes())) + if err != nil { + t.Fatalf("output is not JSON: %v\n%s", err, out.String()) + } + if err := sch.Validate(doc); err != nil { + t.Fatalf("output does not validate against %s: %v\n%s", schemaPath, err, out.String()) + } + if err := sch.Validate(map[string]any{"scanStatus": "NOT_A_STATUS"}); err == nil { + t.Fatal("schema accepted an invalid report; validation is not effective") + } + + var got touchstonev1.ScanReport + if err := protojson.Unmarshal(out.Bytes(), &got); err != nil { + t.Fatalf("protojson.Unmarshal: %v", err) + } + if got.GetTargetUrl() != "https://example.com" { + t.Errorf("target_url = %q, want https://example.com", got.GetTargetUrl()) + } + if got.GetSchemaVersion() != "1" { + t.Errorf("schema_version = %q, want 1", got.GetSchemaVersion()) + } + + // testdata/report.golden.json is also decoded by web/src/report.test.ts, + // so drift between the Go and TS sides of the contract fails a test. + if *update { + var indented bytes.Buffer + if err := json.Indent(&indented, out.Bytes(), "", " "); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(goldenPath, indented.Bytes(), 0o644); err != nil { + t.Fatal(err) + } + } + golden, err := os.ReadFile(goldenPath) + if err != nil { + t.Fatalf("read golden (run go test ./cmd/touchstone -update): %v", err) + } + var want touchstonev1.ScanReport + if err := protojson.Unmarshal(golden, &want); err != nil { + t.Fatalf("golden: %v", err) + } + if !proto.Equal(&got, &want) { + t.Errorf("report differs from %s (run go test ./cmd/touchstone -update)\ngot:\n%s", goldenPath, out.String()) + } +} + +func TestScanRejectsBadArgs(t *testing.T) { + for _, args := range [][]string{nil, {"scan"}, {"serve", "https://example.com"}, {"scan", "ftp://example.com"}, {"scan", "example.com"}} { + if err := run(context.Background(), args, &bytes.Buffer{}, fixedStub()); err == nil { + t.Errorf("run(%q) = nil error, want error", args) + } + } +} diff --git a/gen/go/touchstone/v1/scan.pb.go b/gen/go/touchstone/v1/scan.pb.go new file mode 100644 index 0000000..a744ee4 --- /dev/null +++ b/gen/go/touchstone/v1/scan.pb.go @@ -0,0 +1,1238 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc (unknown) +// source: touchstone/v1/scan.proto + +package touchstonev1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Confidence int32 + +const ( + Confidence_CONFIDENCE_UNSPECIFIED Confidence = 0 + Confidence_CONFIDENCE_LOW Confidence = 1 + Confidence_CONFIDENCE_MEDIUM Confidence = 2 + Confidence_CONFIDENCE_HIGH Confidence = 3 +) + +// Enum value maps for Confidence. +var ( + Confidence_name = map[int32]string{ + 0: "CONFIDENCE_UNSPECIFIED", + 1: "CONFIDENCE_LOW", + 2: "CONFIDENCE_MEDIUM", + 3: "CONFIDENCE_HIGH", + } + Confidence_value = map[string]int32{ + "CONFIDENCE_UNSPECIFIED": 0, + "CONFIDENCE_LOW": 1, + "CONFIDENCE_MEDIUM": 2, + "CONFIDENCE_HIGH": 3, + } +) + +func (x Confidence) Enum() *Confidence { + p := new(Confidence) + *p = x + return p +} + +func (x Confidence) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Confidence) Descriptor() protoreflect.EnumDescriptor { + return file_touchstone_v1_scan_proto_enumTypes[0].Descriptor() +} + +func (Confidence) Type() protoreflect.EnumType { + return &file_touchstone_v1_scan_proto_enumTypes[0] +} + +func (x Confidence) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Confidence.Descriptor instead. +func (Confidence) EnumDescriptor() ([]byte, []int) { + return file_touchstone_v1_scan_proto_rawDescGZIP(), []int{0} +} + +type EvidenceKind int32 + +const ( + EvidenceKind_EVIDENCE_KIND_UNSPECIFIED EvidenceKind = 0 + EvidenceKind_EVIDENCE_KIND_SBOM_PUBLISHED EvidenceKind = 1 + EvidenceKind_EVIDENCE_KIND_SBOM_SUPPLIED EvidenceKind = 2 + EvidenceKind_EVIDENCE_KIND_VEX EvidenceKind = 3 + EvidenceKind_EVIDENCE_KIND_REPO_LOCKFILE EvidenceKind = 4 + EvidenceKind_EVIDENCE_KIND_FINGERPRINT_ASSET EvidenceKind = 5 + EvidenceKind_EVIDENCE_KIND_FINGERPRINT_HEADER EvidenceKind = 6 +) + +// Enum value maps for EvidenceKind. +var ( + EvidenceKind_name = map[int32]string{ + 0: "EVIDENCE_KIND_UNSPECIFIED", + 1: "EVIDENCE_KIND_SBOM_PUBLISHED", + 2: "EVIDENCE_KIND_SBOM_SUPPLIED", + 3: "EVIDENCE_KIND_VEX", + 4: "EVIDENCE_KIND_REPO_LOCKFILE", + 5: "EVIDENCE_KIND_FINGERPRINT_ASSET", + 6: "EVIDENCE_KIND_FINGERPRINT_HEADER", + } + EvidenceKind_value = map[string]int32{ + "EVIDENCE_KIND_UNSPECIFIED": 0, + "EVIDENCE_KIND_SBOM_PUBLISHED": 1, + "EVIDENCE_KIND_SBOM_SUPPLIED": 2, + "EVIDENCE_KIND_VEX": 3, + "EVIDENCE_KIND_REPO_LOCKFILE": 4, + "EVIDENCE_KIND_FINGERPRINT_ASSET": 5, + "EVIDENCE_KIND_FINGERPRINT_HEADER": 6, + } +) + +func (x EvidenceKind) Enum() *EvidenceKind { + p := new(EvidenceKind) + *p = x + return p +} + +func (x EvidenceKind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (EvidenceKind) Descriptor() protoreflect.EnumDescriptor { + return file_touchstone_v1_scan_proto_enumTypes[1].Descriptor() +} + +func (EvidenceKind) Type() protoreflect.EnumType { + return &file_touchstone_v1_scan_proto_enumTypes[1] +} + +func (x EvidenceKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use EvidenceKind.Descriptor instead. +func (EvidenceKind) EnumDescriptor() ([]byte, []int) { + return file_touchstone_v1_scan_proto_rawDescGZIP(), []int{1} +} + +type VexStatus int32 + +const ( + VexStatus_VEX_STATUS_UNSPECIFIED VexStatus = 0 + VexStatus_VEX_STATUS_AFFECTED VexStatus = 1 + VexStatus_VEX_STATUS_NOT_AFFECTED VexStatus = 2 + VexStatus_VEX_STATUS_FIXED VexStatus = 3 + VexStatus_VEX_STATUS_UNDER_INVESTIGATION VexStatus = 4 +) + +// Enum value maps for VexStatus. +var ( + VexStatus_name = map[int32]string{ + 0: "VEX_STATUS_UNSPECIFIED", + 1: "VEX_STATUS_AFFECTED", + 2: "VEX_STATUS_NOT_AFFECTED", + 3: "VEX_STATUS_FIXED", + 4: "VEX_STATUS_UNDER_INVESTIGATION", + } + VexStatus_value = map[string]int32{ + "VEX_STATUS_UNSPECIFIED": 0, + "VEX_STATUS_AFFECTED": 1, + "VEX_STATUS_NOT_AFFECTED": 2, + "VEX_STATUS_FIXED": 3, + "VEX_STATUS_UNDER_INVESTIGATION": 4, + } +) + +func (x VexStatus) Enum() *VexStatus { + p := new(VexStatus) + *p = x + return p +} + +func (x VexStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (VexStatus) Descriptor() protoreflect.EnumDescriptor { + return file_touchstone_v1_scan_proto_enumTypes[2].Descriptor() +} + +func (VexStatus) Type() protoreflect.EnumType { + return &file_touchstone_v1_scan_proto_enumTypes[2] +} + +func (x VexStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use VexStatus.Descriptor instead. +func (VexStatus) EnumDescriptor() ([]byte, []int) { + return file_touchstone_v1_scan_proto_rawDescGZIP(), []int{2} +} + +type ScanStatus int32 + +const ( + ScanStatus_SCAN_STATUS_UNSPECIFIED ScanStatus = 0 + ScanStatus_SCAN_STATUS_OK ScanStatus = 1 + ScanStatus_SCAN_STATUS_TARGET_UNREACHABLE ScanStatus = 2 +) + +// Enum value maps for ScanStatus. +var ( + ScanStatus_name = map[int32]string{ + 0: "SCAN_STATUS_UNSPECIFIED", + 1: "SCAN_STATUS_OK", + 2: "SCAN_STATUS_TARGET_UNREACHABLE", + } + ScanStatus_value = map[string]int32{ + "SCAN_STATUS_UNSPECIFIED": 0, + "SCAN_STATUS_OK": 1, + "SCAN_STATUS_TARGET_UNREACHABLE": 2, + } +) + +func (x ScanStatus) Enum() *ScanStatus { + p := new(ScanStatus) + *p = x + return p +} + +func (x ScanStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ScanStatus) Descriptor() protoreflect.EnumDescriptor { + return file_touchstone_v1_scan_proto_enumTypes[3].Descriptor() +} + +func (ScanStatus) Type() protoreflect.EnumType { + return &file_touchstone_v1_scan_proto_enumTypes[3] +} + +func (x ScanStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ScanStatus.Descriptor instead. +func (ScanStatus) EnumDescriptor() ([]byte, []int) { + return file_touchstone_v1_scan_proto_rawDescGZIP(), []int{3} +} + +type LookupStatus int32 + +const ( + LookupStatus_LOOKUP_STATUS_UNSPECIFIED LookupStatus = 0 + LookupStatus_LOOKUP_STATUS_COMPLETE LookupStatus = 1 + LookupStatus_LOOKUP_STATUS_PARTIAL LookupStatus = 2 + LookupStatus_LOOKUP_STATUS_FAILED LookupStatus = 3 +) + +// Enum value maps for LookupStatus. +var ( + LookupStatus_name = map[int32]string{ + 0: "LOOKUP_STATUS_UNSPECIFIED", + 1: "LOOKUP_STATUS_COMPLETE", + 2: "LOOKUP_STATUS_PARTIAL", + 3: "LOOKUP_STATUS_FAILED", + } + LookupStatus_value = map[string]int32{ + "LOOKUP_STATUS_UNSPECIFIED": 0, + "LOOKUP_STATUS_COMPLETE": 1, + "LOOKUP_STATUS_PARTIAL": 2, + "LOOKUP_STATUS_FAILED": 3, + } +) + +func (x LookupStatus) Enum() *LookupStatus { + p := new(LookupStatus) + *p = x + return p +} + +func (x LookupStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (LookupStatus) Descriptor() protoreflect.EnumDescriptor { + return file_touchstone_v1_scan_proto_enumTypes[4].Descriptor() +} + +func (LookupStatus) Type() protoreflect.EnumType { + return &file_touchstone_v1_scan_proto_enumTypes[4] +} + +func (x LookupStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use LookupStatus.Descriptor instead. +func (LookupStatus) EnumDescriptor() ([]byte, []int) { + return file_touchstone_v1_scan_proto_rawDescGZIP(), []int{4} +} + +type CheckedLocation_Result int32 + +const ( + CheckedLocation_RESULT_UNSPECIFIED CheckedLocation_Result = 0 + CheckedLocation_RESULT_FOUND CheckedLocation_Result = 1 + CheckedLocation_RESULT_NOT_FOUND CheckedLocation_Result = 2 + CheckedLocation_RESULT_ERROR CheckedLocation_Result = 3 + CheckedLocation_RESULT_REFUSED CheckedLocation_Result = 4 +) + +// Enum value maps for CheckedLocation_Result. +var ( + CheckedLocation_Result_name = map[int32]string{ + 0: "RESULT_UNSPECIFIED", + 1: "RESULT_FOUND", + 2: "RESULT_NOT_FOUND", + 3: "RESULT_ERROR", + 4: "RESULT_REFUSED", + } + CheckedLocation_Result_value = map[string]int32{ + "RESULT_UNSPECIFIED": 0, + "RESULT_FOUND": 1, + "RESULT_NOT_FOUND": 2, + "RESULT_ERROR": 3, + "RESULT_REFUSED": 4, + } +) + +func (x CheckedLocation_Result) Enum() *CheckedLocation_Result { + p := new(CheckedLocation_Result) + *p = x + return p +} + +func (x CheckedLocation_Result) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CheckedLocation_Result) Descriptor() protoreflect.EnumDescriptor { + return file_touchstone_v1_scan_proto_enumTypes[5].Descriptor() +} + +func (CheckedLocation_Result) Type() protoreflect.EnumType { + return &file_touchstone_v1_scan_proto_enumTypes[5] +} + +func (x CheckedLocation_Result) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CheckedLocation_Result.Descriptor instead. +func (CheckedLocation_Result) EnumDescriptor() ([]byte, []int) { + return file_touchstone_v1_scan_proto_rawDescGZIP(), []int{5, 0} +} + +type ScanRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` // required, http(s) + RepoUrl string `protobuf:"bytes,2,opt,name=repo_url,json=repoUrl,proto3" json:"repo_url,omitempty"` // optional, user-supplied repo (#3) + SbomUrl string `protobuf:"bytes,3,opt,name=sbom_url,json=sbomUrl,proto3" json:"sbom_url,omitempty"` // optional, user-supplied SBOM/VEX (D1) + SbomDocument []byte `protobuf:"bytes,4,opt,name=sbom_document,json=sbomDocument,proto3" json:"sbom_document,omitempty"` // optional, uploaded SBOM/VEX (D1) + RequestBudget uint32 `protobuf:"varint,5,opt,name=request_budget,json=requestBudget,proto3" json:"request_budget,omitempty"` // 0 = default 50, max 200 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ScanRequest) Reset() { + *x = ScanRequest{} + mi := &file_touchstone_v1_scan_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ScanRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScanRequest) ProtoMessage() {} + +func (x *ScanRequest) ProtoReflect() protoreflect.Message { + mi := &file_touchstone_v1_scan_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScanRequest.ProtoReflect.Descriptor instead. +func (*ScanRequest) Descriptor() ([]byte, []int) { + return file_touchstone_v1_scan_proto_rawDescGZIP(), []int{0} +} + +func (x *ScanRequest) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *ScanRequest) GetRepoUrl() string { + if x != nil { + return x.RepoUrl + } + return "" +} + +func (x *ScanRequest) GetSbomUrl() string { + if x != nil { + return x.SbomUrl + } + return "" +} + +func (x *ScanRequest) GetSbomDocument() []byte { + if x != nil { + return x.SbomDocument + } + return nil +} + +func (x *ScanRequest) GetRequestBudget() uint32 { + if x != nil { + return x.RequestBudget + } + return 0 +} + +type Evidence struct { + state protoimpl.MessageState `protogen:"open.v1"` + Kind EvidenceKind `protobuf:"varint,1,opt,name=kind,proto3,enum=touchstone.v1.EvidenceKind" json:"kind,omitempty"` + Location string `protobuf:"bytes,2,opt,name=location,proto3" json:"location,omitempty"` // URL, or repo@ref:path for lockfiles + Detail string `protobuf:"bytes,3,opt,name=detail,proto3" json:"detail,omitempty"` // matched string or header value + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Evidence) Reset() { + *x = Evidence{} + mi := &file_touchstone_v1_scan_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Evidence) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Evidence) ProtoMessage() {} + +func (x *Evidence) ProtoReflect() protoreflect.Message { + mi := &file_touchstone_v1_scan_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Evidence.ProtoReflect.Descriptor instead. +func (*Evidence) Descriptor() ([]byte, []int) { + return file_touchstone_v1_scan_proto_rawDescGZIP(), []int{1} +} + +func (x *Evidence) GetKind() EvidenceKind { + if x != nil { + return x.Kind + } + return EvidenceKind_EVIDENCE_KIND_UNSPECIFIED +} + +func (x *Evidence) GetLocation() string { + if x != nil { + return x.Location + } + return "" +} + +func (x *Evidence) GetDetail() string { + if x != nil { + return x.Detail + } + return "" +} + +type Vulnerability struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // CVE id when one exists, else OSV id + Aliases []string `protobuf:"bytes,2,rep,name=aliases,proto3" json:"aliases,omitempty"` + Summary string `protobuf:"bytes,3,opt,name=summary,proto3" json:"summary,omitempty"` + Kev bool `protobuf:"varint,4,opt,name=kev,proto3" json:"kev,omitempty"` + Epss *float64 `protobuf:"fixed64,5,opt,name=epss,proto3,oneof" json:"epss,omitempty"` // absent = no EPSS data + CvssBase *float64 `protobuf:"fixed64,6,opt,name=cvss_base,json=cvssBase,proto3,oneof" json:"cvss_base,omitempty"` + VexStatus VexStatus `protobuf:"varint,7,opt,name=vex_status,json=vexStatus,proto3,enum=touchstone.v1.VexStatus" json:"vex_status,omitempty"` + VexJustification string `protobuf:"bytes,8,opt,name=vex_justification,json=vexJustification,proto3" json:"vex_justification,omitempty"` + Suppressed bool `protobuf:"varint,9,opt,name=suppressed,proto3" json:"suppressed,omitempty"` // true for NOT_AFFECTED / FIXED + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Vulnerability) Reset() { + *x = Vulnerability{} + mi := &file_touchstone_v1_scan_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Vulnerability) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Vulnerability) ProtoMessage() {} + +func (x *Vulnerability) ProtoReflect() protoreflect.Message { + mi := &file_touchstone_v1_scan_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Vulnerability.ProtoReflect.Descriptor instead. +func (*Vulnerability) Descriptor() ([]byte, []int) { + return file_touchstone_v1_scan_proto_rawDescGZIP(), []int{2} +} + +func (x *Vulnerability) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Vulnerability) GetAliases() []string { + if x != nil { + return x.Aliases + } + return nil +} + +func (x *Vulnerability) GetSummary() string { + if x != nil { + return x.Summary + } + return "" +} + +func (x *Vulnerability) GetKev() bool { + if x != nil { + return x.Kev + } + return false +} + +func (x *Vulnerability) GetEpss() float64 { + if x != nil && x.Epss != nil { + return *x.Epss + } + return 0 +} + +func (x *Vulnerability) GetCvssBase() float64 { + if x != nil && x.CvssBase != nil { + return *x.CvssBase + } + return 0 +} + +func (x *Vulnerability) GetVexStatus() VexStatus { + if x != nil { + return x.VexStatus + } + return VexStatus_VEX_STATUS_UNSPECIFIED +} + +func (x *Vulnerability) GetVexJustification() string { + if x != nil { + return x.VexJustification + } + return "" +} + +func (x *Vulnerability) GetSuppressed() bool { + if x != nil { + return x.Suppressed + } + return false +} + +type Component struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + Purl string `protobuf:"bytes,3,opt,name=purl,proto3" json:"purl,omitempty"` + Cpe string `protobuf:"bytes,4,opt,name=cpe,proto3" json:"cpe,omitempty"` + Confidence Confidence `protobuf:"varint,5,opt,name=confidence,proto3,enum=touchstone.v1.Confidence" json:"confidence,omitempty"` + Evidence []*Evidence `protobuf:"bytes,6,rep,name=evidence,proto3" json:"evidence,omitempty"` + Caveats []string `protobuf:"bytes,7,rep,name=caveats,proto3" json:"caveats,omitempty"` // e.g. MAY_BE_BACKPORTED, DEFAULT_BRANCH_NOT_RELEASE + Vulnerabilities []*Vulnerability `protobuf:"bytes,8,rep,name=vulnerabilities,proto3" json:"vulnerabilities,omitempty"` // ranked + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Component) Reset() { + *x = Component{} + mi := &file_touchstone_v1_scan_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Component) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Component) ProtoMessage() {} + +func (x *Component) ProtoReflect() protoreflect.Message { + mi := &file_touchstone_v1_scan_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Component.ProtoReflect.Descriptor instead. +func (*Component) Descriptor() ([]byte, []int) { + return file_touchstone_v1_scan_proto_rawDescGZIP(), []int{3} +} + +func (x *Component) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Component) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *Component) GetPurl() string { + if x != nil { + return x.Purl + } + return "" +} + +func (x *Component) GetCpe() string { + if x != nil { + return x.Cpe + } + return "" +} + +func (x *Component) GetConfidence() Confidence { + if x != nil { + return x.Confidence + } + return Confidence_CONFIDENCE_UNSPECIFIED +} + +func (x *Component) GetEvidence() []*Evidence { + if x != nil { + return x.Evidence + } + return nil +} + +func (x *Component) GetCaveats() []string { + if x != nil { + return x.Caveats + } + return nil +} + +func (x *Component) GetVulnerabilities() []*Vulnerability { + if x != nil { + return x.Vulnerabilities + } + return nil +} + +type Conflict struct { + state protoimpl.MessageState `protogen:"open.v1"` + ComponentKey string `protobuf:"bytes,1,opt,name=component_key,json=componentKey,proto3" json:"component_key,omitempty"` + Versions []string `protobuf:"bytes,2,rep,name=versions,proto3" json:"versions,omitempty"` + Evidence []*Evidence `protobuf:"bytes,3,rep,name=evidence,proto3" json:"evidence,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Conflict) Reset() { + *x = Conflict{} + mi := &file_touchstone_v1_scan_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Conflict) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Conflict) ProtoMessage() {} + +func (x *Conflict) ProtoReflect() protoreflect.Message { + mi := &file_touchstone_v1_scan_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Conflict.ProtoReflect.Descriptor instead. +func (*Conflict) Descriptor() ([]byte, []int) { + return file_touchstone_v1_scan_proto_rawDescGZIP(), []int{4} +} + +func (x *Conflict) GetComponentKey() string { + if x != nil { + return x.ComponentKey + } + return "" +} + +func (x *Conflict) GetVersions() []string { + if x != nil { + return x.Versions + } + return nil +} + +func (x *Conflict) GetEvidence() []*Evidence { + if x != nil { + return x.Evidence + } + return nil +} + +type CheckedLocation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` // TARGET | SUBRESOURCE | DISCOVERY_PATH | DECLARED | FORGE_API + Result CheckedLocation_Result `protobuf:"varint,3,opt,name=result,proto3,enum=touchstone.v1.CheckedLocation_Result" json:"result,omitempty"` + Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckedLocation) Reset() { + *x = CheckedLocation{} + mi := &file_touchstone_v1_scan_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckedLocation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckedLocation) ProtoMessage() {} + +func (x *CheckedLocation) ProtoReflect() protoreflect.Message { + mi := &file_touchstone_v1_scan_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckedLocation.ProtoReflect.Descriptor instead. +func (*CheckedLocation) Descriptor() ([]byte, []int) { + return file_touchstone_v1_scan_proto_rawDescGZIP(), []int{5} +} + +func (x *CheckedLocation) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *CheckedLocation) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *CheckedLocation) GetResult() CheckedLocation_Result { + if x != nil { + return x.Result + } + return CheckedLocation_RESULT_UNSPECIFIED +} + +func (x *CheckedLocation) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type SourceStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Source string `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` + Ok bool `protobuf:"varint,2,opt,name=ok,proto3" json:"ok,omitempty"` + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SourceStatus) Reset() { + *x = SourceStatus{} + mi := &file_touchstone_v1_scan_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SourceStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SourceStatus) ProtoMessage() {} + +func (x *SourceStatus) ProtoReflect() protoreflect.Message { + mi := &file_touchstone_v1_scan_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SourceStatus.ProtoReflect.Descriptor instead. +func (*SourceStatus) Descriptor() ([]byte, []int) { + return file_touchstone_v1_scan_proto_rawDescGZIP(), []int{6} +} + +func (x *SourceStatus) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *SourceStatus) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *SourceStatus) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type ScanReport struct { + state protoimpl.MessageState `protogen:"open.v1"` + SchemaVersion string `protobuf:"bytes,1,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"` // "1"; bumped only with a new proto package + TargetUrl string `protobuf:"bytes,2,opt,name=target_url,json=targetUrl,proto3" json:"target_url,omitempty"` + ScanStatus ScanStatus `protobuf:"varint,3,opt,name=scan_status,json=scanStatus,proto3,enum=touchstone.v1.ScanStatus" json:"scan_status,omitempty"` + LookupStatus LookupStatus `protobuf:"varint,4,opt,name=lookup_status,json=lookupStatus,proto3,enum=touchstone.v1.LookupStatus" json:"lookup_status,omitempty"` + StartedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` + CompletedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=completed_at,json=completedAt,proto3" json:"completed_at,omitempty"` + ToolVersion string `protobuf:"bytes,7,opt,name=tool_version,json=toolVersion,proto3" json:"tool_version,omitempty"` + VulnDataAsOf *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=vuln_data_as_of,json=vulnDataAsOf,proto3" json:"vuln_data_as_of,omitempty"` + Components []*Component `protobuf:"bytes,9,rep,name=components,proto3" json:"components,omitempty"` + Conflicts []*Conflict `protobuf:"bytes,10,rep,name=conflicts,proto3" json:"conflicts,omitempty"` + Checked []*CheckedLocation `protobuf:"bytes,11,rep,name=checked,proto3" json:"checked,omitempty"` + RequestsUsed uint32 `protobuf:"varint,12,opt,name=requests_used,json=requestsUsed,proto3" json:"requests_used,omitempty"` + RequestBudget uint32 `protobuf:"varint,13,opt,name=request_budget,json=requestBudget,proto3" json:"request_budget,omitempty"` + Sources []*SourceStatus `protobuf:"bytes,14,rep,name=sources,proto3" json:"sources,omitempty"` + Disclaimer string `protobuf:"bytes,15,opt,name=disclaimer,proto3" json:"disclaimer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ScanReport) Reset() { + *x = ScanReport{} + mi := &file_touchstone_v1_scan_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ScanReport) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScanReport) ProtoMessage() {} + +func (x *ScanReport) ProtoReflect() protoreflect.Message { + mi := &file_touchstone_v1_scan_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScanReport.ProtoReflect.Descriptor instead. +func (*ScanReport) Descriptor() ([]byte, []int) { + return file_touchstone_v1_scan_proto_rawDescGZIP(), []int{7} +} + +func (x *ScanReport) GetSchemaVersion() string { + if x != nil { + return x.SchemaVersion + } + return "" +} + +func (x *ScanReport) GetTargetUrl() string { + if x != nil { + return x.TargetUrl + } + return "" +} + +func (x *ScanReport) GetScanStatus() ScanStatus { + if x != nil { + return x.ScanStatus + } + return ScanStatus_SCAN_STATUS_UNSPECIFIED +} + +func (x *ScanReport) GetLookupStatus() LookupStatus { + if x != nil { + return x.LookupStatus + } + return LookupStatus_LOOKUP_STATUS_UNSPECIFIED +} + +func (x *ScanReport) GetStartedAt() *timestamppb.Timestamp { + if x != nil { + return x.StartedAt + } + return nil +} + +func (x *ScanReport) GetCompletedAt() *timestamppb.Timestamp { + if x != nil { + return x.CompletedAt + } + return nil +} + +func (x *ScanReport) GetToolVersion() string { + if x != nil { + return x.ToolVersion + } + return "" +} + +func (x *ScanReport) GetVulnDataAsOf() *timestamppb.Timestamp { + if x != nil { + return x.VulnDataAsOf + } + return nil +} + +func (x *ScanReport) GetComponents() []*Component { + if x != nil { + return x.Components + } + return nil +} + +func (x *ScanReport) GetConflicts() []*Conflict { + if x != nil { + return x.Conflicts + } + return nil +} + +func (x *ScanReport) GetChecked() []*CheckedLocation { + if x != nil { + return x.Checked + } + return nil +} + +func (x *ScanReport) GetRequestsUsed() uint32 { + if x != nil { + return x.RequestsUsed + } + return 0 +} + +func (x *ScanReport) GetRequestBudget() uint32 { + if x != nil { + return x.RequestBudget + } + return 0 +} + +func (x *ScanReport) GetSources() []*SourceStatus { + if x != nil { + return x.Sources + } + return nil +} + +func (x *ScanReport) GetDisclaimer() string { + if x != nil { + return x.Disclaimer + } + return "" +} + +var File_touchstone_v1_scan_proto protoreflect.FileDescriptor + +const file_touchstone_v1_scan_proto_rawDesc = "" + + "\n" + + "\x18touchstone/v1/scan.proto\x12\rtouchstone.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xa1\x01\n" + + "\vScanRequest\x12\x10\n" + + "\x03url\x18\x01 \x01(\tR\x03url\x12\x19\n" + + "\brepo_url\x18\x02 \x01(\tR\arepoUrl\x12\x19\n" + + "\bsbom_url\x18\x03 \x01(\tR\asbomUrl\x12#\n" + + "\rsbom_document\x18\x04 \x01(\fR\fsbomDocument\x12%\n" + + "\x0erequest_budget\x18\x05 \x01(\rR\rrequestBudget\"o\n" + + "\bEvidence\x12/\n" + + "\x04kind\x18\x01 \x01(\x0e2\x1b.touchstone.v1.EvidenceKindR\x04kind\x12\x1a\n" + + "\blocation\x18\x02 \x01(\tR\blocation\x12\x16\n" + + "\x06detail\x18\x03 \x01(\tR\x06detail\"\xbd\x02\n" + + "\rVulnerability\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n" + + "\aaliases\x18\x02 \x03(\tR\aaliases\x12\x18\n" + + "\asummary\x18\x03 \x01(\tR\asummary\x12\x10\n" + + "\x03kev\x18\x04 \x01(\bR\x03kev\x12\x17\n" + + "\x04epss\x18\x05 \x01(\x01H\x00R\x04epss\x88\x01\x01\x12 \n" + + "\tcvss_base\x18\x06 \x01(\x01H\x01R\bcvssBase\x88\x01\x01\x127\n" + + "\n" + + "vex_status\x18\a \x01(\x0e2\x18.touchstone.v1.VexStatusR\tvexStatus\x12+\n" + + "\x11vex_justification\x18\b \x01(\tR\x10vexJustification\x12\x1e\n" + + "\n" + + "suppressed\x18\t \x01(\bR\n" + + "suppressedB\a\n" + + "\x05_epssB\f\n" + + "\n" + + "_cvss_base\"\xb1\x02\n" + + "\tComponent\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + + "\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" + + "\x04purl\x18\x03 \x01(\tR\x04purl\x12\x10\n" + + "\x03cpe\x18\x04 \x01(\tR\x03cpe\x129\n" + + "\n" + + "confidence\x18\x05 \x01(\x0e2\x19.touchstone.v1.ConfidenceR\n" + + "confidence\x123\n" + + "\bevidence\x18\x06 \x03(\v2\x17.touchstone.v1.EvidenceR\bevidence\x12\x18\n" + + "\acaveats\x18\a \x03(\tR\acaveats\x12F\n" + + "\x0fvulnerabilities\x18\b \x03(\v2\x1c.touchstone.v1.VulnerabilityR\x0fvulnerabilities\"\x80\x01\n" + + "\bConflict\x12#\n" + + "\rcomponent_key\x18\x01 \x01(\tR\fcomponentKey\x12\x1a\n" + + "\bversions\x18\x02 \x03(\tR\bversions\x123\n" + + "\bevidence\x18\x03 \x03(\v2\x17.touchstone.v1.EvidenceR\bevidence\"\x80\x02\n" + + "\x0fCheckedLocation\x12\x10\n" + + "\x03url\x18\x01 \x01(\tR\x03url\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\x12=\n" + + "\x06result\x18\x03 \x01(\x0e2%.touchstone.v1.CheckedLocation.ResultR\x06result\x12\x14\n" + + "\x05error\x18\x04 \x01(\tR\x05error\"n\n" + + "\x06Result\x12\x16\n" + + "\x12RESULT_UNSPECIFIED\x10\x00\x12\x10\n" + + "\fRESULT_FOUND\x10\x01\x12\x14\n" + + "\x10RESULT_NOT_FOUND\x10\x02\x12\x10\n" + + "\fRESULT_ERROR\x10\x03\x12\x12\n" + + "\x0eRESULT_REFUSED\x10\x04\"L\n" + + "\fSourceStatus\x12\x16\n" + + "\x06source\x18\x01 \x01(\tR\x06source\x12\x0e\n" + + "\x02ok\x18\x02 \x01(\bR\x02ok\x12\x14\n" + + "\x05error\x18\x03 \x01(\tR\x05error\"\xfe\x05\n" + + "\n" + + "ScanReport\x12%\n" + + "\x0eschema_version\x18\x01 \x01(\tR\rschemaVersion\x12\x1d\n" + + "\n" + + "target_url\x18\x02 \x01(\tR\ttargetUrl\x12:\n" + + "\vscan_status\x18\x03 \x01(\x0e2\x19.touchstone.v1.ScanStatusR\n" + + "scanStatus\x12@\n" + + "\rlookup_status\x18\x04 \x01(\x0e2\x1b.touchstone.v1.LookupStatusR\flookupStatus\x129\n" + + "\n" + + "started_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tstartedAt\x12=\n" + + "\fcompleted_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\vcompletedAt\x12!\n" + + "\ftool_version\x18\a \x01(\tR\vtoolVersion\x12A\n" + + "\x0fvuln_data_as_of\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\fvulnDataAsOf\x128\n" + + "\n" + + "components\x18\t \x03(\v2\x18.touchstone.v1.ComponentR\n" + + "components\x125\n" + + "\tconflicts\x18\n" + + " \x03(\v2\x17.touchstone.v1.ConflictR\tconflicts\x128\n" + + "\achecked\x18\v \x03(\v2\x1e.touchstone.v1.CheckedLocationR\achecked\x12#\n" + + "\rrequests_used\x18\f \x01(\rR\frequestsUsed\x12%\n" + + "\x0erequest_budget\x18\r \x01(\rR\rrequestBudget\x125\n" + + "\asources\x18\x0e \x03(\v2\x1b.touchstone.v1.SourceStatusR\asources\x12\x1e\n" + + "\n" + + "disclaimer\x18\x0f \x01(\tR\n" + + "disclaimer*h\n" + + "\n" + + "Confidence\x12\x1a\n" + + "\x16CONFIDENCE_UNSPECIFIED\x10\x00\x12\x12\n" + + "\x0eCONFIDENCE_LOW\x10\x01\x12\x15\n" + + "\x11CONFIDENCE_MEDIUM\x10\x02\x12\x13\n" + + "\x0fCONFIDENCE_HIGH\x10\x03*\xf3\x01\n" + + "\fEvidenceKind\x12\x1d\n" + + "\x19EVIDENCE_KIND_UNSPECIFIED\x10\x00\x12 \n" + + "\x1cEVIDENCE_KIND_SBOM_PUBLISHED\x10\x01\x12\x1f\n" + + "\x1bEVIDENCE_KIND_SBOM_SUPPLIED\x10\x02\x12\x15\n" + + "\x11EVIDENCE_KIND_VEX\x10\x03\x12\x1f\n" + + "\x1bEVIDENCE_KIND_REPO_LOCKFILE\x10\x04\x12#\n" + + "\x1fEVIDENCE_KIND_FINGERPRINT_ASSET\x10\x05\x12$\n" + + " EVIDENCE_KIND_FINGERPRINT_HEADER\x10\x06*\x97\x01\n" + + "\tVexStatus\x12\x1a\n" + + "\x16VEX_STATUS_UNSPECIFIED\x10\x00\x12\x17\n" + + "\x13VEX_STATUS_AFFECTED\x10\x01\x12\x1b\n" + + "\x17VEX_STATUS_NOT_AFFECTED\x10\x02\x12\x14\n" + + "\x10VEX_STATUS_FIXED\x10\x03\x12\"\n" + + "\x1eVEX_STATUS_UNDER_INVESTIGATION\x10\x04*a\n" + + "\n" + + "ScanStatus\x12\x1b\n" + + "\x17SCAN_STATUS_UNSPECIFIED\x10\x00\x12\x12\n" + + "\x0eSCAN_STATUS_OK\x10\x01\x12\"\n" + + "\x1eSCAN_STATUS_TARGET_UNREACHABLE\x10\x02*~\n" + + "\fLookupStatus\x12\x1d\n" + + "\x19LOOKUP_STATUS_UNSPECIFIED\x10\x00\x12\x1a\n" + + "\x16LOOKUP_STATUS_COMPLETE\x10\x01\x12\x19\n" + + "\x15LOOKUP_STATUS_PARTIAL\x10\x02\x12\x18\n" + + "\x14LOOKUP_STATUS_FAILED\x10\x032b\n" + + "\vScanService\x12S\n" + + "\x04Scan\x12\x1a.touchstone.v1.ScanRequest\x1a\x19.touchstone.v1.ScanReport\"\x14\x82\xd3\xe4\x93\x02\x0e:\x01*\"\t/v1/scansBEZCgithub.com/FootprintAI/touchstone/gen/go/touchstone/v1;touchstonev1b\x06proto3" + +var ( + file_touchstone_v1_scan_proto_rawDescOnce sync.Once + file_touchstone_v1_scan_proto_rawDescData []byte +) + +func file_touchstone_v1_scan_proto_rawDescGZIP() []byte { + file_touchstone_v1_scan_proto_rawDescOnce.Do(func() { + file_touchstone_v1_scan_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_touchstone_v1_scan_proto_rawDesc), len(file_touchstone_v1_scan_proto_rawDesc))) + }) + return file_touchstone_v1_scan_proto_rawDescData +} + +var file_touchstone_v1_scan_proto_enumTypes = make([]protoimpl.EnumInfo, 6) +var file_touchstone_v1_scan_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_touchstone_v1_scan_proto_goTypes = []any{ + (Confidence)(0), // 0: touchstone.v1.Confidence + (EvidenceKind)(0), // 1: touchstone.v1.EvidenceKind + (VexStatus)(0), // 2: touchstone.v1.VexStatus + (ScanStatus)(0), // 3: touchstone.v1.ScanStatus + (LookupStatus)(0), // 4: touchstone.v1.LookupStatus + (CheckedLocation_Result)(0), // 5: touchstone.v1.CheckedLocation.Result + (*ScanRequest)(nil), // 6: touchstone.v1.ScanRequest + (*Evidence)(nil), // 7: touchstone.v1.Evidence + (*Vulnerability)(nil), // 8: touchstone.v1.Vulnerability + (*Component)(nil), // 9: touchstone.v1.Component + (*Conflict)(nil), // 10: touchstone.v1.Conflict + (*CheckedLocation)(nil), // 11: touchstone.v1.CheckedLocation + (*SourceStatus)(nil), // 12: touchstone.v1.SourceStatus + (*ScanReport)(nil), // 13: touchstone.v1.ScanReport + (*timestamppb.Timestamp)(nil), // 14: google.protobuf.Timestamp +} +var file_touchstone_v1_scan_proto_depIdxs = []int32{ + 1, // 0: touchstone.v1.Evidence.kind:type_name -> touchstone.v1.EvidenceKind + 2, // 1: touchstone.v1.Vulnerability.vex_status:type_name -> touchstone.v1.VexStatus + 0, // 2: touchstone.v1.Component.confidence:type_name -> touchstone.v1.Confidence + 7, // 3: touchstone.v1.Component.evidence:type_name -> touchstone.v1.Evidence + 8, // 4: touchstone.v1.Component.vulnerabilities:type_name -> touchstone.v1.Vulnerability + 7, // 5: touchstone.v1.Conflict.evidence:type_name -> touchstone.v1.Evidence + 5, // 6: touchstone.v1.CheckedLocation.result:type_name -> touchstone.v1.CheckedLocation.Result + 3, // 7: touchstone.v1.ScanReport.scan_status:type_name -> touchstone.v1.ScanStatus + 4, // 8: touchstone.v1.ScanReport.lookup_status:type_name -> touchstone.v1.LookupStatus + 14, // 9: touchstone.v1.ScanReport.started_at:type_name -> google.protobuf.Timestamp + 14, // 10: touchstone.v1.ScanReport.completed_at:type_name -> google.protobuf.Timestamp + 14, // 11: touchstone.v1.ScanReport.vuln_data_as_of:type_name -> google.protobuf.Timestamp + 9, // 12: touchstone.v1.ScanReport.components:type_name -> touchstone.v1.Component + 10, // 13: touchstone.v1.ScanReport.conflicts:type_name -> touchstone.v1.Conflict + 11, // 14: touchstone.v1.ScanReport.checked:type_name -> touchstone.v1.CheckedLocation + 12, // 15: touchstone.v1.ScanReport.sources:type_name -> touchstone.v1.SourceStatus + 6, // 16: touchstone.v1.ScanService.Scan:input_type -> touchstone.v1.ScanRequest + 13, // 17: touchstone.v1.ScanService.Scan:output_type -> touchstone.v1.ScanReport + 17, // [17:18] is the sub-list for method output_type + 16, // [16:17] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension type_name + 16, // [16:16] is the sub-list for extension extendee + 0, // [0:16] is the sub-list for field type_name +} + +func init() { file_touchstone_v1_scan_proto_init() } +func file_touchstone_v1_scan_proto_init() { + if File_touchstone_v1_scan_proto != nil { + return + } + file_touchstone_v1_scan_proto_msgTypes[2].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_touchstone_v1_scan_proto_rawDesc), len(file_touchstone_v1_scan_proto_rawDesc)), + NumEnums: 6, + NumMessages: 8, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_touchstone_v1_scan_proto_goTypes, + DependencyIndexes: file_touchstone_v1_scan_proto_depIdxs, + EnumInfos: file_touchstone_v1_scan_proto_enumTypes, + MessageInfos: file_touchstone_v1_scan_proto_msgTypes, + }.Build() + File_touchstone_v1_scan_proto = out.File + file_touchstone_v1_scan_proto_goTypes = nil + file_touchstone_v1_scan_proto_depIdxs = nil +} diff --git a/gen/go/touchstone/v1/scan.pb.gw.go b/gen/go/touchstone/v1/scan.pb.gw.go new file mode 100644 index 0000000..7562694 --- /dev/null +++ b/gen/go/touchstone/v1/scan.pb.gw.go @@ -0,0 +1,157 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: touchstone/v1/scan.proto + +/* +Package touchstonev1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package touchstonev1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +func request_ScanService_Scan_0(ctx context.Context, marshaler runtime.Marshaler, client ScanServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ScanRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.Scan(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ScanService_Scan_0(ctx context.Context, marshaler runtime.Marshaler, server ScanServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ScanRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.Scan(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterScanServiceHandlerServer registers the http handlers for service ScanService to "mux". +// UnaryRPC :call ScanServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterScanServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterScanServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ScanServiceServer) error { + mux.Handle(http.MethodPost, pattern_ScanService_Scan_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/touchstone.v1.ScanService/Scan", runtime.WithHTTPPathPattern("/v1/scans")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ScanService_Scan_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ScanService_Scan_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterScanServiceHandlerFromEndpoint is same as RegisterScanServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterScanServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterScanServiceHandler(ctx, mux, conn) +} + +// RegisterScanServiceHandler registers the http handlers for service ScanService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterScanServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterScanServiceHandlerClient(ctx, mux, NewScanServiceClient(conn)) +} + +// RegisterScanServiceHandlerClient registers the http handlers for service ScanService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ScanServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ScanServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "ScanServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterScanServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ScanServiceClient) error { + mux.Handle(http.MethodPost, pattern_ScanService_Scan_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/touchstone.v1.ScanService/Scan", runtime.WithHTTPPathPattern("/v1/scans")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ScanService_Scan_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ScanService_Scan_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_ScanService_Scan_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "scans"}, "")) +) + +var ( + forward_ScanService_Scan_0 = runtime.ForwardResponseMessage +) diff --git a/gen/go/touchstone/v1/scan_grpc.pb.go b/gen/go/touchstone/v1/scan_grpc.pb.go new file mode 100644 index 0000000..8b1c5ff --- /dev/null +++ b/gen/go/touchstone/v1/scan_grpc.pb.go @@ -0,0 +1,121 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc (unknown) +// source: touchstone/v1/scan.proto + +package touchstonev1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ScanService_Scan_FullMethodName = "/touchstone.v1.ScanService/Scan" +) + +// ScanServiceClient is the client API for ScanService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ScanServiceClient interface { + Scan(ctx context.Context, in *ScanRequest, opts ...grpc.CallOption) (*ScanReport, error) +} + +type scanServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewScanServiceClient(cc grpc.ClientConnInterface) ScanServiceClient { + return &scanServiceClient{cc} +} + +func (c *scanServiceClient) Scan(ctx context.Context, in *ScanRequest, opts ...grpc.CallOption) (*ScanReport, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ScanReport) + err := c.cc.Invoke(ctx, ScanService_Scan_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ScanServiceServer is the server API for ScanService service. +// All implementations must embed UnimplementedScanServiceServer +// for forward compatibility. +type ScanServiceServer interface { + Scan(context.Context, *ScanRequest) (*ScanReport, error) + mustEmbedUnimplementedScanServiceServer() +} + +// UnimplementedScanServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedScanServiceServer struct{} + +func (UnimplementedScanServiceServer) Scan(context.Context, *ScanRequest) (*ScanReport, error) { + return nil, status.Error(codes.Unimplemented, "method Scan not implemented") +} +func (UnimplementedScanServiceServer) mustEmbedUnimplementedScanServiceServer() {} +func (UnimplementedScanServiceServer) testEmbeddedByValue() {} + +// UnsafeScanServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ScanServiceServer will +// result in compilation errors. +type UnsafeScanServiceServer interface { + mustEmbedUnimplementedScanServiceServer() +} + +func RegisterScanServiceServer(s grpc.ServiceRegistrar, srv ScanServiceServer) { + // If the following call panics, it indicates UnimplementedScanServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ScanService_ServiceDesc, srv) +} + +func _ScanService_Scan_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ScanRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ScanServiceServer).Scan(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ScanService_Scan_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ScanServiceServer).Scan(ctx, req.(*ScanRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ScanService_ServiceDesc is the grpc.ServiceDesc for ScanService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ScanService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "touchstone.v1.ScanService", + HandlerType: (*ScanServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Scan", + Handler: _ScanService_Scan_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "touchstone/v1/scan.proto", +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bf9ff3d --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module github.com/FootprintAI/touchstone + +go 1.26.0 + +require ( + github.com/grpc-ecosystem/grpc-gateway/v2 v2.32.0 + github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 + google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b + google.golang.org/grpc v1.84.0 + google.golang.org/protobuf v1.36.12 +) + +require ( + golang.org/x/net v0.61.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/text v0.43.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..26c7557 --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.32.0 h1:QMILzF8BA7xftE4zVORkRrlv08m12St3csK4RsyRgRs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.32.0/go.mod h1:+ZQ6iUL8+ATyxGDQNuRKpNwJFdGDUi0e7E1QL9MvI7U= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b h1:nB3spRIaSPtxJpxb0k41yljROvm2Jrwfs2pSNdKS5y8= +google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b/go.mod h1:9N4vtjEwvgQKEdlFVQO0M44ErhecQ9DCSQfGoZ51M6w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941 h1:QG6hS4zFplRZvOQedzei4t2pT/zkQc3VR1u+S9/rcAw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/internal/fetch/fake.go b/internal/fetch/fake.go new file mode 100644 index 0000000..aea1100 --- /dev/null +++ b/internal/fetch/fake.go @@ -0,0 +1,51 @@ +package fetch + +import ( + "context" + "fmt" +) + +// Call records one Get invocation made against a Fake. +type Call struct { + URL string + Reason Reason +} + +// Fake is an in-memory Fetcher for tests: it serves canned responses by +// URL and records every call it receives so a test can assert exactly +// what was fetched and why. +type Fake struct { + Responses map[string]*Response + Errors map[string]error + Calls []Call +} + +// NewFake returns a Fake with no responses configured. +func NewFake() *Fake { + return &Fake{ + Responses: make(map[string]*Response), + Errors: make(map[string]error), + } +} + +// SetResponse configures the Response returned for a URL regardless of +// reason. +func (f *Fake) SetResponse(url string, resp *Response) { + f.Responses[url] = resp +} + +// SetError configures the error returned for a URL regardless of reason. +func (f *Fake) SetError(url string, err error) { + f.Errors[url] = err +} + +func (f *Fake) Get(_ context.Context, url string, reason Reason) (*Response, error) { + f.Calls = append(f.Calls, Call{URL: url, Reason: reason}) + if err, ok := f.Errors[url]; ok { + return nil, err + } + if resp, ok := f.Responses[url]; ok { + return resp, nil + } + return nil, fmt.Errorf("fetch: fake has no response configured for %q", url) +} diff --git a/internal/fetch/fake_test.go b/internal/fetch/fake_test.go new file mode 100644 index 0000000..d26bcd0 --- /dev/null +++ b/internal/fetch/fake_test.go @@ -0,0 +1,46 @@ +package fetch + +import ( + "context" + "errors" + "testing" +) + +func TestFakeReturnsConfiguredResponse(t *testing.T) { + f := NewFake() + want := &Response{URL: "https://example.com/", StatusCode: 200, Body: []byte("ok")} + f.SetResponse("https://example.com/", want) + + got, err := f.Get(context.Background(), "https://example.com/", ReasonTarget) + if err != nil { + t.Fatalf("Get: unexpected error: %v", err) + } + if got != want { + t.Fatalf("Get: got %+v, want %+v", got, want) + } + + if len(f.Calls) != 1 { + t.Fatalf("Calls: got %d, want 1", len(f.Calls)) + } + if f.Calls[0] != (Call{URL: "https://example.com/", Reason: ReasonTarget}) { + t.Fatalf("Calls[0]: got %+v", f.Calls[0]) + } +} + +func TestFakeReturnsConfiguredError(t *testing.T) { + f := NewFake() + wantErr := errors.New("refused") + f.SetError("https://example.com/private", wantErr) + + _, err := f.Get(context.Background(), "https://example.com/private", ReasonSubresource) + if !errors.Is(err, wantErr) { + t.Fatalf("Get: got error %v, want %v", err, wantErr) + } +} + +func TestFakeUnconfiguredURLErrors(t *testing.T) { + f := NewFake() + if _, err := f.Get(context.Background(), "https://unknown.example/", ReasonTarget); err == nil { + t.Fatal("Get: want error for unconfigured URL, got nil") + } +} diff --git a/internal/fetch/fetch.go b/internal/fetch/fetch.go new file mode 100644 index 0000000..4413609 --- /dev/null +++ b/internal/fetch/fetch.go @@ -0,0 +1,36 @@ +// Package fetch defines the Fetcher seam: the one way any other package +// is allowed to read bytes from a vendor site. The real implementation +// (PassiveFetcher, see docs/architecture/touchstone.md) lands in slice +// S1; this package only fixes the interface so S0 callers and their +// tests can be written against it now. +package fetch + +import "context" + +// Reason is why a request is allowed. It mirrors the CheckedLocation.reason +// values in proto/touchstone/v1/scan.proto (TARGET, SUBRESOURCE, +// DISCOVERY_PATH, DECLARED); FORGE_API is handled by repo.Forge instead, +// since it never counts against the vendor's request budget. +type Reason string + +const ( + ReasonTarget Reason = "TARGET" + ReasonSubresource Reason = "SUBRESOURCE" + ReasonDiscoveryPath Reason = "DISCOVERY_PATH" + ReasonDeclared Reason = "DECLARED" +) + +// Response is the subset of an HTTP response any evidence source needs. +type Response struct { + URL string + StatusCode int + Header map[string][]string + Body []byte +} + +// Fetcher is the only interface through which code may contact a vendor +// site. Implementations must refuse any (url, reason) pair outside their +// allowlist. +type Fetcher interface { + Get(ctx context.Context, url string, reason Reason) (*Response, error) +} diff --git a/internal/pipeline/fake.go b/internal/pipeline/fake.go new file mode 100644 index 0000000..783b714 --- /dev/null +++ b/internal/pipeline/fake.go @@ -0,0 +1,20 @@ +package pipeline + +import "context" + +// FakeEvidenceSource is an in-memory EvidenceSource for tests: it returns +// a fixed set of components (or a fixed error) regardless of target. +type FakeEvidenceSource struct { + SourceName string + Components []Component + Err error +} + +func (f *FakeEvidenceSource) Name() string { return f.SourceName } + +func (f *FakeEvidenceSource) Collect(_ context.Context, _ string) ([]Component, error) { + if f.Err != nil { + return nil, f.Err + } + return f.Components, nil +} diff --git a/internal/pipeline/fake_test.go b/internal/pipeline/fake_test.go new file mode 100644 index 0000000..8d2d3ca --- /dev/null +++ b/internal/pipeline/fake_test.go @@ -0,0 +1,33 @@ +package pipeline + +import ( + "context" + "errors" + "testing" +) + +func TestFakeEvidenceSourceReturnsConfiguredComponents(t *testing.T) { + want := []Component{{Name: "jquery", Version: "1.12.4", Confidence: "LOW"}} + s := &FakeEvidenceSource{SourceName: "fingerprint", Components: want} + + if got := s.Name(); got != "fingerprint" { + t.Fatalf("Name: got %q, want %q", got, "fingerprint") + } + + got, err := s.Collect(context.Background(), "https://example.com") + if err != nil { + t.Fatalf("Collect: unexpected error: %v", err) + } + if len(got) != 1 || got[0].Name != want[0].Name { + t.Fatalf("Collect: got %+v, want %+v", got, want) + } +} + +func TestFakeEvidenceSourceReturnsConfiguredError(t *testing.T) { + wantErr := errors.New("source unavailable") + s := &FakeEvidenceSource{SourceName: "repo", Err: wantErr} + + if _, err := s.Collect(context.Background(), "https://example.com"); !errors.Is(err, wantErr) { + t.Fatalf("Collect: got error %v, want %v", err, wantErr) + } +} diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go new file mode 100644 index 0000000..19d6eba --- /dev/null +++ b/internal/pipeline/pipeline.go @@ -0,0 +1,38 @@ +// Package pipeline defines the EvidenceSource seam that the pure merge +// core (see docs/architecture/touchstone.md) will orchestrate: discover, +// repo, fingerprint and supplied each implement it. The merge core itself +// lands in slice S2; this package only fixes the interface so S0 callers +// and their tests can be written against it now. +package pipeline + +import "context" + +// Evidence is why a Component was reported, independent of the proto +// representation: location is a URL, or repo@ref:path for lockfiles. +type Evidence struct { + Kind string + Location string + Detail string +} + +// Component is one piece of software an EvidenceSource found. Confidence +// reflects the source's tier (e.g. "HIGH" for a declared SBOM, "LOW" for a +// fingerprint match); pipeline's merge step resolves conflicts across +// sources. +type Component struct { + Name string + Version string + Purl string + CPE string + Confidence string + Evidence []Evidence + Caveats []string +} + +// EvidenceSource collects components for a scan target. One source +// failing must never fail the whole scan: pipeline records a +// SourceStatus and continues with the others. +type EvidenceSource interface { + Name() string + Collect(ctx context.Context, targetURL string) ([]Component, error) +} diff --git a/internal/repo/fake.go b/internal/repo/fake.go new file mode 100644 index 0000000..85bae5a --- /dev/null +++ b/internal/repo/fake.go @@ -0,0 +1,49 @@ +package repo + +import ( + "context" + "fmt" +) + +// Fake is an in-memory Forge for tests: it resolves releases and serves +// tarball bytes from maps keyed by repo URL / tarball URL. +type Fake struct { + ForgeName string + Releases map[string]*Release + Tarballs map[string][]byte + Errors map[string]error +} + +// NewFake returns a Fake with no releases or tarballs configured. +func NewFake(name string) *Fake { + return &Fake{ + ForgeName: name, + Releases: make(map[string]*Release), + Tarballs: make(map[string][]byte), + Errors: make(map[string]error), + } +} + +func (f *Fake) Name() string { return f.ForgeName } + +func (f *Fake) LatestRelease(_ context.Context, repoURL string) (*Release, error) { + if err, ok := f.Errors[repoURL]; ok { + return nil, err + } + rel, ok := f.Releases[repoURL] + if !ok { + return nil, fmt.Errorf("repo: fake has no release configured for %q", repoURL) + } + return rel, nil +} + +func (f *Fake) FetchTarball(_ context.Context, tarballURL string) ([]byte, error) { + if err, ok := f.Errors[tarballURL]; ok { + return nil, err + } + data, ok := f.Tarballs[tarballURL] + if !ok { + return nil, fmt.Errorf("repo: fake has no tarball configured for %q", tarballURL) + } + return data, nil +} diff --git a/internal/repo/fake_test.go b/internal/repo/fake_test.go new file mode 100644 index 0000000..9c4751f --- /dev/null +++ b/internal/repo/fake_test.go @@ -0,0 +1,41 @@ +package repo + +import ( + "context" + "testing" +) + +func TestFakeResolvesConfiguredRelease(t *testing.T) { + f := NewFake("github") + want := &Release{Tag: "v1.2.0", TarballURL: "https://github.com/example/repo/archive/v1.2.0.tar.gz"} + f.Releases["https://github.com/example/repo"] = want + + got, err := f.LatestRelease(context.Background(), "https://github.com/example/repo") + if err != nil { + t.Fatalf("LatestRelease: unexpected error: %v", err) + } + if got != want { + t.Fatalf("LatestRelease: got %+v, want %+v", got, want) + } +} + +func TestFakeUnconfiguredRepoErrors(t *testing.T) { + f := NewFake("github") + if _, err := f.LatestRelease(context.Background(), "https://github.com/unknown/repo"); err == nil { + t.Fatal("LatestRelease: want error for unconfigured repo, got nil") + } +} + +func TestFakeFetchesConfiguredTarball(t *testing.T) { + f := NewFake("github") + want := []byte("tarball-bytes") + f.Tarballs["https://example.com/a.tar.gz"] = want + + got, err := f.FetchTarball(context.Background(), "https://example.com/a.tar.gz") + if err != nil { + t.Fatalf("FetchTarball: unexpected error: %v", err) + } + if string(got) != string(want) { + t.Fatalf("FetchTarball: got %q, want %q", got, want) + } +} diff --git a/internal/repo/repo.go b/internal/repo/repo.go new file mode 100644 index 0000000..9562bb3 --- /dev/null +++ b/internal/repo/repo.go @@ -0,0 +1,24 @@ +// Package repo defines the Forge seam for resolving a public repo's +// latest release and fetching its tarball. Real implementations (GitHub, +// GitLab; see docs/architecture/touchstone.md) land in slice S6; this +// package only fixes the interface so S0 callers and their tests can be +// written against it now. +package repo + +import "context" + +// Release is a resolved repo release: the tag used as evidence location +// (repo@:) and the tarball URL to fetch it from. +type Release struct { + Tag string + TarballURL string +} + +// Forge resolves a repo URL to its latest release and fetches release +// tarballs through the forge's API (never through fetch.Fetcher: forge +// calls don't count against the vendor's passive request budget). +type Forge interface { + Name() string + LatestRelease(ctx context.Context, repoURL string) (*Release, error) + FetchTarball(ctx context.Context, tarballURL string) ([]byte, error) +} diff --git a/internal/service/scan.go b/internal/service/scan.go new file mode 100644 index 0000000..6642ad5 --- /dev/null +++ b/internal/service/scan.go @@ -0,0 +1,99 @@ +// Package service implements touchstone.v1.ScanService. In slice S0 it is +// a stub: Scan returns a canned ScanReport so the CLI, the gateway and the +// web page can be built against the contract before the pipeline exists +// (see docs/architecture/touchstone.md, "Delivery slices"). +package service + +import ( + "context" + "net/url" + "time" + + touchstonev1 "github.com/FootprintAI/touchstone/gen/go/touchstone/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// ToolVersion is reported in every ScanReport. +const ToolVersion = "0.0.0-dev" + +// Disclaimer is reported in every ScanReport. +const Disclaimer = "Touchstone reports publicly observable evidence only. " + + "Findings are graded by confidence and are not proof that a vulnerability is exploitable." + +const defaultRequestBudget = 50 + +// Stub is a ScanService that returns a canned report for any valid URL +// without contacting it. +type Stub struct { + touchstonev1.UnimplementedScanServiceServer + + // Now returns the scan's start and completion time; time.Now if nil. + Now func() time.Time +} + +var _ touchstonev1.ScanServiceServer = (*Stub)(nil) + +// Scan validates req.Url and returns a canned report for it. +func (s *Stub) Scan(_ context.Context, req *touchstonev1.ScanRequest) (*touchstonev1.ScanReport, error) { + u, err := url.Parse(req.GetUrl()) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { + return nil, status.Errorf(codes.InvalidArgument, "url must be an absolute http(s) URL, got %q", req.GetUrl()) + } + now := time.Now + if s.Now != nil { + now = s.Now + } + budget := req.GetRequestBudget() + if budget == 0 { + budget = defaultRequestBudget + } + return CannedReport(u.String(), budget, now()), nil +} + +// CannedReport is the fixed report the stub returns: one Low-confidence +// fingerprinted component with one vulnerability. Nothing is fetched, so +// checked is empty and requests_used is 0; the component +// carries a CANNED_STUB_REPORT caveat so the output can't be mistaken +// for a real finding. +func CannedReport(targetURL string, budget uint32, at time.Time) *touchstonev1.ScanReport { + ts := timestamppb.New(at.UTC()) + epss := 0.0123 + cvss := 6.1 + return &touchstonev1.ScanReport{ + SchemaVersion: "1", + TargetUrl: targetURL, + ScanStatus: touchstonev1.ScanStatus_SCAN_STATUS_OK, + LookupStatus: touchstonev1.LookupStatus_LOOKUP_STATUS_COMPLETE, + StartedAt: ts, + CompletedAt: ts, + ToolVersion: ToolVersion, + VulnDataAsOf: ts, + Components: []*touchstonev1.Component{{ + Name: "jquery", + Version: "1.12.4", + Purl: "pkg:npm/jquery@1.12.4", + Confidence: touchstonev1.Confidence_CONFIDENCE_LOW, + Evidence: []*touchstonev1.Evidence{{ + Kind: touchstonev1.EvidenceKind_EVIDENCE_KIND_FINGERPRINT_ASSET, + Location: targetURL, + Detail: "jquery-1.12.4.min.js", + }}, + Caveats: []string{"CANNED_STUB_REPORT"}, + Vulnerabilities: []*touchstonev1.Vulnerability{{ + Id: "CVE-2020-11022", + Aliases: []string{"GHSA-gxr4-xjj5-5px2"}, + Summary: "Potential XSS vulnerability in jQuery.htmlPrefilter", + Epss: &epss, + CvssBase: &cvss, + VexStatus: touchstonev1.VexStatus_VEX_STATUS_UNSPECIFIED, + }}, + }}, + RequestBudget: budget, + Sources: []*touchstonev1.SourceStatus{ + {Source: "stub", Ok: true}, + }, + Disclaimer: Disclaimer, + } +} diff --git a/internal/vuln/fake.go b/internal/vuln/fake.go new file mode 100644 index 0000000..b43332f --- /dev/null +++ b/internal/vuln/fake.go @@ -0,0 +1,43 @@ +package vuln + +import "context" + +// FakeSource is an in-memory Source for tests: it returns a fixed set of +// findings (or a fixed error) regardless of the queries it's given. +type FakeSource struct { + SourceName string + Findings []Finding + Err error +} + +func (f *FakeSource) Name() string { return f.SourceName } + +func (f *FakeSource) Lookup(_ context.Context, _ []Query) ([]Finding, error) { + if f.Err != nil { + return nil, f.Err + } + return f.Findings, nil +} + +// FakeEnricher is an in-memory Enricher for tests: it returns canned +// enrichments keyed by finding id. +type FakeEnricher struct { + EnricherName string + Enrichments map[string]Enrichment + Err error +} + +func (f *FakeEnricher) Name() string { return f.EnricherName } + +func (f *FakeEnricher) Enrich(_ context.Context, findingIDs []string) (map[string]Enrichment, error) { + if f.Err != nil { + return nil, f.Err + } + out := make(map[string]Enrichment, len(findingIDs)) + for _, id := range findingIDs { + if e, ok := f.Enrichments[id]; ok { + out[id] = e + } + } + return out, nil +} diff --git a/internal/vuln/fake_test.go b/internal/vuln/fake_test.go new file mode 100644 index 0000000..a96fb7c --- /dev/null +++ b/internal/vuln/fake_test.go @@ -0,0 +1,53 @@ +package vuln + +import ( + "context" + "errors" + "testing" +) + +func TestFakeSourceReturnsConfiguredFindings(t *testing.T) { + want := []Finding{{ID: "CVE-2021-1234", Summary: "example"}} + s := &FakeSource{SourceName: "osv", Findings: want} + + if got := s.Name(); got != "osv" { + t.Fatalf("Name: got %q, want %q", got, "osv") + } + + got, err := s.Lookup(context.Background(), []Query{{Purl: "pkg:npm/example@1.0.0"}}) + if err != nil { + t.Fatalf("Lookup: unexpected error: %v", err) + } + if len(got) != 1 || got[0].ID != want[0].ID { + t.Fatalf("Lookup: got %+v, want %+v", got, want) + } +} + +func TestFakeSourceReturnsConfiguredError(t *testing.T) { + wantErr := errors.New("osv down") + s := &FakeSource{SourceName: "osv", Err: wantErr} + + if _, err := s.Lookup(context.Background(), nil); !errors.Is(err, wantErr) { + t.Fatalf("Lookup: got error %v, want %v", err, wantErr) + } +} + +func TestFakeEnricherReturnsOnlyKnownIDs(t *testing.T) { + e := &FakeEnricher{ + EnricherName: "kev", + Enrichments: map[string]Enrichment{ + "CVE-2021-1234": {KEV: true}, + }, + } + + got, err := e.Enrich(context.Background(), []string{"CVE-2021-1234", "CVE-9999-0000"}) + if err != nil { + t.Fatalf("Enrich: unexpected error: %v", err) + } + if len(got) != 1 { + t.Fatalf("Enrich: got %d entries, want 1", len(got)) + } + if !got["CVE-2021-1234"].KEV { + t.Fatalf("Enrich: CVE-2021-1234 KEV = false, want true") + } +} diff --git a/internal/vuln/vuln.go b/internal/vuln/vuln.go new file mode 100644 index 0000000..e693a33 --- /dev/null +++ b/internal/vuln/vuln.go @@ -0,0 +1,45 @@ +// Package vuln defines the Source and Enricher seams used to look up and +// enrich vulnerabilities for a component. Real implementations (OSV, NVD, +// KEV, EPSS; see docs/architecture/touchstone.md) land in slice S3; this +// package only fixes the interfaces so S0 callers and their tests can be +// written against them now. +package vuln + +import "context" + +// Query identifies a component to look up. At least one of Purl or CPE is +// set; Source implementations that only support one kind ignore queries +// missing it. +type Query struct { + Purl string + CPE string +} + +// Finding is one vulnerability returned by a Source, before enrichment. +type Finding struct { + ID string // CVE id when one exists, else the source's native id + Aliases []string + Summary string + CVSSBase *float64 +} + +// Source looks up vulnerabilities for a batch of components. Each +// implementation (OSV, NVD) reports its own name so pipeline can record a +// per-source SourceStatus. +type Source interface { + Name() string + Lookup(ctx context.Context, queries []Query) ([]Finding, error) +} + +// Enrichment is additional data an Enricher attaches to a Finding by id. +type Enrichment struct { + KEV bool + EPSS *float64 +} + +// Enricher adds data to findings already produced by a Source (KEV, EPSS). +// It never removes or invents findings. +type Enricher interface { + Name() string + Enrich(ctx context.Context, findingIDs []string) (map[string]Enrichment, error) +} diff --git a/openapi/touchstone_v1.swagger.json b/openapi/touchstone_v1.swagger.json new file mode 100644 index 0000000..792990d --- /dev/null +++ b/openapi/touchstone_v1.swagger.json @@ -0,0 +1,396 @@ +{ + "swagger": "2.0", + "info": { + "title": "touchstone/v1/scan.proto", + "version": "version not set" + }, + "tags": [ + { + "name": "ScanService" + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/v1/scans": { + "post": { + "operationId": "ScanService_Scan", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ScanReport" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1ScanRequest" + } + } + ], + "tags": [ + "ScanService" + ] + } + } + }, + "definitions": { + "CheckedLocationResult": { + "type": "string", + "enum": [ + "RESULT_UNSPECIFIED", + "RESULT_FOUND", + "RESULT_NOT_FOUND", + "RESULT_ERROR", + "RESULT_REFUSED" + ], + "default": "RESULT_UNSPECIFIED" + }, + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": true + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "v1CheckedLocation": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "reason": { + "type": "string", + "title": "TARGET | SUBRESOURCE | DISCOVERY_PATH | DECLARED | FORGE_API" + }, + "result": { + "$ref": "#/definitions/CheckedLocationResult" + }, + "error": { + "type": "string" + } + } + }, + "v1Component": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "purl": { + "type": "string" + }, + "cpe": { + "type": "string" + }, + "confidence": { + "$ref": "#/definitions/v1Confidence" + }, + "evidence": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1Evidence" + } + }, + "caveats": { + "type": "array", + "items": { + "type": "string" + }, + "title": "e.g. MAY_BE_BACKPORTED, DEFAULT_BRANCH_NOT_RELEASE" + }, + "vulnerabilities": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1Vulnerability" + }, + "title": "ranked" + } + } + }, + "v1Confidence": { + "type": "string", + "enum": [ + "CONFIDENCE_UNSPECIFIED", + "CONFIDENCE_LOW", + "CONFIDENCE_MEDIUM", + "CONFIDENCE_HIGH" + ], + "default": "CONFIDENCE_UNSPECIFIED" + }, + "v1Conflict": { + "type": "object", + "properties": { + "componentKey": { + "type": "string" + }, + "versions": { + "type": "array", + "items": { + "type": "string" + } + }, + "evidence": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1Evidence" + } + } + } + }, + "v1Evidence": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/definitions/v1EvidenceKind" + }, + "location": { + "type": "string", + "title": "URL, or repo@ref:path for lockfiles" + }, + "detail": { + "type": "string", + "title": "matched string or header value" + } + } + }, + "v1EvidenceKind": { + "type": "string", + "enum": [ + "EVIDENCE_KIND_UNSPECIFIED", + "EVIDENCE_KIND_SBOM_PUBLISHED", + "EVIDENCE_KIND_SBOM_SUPPLIED", + "EVIDENCE_KIND_VEX", + "EVIDENCE_KIND_REPO_LOCKFILE", + "EVIDENCE_KIND_FINGERPRINT_ASSET", + "EVIDENCE_KIND_FINGERPRINT_HEADER" + ], + "default": "EVIDENCE_KIND_UNSPECIFIED" + }, + "v1LookupStatus": { + "type": "string", + "enum": [ + "LOOKUP_STATUS_UNSPECIFIED", + "LOOKUP_STATUS_COMPLETE", + "LOOKUP_STATUS_PARTIAL", + "LOOKUP_STATUS_FAILED" + ], + "default": "LOOKUP_STATUS_UNSPECIFIED" + }, + "v1ScanReport": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "title": "\"1\"; bumped only with a new proto package" + }, + "targetUrl": { + "type": "string" + }, + "scanStatus": { + "$ref": "#/definitions/v1ScanStatus" + }, + "lookupStatus": { + "$ref": "#/definitions/v1LookupStatus" + }, + "startedAt": { + "type": "string", + "format": "date-time" + }, + "completedAt": { + "type": "string", + "format": "date-time" + }, + "toolVersion": { + "type": "string" + }, + "vulnDataAsOf": { + "type": "string", + "format": "date-time" + }, + "components": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1Component" + } + }, + "conflicts": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1Conflict" + } + }, + "checked": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1CheckedLocation" + } + }, + "requestsUsed": { + "type": "integer", + "format": "int64" + }, + "requestBudget": { + "type": "integer", + "format": "int64" + }, + "sources": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1SourceStatus" + } + }, + "disclaimer": { + "type": "string" + } + } + }, + "v1ScanRequest": { + "type": "object", + "properties": { + "url": { + "type": "string", + "title": "required, http(s)" + }, + "repoUrl": { + "type": "string", + "title": "optional, user-supplied repo (#3)" + }, + "sbomUrl": { + "type": "string", + "title": "optional, user-supplied SBOM/VEX (D1)" + }, + "sbomDocument": { + "type": "string", + "format": "byte", + "title": "optional, uploaded SBOM/VEX (D1)" + }, + "requestBudget": { + "type": "integer", + "format": "int64", + "title": "0 = default 50, max 200" + } + } + }, + "v1ScanStatus": { + "type": "string", + "enum": [ + "SCAN_STATUS_UNSPECIFIED", + "SCAN_STATUS_OK", + "SCAN_STATUS_TARGET_UNREACHABLE" + ], + "default": "SCAN_STATUS_UNSPECIFIED" + }, + "v1SourceStatus": { + "type": "object", + "properties": { + "source": { + "type": "string" + }, + "ok": { + "type": "boolean" + }, + "error": { + "type": "string" + } + } + }, + "v1VexStatus": { + "type": "string", + "enum": [ + "VEX_STATUS_UNSPECIFIED", + "VEX_STATUS_AFFECTED", + "VEX_STATUS_NOT_AFFECTED", + "VEX_STATUS_FIXED", + "VEX_STATUS_UNDER_INVESTIGATION" + ], + "default": "VEX_STATUS_UNSPECIFIED" + }, + "v1Vulnerability": { + "type": "object", + "properties": { + "id": { + "type": "string", + "title": "CVE id when one exists, else OSV id" + }, + "aliases": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "kev": { + "type": "boolean" + }, + "epss": { + "type": "number", + "format": "double", + "title": "absent = no EPSS data" + }, + "cvssBase": { + "type": "number", + "format": "double" + }, + "vexStatus": { + "$ref": "#/definitions/v1VexStatus" + }, + "vexJustification": { + "type": "string" + }, + "suppressed": { + "type": "boolean", + "title": "true for NOT_AFFECTED / FIXED" + } + } + } + } +} diff --git a/proto/touchstone/v1/scan.proto b/proto/touchstone/v1/scan.proto new file mode 100644 index 0000000..7e7dde1 --- /dev/null +++ b/proto/touchstone/v1/scan.proto @@ -0,0 +1,90 @@ +syntax = "proto3"; +package touchstone.v1; + +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/FootprintAI/touchstone/gen/go/touchstone/v1;touchstonev1"; + +service ScanService { + rpc Scan(ScanRequest) returns (ScanReport) { + option (google.api.http) = { post: "/v1/scans" body: "*" }; + } +} + +message ScanRequest { + string url = 1; // required, http(s) + string repo_url = 2; // optional, user-supplied repo (#3) + string sbom_url = 3; // optional, user-supplied SBOM/VEX (D1) + bytes sbom_document = 4; // optional, uploaded SBOM/VEX (D1) + uint32 request_budget = 5; // 0 = default 50, max 200 +} + +enum Confidence { CONFIDENCE_UNSPECIFIED = 0; CONFIDENCE_LOW = 1; CONFIDENCE_MEDIUM = 2; CONFIDENCE_HIGH = 3; } +enum EvidenceKind { + EVIDENCE_KIND_UNSPECIFIED = 0; EVIDENCE_KIND_SBOM_PUBLISHED = 1; EVIDENCE_KIND_SBOM_SUPPLIED = 2; + EVIDENCE_KIND_VEX = 3; EVIDENCE_KIND_REPO_LOCKFILE = 4; EVIDENCE_KIND_FINGERPRINT_ASSET = 5; + EVIDENCE_KIND_FINGERPRINT_HEADER = 6; +} +enum VexStatus { VEX_STATUS_UNSPECIFIED = 0; VEX_STATUS_AFFECTED = 1; VEX_STATUS_NOT_AFFECTED = 2; VEX_STATUS_FIXED = 3; VEX_STATUS_UNDER_INVESTIGATION = 4; } +enum ScanStatus { SCAN_STATUS_UNSPECIFIED = 0; SCAN_STATUS_OK = 1; SCAN_STATUS_TARGET_UNREACHABLE = 2; } +enum LookupStatus { LOOKUP_STATUS_UNSPECIFIED = 0; LOOKUP_STATUS_COMPLETE = 1; LOOKUP_STATUS_PARTIAL = 2; LOOKUP_STATUS_FAILED = 3; } + +message Evidence { + EvidenceKind kind = 1; + string location = 2; // URL, or repo@ref:path for lockfiles + string detail = 3; // matched string or header value +} + +message Vulnerability { + string id = 1; // CVE id when one exists, else OSV id + repeated string aliases = 2; + string summary = 3; + bool kev = 4; + optional double epss = 5; // absent = no EPSS data + optional double cvss_base = 6; + VexStatus vex_status = 7; + string vex_justification = 8; + bool suppressed = 9; // true for NOT_AFFECTED / FIXED +} + +message Component { + string name = 1; + string version = 2; + string purl = 3; + string cpe = 4; + Confidence confidence = 5; + repeated Evidence evidence = 6; + repeated string caveats = 7; // e.g. MAY_BE_BACKPORTED, DEFAULT_BRANCH_NOT_RELEASE + repeated Vulnerability vulnerabilities = 8; // ranked +} + +message Conflict { string component_key = 1; repeated string versions = 2; repeated Evidence evidence = 3; } + +message CheckedLocation { + string url = 1; + string reason = 2; // TARGET | SUBRESOURCE | DISCOVERY_PATH | DECLARED | FORGE_API + enum Result { RESULT_UNSPECIFIED = 0; RESULT_FOUND = 1; RESULT_NOT_FOUND = 2; RESULT_ERROR = 3; RESULT_REFUSED = 4; } + Result result = 3; + string error = 4; +} + +message SourceStatus { string source = 1; bool ok = 2; string error = 3; } + +message ScanReport { + string schema_version = 1; // "1"; bumped only with a new proto package + string target_url = 2; + ScanStatus scan_status = 3; + LookupStatus lookup_status = 4; + google.protobuf.Timestamp started_at = 5; + google.protobuf.Timestamp completed_at = 6; + string tool_version = 7; + google.protobuf.Timestamp vuln_data_as_of = 8; + repeated Component components = 9; + repeated Conflict conflicts = 10; + repeated CheckedLocation checked = 11; + uint32 requests_used = 12; + uint32 request_budget = 13; + repeated SourceStatus sources = 14; + string disclaimer = 15; +} diff --git a/schema/scan-report.v1.json b/schema/scan-report.v1.json new file mode 100644 index 0000000..27c1b11 --- /dev/null +++ b/schema/scan-report.v1.json @@ -0,0 +1,582 @@ +{ + "$defs": { + "google.protobuf.Timestamp.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "touchstone.v1.CheckedLocation.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "error": { + "default": "", + "type": "string" + }, + "reason": { + "default": "", + "type": "string" + }, + "result": { + "anyOf": [ + { + "pattern": "^RESULT_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "RESULT_FOUND", + "RESULT_NOT_FOUND", + "RESULT_ERROR", + "RESULT_REFUSED" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Result" + }, + "url": { + "default": "", + "type": "string" + } + }, + "title": "Checked Location", + "type": "object" + }, + "touchstone.v1.Component.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "caveats": { + "items": { + "type": "string" + }, + "type": "array" + }, + "confidence": { + "anyOf": [ + { + "pattern": "^CONFIDENCE_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "CONFIDENCE_LOW", + "CONFIDENCE_MEDIUM", + "CONFIDENCE_HIGH" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Confidence" + }, + "cpe": { + "default": "", + "type": "string" + }, + "evidence": { + "items": { + "$ref": "#/$defs/touchstone.v1.Evidence.jsonschema.json" + }, + "type": "array" + }, + "name": { + "default": "", + "type": "string" + }, + "purl": { + "default": "", + "type": "string" + }, + "version": { + "default": "", + "type": "string" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/touchstone.v1.Vulnerability.jsonschema.json" + }, + "type": "array" + } + }, + "title": "Component", + "type": "object" + }, + "touchstone.v1.Conflict.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "patternProperties": { + "^(component_key)$": { + "default": "", + "type": "string" + } + }, + "properties": { + "componentKey": { + "default": "", + "type": "string" + }, + "evidence": { + "items": { + "$ref": "#/$defs/touchstone.v1.Evidence.jsonschema.json" + }, + "type": "array" + }, + "versions": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "title": "Conflict", + "type": "object" + }, + "touchstone.v1.Evidence.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "detail": { + "default": "", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "pattern": "^EVIDENCE_KIND_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "EVIDENCE_KIND_SBOM_PUBLISHED", + "EVIDENCE_KIND_SBOM_SUPPLIED", + "EVIDENCE_KIND_VEX", + "EVIDENCE_KIND_REPO_LOCKFILE", + "EVIDENCE_KIND_FINGERPRINT_ASSET", + "EVIDENCE_KIND_FINGERPRINT_HEADER" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Evidence Kind" + }, + "location": { + "default": "", + "type": "string" + } + }, + "title": "Evidence", + "type": "object" + }, + "touchstone.v1.ScanReport.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "patternProperties": { + "^(completed_at)$": { + "$ref": "#/$defs/google.protobuf.Timestamp.jsonschema.json" + }, + "^(lookup_status)$": { + "anyOf": [ + { + "pattern": "^LOOKUP_STATUS_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "LOOKUP_STATUS_COMPLETE", + "LOOKUP_STATUS_PARTIAL", + "LOOKUP_STATUS_FAILED" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Lookup Status" + }, + "^(request_budget)$": { + "anyOf": [ + { + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + { + "pattern": "^[0-9]+$", + "type": "string" + } + ], + "default": 0 + }, + "^(requests_used)$": { + "anyOf": [ + { + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + { + "pattern": "^[0-9]+$", + "type": "string" + } + ], + "default": 0 + }, + "^(scan_status)$": { + "anyOf": [ + { + "pattern": "^SCAN_STATUS_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "SCAN_STATUS_OK", + "SCAN_STATUS_TARGET_UNREACHABLE" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Scan Status" + }, + "^(schema_version)$": { + "default": "", + "type": "string" + }, + "^(started_at)$": { + "$ref": "#/$defs/google.protobuf.Timestamp.jsonschema.json" + }, + "^(target_url)$": { + "default": "", + "type": "string" + }, + "^(tool_version)$": { + "default": "", + "type": "string" + }, + "^(vuln_data_as_of)$": { + "$ref": "#/$defs/google.protobuf.Timestamp.jsonschema.json" + } + }, + "properties": { + "checked": { + "items": { + "$ref": "#/$defs/touchstone.v1.CheckedLocation.jsonschema.json" + }, + "type": "array" + }, + "completedAt": { + "$ref": "#/$defs/google.protobuf.Timestamp.jsonschema.json" + }, + "components": { + "items": { + "$ref": "#/$defs/touchstone.v1.Component.jsonschema.json" + }, + "type": "array" + }, + "conflicts": { + "items": { + "$ref": "#/$defs/touchstone.v1.Conflict.jsonschema.json" + }, + "type": "array" + }, + "disclaimer": { + "default": "", + "type": "string" + }, + "lookupStatus": { + "anyOf": [ + { + "pattern": "^LOOKUP_STATUS_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "LOOKUP_STATUS_COMPLETE", + "LOOKUP_STATUS_PARTIAL", + "LOOKUP_STATUS_FAILED" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Lookup Status" + }, + "requestBudget": { + "anyOf": [ + { + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + { + "pattern": "^[0-9]+$", + "type": "string" + } + ], + "default": 0 + }, + "requestsUsed": { + "anyOf": [ + { + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + { + "pattern": "^[0-9]+$", + "type": "string" + } + ], + "default": 0 + }, + "scanStatus": { + "anyOf": [ + { + "pattern": "^SCAN_STATUS_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "SCAN_STATUS_OK", + "SCAN_STATUS_TARGET_UNREACHABLE" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Scan Status" + }, + "schemaVersion": { + "default": "", + "type": "string" + }, + "sources": { + "items": { + "$ref": "#/$defs/touchstone.v1.SourceStatus.jsonschema.json" + }, + "type": "array" + }, + "startedAt": { + "$ref": "#/$defs/google.protobuf.Timestamp.jsonschema.json" + }, + "targetUrl": { + "default": "", + "type": "string" + }, + "toolVersion": { + "default": "", + "type": "string" + }, + "vulnDataAsOf": { + "$ref": "#/$defs/google.protobuf.Timestamp.jsonschema.json" + } + }, + "title": "Scan Report", + "type": "object" + }, + "touchstone.v1.SourceStatus.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "error": { + "default": "", + "type": "string" + }, + "ok": { + "default": false, + "type": "boolean" + }, + "source": { + "default": "", + "type": "string" + } + }, + "title": "Source Status", + "type": "object" + }, + "touchstone.v1.Vulnerability.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "patternProperties": { + "^(cvss_base)$": { + "anyOf": [ + { + "type": "number" + }, + { + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ], + "type": "string" + }, + { + "type": "string" + } + ] + }, + "^(vex_justification)$": { + "default": "", + "type": "string" + }, + "^(vex_status)$": { + "anyOf": [ + { + "pattern": "^VEX_STATUS_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "VEX_STATUS_AFFECTED", + "VEX_STATUS_NOT_AFFECTED", + "VEX_STATUS_FIXED", + "VEX_STATUS_UNDER_INVESTIGATION" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Vex Status" + } + }, + "properties": { + "aliases": { + "items": { + "type": "string" + }, + "type": "array" + }, + "cvssBase": { + "anyOf": [ + { + "type": "number" + }, + { + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ], + "type": "string" + }, + { + "type": "string" + } + ] + }, + "epss": { + "anyOf": [ + { + "type": "number" + }, + { + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ], + "type": "string" + }, + { + "type": "string" + } + ] + }, + "id": { + "default": "", + "type": "string" + }, + "kev": { + "default": false, + "type": "boolean" + }, + "summary": { + "default": "", + "type": "string" + }, + "suppressed": { + "default": false, + "type": "boolean" + }, + "vexJustification": { + "default": "", + "type": "string" + }, + "vexStatus": { + "anyOf": [ + { + "pattern": "^VEX_STATUS_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "VEX_STATUS_AFFECTED", + "VEX_STATUS_NOT_AFFECTED", + "VEX_STATUS_FIXED", + "VEX_STATUS_UNDER_INVESTIGATION" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Vex Status" + } + }, + "title": "Vulnerability", + "type": "object" + } + }, + "$id": "touchstone.v1.ScanReport.jsonschema.bundle.json", + "$ref": "#/$defs/touchstone.v1.ScanReport.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/schema/touchstone.v1.CheckedLocation.jsonschema.bundle.json b/schema/touchstone.v1.CheckedLocation.jsonschema.bundle.json new file mode 100644 index 0000000..a1540a5 --- /dev/null +++ b/schema/touchstone.v1.CheckedLocation.jsonschema.bundle.json @@ -0,0 +1,51 @@ +{ + "$defs": { + "touchstone.v1.CheckedLocation.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "error": { + "default": "", + "type": "string" + }, + "reason": { + "default": "", + "type": "string" + }, + "result": { + "anyOf": [ + { + "pattern": "^RESULT_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "RESULT_FOUND", + "RESULT_NOT_FOUND", + "RESULT_ERROR", + "RESULT_REFUSED" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Result" + }, + "url": { + "default": "", + "type": "string" + } + }, + "title": "Checked Location", + "type": "object" + } + }, + "$id": "touchstone.v1.CheckedLocation.jsonschema.bundle.json", + "$ref": "#/$defs/touchstone.v1.CheckedLocation.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/schema/touchstone.v1.Component.jsonschema.bundle.json b/schema/touchstone.v1.Component.jsonschema.bundle.json new file mode 100644 index 0000000..c29b4cc --- /dev/null +++ b/schema/touchstone.v1.Component.jsonschema.bundle.json @@ -0,0 +1,256 @@ +{ + "$defs": { + "touchstone.v1.Component.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "caveats": { + "items": { + "type": "string" + }, + "type": "array" + }, + "confidence": { + "anyOf": [ + { + "pattern": "^CONFIDENCE_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "CONFIDENCE_LOW", + "CONFIDENCE_MEDIUM", + "CONFIDENCE_HIGH" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Confidence" + }, + "cpe": { + "default": "", + "type": "string" + }, + "evidence": { + "items": { + "$ref": "#/$defs/touchstone.v1.Evidence.jsonschema.json" + }, + "type": "array" + }, + "name": { + "default": "", + "type": "string" + }, + "purl": { + "default": "", + "type": "string" + }, + "version": { + "default": "", + "type": "string" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/touchstone.v1.Vulnerability.jsonschema.json" + }, + "type": "array" + } + }, + "title": "Component", + "type": "object" + }, + "touchstone.v1.Evidence.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "detail": { + "default": "", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "pattern": "^EVIDENCE_KIND_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "EVIDENCE_KIND_SBOM_PUBLISHED", + "EVIDENCE_KIND_SBOM_SUPPLIED", + "EVIDENCE_KIND_VEX", + "EVIDENCE_KIND_REPO_LOCKFILE", + "EVIDENCE_KIND_FINGERPRINT_ASSET", + "EVIDENCE_KIND_FINGERPRINT_HEADER" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Evidence Kind" + }, + "location": { + "default": "", + "type": "string" + } + }, + "title": "Evidence", + "type": "object" + }, + "touchstone.v1.Vulnerability.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "patternProperties": { + "^(cvss_base)$": { + "anyOf": [ + { + "type": "number" + }, + { + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ], + "type": "string" + }, + { + "type": "string" + } + ] + }, + "^(vex_justification)$": { + "default": "", + "type": "string" + }, + "^(vex_status)$": { + "anyOf": [ + { + "pattern": "^VEX_STATUS_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "VEX_STATUS_AFFECTED", + "VEX_STATUS_NOT_AFFECTED", + "VEX_STATUS_FIXED", + "VEX_STATUS_UNDER_INVESTIGATION" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Vex Status" + } + }, + "properties": { + "aliases": { + "items": { + "type": "string" + }, + "type": "array" + }, + "cvssBase": { + "anyOf": [ + { + "type": "number" + }, + { + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ], + "type": "string" + }, + { + "type": "string" + } + ] + }, + "epss": { + "anyOf": [ + { + "type": "number" + }, + { + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ], + "type": "string" + }, + { + "type": "string" + } + ] + }, + "id": { + "default": "", + "type": "string" + }, + "kev": { + "default": false, + "type": "boolean" + }, + "summary": { + "default": "", + "type": "string" + }, + "suppressed": { + "default": false, + "type": "boolean" + }, + "vexJustification": { + "default": "", + "type": "string" + }, + "vexStatus": { + "anyOf": [ + { + "pattern": "^VEX_STATUS_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "VEX_STATUS_AFFECTED", + "VEX_STATUS_NOT_AFFECTED", + "VEX_STATUS_FIXED", + "VEX_STATUS_UNDER_INVESTIGATION" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Vex Status" + } + }, + "title": "Vulnerability", + "type": "object" + } + }, + "$id": "touchstone.v1.Component.jsonschema.bundle.json", + "$ref": "#/$defs/touchstone.v1.Component.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/schema/touchstone.v1.Conflict.jsonschema.bundle.json b/schema/touchstone.v1.Conflict.jsonschema.bundle.json new file mode 100644 index 0000000..ec0f81d --- /dev/null +++ b/schema/touchstone.v1.Conflict.jsonschema.bundle.json @@ -0,0 +1,79 @@ +{ + "$defs": { + "touchstone.v1.Conflict.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "patternProperties": { + "^(component_key)$": { + "default": "", + "type": "string" + } + }, + "properties": { + "componentKey": { + "default": "", + "type": "string" + }, + "evidence": { + "items": { + "$ref": "#/$defs/touchstone.v1.Evidence.jsonschema.json" + }, + "type": "array" + }, + "versions": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "title": "Conflict", + "type": "object" + }, + "touchstone.v1.Evidence.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "detail": { + "default": "", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "pattern": "^EVIDENCE_KIND_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "EVIDENCE_KIND_SBOM_PUBLISHED", + "EVIDENCE_KIND_SBOM_SUPPLIED", + "EVIDENCE_KIND_VEX", + "EVIDENCE_KIND_REPO_LOCKFILE", + "EVIDENCE_KIND_FINGERPRINT_ASSET", + "EVIDENCE_KIND_FINGERPRINT_HEADER" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Evidence Kind" + }, + "location": { + "default": "", + "type": "string" + } + }, + "title": "Evidence", + "type": "object" + } + }, + "$id": "touchstone.v1.Conflict.jsonschema.bundle.json", + "$ref": "#/$defs/touchstone.v1.Conflict.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/schema/touchstone.v1.Evidence.jsonschema.bundle.json b/schema/touchstone.v1.Evidence.jsonschema.bundle.json new file mode 100644 index 0000000..816f952 --- /dev/null +++ b/schema/touchstone.v1.Evidence.jsonschema.bundle.json @@ -0,0 +1,49 @@ +{ + "$defs": { + "touchstone.v1.Evidence.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "detail": { + "default": "", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "pattern": "^EVIDENCE_KIND_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "EVIDENCE_KIND_SBOM_PUBLISHED", + "EVIDENCE_KIND_SBOM_SUPPLIED", + "EVIDENCE_KIND_VEX", + "EVIDENCE_KIND_REPO_LOCKFILE", + "EVIDENCE_KIND_FINGERPRINT_ASSET", + "EVIDENCE_KIND_FINGERPRINT_HEADER" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Evidence Kind" + }, + "location": { + "default": "", + "type": "string" + } + }, + "title": "Evidence", + "type": "object" + } + }, + "$id": "touchstone.v1.Evidence.jsonschema.bundle.json", + "$ref": "#/$defs/touchstone.v1.Evidence.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/schema/touchstone.v1.SourceStatus.jsonschema.bundle.json b/schema/touchstone.v1.SourceStatus.jsonschema.bundle.json new file mode 100644 index 0000000..675967d --- /dev/null +++ b/schema/touchstone.v1.SourceStatus.jsonschema.bundle.json @@ -0,0 +1,27 @@ +{ + "$defs": { + "touchstone.v1.SourceStatus.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "error": { + "default": "", + "type": "string" + }, + "ok": { + "default": false, + "type": "boolean" + }, + "source": { + "default": "", + "type": "string" + } + }, + "title": "Source Status", + "type": "object" + } + }, + "$id": "touchstone.v1.SourceStatus.jsonschema.bundle.json", + "$ref": "#/$defs/touchstone.v1.SourceStatus.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/schema/touchstone.v1.Vulnerability.jsonschema.bundle.json b/schema/touchstone.v1.Vulnerability.jsonschema.bundle.json new file mode 100644 index 0000000..3465cd3 --- /dev/null +++ b/schema/touchstone.v1.Vulnerability.jsonschema.bundle.json @@ -0,0 +1,149 @@ +{ + "$defs": { + "touchstone.v1.Vulnerability.jsonschema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "patternProperties": { + "^(cvss_base)$": { + "anyOf": [ + { + "type": "number" + }, + { + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ], + "type": "string" + }, + { + "type": "string" + } + ] + }, + "^(vex_justification)$": { + "default": "", + "type": "string" + }, + "^(vex_status)$": { + "anyOf": [ + { + "pattern": "^VEX_STATUS_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "VEX_STATUS_AFFECTED", + "VEX_STATUS_NOT_AFFECTED", + "VEX_STATUS_FIXED", + "VEX_STATUS_UNDER_INVESTIGATION" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Vex Status" + } + }, + "properties": { + "aliases": { + "items": { + "type": "string" + }, + "type": "array" + }, + "cvssBase": { + "anyOf": [ + { + "type": "number" + }, + { + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ], + "type": "string" + }, + { + "type": "string" + } + ] + }, + "epss": { + "anyOf": [ + { + "type": "number" + }, + { + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ], + "type": "string" + }, + { + "type": "string" + } + ] + }, + "id": { + "default": "", + "type": "string" + }, + "kev": { + "default": false, + "type": "boolean" + }, + "summary": { + "default": "", + "type": "string" + }, + "suppressed": { + "default": false, + "type": "boolean" + }, + "vexJustification": { + "default": "", + "type": "string" + }, + "vexStatus": { + "anyOf": [ + { + "pattern": "^VEX_STATUS_UNSPECIFIED$", + "type": "string" + }, + { + "enum": [ + "VEX_STATUS_AFFECTED", + "VEX_STATUS_NOT_AFFECTED", + "VEX_STATUS_FIXED", + "VEX_STATUS_UNDER_INVESTIGATION" + ], + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "default": 0, + "title": "Vex Status" + } + }, + "title": "Vulnerability", + "type": "object" + } + }, + "$id": "touchstone.v1.Vulnerability.jsonschema.bundle.json", + "$ref": "#/$defs/touchstone.v1.Vulnerability.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/testdata/report.golden.json b/testdata/report.golden.json new file mode 100644 index 0000000..b018e5a --- /dev/null +++ b/testdata/report.golden.json @@ -0,0 +1,47 @@ +{ + "schemaVersion": "1", + "targetUrl": "https://example.com", + "scanStatus": "SCAN_STATUS_OK", + "lookupStatus": "LOOKUP_STATUS_COMPLETE", + "startedAt": "2026-10-22T00:00:00Z", + "completedAt": "2026-10-22T00:00:00Z", + "toolVersion": "0.0.0-dev", + "vulnDataAsOf": "2026-10-22T00:00:00Z", + "components": [ + { + "name": "jquery", + "version": "1.12.4", + "purl": "pkg:npm/jquery@1.12.4", + "confidence": "CONFIDENCE_LOW", + "evidence": [ + { + "kind": "EVIDENCE_KIND_FINGERPRINT_ASSET", + "location": "https://example.com", + "detail": "jquery-1.12.4.min.js" + } + ], + "caveats": [ + "CANNED_STUB_REPORT" + ], + "vulnerabilities": [ + { + "id": "CVE-2020-11022", + "aliases": [ + "GHSA-gxr4-xjj5-5px2" + ], + "summary": "Potential XSS vulnerability in jQuery.htmlPrefilter", + "epss": 0.0123, + "cvssBase": 6.1 + } + ] + } + ], + "requestBudget": 50, + "sources": [ + { + "source": "stub", + "ok": true + } + ], + "disclaimer": "Touchstone reports publicly observable evidence only. Findings are graded by confidence and are not proof that a vulnerability is exploitable." +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..65aba8c --- /dev/null +++ b/web/package.json @@ -0,0 +1,17 @@ +{ + "name": "touchstone-web", + "private": true, + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@bufbuild/protobuf": "2.16.0" + }, + "devDependencies": { + "@types/node": "24.19.2", + "typescript": "5.9.3", + "vitest": "3.2.7" + } +} diff --git a/web/src/gen/google/api/annotations_pb.ts b/web/src/gen/google/api/annotations_pb.ts new file mode 100644 index 0000000..07177e7 --- /dev/null +++ b/web/src/gen/google/api/annotations_pb.ts @@ -0,0 +1,39 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @generated by protoc-gen-es v2.16.0 with parameter "target=ts" +// @generated from file google/api/annotations.proto (package google.api, syntax proto3) +/* eslint-disable */ + +import type { GenExtension, GenFile } from "@bufbuild/protobuf/codegenv2"; +import { extDesc, fileDesc } from "@bufbuild/protobuf/codegenv2"; +import type { HttpRule } from "./http_pb"; +import { file_google_api_http } from "./http_pb"; +import type { MethodOptions } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_descriptor } from "@bufbuild/protobuf/wkt"; + +/** + * Describes the file google/api/annotations.proto. + */ +export const file_google_api_annotations: GenFile = /*@__PURE__*/ + fileDesc("Chxnb29nbGUvYXBpL2Fubm90YXRpb25zLnByb3RvEgpnb29nbGUuYXBpOksKBGh0dHASHi5nb29nbGUucHJvdG9idWYuTWV0aG9kT3B0aW9ucxiwyrwiIAEoCzIULmdvb2dsZS5hcGkuSHR0cFJ1bGVSBGh0dHBCbgoOY29tLmdvb2dsZS5hcGlCEEFubm90YXRpb25zUHJvdG9QAVpBZ29vZ2xlLmdvbGFuZy5vcmcvZ2VucHJvdG8vZ29vZ2xlYXBpcy9hcGkvYW5ub3RhdGlvbnM7YW5ub3RhdGlvbnOiAgRHQVBJYgZwcm90bzM", [file_google_api_http, file_google_protobuf_descriptor]); + +/** + * See `HttpRule`. + * + * @generated from extension: google.api.HttpRule http = 72295728; + */ +export const http: GenExtension = /*@__PURE__*/ + extDesc(file_google_api_annotations, 0); + diff --git a/web/src/gen/google/api/http_pb.ts b/web/src/gen/google/api/http_pb.ts new file mode 100644 index 0000000..7cf5c18 --- /dev/null +++ b/web/src/gen/google/api/http_pb.ts @@ -0,0 +1,474 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @generated by protoc-gen-es v2.16.0 with parameter "target=ts" +// @generated from file google/api/http.proto (package google.api, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file google/api/http.proto. + */ +export const file_google_api_http: GenFile = /*@__PURE__*/ + fileDesc("ChVnb29nbGUvYXBpL2h0dHAucHJvdG8SCmdvb2dsZS5hcGkiVAoESHR0cBIjCgVydWxlcxgBIAMoCzIULmdvb2dsZS5hcGkuSHR0cFJ1bGUSJwofZnVsbHlfZGVjb2RlX3Jlc2VydmVkX2V4cGFuc2lvbhgCIAEoCCKBAgoISHR0cFJ1bGUSEAoIc2VsZWN0b3IYASABKAkSDQoDZ2V0GAIgASgJSAASDQoDcHV0GAMgASgJSAASDgoEcG9zdBgEIAEoCUgAEhAKBmRlbGV0ZRgFIAEoCUgAEg8KBXBhdGNoGAYgASgJSAASLwoGY3VzdG9tGAggASgLMh0uZ29vZ2xlLmFwaS5DdXN0b21IdHRwUGF0dGVybkgAEgwKBGJvZHkYByABKAkSFQoNcmVzcG9uc2VfYm9keRgMIAEoCRIxChNhZGRpdGlvbmFsX2JpbmRpbmdzGAsgAygLMhQuZ29vZ2xlLmFwaS5IdHRwUnVsZUIJCgdwYXR0ZXJuIi8KEUN1c3RvbUh0dHBQYXR0ZXJuEgwKBGtpbmQYASABKAkSDAoEcGF0aBgCIAEoCUJnCg5jb20uZ29vZ2xlLmFwaUIJSHR0cFByb3RvUAFaQWdvb2dsZS5nb2xhbmcub3JnL2dlbnByb3RvL2dvb2dsZWFwaXMvYXBpL2Fubm90YXRpb25zO2Fubm90YXRpb25zogIER0FQSWIGcHJvdG8z"); + +/** + * Defines the HTTP configuration for an API service. It contains a list of + * [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method + * to one or more HTTP REST API methods. + * + * @generated from message google.api.Http + */ +export type Http = Message<"google.api.Http"> & { + /** + * A list of HTTP configuration rules that apply to individual API methods. + * + * **NOTE:** All service configuration rules follow "last one wins" order. + * + * @generated from field: repeated google.api.HttpRule rules = 1; + */ + rules: HttpRule[]; + + /** + * When set to true, URL path parameters will be fully URI-decoded except in + * cases of single segment matches in reserved expansion, where "%2F" will be + * left encoded. + * + * The default behavior is to not decode RFC 6570 reserved characters in multi + * segment matches. + * + * @generated from field: bool fully_decode_reserved_expansion = 2; + */ + fullyDecodeReservedExpansion: boolean; +}; + +/** + * Describes the message google.api.Http. + * Use `create(HttpSchema)` to create a new message. + */ +export const HttpSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_google_api_http, 0); + +/** + * gRPC Transcoding + * + * gRPC Transcoding is a feature for mapping between a gRPC method and one or + * more HTTP REST endpoints. It allows developers to build a single API service + * that supports both gRPC APIs and REST APIs. Many systems, including [Google + * APIs](https://github.com/googleapis/googleapis), + * [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC + * Gateway](https://github.com/grpc-ecosystem/grpc-gateway), + * and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature + * and use it for large scale production services. + * + * `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies + * how different portions of the gRPC request message are mapped to the URL + * path, URL query parameters, and HTTP request body. It also controls how the + * gRPC response message is mapped to the HTTP response body. `HttpRule` is + * typically specified as an `google.api.http` annotation on the gRPC method. + * + * Each mapping specifies a URL path template and an HTTP method. The path + * template may refer to one or more fields in the gRPC request message, as long + * as each field is a non-repeated field with a primitive (non-message) type. + * The path template controls how fields of the request message are mapped to + * the URL path. + * + * Example: + * + * service Messaging { + * rpc GetMessage(GetMessageRequest) returns (Message) { + * option (google.api.http) = { + * get: "/v1/{name=messages/*}" + * }; + * } + * } + * message GetMessageRequest { + * string name = 1; // Mapped to URL path. + * } + * message Message { + * string text = 1; // The resource content. + * } + * + * This enables an HTTP REST to gRPC mapping as below: + * + * - HTTP: `GET /v1/messages/123456` + * - gRPC: `GetMessage(name: "messages/123456")` + * + * Any fields in the request message which are not bound by the path template + * automatically become HTTP query parameters if there is no HTTP request body. + * For example: + * + * service Messaging { + * rpc GetMessage(GetMessageRequest) returns (Message) { + * option (google.api.http) = { + * get:"/v1/messages/{message_id}" + * }; + * } + * } + * message GetMessageRequest { + * message SubMessage { + * string subfield = 1; + * } + * string message_id = 1; // Mapped to URL path. + * int64 revision = 2; // Mapped to URL query parameter `revision`. + * SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. + * } + * + * This enables a HTTP JSON to RPC mapping as below: + * + * - HTTP: `GET /v1/messages/123456?revision=2&sub.subfield=foo` + * - gRPC: `GetMessage(message_id: "123456" revision: 2 sub: + * SubMessage(subfield: "foo"))` + * + * Note that fields which are mapped to URL query parameters must have a + * primitive type or a repeated primitive type or a non-repeated message type. + * In the case of a repeated type, the parameter can be repeated in the URL + * as `...?param=A¶m=B`. In the case of a message type, each field of the + * message is mapped to a separate parameter, such as + * `...?foo.a=A&foo.b=B&foo.c=C`. + * + * For HTTP methods that allow a request body, the `body` field + * specifies the mapping. Consider a REST update method on the + * message resource collection: + * + * service Messaging { + * rpc UpdateMessage(UpdateMessageRequest) returns (Message) { + * option (google.api.http) = { + * patch: "/v1/messages/{message_id}" + * body: "message" + * }; + * } + * } + * message UpdateMessageRequest { + * string message_id = 1; // mapped to the URL + * Message message = 2; // mapped to the body + * } + * + * The following HTTP JSON to RPC mapping is enabled, where the + * representation of the JSON in the request body is determined by + * protos JSON encoding: + * + * - HTTP: `PATCH /v1/messages/123456 { "text": "Hi!" }` + * - gRPC: `UpdateMessage(message_id: "123456" message { text: "Hi!" })` + * + * The special name `*` can be used in the body mapping to define that + * every field not bound by the path template should be mapped to the + * request body. This enables the following alternative definition of + * the update method: + * + * service Messaging { + * rpc UpdateMessage(Message) returns (Message) { + * option (google.api.http) = { + * patch: "/v1/messages/{message_id}" + * body: "*" + * }; + * } + * } + * message Message { + * string message_id = 1; + * string text = 2; + * } + * + * + * The following HTTP JSON to RPC mapping is enabled: + * + * - HTTP: `PATCH /v1/messages/123456 { "text": "Hi!" }` + * - gRPC: `UpdateMessage(message_id: "123456" text: "Hi!")` + * + * Note that when using `*` in the body mapping, it is not possible to + * have HTTP parameters, as all fields not bound by the path end in + * the body. This makes this option more rarely used in practice when + * defining REST APIs. The common usage of `*` is in custom methods + * which don't use the URL at all for transferring data. + * + * It is possible to define multiple HTTP methods for one RPC by using + * the `additional_bindings` option. Example: + * + * service Messaging { + * rpc GetMessage(GetMessageRequest) returns (Message) { + * option (google.api.http) = { + * get: "/v1/messages/{message_id}" + * additional_bindings { + * get: "/v1/users/{user_id}/messages/{message_id}" + * } + * }; + * } + * } + * message GetMessageRequest { + * string message_id = 1; + * string user_id = 2; + * } + * + * This enables the following two alternative HTTP JSON to RPC mappings: + * + * - HTTP: `GET /v1/messages/123456` + * - gRPC: `GetMessage(message_id: "123456")` + * + * - HTTP: `GET /v1/users/me/messages/123456` + * - gRPC: `GetMessage(user_id: "me" message_id: "123456")` + * + * Rules for HTTP mapping + * + * 1. Leaf request fields (recursive expansion nested messages in the request + * message) are classified into three categories: + * - Fields referred by the path template. They are passed via the URL path. + * - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They + * are passed via the HTTP + * request body. + * - All other fields are passed via the URL query parameters, and the + * parameter name is the field path in the request message. A repeated + * field can be represented as multiple query parameters under the same + * name. + * 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL + * query parameter, all fields + * are passed via URL path and HTTP request body. + * 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP + * request body, all + * fields are passed via URL path and URL query parameters. + * + * Path template syntax + * + * Template = "/" Segments [ Verb ] ; + * Segments = Segment { "/" Segment } ; + * Segment = "*" | "**" | LITERAL | Variable ; + * Variable = "{" FieldPath [ "=" Segments ] "}" ; + * FieldPath = IDENT { "." IDENT } ; + * Verb = ":" LITERAL ; + * + * The syntax `*` matches a single URL path segment. The syntax `**` matches + * zero or more URL path segments, which must be the last part of the URL path + * except the `Verb`. + * + * The syntax `Variable` matches part of the URL path as specified by its + * template. A variable template must not contain other variables. If a variable + * matches a single path segment, its template may be omitted, e.g. `{var}` + * is equivalent to `{var=*}`. + * + * The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` + * contains any reserved character, such characters should be percent-encoded + * before the matching. + * + * If a variable contains exactly one path segment, such as `"{var}"` or + * `"{var=*}"`, when such a variable is expanded into a URL path on the client + * side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The + * server side does the reverse decoding. Such variables show up in the + * [Discovery + * Document](https://developers.google.com/discovery/v1/reference/apis) as + * `{var}`. + * + * If a variable contains multiple path segments, such as `"{var=foo/*}"` + * or `"{var=**}"`, when such a variable is expanded into a URL path on the + * client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. + * The server side does the reverse decoding, except "%2F" and "%2f" are left + * unchanged. Such variables show up in the + * [Discovery + * Document](https://developers.google.com/discovery/v1/reference/apis) as + * `{+var}`. + * + * Using gRPC API Service Configuration + * + * gRPC API Service Configuration (service config) is a configuration language + * for configuring a gRPC service to become a user-facing product. The + * service config is simply the YAML representation of the `google.api.Service` + * proto message. + * + * As an alternative to annotating your proto file, you can configure gRPC + * transcoding in your service config YAML files. You do this by specifying a + * `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same + * effect as the proto annotation. This can be particularly useful if you + * have a proto that is reused in multiple services. Note that any transcoding + * specified in the service config will override any matching transcoding + * configuration in the proto. + * + * The following example selects a gRPC method and applies an `HttpRule` to it: + * + * http: + * rules: + * - selector: example.v1.Messaging.GetMessage + * get: /v1/messages/{message_id}/{sub.subfield} + * + * Special notes + * + * When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the + * proto to JSON conversion must follow the [proto3 + * specification](https://developers.google.com/protocol-buffers/docs/proto3#json). + * + * While the single segment variable follows the semantics of + * [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String + * Expansion, the multi segment variable **does not** follow RFC 6570 Section + * 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion + * does not expand special characters like `?` and `#`, which would lead + * to invalid URLs. As the result, gRPC Transcoding uses a custom encoding + * for multi segment variables. + * + * The path variables **must not** refer to any repeated or mapped field, + * because client libraries are not capable of handling such variable expansion. + * + * The path variables **must not** capture the leading "/" character. The reason + * is that the most common use case "{var}" does not capture the leading "/" + * character. For consistency, all path variables must share the same behavior. + * + * Repeated message fields must not be mapped to URL query parameters, because + * no client library can support such complicated mapping. + * + * If an API needs to use a JSON array for request or response body, it can map + * the request or response body to a repeated field. However, some gRPC + * Transcoding implementations may not support this feature. + * + * @generated from message google.api.HttpRule + */ +export type HttpRule = Message<"google.api.HttpRule"> & { + /** + * Selects a method to which this rule applies. + * + * Refer to [selector][google.api.DocumentationRule.selector] for syntax + * details. + * + * @generated from field: string selector = 1; + */ + selector: string; + + /** + * Determines the URL pattern is matched by this rules. This pattern can be + * used with any of the {get|put|post|delete|patch} methods. A custom method + * can be defined using the 'custom' field. + * + * @generated from oneof google.api.HttpRule.pattern + */ + pattern: { + /** + * Maps to HTTP GET. Used for listing and getting information about + * resources. + * + * @generated from field: string get = 2; + */ + value: string; + case: "get"; + } | { + /** + * Maps to HTTP PUT. Used for replacing a resource. + * + * @generated from field: string put = 3; + */ + value: string; + case: "put"; + } | { + /** + * Maps to HTTP POST. Used for creating a resource or performing an action. + * + * @generated from field: string post = 4; + */ + value: string; + case: "post"; + } | { + /** + * Maps to HTTP DELETE. Used for deleting a resource. + * + * @generated from field: string delete = 5; + */ + value: string; + case: "delete"; + } | { + /** + * Maps to HTTP PATCH. Used for updating a resource. + * + * @generated from field: string patch = 6; + */ + value: string; + case: "patch"; + } | { + /** + * The custom pattern is used for specifying an HTTP method that is not + * included in the `pattern` field, such as HEAD, or "*" to leave the + * HTTP method unspecified for this rule. The wild-card rule is useful + * for services that provide content to Web (HTML) clients. + * + * @generated from field: google.api.CustomHttpPattern custom = 8; + */ + value: CustomHttpPattern; + case: "custom"; + } | { case: undefined; value?: undefined }; + + /** + * The name of the request field whose value is mapped to the HTTP request + * body, or `*` for mapping all request fields not captured by the path + * pattern to the HTTP body, or omitted for not having any HTTP request body. + * + * NOTE: the referred field must be present at the top-level of the request + * message type. + * + * @generated from field: string body = 7; + */ + body: string; + + /** + * Optional. The name of the response field whose value is mapped to the HTTP + * response body. When omitted, the entire response message will be used + * as the HTTP response body. + * + * NOTE: The referred field must be present at the top-level of the response + * message type. + * + * @generated from field: string response_body = 12; + */ + responseBody: string; + + /** + * Additional HTTP bindings for the selector. Nested bindings must + * not contain an `additional_bindings` field themselves (that is, + * the nesting may only be one level deep). + * + * @generated from field: repeated google.api.HttpRule additional_bindings = 11; + */ + additionalBindings: HttpRule[]; +}; + +/** + * Describes the message google.api.HttpRule. + * Use `create(HttpRuleSchema)` to create a new message. + */ +export const HttpRuleSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_google_api_http, 1); + +/** + * A custom pattern is used for defining custom HTTP verb. + * + * @generated from message google.api.CustomHttpPattern + */ +export type CustomHttpPattern = Message<"google.api.CustomHttpPattern"> & { + /** + * The name of this custom HTTP verb. + * + * @generated from field: string kind = 1; + */ + kind: string; + + /** + * The path matched by this custom verb. + * + * @generated from field: string path = 2; + */ + path: string; +}; + +/** + * Describes the message google.api.CustomHttpPattern. + * Use `create(CustomHttpPatternSchema)` to create a new message. + */ +export const CustomHttpPatternSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_google_api_http, 2); + diff --git a/web/src/gen/touchstone/v1/scan_pb.ts b/web/src/gen/touchstone/v1/scan_pb.ts new file mode 100644 index 0000000..20e61d1 --- /dev/null +++ b/web/src/gen/touchstone/v1/scan_pb.ts @@ -0,0 +1,612 @@ +// @generated by protoc-gen-es v2.16.0 with parameter "target=ts" +// @generated from file touchstone/v1/scan.proto (package touchstone.v1, syntax proto3) +/* eslint-disable */ + +import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_google_api_annotations } from "../../google/api/annotations_pb"; +import type { Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file touchstone/v1/scan.proto. + */ +export const file_touchstone_v1_scan: GenFile = /*@__PURE__*/ + fileDesc("Chh0b3VjaHN0b25lL3YxL3NjYW4ucHJvdG8SDXRvdWNoc3RvbmUudjEibQoLU2NhblJlcXVlc3QSCwoDdXJsGAEgASgJEhAKCHJlcG9fdXJsGAIgASgJEhAKCHNib21fdXJsGAMgASgJEhUKDXNib21fZG9jdW1lbnQYBCABKAwSFgoOcmVxdWVzdF9idWRnZXQYBSABKA0iVwoIRXZpZGVuY2USKQoEa2luZBgBIAEoDjIbLnRvdWNoc3RvbmUudjEuRXZpZGVuY2VLaW5kEhAKCGxvY2F0aW9uGAIgASgJEg4KBmRldGFpbBgDIAEoCSLpAQoNVnVsbmVyYWJpbGl0eRIKCgJpZBgBIAEoCRIPCgdhbGlhc2VzGAIgAygJEg8KB3N1bW1hcnkYAyABKAkSCwoDa2V2GAQgASgIEhEKBGVwc3MYBSABKAFIAIgBARIWCgljdnNzX2Jhc2UYBiABKAFIAYgBARIsCgp2ZXhfc3RhdHVzGAcgASgOMhgudG91Y2hzdG9uZS52MS5WZXhTdGF0dXMSGQoRdmV4X2p1c3RpZmljYXRpb24YCCABKAkSEgoKc3VwcHJlc3NlZBgJIAEoCEIHCgVfZXBzc0IMCgpfY3Zzc19iYXNlIucBCglDb21wb25lbnQSDAoEbmFtZRgBIAEoCRIPCgd2ZXJzaW9uGAIgASgJEgwKBHB1cmwYAyABKAkSCwoDY3BlGAQgASgJEi0KCmNvbmZpZGVuY2UYBSABKA4yGS50b3VjaHN0b25lLnYxLkNvbmZpZGVuY2USKQoIZXZpZGVuY2UYBiADKAsyFy50b3VjaHN0b25lLnYxLkV2aWRlbmNlEg8KB2NhdmVhdHMYByADKAkSNQoPdnVsbmVyYWJpbGl0aWVzGAggAygLMhwudG91Y2hzdG9uZS52MS5WdWxuZXJhYmlsaXR5Il4KCENvbmZsaWN0EhUKDWNvbXBvbmVudF9rZXkYASABKAkSEAoIdmVyc2lvbnMYAiADKAkSKQoIZXZpZGVuY2UYAyADKAsyFy50b3VjaHN0b25lLnYxLkV2aWRlbmNlIuQBCg9DaGVja2VkTG9jYXRpb24SCwoDdXJsGAEgASgJEg4KBnJlYXNvbhgCIAEoCRI1CgZyZXN1bHQYAyABKA4yJS50b3VjaHN0b25lLnYxLkNoZWNrZWRMb2NhdGlvbi5SZXN1bHQSDQoFZXJyb3IYBCABKAkibgoGUmVzdWx0EhYKElJFU1VMVF9VTlNQRUNJRklFRBAAEhAKDFJFU1VMVF9GT1VORBABEhQKEFJFU1VMVF9OT1RfRk9VTkQQAhIQCgxSRVNVTFRfRVJST1IQAxISCg5SRVNVTFRfUkVGVVNFRBAEIjkKDFNvdXJjZVN0YXR1cxIOCgZzb3VyY2UYASABKAkSCgoCb2sYAiABKAgSDQoFZXJyb3IYAyABKAkixQQKClNjYW5SZXBvcnQSFgoOc2NoZW1hX3ZlcnNpb24YASABKAkSEgoKdGFyZ2V0X3VybBgCIAEoCRIuCgtzY2FuX3N0YXR1cxgDIAEoDjIZLnRvdWNoc3RvbmUudjEuU2NhblN0YXR1cxIyCg1sb29rdXBfc3RhdHVzGAQgASgOMhsudG91Y2hzdG9uZS52MS5Mb29rdXBTdGF0dXMSLgoKc3RhcnRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMAoMY29tcGxldGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIUCgx0b29sX3ZlcnNpb24YByABKAkSMwoPdnVsbl9kYXRhX2FzX29mGAggASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIsCgpjb21wb25lbnRzGAkgAygLMhgudG91Y2hzdG9uZS52MS5Db21wb25lbnQSKgoJY29uZmxpY3RzGAogAygLMhcudG91Y2hzdG9uZS52MS5Db25mbGljdBIvCgdjaGVja2VkGAsgAygLMh4udG91Y2hzdG9uZS52MS5DaGVja2VkTG9jYXRpb24SFQoNcmVxdWVzdHNfdXNlZBgMIAEoDRIWCg5yZXF1ZXN0X2J1ZGdldBgNIAEoDRIsCgdzb3VyY2VzGA4gAygLMhsudG91Y2hzdG9uZS52MS5Tb3VyY2VTdGF0dXMSEgoKZGlzY2xhaW1lchgPIAEoCSpoCgpDb25maWRlbmNlEhoKFkNPTkZJREVOQ0VfVU5TUEVDSUZJRUQQABISCg5DT05GSURFTkNFX0xPVxABEhUKEUNPTkZJREVOQ0VfTUVESVVNEAISEwoPQ09ORklERU5DRV9ISUdIEAMq8wEKDEV2aWRlbmNlS2luZBIdChlFVklERU5DRV9LSU5EX1VOU1BFQ0lGSUVEEAASIAocRVZJREVOQ0VfS0lORF9TQk9NX1BVQkxJU0hFRBABEh8KG0VWSURFTkNFX0tJTkRfU0JPTV9TVVBQTElFRBACEhUKEUVWSURFTkNFX0tJTkRfVkVYEAMSHwobRVZJREVOQ0VfS0lORF9SRVBPX0xPQ0tGSUxFEAQSIwofRVZJREVOQ0VfS0lORF9GSU5HRVJQUklOVF9BU1NFVBAFEiQKIEVWSURFTkNFX0tJTkRfRklOR0VSUFJJTlRfSEVBREVSEAYqlwEKCVZleFN0YXR1cxIaChZWRVhfU1RBVFVTX1VOU1BFQ0lGSUVEEAASFwoTVkVYX1NUQVRVU19BRkZFQ1RFRBABEhsKF1ZFWF9TVEFUVVNfTk9UX0FGRkVDVEVEEAISFAoQVkVYX1NUQVRVU19GSVhFRBADEiIKHlZFWF9TVEFUVVNfVU5ERVJfSU5WRVNUSUdBVElPThAEKmEKClNjYW5TdGF0dXMSGwoXU0NBTl9TVEFUVVNfVU5TUEVDSUZJRUQQABISCg5TQ0FOX1NUQVRVU19PSxABEiIKHlNDQU5fU1RBVFVTX1RBUkdFVF9VTlJFQUNIQUJMRRACKn4KDExvb2t1cFN0YXR1cxIdChlMT09LVVBfU1RBVFVTX1VOU1BFQ0lGSUVEEAASGgoWTE9PS1VQX1NUQVRVU19DT01QTEVURRABEhkKFUxPT0tVUF9TVEFUVVNfUEFSVElBTBACEhgKFExPT0tVUF9TVEFUVVNfRkFJTEVEEAMyYgoLU2NhblNlcnZpY2USUwoEU2NhbhIaLnRvdWNoc3RvbmUudjEuU2NhblJlcXVlc3QaGS50b3VjaHN0b25lLnYxLlNjYW5SZXBvcnQiFILT5JMCDjoBKiIJL3YxL3NjYW5zQkVaQ2dpdGh1Yi5jb20vRm9vdHByaW50QUkvdG91Y2hzdG9uZS9nZW4vZ28vdG91Y2hzdG9uZS92MTt0b3VjaHN0b25ldjFiBnByb3RvMw", [file_google_api_annotations, file_google_protobuf_timestamp]); + +/** + * @generated from message touchstone.v1.ScanRequest + */ +export type ScanRequest = Message<"touchstone.v1.ScanRequest"> & { + /** + * required, http(s) + * + * @generated from field: string url = 1; + */ + url: string; + + /** + * optional, user-supplied repo (#3) + * + * @generated from field: string repo_url = 2; + */ + repoUrl: string; + + /** + * optional, user-supplied SBOM/VEX (D1) + * + * @generated from field: string sbom_url = 3; + */ + sbomUrl: string; + + /** + * optional, uploaded SBOM/VEX (D1) + * + * @generated from field: bytes sbom_document = 4; + */ + sbomDocument: Uint8Array; + + /** + * 0 = default 50, max 200 + * + * @generated from field: uint32 request_budget = 5; + */ + requestBudget: number; +}; + +/** + * Describes the message touchstone.v1.ScanRequest. + * Use `create(ScanRequestSchema)` to create a new message. + */ +export const ScanRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_touchstone_v1_scan, 0); + +/** + * @generated from message touchstone.v1.Evidence + */ +export type Evidence = Message<"touchstone.v1.Evidence"> & { + /** + * @generated from field: touchstone.v1.EvidenceKind kind = 1; + */ + kind: EvidenceKind; + + /** + * URL, or repo@ref:path for lockfiles + * + * @generated from field: string location = 2; + */ + location: string; + + /** + * matched string or header value + * + * @generated from field: string detail = 3; + */ + detail: string; +}; + +/** + * Describes the message touchstone.v1.Evidence. + * Use `create(EvidenceSchema)` to create a new message. + */ +export const EvidenceSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_touchstone_v1_scan, 1); + +/** + * @generated from message touchstone.v1.Vulnerability + */ +export type Vulnerability = Message<"touchstone.v1.Vulnerability"> & { + /** + * CVE id when one exists, else OSV id + * + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: repeated string aliases = 2; + */ + aliases: string[]; + + /** + * @generated from field: string summary = 3; + */ + summary: string; + + /** + * @generated from field: bool kev = 4; + */ + kev: boolean; + + /** + * absent = no EPSS data + * + * @generated from field: optional double epss = 5; + */ + epss?: number | undefined; + + /** + * @generated from field: optional double cvss_base = 6; + */ + cvssBase?: number | undefined; + + /** + * @generated from field: touchstone.v1.VexStatus vex_status = 7; + */ + vexStatus: VexStatus; + + /** + * @generated from field: string vex_justification = 8; + */ + vexJustification: string; + + /** + * true for NOT_AFFECTED / FIXED + * + * @generated from field: bool suppressed = 9; + */ + suppressed: boolean; +}; + +/** + * Describes the message touchstone.v1.Vulnerability. + * Use `create(VulnerabilitySchema)` to create a new message. + */ +export const VulnerabilitySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_touchstone_v1_scan, 2); + +/** + * @generated from message touchstone.v1.Component + */ +export type Component = Message<"touchstone.v1.Component"> & { + /** + * @generated from field: string name = 1; + */ + name: string; + + /** + * @generated from field: string version = 2; + */ + version: string; + + /** + * @generated from field: string purl = 3; + */ + purl: string; + + /** + * @generated from field: string cpe = 4; + */ + cpe: string; + + /** + * @generated from field: touchstone.v1.Confidence confidence = 5; + */ + confidence: Confidence; + + /** + * @generated from field: repeated touchstone.v1.Evidence evidence = 6; + */ + evidence: Evidence[]; + + /** + * e.g. MAY_BE_BACKPORTED, DEFAULT_BRANCH_NOT_RELEASE + * + * @generated from field: repeated string caveats = 7; + */ + caveats: string[]; + + /** + * ranked + * + * @generated from field: repeated touchstone.v1.Vulnerability vulnerabilities = 8; + */ + vulnerabilities: Vulnerability[]; +}; + +/** + * Describes the message touchstone.v1.Component. + * Use `create(ComponentSchema)` to create a new message. + */ +export const ComponentSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_touchstone_v1_scan, 3); + +/** + * @generated from message touchstone.v1.Conflict + */ +export type Conflict = Message<"touchstone.v1.Conflict"> & { + /** + * @generated from field: string component_key = 1; + */ + componentKey: string; + + /** + * @generated from field: repeated string versions = 2; + */ + versions: string[]; + + /** + * @generated from field: repeated touchstone.v1.Evidence evidence = 3; + */ + evidence: Evidence[]; +}; + +/** + * Describes the message touchstone.v1.Conflict. + * Use `create(ConflictSchema)` to create a new message. + */ +export const ConflictSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_touchstone_v1_scan, 4); + +/** + * @generated from message touchstone.v1.CheckedLocation + */ +export type CheckedLocation = Message<"touchstone.v1.CheckedLocation"> & { + /** + * @generated from field: string url = 1; + */ + url: string; + + /** + * TARGET | SUBRESOURCE | DISCOVERY_PATH | DECLARED | FORGE_API + * + * @generated from field: string reason = 2; + */ + reason: string; + + /** + * @generated from field: touchstone.v1.CheckedLocation.Result result = 3; + */ + result: CheckedLocation_Result; + + /** + * @generated from field: string error = 4; + */ + error: string; +}; + +/** + * Describes the message touchstone.v1.CheckedLocation. + * Use `create(CheckedLocationSchema)` to create a new message. + */ +export const CheckedLocationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_touchstone_v1_scan, 5); + +/** + * @generated from enum touchstone.v1.CheckedLocation.Result + */ +export enum CheckedLocation_Result { + /** + * @generated from enum value: RESULT_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: RESULT_FOUND = 1; + */ + FOUND = 1, + + /** + * @generated from enum value: RESULT_NOT_FOUND = 2; + */ + NOT_FOUND = 2, + + /** + * @generated from enum value: RESULT_ERROR = 3; + */ + ERROR = 3, + + /** + * @generated from enum value: RESULT_REFUSED = 4; + */ + REFUSED = 4, +} + +/** + * Describes the enum touchstone.v1.CheckedLocation.Result. + */ +export const CheckedLocation_ResultSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_touchstone_v1_scan, 5, 0); + +/** + * @generated from message touchstone.v1.SourceStatus + */ +export type SourceStatus = Message<"touchstone.v1.SourceStatus"> & { + /** + * @generated from field: string source = 1; + */ + source: string; + + /** + * @generated from field: bool ok = 2; + */ + ok: boolean; + + /** + * @generated from field: string error = 3; + */ + error: string; +}; + +/** + * Describes the message touchstone.v1.SourceStatus. + * Use `create(SourceStatusSchema)` to create a new message. + */ +export const SourceStatusSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_touchstone_v1_scan, 6); + +/** + * @generated from message touchstone.v1.ScanReport + */ +export type ScanReport = Message<"touchstone.v1.ScanReport"> & { + /** + * "1"; bumped only with a new proto package + * + * @generated from field: string schema_version = 1; + */ + schemaVersion: string; + + /** + * @generated from field: string target_url = 2; + */ + targetUrl: string; + + /** + * @generated from field: touchstone.v1.ScanStatus scan_status = 3; + */ + scanStatus: ScanStatus; + + /** + * @generated from field: touchstone.v1.LookupStatus lookup_status = 4; + */ + lookupStatus: LookupStatus; + + /** + * @generated from field: google.protobuf.Timestamp started_at = 5; + */ + startedAt?: Timestamp | undefined; + + /** + * @generated from field: google.protobuf.Timestamp completed_at = 6; + */ + completedAt?: Timestamp | undefined; + + /** + * @generated from field: string tool_version = 7; + */ + toolVersion: string; + + /** + * @generated from field: google.protobuf.Timestamp vuln_data_as_of = 8; + */ + vulnDataAsOf?: Timestamp | undefined; + + /** + * @generated from field: repeated touchstone.v1.Component components = 9; + */ + components: Component[]; + + /** + * @generated from field: repeated touchstone.v1.Conflict conflicts = 10; + */ + conflicts: Conflict[]; + + /** + * @generated from field: repeated touchstone.v1.CheckedLocation checked = 11; + */ + checked: CheckedLocation[]; + + /** + * @generated from field: uint32 requests_used = 12; + */ + requestsUsed: number; + + /** + * @generated from field: uint32 request_budget = 13; + */ + requestBudget: number; + + /** + * @generated from field: repeated touchstone.v1.SourceStatus sources = 14; + */ + sources: SourceStatus[]; + + /** + * @generated from field: string disclaimer = 15; + */ + disclaimer: string; +}; + +/** + * Describes the message touchstone.v1.ScanReport. + * Use `create(ScanReportSchema)` to create a new message. + */ +export const ScanReportSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_touchstone_v1_scan, 7); + +/** + * @generated from enum touchstone.v1.Confidence + */ +export enum Confidence { + /** + * @generated from enum value: CONFIDENCE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: CONFIDENCE_LOW = 1; + */ + LOW = 1, + + /** + * @generated from enum value: CONFIDENCE_MEDIUM = 2; + */ + MEDIUM = 2, + + /** + * @generated from enum value: CONFIDENCE_HIGH = 3; + */ + HIGH = 3, +} + +/** + * Describes the enum touchstone.v1.Confidence. + */ +export const ConfidenceSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_touchstone_v1_scan, 0); + +/** + * @generated from enum touchstone.v1.EvidenceKind + */ +export enum EvidenceKind { + /** + * @generated from enum value: EVIDENCE_KIND_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: EVIDENCE_KIND_SBOM_PUBLISHED = 1; + */ + SBOM_PUBLISHED = 1, + + /** + * @generated from enum value: EVIDENCE_KIND_SBOM_SUPPLIED = 2; + */ + SBOM_SUPPLIED = 2, + + /** + * @generated from enum value: EVIDENCE_KIND_VEX = 3; + */ + VEX = 3, + + /** + * @generated from enum value: EVIDENCE_KIND_REPO_LOCKFILE = 4; + */ + REPO_LOCKFILE = 4, + + /** + * @generated from enum value: EVIDENCE_KIND_FINGERPRINT_ASSET = 5; + */ + FINGERPRINT_ASSET = 5, + + /** + * @generated from enum value: EVIDENCE_KIND_FINGERPRINT_HEADER = 6; + */ + FINGERPRINT_HEADER = 6, +} + +/** + * Describes the enum touchstone.v1.EvidenceKind. + */ +export const EvidenceKindSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_touchstone_v1_scan, 1); + +/** + * @generated from enum touchstone.v1.VexStatus + */ +export enum VexStatus { + /** + * @generated from enum value: VEX_STATUS_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: VEX_STATUS_AFFECTED = 1; + */ + AFFECTED = 1, + + /** + * @generated from enum value: VEX_STATUS_NOT_AFFECTED = 2; + */ + NOT_AFFECTED = 2, + + /** + * @generated from enum value: VEX_STATUS_FIXED = 3; + */ + FIXED = 3, + + /** + * @generated from enum value: VEX_STATUS_UNDER_INVESTIGATION = 4; + */ + UNDER_INVESTIGATION = 4, +} + +/** + * Describes the enum touchstone.v1.VexStatus. + */ +export const VexStatusSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_touchstone_v1_scan, 2); + +/** + * @generated from enum touchstone.v1.ScanStatus + */ +export enum ScanStatus { + /** + * @generated from enum value: SCAN_STATUS_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: SCAN_STATUS_OK = 1; + */ + OK = 1, + + /** + * @generated from enum value: SCAN_STATUS_TARGET_UNREACHABLE = 2; + */ + TARGET_UNREACHABLE = 2, +} + +/** + * Describes the enum touchstone.v1.ScanStatus. + */ +export const ScanStatusSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_touchstone_v1_scan, 3); + +/** + * @generated from enum touchstone.v1.LookupStatus + */ +export enum LookupStatus { + /** + * @generated from enum value: LOOKUP_STATUS_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: LOOKUP_STATUS_COMPLETE = 1; + */ + COMPLETE = 1, + + /** + * @generated from enum value: LOOKUP_STATUS_PARTIAL = 2; + */ + PARTIAL = 2, + + /** + * @generated from enum value: LOOKUP_STATUS_FAILED = 3; + */ + FAILED = 3, +} + +/** + * Describes the enum touchstone.v1.LookupStatus. + */ +export const LookupStatusSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_touchstone_v1_scan, 4); + +/** + * @generated from service touchstone.v1.ScanService + */ +export const ScanService: GenService<{ + /** + * @generated from rpc touchstone.v1.ScanService.Scan + */ + scan: { + methodKind: "unary"; + input: typeof ScanRequestSchema; + output: typeof ScanReportSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_touchstone_v1_scan, 0); + diff --git a/web/src/report.test.ts b/web/src/report.test.ts new file mode 100644 index 0000000..606de94 --- /dev/null +++ b/web/src/report.test.ts @@ -0,0 +1,29 @@ +import { readFileSync } from "node:fs"; +import { fromJson, type JsonValue } from "@bufbuild/protobuf"; +import { describe, expect, it } from "vitest"; +import { + Confidence, + LookupStatus, + ScanReportSchema, + ScanStatus, +} from "./gen/touchstone/v1/scan_pb"; + +// Written by the Go CLI test (go test ./cmd/touchstone -update), so drift +// between the Go and TS sides of scan.proto fails one of the two suites. +const golden = JSON.parse( + readFileSync(new URL("../../testdata/report.golden.json", import.meta.url), "utf8"), +) as JsonValue; + +describe("ScanReport golden", () => { + it("decodes with the generated parser", () => { + const report = fromJson(ScanReportSchema, golden); + + expect(report.schemaVersion).toBe("1"); + expect(report.targetUrl).toBe("https://example.com"); + expect(report.scanStatus).toBe(ScanStatus.OK); + expect(report.lookupStatus).toBe(LookupStatus.COMPLETE); + expect(report.components).toHaveLength(1); + expect(report.components[0]?.confidence).toBe(Confidence.LOW); + expect(report.components[0]?.vulnerabilities[0]?.id).toBe("CVE-2020-11022"); + }); +}); diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..ef89603 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "types": ["node"], + "strict": true, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +}