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
116 lines
3.3 KiB
Go
116 lines
3.3 KiB
Go
package astquery
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/pelletier/go-toml/v2"
|
|
)
|
|
|
|
// userRuleFile is the TOML schema for a pluggable domain-extractor
|
|
// rule file:
|
|
//
|
|
// [[rule]]
|
|
// name = "feature-flag-register"
|
|
// description = "calls to flags.Register"
|
|
// domain = "feature_flag"
|
|
// severity = "info"
|
|
// tags = ["flags"]
|
|
// [rule.query]
|
|
// go = '(call_expression function: (selector_expression ...)) @match'
|
|
type userRuleFile struct {
|
|
Rule []userRule `toml:"rule"`
|
|
}
|
|
|
|
// userRule is one TOML-defined domain-extractor rule. Each becomes an
|
|
// astquery Detector registered under Category "domain", so the whole
|
|
// rule-execution and `analyze kind=domain` surface is reused.
|
|
type userRule struct {
|
|
Name string `toml:"name"`
|
|
Description string `toml:"description"`
|
|
// Domain labels the rule's purpose — http_route / cli_command /
|
|
// feature_flag / i18n / event_bus / custom. It becomes a tag so
|
|
// `analyze kind=domain tag:<domain>` filters to it.
|
|
Domain string `toml:"domain"`
|
|
Severity string `toml:"severity"`
|
|
Tags []string `toml:"tags"`
|
|
// Query maps a language ("go", "python", …) to a tree-sitter
|
|
// S-expression. A capture named `match` anchors the row.
|
|
Query map[string]string `toml:"query"`
|
|
}
|
|
|
|
// LoadUserRulesData parses TOML domain-extractor rule definitions and
|
|
// registers each as an astquery Detector under Category "domain".
|
|
// Returns the number of rules registered. A parse error or an invalid
|
|
// rule aborts the load — already-registered rules from the same file
|
|
// stay registered.
|
|
func LoadUserRulesData(data []byte) (int, error) {
|
|
var f userRuleFile
|
|
if err := toml.Unmarshal(data, &f); err != nil {
|
|
return 0, fmt.Errorf("astquery: parse user rules: %w", err)
|
|
}
|
|
n := 0
|
|
for _, r := range f.Rule {
|
|
d, err := r.toDetector()
|
|
if err != nil {
|
|
return n, err
|
|
}
|
|
RegisterDetector(d)
|
|
n++
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// LoadUserRulesFile reads a TOML rule file and registers its rules.
|
|
func LoadUserRulesFile(path string) (int, error) {
|
|
data, err := os.ReadFile(path) //nolint:gosec // path is operator-supplied config
|
|
if err != nil {
|
|
return 0, fmt.Errorf("astquery: read user rules %s: %w", path, err)
|
|
}
|
|
return LoadUserRulesData(data)
|
|
}
|
|
|
|
// toDetector converts a TOML rule into a registrable Detector. The
|
|
// detector name is prefixed `user:` so a user rule can never clobber a
|
|
// bundled SAST / hygiene rule.
|
|
func (r userRule) toDetector() (*Detector, error) {
|
|
name := strings.TrimSpace(r.Name)
|
|
if name == "" {
|
|
return nil, fmt.Errorf("astquery: user rule missing name")
|
|
}
|
|
langs := map[string]string{}
|
|
for lang, q := range r.Query {
|
|
lang = strings.ToLower(strings.TrimSpace(lang))
|
|
q = strings.TrimSpace(q)
|
|
if lang != "" && q != "" {
|
|
langs[lang] = q
|
|
}
|
|
}
|
|
if len(langs) == 0 {
|
|
return nil, fmt.Errorf("astquery: user rule %q has no query patterns", name)
|
|
}
|
|
domain := strings.TrimSpace(r.Domain)
|
|
if domain == "" {
|
|
domain = "custom"
|
|
}
|
|
severity := strings.TrimSpace(r.Severity)
|
|
if severity == "" {
|
|
severity = "info"
|
|
}
|
|
desc := strings.TrimSpace(r.Description)
|
|
if desc == "" {
|
|
desc = "user-defined " + domain + " rule"
|
|
}
|
|
tags := append([]string{"user-rule", domain}, r.Tags...)
|
|
return &Detector{
|
|
Name: "user:" + name,
|
|
Description: desc,
|
|
Severity: severity,
|
|
Category: "domain",
|
|
Tags: tags,
|
|
Languages: langs,
|
|
ExcludeTests: false,
|
|
}, nil
|
|
}
|