-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtemplate.go
More file actions
174 lines (160 loc) · 5.21 KB
/
Copy pathtemplate.go
File metadata and controls
174 lines (160 loc) · 5.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
package inspect
import (
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
// Template is the YAML representation of a declarative RuleCheck. A single YAML
// file may contain one template (a mapping) or several (a top-level "templates"
// list). This is inspect's equivalent of Nuclei templates: define a check with
// pattern matching, no Go code required.
//
// Example:
//
// name: missing-hsts
// severity: high
// description: Strict-Transport-Security header is missing
// header_missing: [Strict-Transport-Security]
// fix: Add a Strict-Transport-Security response header
type Template struct {
Name string `yaml:"name"`
Severity string `yaml:"severity"`
Description string `yaml:"description"`
HeaderMatch map[string]string `yaml:"header_match,omitempty"`
HeaderMissing []string `yaml:"header_missing,omitempty"`
BodyMatch []string `yaml:"body_match,omitempty"`
BodyMissing []string `yaml:"body_missing,omitempty"`
URLMatch string `yaml:"url_match,omitempty"`
StatusCodes []int `yaml:"status_codes,omitempty"`
Fix string `yaml:"fix,omitempty"`
}
// templateFile is the on-disk schema: either a single template or a list under
// "templates".
type templateFile struct {
Templates []Template `yaml:"templates"`
// Inline single-template fields (used when the file is one template).
Template `yaml:",inline"`
}
// toRuleCheck converts a Template into a RuleCheck, validating required fields.
func (t Template) toRuleCheck() (RuleCheck, error) {
if strings.TrimSpace(t.Name) == "" {
return RuleCheck{}, fmt.Errorf("inspect: template missing required field 'name'")
}
if t.Description == "" {
t.Description = t.Name
}
if !t.hasCondition() {
return RuleCheck{}, fmt.Errorf("inspect: template %q has no match conditions", t.Name)
}
sev := SeverityMedium
if t.Severity != "" {
sev = ParseSeverity(t.Severity)
}
return RuleCheck{
RuleName: t.Name,
RuleSeverity: sev,
Description: t.Description,
HeaderMatch: t.HeaderMatch,
HeaderMissing: t.HeaderMissing,
BodyMatch: t.BodyMatch,
BodyMissing: t.BodyMissing,
URLMatch: t.URLMatch,
StatusCodes: t.StatusCodes,
FixSuggestion: t.Fix,
}, nil
}
func (t Template) hasCondition() bool {
return len(t.HeaderMatch) > 0 || len(t.HeaderMissing) > 0 ||
len(t.BodyMatch) > 0 || len(t.BodyMissing) > 0
}
// ParseTemplates decodes one or more templates from YAML bytes into RuleChecks.
func ParseTemplates(data []byte) ([]RuleCheck, error) {
var tf templateFile
if err := yaml.Unmarshal(data, &tf); err != nil {
return nil, fmt.Errorf("inspect: parse template YAML: %w", err)
}
templates := tf.Templates
// If the file used the single-template form, the inline fields are populated.
if len(templates) == 0 && tf.Template.Name != "" {
templates = []Template{tf.Template}
}
if len(templates) == 0 {
return nil, fmt.Errorf("inspect: no templates found in YAML")
}
rules := make([]RuleCheck, 0, len(templates))
for _, t := range templates {
rc, err := t.toRuleCheck()
if err != nil {
return nil, err
}
rules = append(rules, rc)
}
return rules, nil
}
// LoadTemplateFile reads and parses a single YAML template file.
func LoadTemplateFile(path string) ([]RuleCheck, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("inspect: read template %s: %w", path, err)
}
return ParseTemplates(data)
}
// LoadTemplateDir loads all .yaml/.yml templates in a directory (non-recursive).
func LoadTemplateDir(dir string) ([]RuleCheck, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("inspect: read template dir %s: %w", dir, err)
}
var rules []RuleCheck
for _, e := range entries {
if e.IsDir() {
continue
}
ext := strings.ToLower(filepath.Ext(e.Name()))
if ext != ".yaml" && ext != ".yml" {
continue
}
fileRules, err := LoadTemplateFile(filepath.Join(dir, e.Name()))
if err != nil {
return nil, err
}
rules = append(rules, fileRules...)
}
return rules, nil
}
// WithTemplateFile loads declarative check templates from a YAML file and
// registers them as scan-scoped rules. A parse error is surfaced lazily as a
// rule named "<template-load-error>" so callers using the functional-option
// form still observe the failure during scanning configuration.
func WithTemplateFile(path string) Option {
return optFunc(func(c *config) {
rules, err := LoadTemplateFile(path)
if err != nil {
c.customRules = append(c.customRules, errorRule(err))
return
}
c.customRules = append(c.customRules, rules...)
})
}
// WithTemplateDir loads all YAML templates from a directory as scan-scoped rules.
func WithTemplateDir(dir string) Option {
return optFunc(func(c *config) {
rules, err := LoadTemplateDir(dir)
if err != nil {
c.customRules = append(c.customRules, errorRule(err))
return
}
c.customRules = append(c.customRules, rules...)
})
}
// errorRule produces a no-op rule that records a template loading failure so it
// is visible rather than silently dropped.
func errorRule(err error) RuleCheck {
return RuleCheck{
RuleName: "<template-load-error>",
RuleSeverity: SeverityInfo,
Description: err.Error(),
}
}