Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions internal/profiles/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,17 @@ type cliIntent struct {
}

var judgePathPlaceholderPattern = regexp.MustCompile(`\{\{\s*(prompt|output_schema):([^}]+)\}\}`)
var scannerIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]*$`)

// scannerIDPattern restricts user-defined scanner IDs to lowercase. IDs are
// lowercased when used as evidence file names, so allowing uppercase would let
// two case-distinct IDs (Foo and foo) collide on the same output file.
var scannerIDPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]*$`)
var scannerTargetPlaceholderPattern = regexp.MustCompile(`\{\{\s*target\s*\}\}`)

// maxScannerIDLength bounds user-defined scanner IDs so <id>.json evidence file
// names stay within filesystem limits.
const maxScannerIDLength = 64

type ResolvedRunSet struct {
Options []runner.Options
OutputPath string
Expand Down Expand Up @@ -952,7 +960,10 @@ func validateProfile(name string, profile Profile) error {
return fmt.Errorf("User-defined scanner in profile %s must include a non-empty id", name)
}
if scanner.custom && !scannerIDPattern.MatchString(scanner.ID) {
return fmt.Errorf("User-defined scanner %s in profile %s has invalid id; use letters, digits, underscores, and hyphens, starting with a letter or digit", scanner.ID, name)
return fmt.Errorf("User-defined scanner %s in profile %s has invalid id; use lowercase letters, digits, underscores, and hyphens, starting with a letter or digit", scanner.ID, name)
}
if scanner.custom && len(scanner.ID) > maxScannerIDLength {
return fmt.Errorf("User-defined scanner id in profile %s is %d characters; scanner IDs are used as file names and must be at most %d characters", name, len(scanner.ID), maxScannerIDLength)
}
if scanner.custom && strings.TrimSpace(scanner.Command) == "" {
return fmt.Errorf("User-defined scanner %s in profile %s must include a non-empty command", scanner.ID, name)
Expand Down
37 changes: 36 additions & 1 deletion internal/profiles/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,42 @@ profiles:
`)

_, err := ResolveArgs([]string{"./skill", "--config", config, "--profile", "review"}, dir)
if err == nil || err.Error() != "User-defined scanner foo=bar in profile review has invalid id; use letters, digits, underscores, and hyphens, starting with a letter or digit" {
if err == nil || err.Error() != "User-defined scanner foo=bar in profile review has invalid id; use lowercase letters, digits, underscores, and hyphens, starting with a letter or digit" {
t.Fatalf("err = %v", err)
}
}

func TestResolveArgsRejectsUppercaseUserDefinedScannerID(t *testing.T) {
dir := t.TempDir()
config := filepath.Join(dir, ".clawscan.yml")
writeFile(t, config, `version: 1
profiles:
review:
scanners:
- id: Foo
command: scanner {{target}}
`)

_, err := ResolveArgs([]string{"./skill", "--config", config, "--profile", "review"}, dir)
if err == nil || err.Error() != "User-defined scanner Foo in profile review has invalid id; use lowercase letters, digits, underscores, and hyphens, starting with a letter or digit" {
t.Fatalf("err = %v", err)
}
}

func TestResolveArgsRejectsOversizedUserDefinedScannerID(t *testing.T) {
dir := t.TempDir()
config := filepath.Join(dir, ".clawscan.yml")
longID := strings.Repeat("a", 65)
writeFile(t, config, `version: 1
profiles:
review:
scanners:
- id: `+longID+`
command: scanner {{target}}
`)

_, err := ResolveArgs([]string{"./skill", "--config", config, "--profile", "review"}, dir)
if err == nil || err.Error() != "User-defined scanner id in profile review is 65 characters; scanner IDs are used as file names and must be at most 64 characters" {
t.Fatalf("err = %v", err)
}
}
Expand Down
Loading