a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
161 lines
4.7 KiB
Go
161 lines
4.7 KiB
Go
package analysis
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/zzet/gortex/internal/config"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// EventBoundaryFamily (PYG-1) evaluates declarative event-boundary rules over
|
|
// the pub/sub graph. A changed symbol's produce edges (EdgeEmits /
|
|
// EdgeProducesTopic) and consume edges (EdgeListensOn / EdgeConsumesTopic) are
|
|
// matched against config rules: which paths may produce or consume a topic,
|
|
// whether a produced topic must have a consumer, and which paths are forbidden
|
|
// from it. It is a RuleFamily, so change_contract runs it like any other.
|
|
type EventBoundaryFamily struct {
|
|
Rules []config.EventRule
|
|
}
|
|
|
|
func (f EventBoundaryFamily) Name() string { return "events" }
|
|
|
|
var (
|
|
produceEdges = []graph.EdgeKind{graph.EdgeEmits, graph.EdgeProducesTopic}
|
|
consumeEdges = []graph.EdgeKind{graph.EdgeListensOn, graph.EdgeConsumesTopic}
|
|
)
|
|
|
|
func edgeKindIn(k graph.EdgeKind, set []graph.EdgeKind) bool {
|
|
for _, e := range set {
|
|
if e == k {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// topicTargets returns the topic/event nodes a symbol links to via the given
|
|
// edge kinds.
|
|
func topicTargets(g graph.Store, id string, kinds []graph.EdgeKind) []*graph.Node {
|
|
var out []*graph.Node
|
|
for _, e := range g.GetOutEdges(id) {
|
|
if !edgeKindIn(e.Kind, kinds) {
|
|
continue
|
|
}
|
|
if t := g.GetNode(e.To); t != nil {
|
|
out = append(out, t)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// topicHasConsumer reports whether any symbol consumes the topic node.
|
|
func topicHasConsumer(g graph.Store, topicID string) bool {
|
|
for _, e := range g.GetInEdges(topicID) {
|
|
if edgeKindIn(e.Kind, consumeEdges) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (f EventBoundaryFamily) Evaluate(g graph.Store, changedSet []string) []GuardViolation {
|
|
if g == nil || len(f.Rules) == 0 {
|
|
return nil
|
|
}
|
|
var violations []GuardViolation
|
|
seen := make(map[string]bool)
|
|
add := func(v GuardViolation) {
|
|
key := v.RuleName + "\x00" + v.Violator + "\x00" + v.Description
|
|
if seen[key] {
|
|
return
|
|
}
|
|
seen[key] = true
|
|
violations = append(violations, v)
|
|
}
|
|
|
|
for _, id := range changedSet {
|
|
n := g.GetNode(id)
|
|
if n == nil {
|
|
continue
|
|
}
|
|
produced := topicTargets(g, id, produceEdges)
|
|
consumed := topicTargets(g, id, consumeEdges)
|
|
if len(produced) == 0 && len(consumed) == 0 {
|
|
continue
|
|
}
|
|
for _, rule := range f.Rules {
|
|
sev := ruleSeverity(rule.Severity)
|
|
for _, topic := range produced {
|
|
if rule.Topic != "" && !globMatch(rule.Topic, topic.Name) {
|
|
continue
|
|
}
|
|
if rule.Producer != "" && !globMatch(rule.Producer, n.FilePath) {
|
|
add(GuardViolation{
|
|
RuleName: eventRuleLabel(rule, topic.Name),
|
|
Kind: "event_boundary",
|
|
Description: eventMessage(rule, fmt.Sprintf("%s produces topic %q from %s, outside the permitted producer path %q", n.Name, topic.Name, n.FilePath, rule.Producer)),
|
|
Violator: n.ID,
|
|
Severity: sev,
|
|
})
|
|
}
|
|
if matchesAnyGlob(n.FilePath, rule.Forbid) {
|
|
add(GuardViolation{
|
|
RuleName: eventRuleLabel(rule, topic.Name),
|
|
Kind: "event_boundary",
|
|
Description: eventMessage(rule, fmt.Sprintf("%s in %s is forbidden from producing topic %q", n.Name, n.FilePath, topic.Name)),
|
|
Violator: n.ID,
|
|
Severity: sev,
|
|
})
|
|
}
|
|
if rule.RequireConsumer && !topicHasConsumer(g, topic.ID) {
|
|
add(GuardViolation{
|
|
RuleName: eventRuleLabel(rule, topic.Name),
|
|
Kind: "event_boundary",
|
|
Description: eventMessage(rule, fmt.Sprintf("topic %q is produced by %s but has no consumer", topic.Name, n.Name)),
|
|
Violator: n.ID,
|
|
Severity: sev,
|
|
})
|
|
}
|
|
}
|
|
for _, topic := range consumed {
|
|
if rule.Topic != "" && !globMatch(rule.Topic, topic.Name) {
|
|
continue
|
|
}
|
|
if rule.Consumer != "" && !globMatch(rule.Consumer, n.FilePath) {
|
|
add(GuardViolation{
|
|
RuleName: eventRuleLabel(rule, topic.Name),
|
|
Kind: "event_boundary",
|
|
Description: eventMessage(rule, fmt.Sprintf("%s consumes topic %q from %s, outside the permitted consumer path %q", n.Name, topic.Name, n.FilePath, rule.Consumer)),
|
|
Violator: n.ID,
|
|
Severity: sev,
|
|
})
|
|
}
|
|
if matchesAnyGlob(n.FilePath, rule.Forbid) {
|
|
add(GuardViolation{
|
|
RuleName: eventRuleLabel(rule, topic.Name),
|
|
Kind: "event_boundary",
|
|
Description: eventMessage(rule, fmt.Sprintf("%s in %s is forbidden from consuming topic %q", n.Name, n.FilePath, topic.Name)),
|
|
Violator: n.ID,
|
|
Severity: sev,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return violations
|
|
}
|
|
|
|
func eventRuleLabel(rule config.EventRule, topic string) string {
|
|
if rule.Name != "" {
|
|
return rule.Name
|
|
}
|
|
return "event:" + topic
|
|
}
|
|
|
|
func eventMessage(rule config.EventRule, fallback string) string {
|
|
if rule.Message != "" {
|
|
return rule.Message
|
|
}
|
|
return fallback
|
|
}
|