Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions go/node_registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cstx

import (
"encoding/json"
"fmt"
"sync"
)

// SCOParser decodes one native CSTX node type. Parsers are registered by the
// package that owns the type, keeping the graph engine extensible.
type SCOParser func([]byte) (SCONode, error)

var (
parserMu sync.RWMutex
parsers = map[string]SCOParser{}
)

func RegisterSCOParser(nodeType string, parser SCOParser) error {
if nodeType == "" || parser == nil {
return fmt.Errorf("cstx: node type and parser are required")
}
parserMu.Lock()
defer parserMu.Unlock()
if _, exists := parsers[nodeType]; exists {
return fmt.Errorf("cstx: parser already registered for %q", nodeType)
}
parsers[nodeType] = parser
return nil
}

func parseRegisteredSCONode(data []byte) (SCONode, bool, error) {
var header struct {
Type string `json:"cstx_type"`
}
if err := json.Unmarshal(data, &header); err != nil {
return nil, false, err
}
parserMu.RLock()
parser := parsers[header.Type]
parserMu.RUnlock()
if parser == nil {
return nil, false, nil
}
node, err := parser(data)
return node, true, err
}
39 changes: 39 additions & 0 deletions go/node_registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package cstx

import (
"encoding/json"
"testing"
)

type registryTestNode struct {
nodeHeader
Value string `json:"value"`
}

func TestRegisteredParserTakesPrecedence(t *testing.T) {
typ := "test_registry_node"
if err := RegisterSCOParser(typ, func(data []byte) (SCONode, error) {
var node registryTestNode
return &node, json.Unmarshal(data, &node)
}); err != nil {
t.Fatal(err)
}
node, err := ParseSCONode([]byte(`{"cstx_type":"test_registry_node","cstx_id":"x","value":"ok"}`))
if err != nil {
t.Fatal(err)
}
if node.CstxType() != typ || node.CstxID() != "x" {
t.Fatalf("unexpected node: %#v", node)
}
}

func TestRegisterSCOParserRejectsDuplicate(t *testing.T) {
typ := "test_registry_duplicate"
parser := func([]byte) (SCONode, error) { return &registryTestNode{}, nil }
if err := RegisterSCOParser(typ, parser); err != nil {
t.Fatal(err)
}
if err := RegisterSCOParser(typ, parser); err == nil {
t.Fatal("expected duplicate registration to fail")
}
}
17 changes: 17 additions & 0 deletions go/sco_easm.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ type SarifVulnNode struct {
Evidence string `json:"evidence,omitempty"`
}

// ReportNode is a domain-neutral report SCO used by consumers that publish
// evidence-backed summaries alongside graph findings.
type ReportNode struct {
nodeHeader
Value string `json:"value,omitempty"`
Title string `json:"title,omitempty"`
Summary string `json:"summary,omitempty"`
Markdown string `json:"markdown,omitempty"`
}

type CertificateNode struct {
nodeHeader
Fingerprint string `json:"fingerprint"`
Expand Down Expand Up @@ -248,6 +258,9 @@ func (h nodeHeader) CstxID() string { return h.ID }

// ParseSCONode unmarshals a JSON node into the correct typed struct.
func ParseSCONode(data []byte) (SCONode, error) {
if node, registered, err := parseRegisteredSCONode(data); registered || err != nil {
return node, err
}
var h nodeHeader
if err := json.Unmarshal(data, &h); err != nil {
return nil, err
Expand Down Expand Up @@ -293,6 +306,10 @@ func ParseSCONode(data []byte) (SCONode, error) {
var v SarifVulnNode
err := json.Unmarshal(data, &v)
return &v, err
case "report":
var v ReportNode
err := json.Unmarshal(data, &v)
return &v, err
case "certificate":
var v CertificateNode
err := json.Unmarshal(data, &v)
Expand Down
Loading