mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 06:21:10 +00:00
188 lines
4.4 KiB
Go
188 lines
4.4 KiB
Go
package tools
|
|
|
|
import (
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/config"
|
|
)
|
|
|
|
type commandKeywordAllowAudit struct {
|
|
RuleID string
|
|
Command string
|
|
Subcommand string
|
|
Arg string
|
|
Keyword string
|
|
Reason string
|
|
}
|
|
|
|
func applyCommandKeywordAllowlist(command string, args []string, rules []config.CommandKeywordAllowlistRule) ([]string, []commandKeywordAllowAudit) {
|
|
if len(args) == 0 || len(rules) == 0 {
|
|
return args, nil
|
|
}
|
|
|
|
var out []string
|
|
var audits []commandKeywordAllowAudit
|
|
for _, rule := range rules {
|
|
if !commandKeywordAllowlistRuleEnabled(rule) {
|
|
continue
|
|
}
|
|
if normalizeBinaryName(rule.Command) != normalizeBinaryName(command) {
|
|
continue
|
|
}
|
|
subcommand, argStart, ok := matchCommandKeywordSubcommand(args, rule.Subcommands)
|
|
if !ok {
|
|
continue
|
|
}
|
|
argNames := commandKeywordSet(rule.Args)
|
|
argPositions := commandKeywordPositionSet(rule.ArgPositions)
|
|
keywords := commandKeywordSet(rule.Keywords)
|
|
if (len(argNames) == 0 && len(argPositions) == 0) || len(keywords) == 0 {
|
|
continue
|
|
}
|
|
if len(argPositions) > 0 && subcommand == "" {
|
|
continue
|
|
}
|
|
if out == nil {
|
|
out = slicesClone(args)
|
|
}
|
|
for i := argStart; i < len(out); i++ {
|
|
argName, valueIndex, inlineValue, ok := commandKeywordArgValue(out, i, argNames)
|
|
if !ok {
|
|
if _, positional := argPositions[i-argStart]; positional {
|
|
argName = "argPositions[" + strconv.Itoa(i-argStart) + "]"
|
|
valueIndex = i
|
|
inlineValue = out[i]
|
|
ok = true
|
|
}
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
value := inlineValue
|
|
if valueIndex != i {
|
|
value = out[valueIndex]
|
|
}
|
|
masked, hits := maskCommandKeywords(value, keywords)
|
|
if len(hits) == 0 {
|
|
continue
|
|
}
|
|
if valueIndex == i {
|
|
prefix := out[i][:strings.Index(out[i], "=")+1]
|
|
out[i] = prefix + masked
|
|
} else {
|
|
out[valueIndex] = masked
|
|
i = valueIndex
|
|
}
|
|
for _, keyword := range hits {
|
|
audits = append(audits, commandKeywordAllowAudit{
|
|
RuleID: rule.ID,
|
|
Command: normalizeBinaryName(command),
|
|
Subcommand: subcommand,
|
|
Arg: argName,
|
|
Keyword: keyword,
|
|
Reason: rule.Reason,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
if out == nil {
|
|
return args, audits
|
|
}
|
|
return out, audits
|
|
}
|
|
|
|
func commandKeywordAllowlistRuleEnabled(rule config.CommandKeywordAllowlistRule) bool {
|
|
return rule.Enabled == nil || *rule.Enabled
|
|
}
|
|
|
|
func matchCommandKeywordSubcommand(args []string, subcommands []string) (string, int, bool) {
|
|
if len(subcommands) == 0 {
|
|
return "", 0, true
|
|
}
|
|
for _, subcommand := range subcommands {
|
|
parts := strings.Fields(strings.ToLower(strings.TrimSpace(subcommand)))
|
|
if len(parts) == 0 || len(parts) > len(args) {
|
|
continue
|
|
}
|
|
matched := true
|
|
for i, part := range parts {
|
|
if strings.ToLower(args[i]) != part {
|
|
matched = false
|
|
break
|
|
}
|
|
}
|
|
if matched {
|
|
return strings.Join(parts, " "), len(parts), true
|
|
}
|
|
}
|
|
return "", 0, false
|
|
}
|
|
|
|
func commandKeywordSet(values []string) map[string]struct{} {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
set := make(map[string]struct{}, len(values))
|
|
for _, value := range values {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
if value != "" {
|
|
set[value] = struct{}{}
|
|
}
|
|
}
|
|
return set
|
|
}
|
|
|
|
func commandKeywordPositionSet(values []int) map[int]struct{} {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
set := make(map[int]struct{}, len(values))
|
|
for _, value := range values {
|
|
if value >= 0 {
|
|
set[value] = struct{}{}
|
|
}
|
|
}
|
|
return set
|
|
}
|
|
|
|
func commandKeywordArgValue(args []string, i int, argNames map[string]struct{}) (string, int, string, bool) {
|
|
arg := args[i]
|
|
if eq := strings.Index(arg, "="); eq > 0 {
|
|
name := strings.ToLower(arg[:eq])
|
|
if _, ok := argNames[name]; ok {
|
|
return name, i, arg[eq+1:], true
|
|
}
|
|
return "", 0, "", false
|
|
}
|
|
name := strings.ToLower(arg)
|
|
if _, ok := argNames[name]; !ok || i+1 >= len(args) {
|
|
return "", 0, "", false
|
|
}
|
|
return name, i + 1, "", true
|
|
}
|
|
|
|
func maskCommandKeywords(value string, keywords map[string]struct{}) (string, []string) {
|
|
if value == "" || len(keywords) == 0 {
|
|
return value, nil
|
|
}
|
|
var hits []string
|
|
masked := value
|
|
for keyword := range keywords {
|
|
re := regexp.MustCompile(`(?i)\b` + regexp.QuoteMeta(keyword) + `\b`)
|
|
if !re.MatchString(masked) {
|
|
continue
|
|
}
|
|
hits = append(hits, keyword)
|
|
masked = re.ReplaceAllString(masked, "__allowlisted_keyword__")
|
|
}
|
|
return masked, hits
|
|
}
|
|
|
|
func slicesClone[T any](src []T) []T {
|
|
dst := make([]T, len(src))
|
|
copy(dst, src)
|
|
return dst
|
|
}
|