-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconvention_check.go
More file actions
170 lines (152 loc) · 4.71 KB
/
convention_check.go
File metadata and controls
170 lines (152 loc) · 4.71 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
package sight
import (
"fmt"
"regexp"
"strings"
)
// ConventionChecker validates diffs against a set of project conventions.
// Integrates with yaad: load conventions from yaad's memory graph and
// check if the diff violates any of them.
type ConventionChecker struct {
conventions []Convention
}
// Convention is a coding rule to enforce during review.
type Convention struct {
Name string
Description string
Pattern string // regex pattern that indicates violation
FilePattern string // only check files matching this glob
Severity Severity
compiledRe *regexp.Regexp // pre-compiled pattern
}
// NewConventionChecker creates a checker with the given conventions.
func NewConventionChecker(conventions []Convention) *ConventionChecker {
// Pre-compile regex patterns for performance.
for i := range conventions {
if conventions[i].Pattern != "" {
if re, err := regexp.Compile(conventions[i].Pattern); err == nil {
conventions[i].compiledRe = re
}
}
}
return &ConventionChecker{conventions: conventions}
}
// FromStrings creates conventions from simple string rules (e.g., from yaad memories).
func ConventionsFromStrings(rules []string) []Convention {
var conventions []Convention
for _, rule := range rules {
conv := parseConvention(rule)
if conv.Name != "" {
conventions = append(conventions, conv)
}
}
return conventions
}
// Check validates a diff against all conventions and returns findings.
func (cc *ConventionChecker) Check(diff string) []Finding {
if len(cc.conventions) == 0 {
return nil
}
var findings []Finding
lines := strings.Split(diff, "\n")
currentFile := ""
for lineNum, line := range lines {
if strings.HasPrefix(line, "+++ b/") {
currentFile = strings.TrimPrefix(line, "+++ b/")
continue
}
// Only check added lines
if !strings.HasPrefix(line, "+") || strings.HasPrefix(line, "+++") {
continue
}
addedContent := line[1:]
for _, conv := range cc.conventions {
if conv.FilePattern != "" && !matchGlob(currentFile, conv.FilePattern) {
continue
}
if conv.compiledRe != nil {
if conv.compiledRe.MatchString(addedContent) {
findings = append(findings, Finding{
File: currentFile,
Line: lineNum,
Severity: conv.Severity,
Message: fmt.Sprintf("Convention violation: %s — %s", conv.Name, conv.Description),
})
}
}
}
}
return findings
}
func parseConvention(rule string) Convention {
lower := strings.ToLower(rule)
conv := Convention{Severity: SeverityMedium}
// "Never use X" → detect X in added lines
if idx := strings.Index(lower, "never use "); idx >= 0 {
thing := extractPhrase(lower[idx+10:])
conv.Name = "no-" + strings.ReplaceAll(thing, " ", "-")
conv.Description = rule
conv.Pattern = `(?i)\b` + regexp.QuoteMeta(thing) + `\b`
return conv
}
// "Don't use X" or "Avoid X"
for _, prefix := range []string{"don't use ", "do not use ", "avoid "} {
if idx := strings.Index(lower, prefix); idx >= 0 {
thing := extractPhrase(lower[idx+len(prefix):])
conv.Name = "avoid-" + strings.ReplaceAll(thing, " ", "-")
conv.Description = rule
conv.Pattern = `(?i)\b` + regexp.QuoteMeta(thing) + `\b`
return conv
}
}
// "Use X not Y" → detect Y
if strings.Contains(lower, " not ") {
parts := strings.SplitN(lower, " not ", 2)
if len(parts) == 2 {
bad := extractPhrase(parts[1])
conv.Name = "prefer-alternative"
conv.Description = rule
conv.Pattern = `(?i)\b` + regexp.QuoteMeta(bad) + `\b`
return conv
}
}
return conv
}
func extractPhrase(s string) string {
s = strings.TrimSpace(s)
// Take up to first punctuation or end
end := strings.IndexAny(s, ".,;!?()")
if end > 0 {
s = s[:end]
}
if len(s) > 40 {
s = s[:40]
}
return strings.TrimSpace(s)
}
func matchGlob(path, pattern string) bool {
// Simple glob matching: * matches any sequence
if pattern == "*" {
return true
}
if strings.HasPrefix(pattern, "*.") {
ext := pattern[1:]
return strings.HasSuffix(path, ext)
}
return strings.Contains(path, pattern)
}
// SecurityConcerns returns security-focused review concerns.
func SecurityConcerns() []string {
return []string{
"SQL injection vulnerabilities (unsanitized user input in queries)",
"XSS vulnerabilities (unescaped output in HTML templates)",
"Command injection (user input in exec/system calls)",
"Path traversal (user input in file paths without sanitization)",
"Hardcoded secrets (API keys, passwords, tokens in source)",
"Insecure crypto (MD5, SHA1 for security, weak random)",
"Missing authentication/authorization checks",
"SSRF vulnerabilities (user-controlled URLs in server requests)",
"Race conditions (shared state without synchronization)",
"Resource leaks (unclosed connections, files, channels)",
}
}