bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
82 lines
1.9 KiB
Go
82 lines
1.9 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package contentsafety
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"sort"
|
|
"sync"
|
|
|
|
extcs "github.com/larksuite/cli/extension/contentsafety"
|
|
"github.com/larksuite/cli/internal/core"
|
|
)
|
|
|
|
// regexProvider implements extcs.Provider using regex rules from config file.
|
|
// Config is loaded on every Scan() call (no caching) so changes take
|
|
// effect immediately. mu serializes lazy config creation.
|
|
type regexProvider struct {
|
|
configDir string
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func (p *regexProvider) Name() string { return "regex" }
|
|
|
|
func (p *regexProvider) Scan(ctx context.Context, req extcs.ScanRequest) (*extcs.Alert, error) {
|
|
cfg, err := p.loadOrCreate(req.ErrOut)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !IsAllowlisted(req.Path, cfg.Allowlist) {
|
|
return nil, nil
|
|
}
|
|
if len(cfg.Rules) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
data := normalize(req.Data)
|
|
s := &scanner{rules: cfg.Rules}
|
|
hits := make(map[string]struct{})
|
|
s.walk(ctx, data, hits, 0)
|
|
|
|
if len(hits) == 0 {
|
|
return nil, nil
|
|
}
|
|
matched := make([]string, 0, len(hits))
|
|
for id := range hits {
|
|
matched = append(matched, id)
|
|
}
|
|
sort.Strings(matched)
|
|
return &extcs.Alert{Provider: p.Name(), MatchedRules: matched}, nil
|
|
}
|
|
|
|
// loadOrCreate loads config, creating the default on first use.
|
|
// mu serializes creation so concurrent Scan calls don't race on first-use.
|
|
func (p *regexProvider) loadOrCreate(errOut io.Writer) (*Config, error) {
|
|
cfg, err := LoadConfig(p.configDir)
|
|
if err == nil {
|
|
return cfg, nil
|
|
}
|
|
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
// Re-check after acquiring the lock (another goroutine may have created it).
|
|
cfg, err = LoadConfig(p.configDir)
|
|
if err == nil {
|
|
return cfg, nil
|
|
}
|
|
if errC := EnsureDefaultConfig(p.configDir, errOut); errC != nil {
|
|
return nil, err
|
|
}
|
|
return LoadConfig(p.configDir)
|
|
}
|
|
|
|
func init() {
|
|
extcs.Register(®exProvider{
|
|
configDir: core.GetConfigDir(),
|
|
})
|
|
}
|