| // Copyright 2024 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 |
| // |
| // https://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. |
| |
| package policy |
| |
| import ( |
| "fmt" |
| "strings" |
| |
| "go.yaml.in/yaml/v3" |
| |
| "github.com/google/cel-go/cel" |
| "github.com/google/cel-go/common" |
| "github.com/google/cel-go/common/ast" |
| ) |
| |
| type semanticType int |
| |
| const ( |
| unspecified semanticType = iota |
| firstMatch |
| ) |
| |
| // NewPolicy creates a policy object which references a policy source and source information. |
| func NewPolicy(src *Source, info *ast.SourceInfo) *Policy { |
| return &Policy{ |
| metadata: map[string]any{}, |
| source: src, |
| info: info, |
| semantic: firstMatch, |
| imports: []*Import{}, |
| } |
| } |
| |
| // Policy declares a name, rule, and evaluation semantic for a given expression graph. |
| type Policy struct { |
| name ValueString |
| description ValueString |
| imports []*Import |
| rule *Rule |
| semantic semanticType |
| info *ast.SourceInfo |
| source *Source |
| |
| metadata map[string]any |
| } |
| |
| // Source returns the policy file contents as a CEL source object. |
| func (p *Policy) Source() *Source { |
| return p.source |
| } |
| |
| // SourceInfo returns the policy file metadata about expression positions. |
| func (p *Policy) SourceInfo() *ast.SourceInfo { |
| return p.info |
| } |
| |
| // Imports returns the list of imports associated with the policy. |
| func (p *Policy) Imports() []*Import { |
| return p.imports |
| } |
| |
| // Name returns the name of the policy. |
| func (p *Policy) Name() ValueString { |
| return p.name |
| } |
| |
| // Description returns the description for the policy. |
| func (p *Policy) Description() ValueString { |
| return p.description |
| } |
| |
| // Rule returns the rule entry point of the policy. |
| func (p *Policy) Rule() *Rule { |
| return p.rule |
| } |
| |
| // Metadata returns a named metadata object if one exists within the policy. |
| func (p *Policy) Metadata(name string) (any, bool) { |
| value, found := p.metadata[name] |
| return value, found |
| } |
| |
| // MetadataKeys returns a list of metadata keys set on the policy. |
| func (p *Policy) MetadataKeys() []string { |
| keys := make([]string, 0, len(p.metadata)) |
| for k := range p.metadata { |
| keys = append(keys, k) |
| } |
| return keys |
| } |
| |
| // AddImport adds an import to the policy. |
| func (p *Policy) AddImport(i *Import) { |
| p.imports = append(p.imports, i) |
| } |
| |
| // SetName configures the policy name. |
| func (p *Policy) SetName(name ValueString) { |
| p.name = name |
| } |
| |
| // SetDescription configures the policy description. |
| func (p *Policy) SetDescription(description ValueString) { |
| p.description = description |
| } |
| |
| // SetRule configures the policy rule entry point. |
| func (p *Policy) SetRule(r *Rule) { |
| p.rule = r |
| } |
| |
| // SetMetadata updates a named metadata key with the given value. |
| func (p *Policy) SetMetadata(name string, value any) { |
| p.metadata[name] = value |
| } |
| |
| // ClearMetadata removes a named metadata key from the policy. |
| func (p *Policy) ClearMetadata(name string) { |
| delete(p.metadata, name) |
| } |
| |
| // GetExplanationOutputPolicy returns a copy of the policy, except the output of each match block |
| // is replaced by the expression in the explanation field. |
| func (p *Policy) GetExplanationOutputPolicy() *Policy { |
| ep := Policy{ |
| name: p.name, |
| semantic: p.semantic, |
| info: p.info, |
| metadata: p.metadata, |
| source: p.source, |
| } |
| if p.rule != nil { |
| ep.rule = p.rule.getExplanationOutputRule() |
| } |
| return &ep |
| } |
| |
| // NewImport creates a new typename import node |
| func NewImport(exprID int64) *Import { |
| return &Import{exprID: exprID} |
| } |
| |
| // Import represents an imported type name which is aliased within CEL expressions. |
| type Import struct { |
| exprID int64 |
| name ValueString |
| } |
| |
| // SourceID returns the source identifier associated with the import. |
| func (i *Import) SourceID() int64 { |
| return i.exprID |
| } |
| |
| // Name returns the fully qualified type name. |
| func (i *Import) Name() ValueString { |
| return i.name |
| } |
| |
| // SetName updates the fully qualified type name for the import. |
| func (i *Import) SetName(name ValueString) { |
| i.name = name |
| } |
| |
| // NewRule creates a Rule instance. |
| func NewRule(exprID int64) *Rule { |
| return &Rule{ |
| exprID: exprID, |
| variables: []*Variable{}, |
| matches: []*Match{}, |
| } |
| } |
| |
| // Rule declares a rule identifier, description, along with a set of variables and match statements. |
| type Rule struct { |
| exprID int64 |
| id *ValueString |
| description *ValueString |
| variables []*Variable |
| matches []*Match |
| } |
| |
| // ID returns the id value of the rule if it is set. |
| func (r *Rule) ID() ValueString { |
| if r.id != nil { |
| return *r.id |
| } |
| return ValueString{} |
| } |
| |
| // Description returns the rule description if it is set. |
| func (r *Rule) Description() ValueString { |
| if r.description != nil { |
| return *r.description |
| } |
| return ValueString{} |
| } |
| |
| // Matches returns the ordered set of Match declarations. |
| func (r *Rule) Matches() []*Match { |
| return r.matches[:] |
| } |
| |
| // Variables returns the order set of Variable tuples. |
| func (r *Rule) Variables() []*Variable { |
| return r.variables[:] |
| } |
| |
| // SetID configures the id for the rule. |
| func (r *Rule) SetID(id ValueString) { |
| r.id = &id |
| } |
| |
| // SetDescription configures the description for the rule. |
| func (r *Rule) SetDescription(desc ValueString) { |
| r.description = &desc |
| } |
| |
| // AddMatch addes a Match to the rule. |
| func (r *Rule) AddMatch(m *Match) { |
| r.matches = append(r.matches, m) |
| } |
| |
| // AddVariable adds a variable to the rule. |
| func (r *Rule) AddVariable(v *Variable) { |
| r.variables = append(r.variables, v) |
| } |
| |
| // AddVariables adds a list of variables to the rule. |
| func (r *Rule) AddVariables(v []*Variable) { |
| r.variables = append(r.variables, v...) |
| } |
| |
| func (r *Rule) getExplanationOutputRule() *Rule { |
| if r == nil { |
| return nil |
| } |
| er := Rule{ |
| id: r.id, |
| description: r.description, |
| } |
| er.AddVariables(r.Variables()) |
| for _, match := range r.matches { |
| em := Match{ |
| condition: match.condition, |
| output: match.explanation, |
| rule: match.rule.getExplanationOutputRule(), |
| } |
| er.matches = append(er.matches, &em) |
| } |
| return &er |
| } |
| |
| // NewVariable creates a variable instance. |
| func NewVariable(exprID int64) *Variable { |
| return &Variable{exprID: exprID} |
| } |
| |
| // Variable is a named expression which may be referenced in subsequent expressions. |
| type Variable struct { |
| exprID int64 |
| name ValueString |
| expression ValueString |
| } |
| |
| // Name returns the variable name. |
| func (v *Variable) Name() ValueString { |
| return v.name |
| } |
| |
| // Expression returns the variable expression. |
| func (v *Variable) Expression() ValueString { |
| return v.expression |
| } |
| |
| // SetName sets the variable name. |
| func (v *Variable) SetName(name ValueString) { |
| v.name = name |
| } |
| |
| // SetExpression sets the variable expression. |
| func (v *Variable) SetExpression(e ValueString) { |
| v.expression = e |
| } |
| |
| // NewMatch creates a match instance. |
| func NewMatch(exprID int64) *Match { |
| return &Match{exprID: exprID} |
| } |
| |
| // Match declares a condition (defaults to true) as well as an output or a rule. |
| // Either the output or the rule field may be set, but not both. |
| type Match struct { |
| exprID int64 |
| condition ValueString |
| output *ValueString |
| explanation *ValueString |
| rule *Rule |
| } |
| |
| // SourceID returns the source identifier associated with the match. |
| func (m *Match) SourceID() int64 { |
| return m.exprID |
| } |
| |
| // Condition returns the condition CEL expression. |
| func (m *Match) Condition() ValueString { |
| return m.condition |
| } |
| |
| // HasOutput indicates whether the output field is set of the match. |
| func (m *Match) HasOutput() bool { |
| return m.output != nil |
| } |
| |
| // Output returns the output expression, or empty expression if output is not set. |
| func (m *Match) Output() ValueString { |
| if m.HasOutput() { |
| return *m.output |
| } |
| return ValueString{} |
| } |
| |
| // HasExplanation indicates whether the explanation field is set of the match. |
| func (m *Match) HasExplanation() bool { |
| return m.explanation != nil |
| } |
| |
| // Explanation returns the explanation expression, or empty expression if output is not set. |
| func (m *Match) Explanation() ValueString { |
| if m.HasExplanation() { |
| return *m.explanation |
| } |
| return ValueString{} |
| } |
| |
| // HasRule indicates whether the rule field is set on a match. |
| func (m *Match) HasRule() bool { |
| return m.rule != nil |
| } |
| |
| // Rule returns the rule value, or nil if the rule is not set. |
| func (m *Match) Rule() *Rule { |
| return m.rule |
| } |
| |
| // SetCondition sets the CEL condition for the match. |
| func (m *Match) SetCondition(c ValueString) { |
| m.condition = c |
| } |
| |
| // SetOutput sets the output expression for the match. |
| func (m *Match) SetOutput(o ValueString) { |
| m.output = &o |
| } |
| |
| // SetExplanation sets the explanation expression for the match. |
| func (m *Match) SetExplanation(e ValueString) { |
| m.explanation = &e |
| } |
| |
| // SetRule sets the rule for the match. |
| func (m *Match) SetRule(r *Rule) { |
| m.rule = r |
| } |
| |
| // ValueString contains an identifier corresponding to source metadata and a simple string. |
| type ValueString struct { |
| ID int64 |
| Value string |
| } |
| |
| // ParserContext declares a set of interfaces for creating and managing metadata for parsed policies. |
| type ParserContext interface { |
| // NextID returns a monotonically increasing identifier for a source fragment. |
| // This ID is implicitly created and tracked within the CollectMetadata method. |
| NextID() int64 |
| |
| // CollectMetadata records the source position information of a given YAML node, and returns |
| // the id associated with the source metadata which is returned in the Policy SourceInfo object. |
| CollectMetadata(*yaml.Node) int64 |
| |
| // NewPolicy creates a new Policy instance with an ID associated with the YAML node. |
| NewPolicy(*yaml.Node) (*Policy, int64) |
| |
| // NewRule creates a new Rule instance with an ID associated with the YAML node. |
| NewRule(*yaml.Node) (*Rule, int64) |
| |
| // NewVariable creates a new Variable instance with an ID associated with the YAML node. |
| NewVariable(*yaml.Node) (*Variable, int64) |
| |
| // NewMatch creates a new Match instance with an ID associated with the YAML node. |
| NewMatch(*yaml.Node) (*Match, int64) |
| |
| // NewString creates a new ValueString from the YAML node. |
| NewString(*yaml.Node) ValueString |
| |
| // ParsePolicy will parse the target yaml node as though it is the top-level policy. |
| ParsePolicy(ParserContext, *yaml.Node) *Policy |
| |
| // ParseRule will parse the current yaml node as though it is the entry point to a rule. |
| ParseRule(ParserContext, *Policy, *yaml.Node) *Rule |
| |
| // ParseMatch will parse the current yaml node as though it is the entry point to a match. |
| ParseMatch(ParserContext, *Policy, *yaml.Node) *Match |
| |
| // ParseVariable will parse the current yaml node as though it is the entry point to a variable. |
| ParseVariable(ParserContext, *Policy, *yaml.Node) *Variable |
| |
| // ReportErrorAtID logs an error during parsing which is included in the issue set returned from |
| // a failed parse. |
| ReportErrorAtID(id int64, msg string, args ...any) |
| } |
| |
| // TagVisitor declares a set of interfaces for handling custom tags which would otherwise be unsupported |
| // within the policy, rule, match, or variable objects. |
| type TagVisitor interface { |
| // PolicyTag accepts a parser context, field id, tag name, yaml node, and parent Policy to allow for |
| // continued parsing within a custom tag. |
| PolicyTag(ParserContext, int64, string, *yaml.Node, *Policy) |
| |
| // RuleTag accepts a parser context, field id, tag name, yaml node, as well as the parent policy and |
| // current rule to allow for continued parsing within custom tags. |
| RuleTag(ParserContext, int64, string, *yaml.Node, *Policy, *Rule) |
| |
| // MatchTag accepts a parser context, field id, tag name, yaml node, as well as the parent policy and |
| // current match to allow for continued parsing within custom tags. |
| MatchTag(ParserContext, int64, string, *yaml.Node, *Policy, *Match) |
| |
| // VariableTag accepts a parser context, field id, tag name, yaml node, as well as the parent policy and |
| // current variable to allow for continued parsing within custom tags. |
| VariableTag(ParserContext, int64, string, *yaml.Node, *Policy, *Variable) |
| } |
| |
| // DefaultTagVisitor creates a tag visitor which produces errors for any non-canonical YAML tag. |
| func DefaultTagVisitor() TagVisitor { |
| return defaultTagVisitor{} |
| } |
| |
| type defaultTagVisitor struct{} |
| |
| func (defaultTagVisitor) PolicyTag(ctx ParserContext, id int64, tagName string, node *yaml.Node, p *Policy) { |
| if tagName == "description" { |
| return |
| } |
| ctx.ReportErrorAtID(id, "unsupported policy tag: %s", tagName) |
| } |
| |
| func (defaultTagVisitor) RuleTag(ctx ParserContext, id int64, tagName string, node *yaml.Node, p *Policy, r *Rule) { |
| ctx.ReportErrorAtID(id, "unsupported rule tag: %s", tagName) |
| } |
| |
| func (defaultTagVisitor) MatchTag(ctx ParserContext, id int64, tagName string, node *yaml.Node, p *Policy, m *Match) { |
| ctx.ReportErrorAtID(id, "unsupported match tag: %s", tagName) |
| } |
| |
| func (defaultTagVisitor) VariableTag(ctx ParserContext, id int64, tagName string, node *yaml.Node, p *Policy, v *Variable) { |
| ctx.ReportErrorAtID(id, "unsupported variable tag: %s", tagName) |
| } |
| |
| // Parser parses policy files into a canonical Policy representation. |
| type Parser struct { |
| TagVisitor |
| |
| inlineStyleVariables bool |
| } |
| |
| // ParserOption is a function parser option for configuring Parser behavior. |
| type ParserOption func(*Parser) (*Parser, error) |
| |
| // SimpleVariables configures the parser to allow for key-value pairs to declare a variable name and |
| // expression. |
| // |
| // For example: |
| // |
| // variables: |
| // - foo: bar |
| // - baz: qux |
| // |
| // This is in contrast to the default behavior, which requires the following syntax: |
| // |
| // variables: |
| // - name: foo |
| // expression: bar |
| // - name: baz |
| // expression: qux |
| func SimpleVariables() ParserOption { |
| return func(p *Parser) (*Parser, error) { |
| p.inlineStyleVariables = true |
| return p, nil |
| } |
| } |
| |
| // CustomTagVisitor configures the parser to use the given TagVisitor for handling custom tags. |
| func CustomTagVisitor(visitor TagVisitor) ParserOption { |
| return func(p *Parser) (*Parser, error) { |
| p.TagVisitor = visitor |
| return p, nil |
| } |
| } |
| |
| // NewParser creates a new Parser object with a set of functional options. |
| func NewParser(opts ...ParserOption) (*Parser, error) { |
| p := &Parser{TagVisitor: defaultTagVisitor{}} |
| var err error |
| for _, o := range opts { |
| p, err = o(p) |
| if err != nil { |
| return nil, err |
| } |
| } |
| return p, nil |
| } |
| |
| // Parse generates an internal parsed policy representation from a YAML input file. |
| // The internal representation ensures that CEL expressions are tracked relative to |
| // where they occur within the file, thus making error messages relative to the whole |
| // file rather than the individual expression. |
| func (parser *Parser) Parse(src *Source) (*Policy, *cel.Issues) { |
| info := ast.NewSourceInfo(src) |
| errs := common.NewErrors(src) |
| iss := cel.NewIssuesWithSourceInfo(errs, info) |
| p := newParserImpl(parser.TagVisitor, info, src, iss, parser.inlineStyleVariables) |
| policy := p.parseYAML(src) |
| if iss.Err() != nil { |
| return nil, iss |
| } |
| return policy, nil |
| } |
| |
| // normalizeEntry extracts a key, value pair as the next two elements from the |
| // content slice. val source position information is normalized depending on style. |
| func normalizeEntry(content []*yaml.Node, i int) (key *yaml.Node, val *yaml.Node) { |
| key = content[i] |
| val = content[i+1] |
| if val.Style == yaml.FoldedStyle || val.Style == yaml.LiteralStyle { |
| val.Line++ |
| val.Column = key.Column + 1 |
| } |
| return key, val |
| } |
| |
| func (p *parserImpl) parseYAML(src *Source) *Policy { |
| // Parse yaml representation from the source to an object model. |
| var docNode yaml.Node |
| err := sourceToYAML(src, &docNode) |
| if err != nil { |
| p.iss.ReportErrorAtID(0, "%s", err.Error()) |
| return nil |
| } |
| // Entry point always has a single Content node |
| return p.ParsePolicy(p, docNode.Content[0]) |
| } |
| |
| func sourceToYAML(src *Source, docNode *yaml.Node) error { |
| err := yaml.Unmarshal([]byte(src.Content()), docNode) |
| if err != nil { |
| return err |
| } |
| if docNode.Kind != yaml.DocumentNode { |
| return fmt.Errorf("got yaml node of kind %v, wanted mapping node", docNode.Kind) |
| } |
| return nil |
| } |
| |
| func newParserImpl(visitor TagVisitor, info *ast.SourceInfo, src *Source, iss *cel.Issues, arrayStyleVariables bool) *parserImpl { |
| return &parserImpl{ |
| visitor: visitor, |
| info: info, |
| src: src, |
| iss: iss, |
| inlineStyleVariables: arrayStyleVariables, |
| } |
| } |
| |
| type parserImpl struct { |
| id int64 |
| visitor TagVisitor |
| info *ast.SourceInfo |
| src *Source |
| iss *cel.Issues |
| |
| YAMLHelper |
| inlineStyleVariables bool |
| } |
| |
| // NextID returns a monotonically increasing identifier for a source fragment. |
| // This ID is implicitly created and tracked within the CollectMetadata method. |
| func (p *parserImpl) NextID() int64 { |
| p.id++ |
| return p.id |
| } |
| |
| // NewPolicy creates a new Policy instance with an ID associated with the YAML node. |
| func (p *parserImpl) NewPolicy(node *yaml.Node) (*Policy, int64) { |
| policy := NewPolicy(p.src, p.info) |
| id := p.CollectMetadata(node) |
| return policy, id |
| } |
| |
| // NewRule creates a new Rule instance with an ID associated with the YAML node. |
| func (p *parserImpl) NewRule(node *yaml.Node) (*Rule, int64) { |
| id := p.CollectMetadata(node) |
| r := NewRule(id) |
| return r, id |
| } |
| |
| // NewVariable creates a new Variable instance with an ID associated with the YAML node. |
| func (p *parserImpl) NewVariable(node *yaml.Node) (*Variable, int64) { |
| id := p.CollectMetadata(node) |
| v := NewVariable(id) |
| return v, id |
| } |
| |
| // NewMatch creates a new Match instance with an ID associated with the YAML node. |
| func (p *parserImpl) NewMatch(node *yaml.Node) (*Match, int64) { |
| id := p.CollectMetadata(node) |
| m := NewMatch(id) |
| return m, id |
| } |
| |
| // NewString creates a new ValueString from the YAML node. |
| func (p *parserImpl) NewString(node *yaml.Node) ValueString { |
| id := p.CollectMetadata(node) |
| nodeType := p.assertYAMLType(id, node, yamlString, yamlText) |
| if nodeType == nil { |
| return ValueString{ID: id, Value: "*error*"} |
| } |
| if *nodeType == yamlText { |
| return ValueString{ID: id, Value: node.Value} |
| } |
| if node.Style == yaml.FoldedStyle || node.Style == yaml.LiteralStyle { |
| col := node.Column |
| line := node.Line |
| txt, found := p.src.Snippet(line) |
| indent := "" |
| for len(indent) < col-1 { |
| indent += " " |
| } |
| var raw strings.Builder |
| for found && strings.HasPrefix(txt, indent) { |
| line++ |
| raw.WriteString(txt) |
| txt, found = p.src.Snippet(line) |
| if found && strings.HasPrefix(txt, indent) { |
| raw.WriteString("\n") |
| } |
| } |
| offset := p.info.OffsetRanges()[p.id] |
| offsetStart := offset.Start - (int32(node.Column) - 1) |
| p.info.SetOffsetRange(p.id, ast.OffsetRange{Start: offsetStart, Stop: offsetStart}) |
| return ValueString{ID: id, Value: raw.String()} |
| } |
| return ValueString{ID: id, Value: node.Value} |
| } |
| |
| // newStrictString creates a new ValueString from the YAML node, but as a string with no special |
| // source position information. Intended for use in descriptions, where the string is just |
| // a human-readable string for presentation. |
| func (p *parserImpl) newStrictString(node *yaml.Node) ValueString { |
| id := p.CollectMetadata(node) |
| nodeType := p.assertYAMLType(id, node, yamlString, yamlText) |
| if nodeType == nil { |
| return ValueString{ID: id, Value: "*error*"} |
| } |
| return ValueString{ID: id, Value: node.Value} |
| } |
| |
| // CollectMetadata records the source position information of a given YAML node, and returns |
| // the id associated with the source metadata which is returned in the Policy SourceInfo object. |
| func (p *parserImpl) CollectMetadata(node *yaml.Node) int64 { |
| id := p.NextID() |
| line := node.Line |
| col := int32(node.Column) |
| switch node.Style { |
| case yaml.DoubleQuotedStyle, yaml.SingleQuotedStyle: |
| col++ |
| } |
| offsetStart := int32(0) |
| if line > 1 { |
| offsetStart = p.info.LineOffsets()[line-2] |
| } |
| p.info.SetOffsetRange(id, ast.OffsetRange{Start: offsetStart + col - 1, Stop: offsetStart + col - 1}) |
| return id |
| } |
| |
| // ParsePolicy will parse the target yaml node as though it is the top-level policy. |
| func (p *parserImpl) ParsePolicy(ctx ParserContext, node *yaml.Node) *Policy { |
| ctx.CollectMetadata(node) |
| policy, id := ctx.NewPolicy(node) |
| if p.assertYAMLType(id, node, yamlMap) == nil || !p.checkMapValid(ctx, id, node) { |
| return policy |
| } |
| p.RangeMap(node, func(key, val *yaml.Node) bool { |
| keyID := p.CollectMetadata(key) |
| fieldName := key.Value |
| switch fieldName { |
| case "imports": |
| p.parseImports(ctx, policy, val) |
| case "name": |
| policy.SetName(ctx.NewString(val)) |
| case "description": |
| policy.SetDescription(p.newStrictString(val)) |
| // Since the description field was not supported initially, some |
| // clients rely on the ability to intercept it. |
| p.visitor.PolicyTag(ctx, keyID, fieldName, val, policy) |
| case "rule": |
| policy.SetRule(p.ParseRule(ctx, policy, val)) |
| default: |
| p.visitor.PolicyTag(ctx, keyID, fieldName, val, policy) |
| } |
| return true |
| }) |
| return policy |
| } |
| |
| func (p *parserImpl) parseImports(ctx ParserContext, policy *Policy, node *yaml.Node) { |
| id := ctx.CollectMetadata(node) |
| if p.assertYAMLType(id, node, yamlList) == nil { |
| return |
| } |
| for _, val := range node.Content { |
| policy.AddImport(p.parseImport(ctx, policy, val)) |
| } |
| } |
| |
| func (p *parserImpl) parseImport(ctx ParserContext, _ *Policy, node *yaml.Node) *Import { |
| id := ctx.CollectMetadata(node) |
| imp := NewImport(id) |
| if p.assertYAMLType(id, node, yamlMap) == nil || !p.checkMapValid(ctx, id, node) { |
| return imp |
| } |
| p.RangeMap(node, func(key *yaml.Node, val *yaml.Node) bool { |
| ctx.CollectMetadata(key) |
| fieldName := key.Value |
| switch fieldName { |
| case "name": |
| imp.SetName(ctx.NewString(val)) |
| } |
| return true |
| }) |
| return imp |
| } |
| |
| // ParseRule will parse the current yaml node as though it is the entry point to a rule. |
| func (p *parserImpl) ParseRule(ctx ParserContext, policy *Policy, node *yaml.Node) *Rule { |
| r, id := ctx.NewRule(node) |
| if p.assertYAMLType(id, node, yamlMap) == nil || !p.checkMapValid(ctx, id, node) { |
| return r |
| } |
| p.RangeMap(node, func(key, val *yaml.Node) bool { |
| tagID := ctx.CollectMetadata(key) |
| fieldName := key.Value |
| switch fieldName { |
| case "id": |
| r.SetID(ctx.NewString(val)) |
| case "description": |
| r.SetDescription(ctx.NewString(val)) |
| case "variables": |
| p.parseVariables(ctx, policy, r, val) |
| case "match": |
| p.parseMatches(ctx, policy, r, val) |
| default: |
| p.visitor.RuleTag(ctx, tagID, fieldName, val, policy, r) |
| } |
| return true |
| }) |
| return r |
| } |
| |
| func (p *parserImpl) parseVariables(ctx ParserContext, policy *Policy, r *Rule, node *yaml.Node) { |
| id := ctx.CollectMetadata(node) |
| if p.assertYAMLType(id, node, yamlList) == nil { |
| return |
| } |
| p.RangeList(node, func(i int, val *yaml.Node) bool { |
| r.AddVariable(p.ParseVariable(ctx, policy, val)) |
| return true |
| }) |
| } |
| |
| // ParseVariable will parse the current yaml node as though it is the entry point to a variable. |
| func (p *parserImpl) ParseVariable(ctx ParserContext, policy *Policy, node *yaml.Node) *Variable { |
| v, id := ctx.NewVariable(node) |
| if p.assertYAMLType(id, node, yamlMap) == nil || !p.checkMapValid(ctx, id, node) { |
| return v |
| } |
| if p.inlineStyleVariables { |
| return p.parseVariableInline(ctx, policy, v, node) |
| } |
| return p.parseVariableObject(ctx, policy, v, node) |
| } |
| |
| func (p *parserImpl) parseVariableInline(ctx ParserContext, policy *Policy, v *Variable, node *yaml.Node) *Variable { |
| iterations := 0 |
| p.RangeMap(node, func(key, val *yaml.Node) bool { |
| keyVal := ctx.NewString(key) |
| v.SetName(keyVal) |
| v.SetExpression(ctx.NewString(val)) |
| iterations++ |
| if iterations > 1 { |
| p.ReportErrorAtID(keyVal.ID, "only one variable may be defined inline") |
| return false |
| } |
| return true |
| }) |
| return v |
| } |
| |
| func (p *parserImpl) parseVariableObject(ctx ParserContext, policy *Policy, v *Variable, node *yaml.Node) *Variable { |
| p.RangeMap(node, func(key, val *yaml.Node) bool { |
| keyID := ctx.CollectMetadata(key) |
| fieldName := key.Value |
| switch fieldName { |
| case "name": |
| v.SetName(ctx.NewString(val)) |
| case "expression": |
| v.SetExpression(ctx.NewString(val)) |
| default: |
| p.visitor.VariableTag(ctx, keyID, fieldName, val, policy, v) |
| } |
| return true |
| }) |
| return v |
| } |
| |
| func (p *parserImpl) parseMatches(ctx ParserContext, policy *Policy, r *Rule, node *yaml.Node) { |
| id := ctx.CollectMetadata(node) |
| if p.assertYAMLType(id, node, yamlList) == nil { |
| return |
| } |
| for _, val := range node.Content { |
| r.AddMatch(p.ParseMatch(ctx, policy, val)) |
| } |
| } |
| |
| // ParseMatch will parse the current yaml node as though it is the entry point to a match. |
| func (p *parserImpl) ParseMatch(ctx ParserContext, policy *Policy, node *yaml.Node) *Match { |
| m, id := ctx.NewMatch(node) |
| if p.assertYAMLType(id, node, yamlMap) == nil || !p.checkMapValid(ctx, id, node) { |
| return m |
| } |
| m.SetCondition(ValueString{ID: ctx.NextID(), Value: "true"}) |
| p.RangeMap(node, func(key, val *yaml.Node) bool { |
| keyID := ctx.CollectMetadata(key) |
| fieldName := key.Value |
| switch fieldName { |
| case "condition": |
| m.SetCondition(ctx.NewString(val)) |
| case "output": |
| if m.HasRule() { |
| p.ReportErrorAtID(keyID, "only the rule or the output may be set") |
| } |
| m.SetOutput(ctx.NewString(val)) |
| case "explanation": |
| if m.HasRule() { |
| p.ReportErrorAtID(keyID, "explanation can only be set on output match cases, not nested rules") |
| } |
| m.SetExplanation(ctx.NewString(val)) |
| case "rule": |
| if m.HasOutput() { |
| p.ReportErrorAtID(keyID, "only the rule or the output may be set") |
| } |
| if m.HasExplanation() { |
| p.ReportErrorAtID(keyID, "explanation can only be set on output match cases, not nested rules") |
| } |
| m.SetRule(p.ParseRule(ctx, policy, val)) |
| default: |
| p.visitor.MatchTag(ctx, keyID, fieldName, val, policy, m) |
| } |
| return true |
| }) |
| if !m.HasOutput() && !m.HasRule() { |
| p.ReportErrorAtID(id, "match does not specify a rule or output") |
| } |
| return m |
| } |
| |
| func (p *parserImpl) assertYAMLType(id int64, node *yaml.Node, nodeTypes ...yamlNodeType) *yamlNodeType { |
| nt, found := yamlTypes[node.LongTag()] |
| if !found { |
| p.ReportErrorAtID(id, "unsupported yaml tag type: %v", node.LongTag()) |
| return nil |
| } |
| for _, nodeType := range nodeTypes { |
| if nt == nodeType { |
| return &nt |
| } |
| } |
| p.ReportErrorAtID(id, "got yaml node type %v, wanted type(s) %v", node.LongTag(), nodeTypes) |
| return nil |
| } |
| |
| // ReportErrorAtID logs an error during parsing which is included in the issue set returned from |
| // a failed parse. |
| func (p *parserImpl) ReportErrorAtID(id int64, format string, args ...any) { |
| p.iss.ReportErrorAtID(id, format, args...) |
| } |
| |
| func (p *parserImpl) checkMapValid(ctx ParserContext, id int64, node *yaml.Node) bool { |
| valid := len(node.Content)%2 == 0 |
| if !valid { |
| ctx.ReportErrorAtID(id, "mismatched key-value pairs in map") |
| } |
| return valid |
| } |
| |
| type yamlNodeType int |
| |
| const ( |
| yamlText yamlNodeType = iota + 1 |
| yamlBool |
| yamlNull |
| yamlString |
| yamlInt |
| yamlDouble |
| yamlList |
| yamlMap |
| yamlTimestamp |
| ) |
| |
| func (yt yamlNodeType) String() string { |
| for k, v := range yamlTypes { |
| if v == yt { |
| return k |
| } |
| } |
| return fmt.Sprintf("%d", yt) |
| } |
| |
| var ( |
| // yamlTypes map of the long tag names supported by the Go YAML v3 library. |
| yamlTypes = map[string]yamlNodeType{ |
| "!txt": yamlText, |
| "tag:yaml.org,2002:bool": yamlBool, |
| "tag:yaml.org,2002:null": yamlNull, |
| "tag:yaml.org,2002:str": yamlString, |
| "tag:yaml.org,2002:int": yamlInt, |
| "tag:yaml.org,2002:float": yamlDouble, |
| "tag:yaml.org,2002:seq": yamlList, |
| "tag:yaml.org,2002:map": yamlMap, |
| "tag:yaml.org,2002:timestamp": yamlTimestamp, |
| } |
| ) |