feat: code security scanner with OWASP pattern detection (#552) #716
@@ -47,15 +47,15 @@ jobs:
|
||||
fi
|
||||
;;
|
||||
fix/*|bugfix/*)
|
||||
if [ "$BASE" != "dev" ]; then
|
||||
if [ "$BASE" != "main" ] && [ "$BASE" != "dev" ]; then
|
||||
ALLOWED=false
|
||||
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||
REASON="Fix branches must target 'main' or 'dev', not '${BASE}'"
|
||||
fi
|
||||
;;
|
||||
patch/*)
|
||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
||||
if [ "$BASE" != "main" ] && [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
||||
ALLOWED=false
|
||||
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
||||
REASON="Patch branches must target 'main', 'dev', or 'rc', not '${BASE}'"
|
||||
fi
|
||||
;;
|
||||
hotfix/*)
|
||||
@@ -86,10 +86,11 @@ jobs:
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`fix/*\` → \`main\` or \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`patch/*\` → \`main\`, \`dev\`, or \`rc\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`rc\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -146,12 +147,21 @@ jobs:
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
||||
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
||||
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
||||
# Query metadata API for platform (manifest.xml is deprecated)
|
||||
PLATFORM=""
|
||||
if [ -n "$MOKOGITEA_TOKEN" ]; then
|
||||
PLATFORM=$(curl -sf -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||
"${MOKOGITEA_URL}/api/v1/repos/${REPO}/metadata" 2>/dev/null \
|
||||
| sed -n 's/.*"platform"\s*:\s*"\([^"]*\)".*/\1/p' | head -1) || true
|
||||
fi
|
||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||
echo "Detected platform: $PLATFORM"
|
||||
|
||||
- name: Setup PHP
|
||||
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Code security scanner: pattern-based detection of SQL injection, XSS, command injection, path traversal, insecure deserialization, hardcoded credentials, and weak cryptography across Go/PHP/Python/JS/TS (#552)
|
||||
- Cascade merge: auto-create PRs to downstream branches after merge with configurable rules per repo (#460)
|
||||
- Issue status presets: 4 built-in templates (default, software-development, support-tickets, bug-tracking) with API + web UI (#507)
|
||||
- Cross-org status migration: copy status definitions from one org to another via API (#507)
|
||||
@@ -56,6 +57,7 @@
|
||||
- Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185)
|
||||
|
||||
### Fixed
|
||||
- PR check: platform detection now queries metadata API instead of removed manifest.xml
|
||||
- Cherry-pick upstream v1.26.2: handle empty pull request files view to allow reviews (#37783)
|
||||
- Cherry-pick upstream v1.26.2: fix "run as root" check with snap container detection (#37622)
|
||||
- Cherry-pick upstream: ack re-sent UpdateLog finalize idempotently (#37885)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MokoGitea
|
||||
|
||||
Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, org metadata, CI standardization, and project board API.
|
||||
Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, cascade merge, security scanning, org metadata, CI standardization, and project board API.
|
||||
|
||||
 
|
||||
|
||||
@@ -11,8 +11,12 @@ Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, org
|
||||
- **Wiki System** -- wikilinks, categories, backlinks, template transclusion, revision diffs, rename redirects, folder ACL, enhanced ToC, print view, ZIP export ([details](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/standards/Wiki-Features))
|
||||
- **DLID Licensing** -- license management, entitlements, domain activations, ed25519-signed downloads
|
||||
- **API Token Scope Editing** -- edit token scopes via API (PATCH) or web UI after creation
|
||||
- **Issue Statuses** -- custom workflow statuses per org with required baseline protection
|
||||
- **Issue Statuses** -- custom workflow statuses per org with required baseline protection, presets, cross-org migration
|
||||
- **Cascade Merge** -- auto-create PRs to downstream branches after merge with configurable rules per repo
|
||||
- **Security Scanning** -- secret detection (pre-receive blocking) + code analysis (SQL injection, XSS, command injection, path traversal, and more) with REST API for alerts, config, and on-demand scans
|
||||
- **Default Org Teams** -- auto-create Developers, Reviewers, and CI/CD teams on org creation
|
||||
- **Org Metadata** -- per-repo metadata API (public GET, admin PUT), platform detection for versioning
|
||||
- **Branch Protection** -- delete allowlist for protected branches (per-user/team/deploy-key)
|
||||
- **Project Board API** -- REST endpoints for project columns and cards
|
||||
- **CI Infrastructure** -- reusable workflows, centralized ci-issue-reporter, standardized MOKOGITEA_TOKEN naming
|
||||
- **Dev Deploy Gate** -- builds deploy to dev environment first, production checks dev health
|
||||
|
||||
@@ -34,6 +34,7 @@ type apiSecurityConfig struct {
|
||||
BlockOnPush bool `json:"block_on_push"`
|
||||
SecretScanner bool `json:"secret_scanner"`
|
||||
DependScanner bool `json:"depend_scanner"`
|
||||
CodeScanner bool `json:"code_scanner"`
|
||||
}
|
||||
|
||||
// ListSecurityAlerts returns all security alerts for a repo.
|
||||
@@ -153,6 +154,7 @@ func UpdateSecurityConfig(ctx *context.APIContext) {
|
||||
BlockOnPush *bool `json:"block_on_push"`
|
||||
SecretScanner *bool `json:"secret_scanner"`
|
||||
DependScanner *bool `json:"depend_scanner"`
|
||||
CodeScanner *bool `json:"code_scanner"`
|
||||
}
|
||||
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
@@ -177,6 +179,9 @@ func UpdateSecurityConfig(ctx *context.APIContext) {
|
||||
if req.DependScanner != nil {
|
||||
cfg.DependScanner = *req.DependScanner
|
||||
}
|
||||
if req.CodeScanner != nil {
|
||||
cfg.CodeScanner = *req.CodeScanner
|
||||
}
|
||||
|
||||
if err := security_model.SaveScannerConfig(ctx, cfg); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
@@ -210,5 +215,6 @@ func toAPISecurityConfig(cfg *security_model.SecurityScannerConfig) *apiSecurity
|
||||
BlockOnPush: cfg.BlockOnPush,
|
||||
SecretScanner: cfg.SecretScanner,
|
||||
DependScanner: cfg.DependScanner,
|
||||
CodeScanner: cfg.CodeScanner,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
)
|
||||
|
||||
// CodeRule defines a pattern to detect insecure code patterns.
|
||||
type CodeRule struct {
|
||||
ID string
|
||||
Title string
|
||||
Pattern *regexp.Regexp
|
||||
Severity security_model.AlertSeverity
|
||||
Description string
|
||||
CWE string
|
||||
Languages []string // file extensions this rule applies to (empty = all)
|
||||
}
|
||||
|
||||
// DefaultCodeRules contains the built-in code security patterns.
|
||||
var DefaultCodeRules = []CodeRule{
|
||||
// ── SQL Injection (CWE-89) ──────────────────────────────────────────
|
||||
{
|
||||
ID: "sqli-string-concat-go", Title: "SQL Injection: String Concatenation (Go)",
|
||||
Severity: security_model.SeverityHigh, CWE: "CWE-89",
|
||||
Pattern: regexp.MustCompile(`(?i)(?:db|tx|sess|engine|orm)\.\s*(?:Exec|Query|QueryRow|Raw|Where|And|Or|Select|SQL)\s*\(\s*(?:"[^"]*\s*\+|fmt\.Sprintf\s*\(\s*"[^"]*(?:%s|%v|%d))`),
|
||||
Description: "SQL query built with string concatenation or fmt.Sprintf — use parameterized queries instead",
|
||||
Languages: []string{".go"},
|
||||
},
|
||||
{
|
||||
ID: "sqli-string-concat-php", Title: "SQL Injection: String Concatenation (PHP)",
|
||||
Severity: security_model.SeverityHigh, CWE: "CWE-89",
|
||||
Pattern: regexp.MustCompile(`(?i)(?:mysql_query|mysqli_query|pg_query|\$(?:db|pdo|conn|dbo|mysqli)->(?:query|exec|prepare|execute))\s*\(\s*(?:["'][^"']*\.\s*\$|["'][^"']*\{\$)`),
|
||||
Description: "SQL query built with variable interpolation — use prepared statements instead",
|
||||
Languages: []string{".php"},
|
||||
},
|
||||
{
|
||||
ID: "sqli-string-concat-py", Title: "SQL Injection: String Concatenation (Python)",
|
||||
Severity: security_model.SeverityHigh, CWE: "CWE-89",
|
||||
Pattern: regexp.MustCompile(`(?i)(?:cursor|conn|db|engine)\.(?:execute|executemany|raw)\s*\(\s*(?:f["']|["'][^"']*%s|["'][^"']*\s*\+|["'][^"']*\.format\()`),
|
||||
Description: "SQL query built with f-strings, %-formatting, or concatenation — use parameterized queries",
|
||||
Languages: []string{".py"},
|
||||
},
|
||||
{
|
||||
ID: "sqli-string-concat-js", Title: "SQL Injection: String Concatenation (JS)",
|
||||
Severity: security_model.SeverityHigh, CWE: "CWE-89",
|
||||
Pattern: regexp.MustCompile("(?i)(?:query|execute|raw|knex\\.raw|sequelize\\.query)\\s*\\(\\s*(?:`[^`]*\\$\\{|[\"'][^\"']*\\s*\\+)"),
|
||||
Description: "SQL query built with template literals or concatenation — use parameterized queries",
|
||||
Languages: []string{".js", ".ts", ".mjs", ".cjs"},
|
||||
},
|
||||
|
||||
// ── Cross-Site Scripting / XSS (CWE-79) ─────────────────────────────
|
||||
{
|
||||
ID: "xss-innerhtml", Title: "XSS: innerHTML Assignment",
|
||||
Severity: security_model.SeverityMedium, CWE: "CWE-79",
|
||||
Pattern: regexp.MustCompile(`(?i)\.innerHTML\s*[+]?=\s*(?:[^"'` + "`" + `]|$)`),
|
||||
Description: "Direct innerHTML assignment with non-literal value — use textContent or sanitize first",
|
||||
Languages: []string{".js", ".ts", ".jsx", ".tsx", ".vue", ".svelte"},
|
||||
},
|
||||
{
|
||||
ID: "xss-document-write", Title: "XSS: document.write()",
|
||||
Severity: security_model.SeverityMedium, CWE: "CWE-79",
|
||||
Pattern: regexp.MustCompile(`(?i)document\.write(?:ln)?\s*\(`),
|
||||
Description: "document.write() can execute injected scripts — use DOM APIs instead",
|
||||
Languages: []string{".js", ".ts", ".jsx", ".tsx"},
|
||||
},
|
||||
{
|
||||
ID: "xss-echo-unescaped-php", Title: "XSS: Unescaped Output (PHP)",
|
||||
Severity: security_model.SeverityMedium, CWE: "CWE-79",
|
||||
Pattern: regexp.MustCompile(`(?:echo|print)\s+\$_(?:GET|POST|REQUEST|COOKIE|SERVER)\s*\[`),
|
||||
Description: "Direct output of superglobal without escaping — use htmlspecialchars()",
|
||||
Languages: []string{".php"},
|
||||
},
|
||||
{
|
||||
ID: "xss-dangerously-set-html", Title: "XSS: dangerouslySetInnerHTML (React)",
|
||||
Severity: security_model.SeverityMedium, CWE: "CWE-79",
|
||||
Pattern: regexp.MustCompile(`dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:`),
|
||||
Description: "dangerouslySetInnerHTML bypasses React's XSS protection — ensure input is sanitized",
|
||||
Languages: []string{".jsx", ".tsx", ".js", ".ts"},
|
||||
},
|
||||
|
||||
// ── Command Injection (CWE-78) ──────────────────────────────────────
|
||||
{
|
||||
ID: "cmdi-exec-go", Title: "Command Injection: exec.Command with Variable (Go)",
|
||||
Severity: security_model.SeverityHigh, CWE: "CWE-78",
|
||||
Pattern: regexp.MustCompile(`exec\.Command\s*\(\s*(?:fmt\.Sprintf|[a-zA-Z_]+\s*\+|[a-zA-Z_]+\s*,)`),
|
||||
Description: "exec.Command with dynamic arguments — validate and sanitize inputs",
|
||||
Languages: []string{".go"},
|
||||
},
|
||||
{
|
||||
ID: "cmdi-shell-exec-php", Title: "Command Injection: Shell Execution (PHP)",
|
||||
Severity: security_model.SeverityCritical, CWE: "CWE-78",
|
||||
Pattern: regexp.MustCompile(`(?i)(?:system|exec|passthru|shell_exec|popen|proc_open)\s*\(\s*\$`),
|
||||
Description: "Shell command with variable input — use escapeshellarg() or avoid shell execution",
|
||||
Languages: []string{".php"},
|
||||
},
|
||||
{
|
||||
ID: "cmdi-child-process-js", Title: "Command Injection: child_process (Node.js)",
|
||||
Severity: security_model.SeverityHigh, CWE: "CWE-78",
|
||||
Pattern: regexp.MustCompile("(?i)(?:child_process|exec|execSync|spawn|spawnSync)\\s*\\(\\s*(?:`[^`]*\\$\\{|[\"'][^\"']*\\s*\\+)"),
|
||||
Description: "Shell command built with dynamic input — use array arguments or sanitize",
|
||||
Languages: []string{".js", ".ts", ".mjs", ".cjs"},
|
||||
},
|
||||
{
|
||||
ID: "cmdi-subprocess-py", Title: "Command Injection: subprocess with shell=True (Python)",
|
||||
Severity: security_model.SeverityHigh, CWE: "CWE-78",
|
||||
Pattern: regexp.MustCompile(`(?i)subprocess\.(?:call|run|Popen|check_output|check_call)\s*\([^)]*shell\s*=\s*True`),
|
||||
Description: "subprocess with shell=True is vulnerable to injection — use shell=False with list args",
|
||||
Languages: []string{".py"},
|
||||
},
|
||||
{
|
||||
ID: "cmdi-os-system-py", Title: "Command Injection: os.system() (Python)",
|
||||
Severity: security_model.SeverityHigh, CWE: "CWE-78",
|
||||
Pattern: regexp.MustCompile(`(?i)os\.(?:system|popen)\s*\(\s*(?:f["']|[a-zA-Z_])`),
|
||||
Description: "os.system/popen with dynamic input — use subprocess with shell=False instead",
|
||||
Languages: []string{".py"},
|
||||
},
|
||||
|
||||
// ── Path Traversal (CWE-22) ─────────────────────────────────────────
|
||||
{
|
||||
ID: "path-traversal-join", Title: "Path Traversal: Unsanitized Path Join",
|
||||
Severity: security_model.SeverityMedium, CWE: "CWE-22",
|
||||
Pattern: regexp.MustCompile(`(?i)(?:filepath\.Join|path\.join|os\.path\.join)\s*\([^)]*(?:ctx\.|req\.|r\.|request\.|params\.|query\.)`),
|
||||
Description: "User input in file path join without sanitization — validate against directory traversal",
|
||||
Languages: []string{".go", ".js", ".ts", ".py"},
|
||||
},
|
||||
{
|
||||
ID: "path-traversal-open", Title: "Path Traversal: File Open with User Input",
|
||||
Severity: security_model.SeverityMedium, CWE: "CWE-22",
|
||||
Pattern: regexp.MustCompile(`(?i)(?:os\.Open|ioutil\.ReadFile|os\.ReadFile)\s*\(\s*(?:fmt\.Sprintf|[a-zA-Z_]+\s*\+)`),
|
||||
Description: "File operation with dynamic path — sanitize and restrict to safe directories",
|
||||
Languages: []string{".go"},
|
||||
},
|
||||
{
|
||||
ID: "path-traversal-php", Title: "Path Traversal: File Include/Open (PHP)",
|
||||
Severity: security_model.SeverityHigh, CWE: "CWE-22",
|
||||
Pattern: regexp.MustCompile(`(?i)(?:include|require|include_once|require_once|file_get_contents|fopen|readfile)\s*\(\s*\$`),
|
||||
Description: "File operation with variable path — validate path against traversal",
|
||||
Languages: []string{".php"},
|
||||
},
|
||||
|
||||
// ── Insecure Deserialization (CWE-502) ──────────────────────────────
|
||||
{
|
||||
ID: "deserialize-php", Title: "Insecure Deserialization: unserialize() (PHP)",
|
||||
Severity: security_model.SeverityHigh, CWE: "CWE-502",
|
||||
Pattern: regexp.MustCompile(`(?i)unserialize\s*\(\s*\$`),
|
||||
Description: "unserialize() with untrusted data can lead to remote code execution",
|
||||
Languages: []string{".php"},
|
||||
},
|
||||
{
|
||||
ID: "deserialize-yaml-py", Title: "Insecure Deserialization: yaml.load() (Python)",
|
||||
Severity: security_model.SeverityHigh, CWE: "CWE-502",
|
||||
Pattern: regexp.MustCompile(`yaml\.load\s*\([^)]*(?:Loader\s*=\s*yaml\.(?:Unsafe|Full)Loader|[^)]*\)(?!\s*#))`),
|
||||
Description: "yaml.load() without SafeLoader allows arbitrary code execution — use yaml.safe_load()",
|
||||
Languages: []string{".py"},
|
||||
},
|
||||
|
||||
// ── Hardcoded Credentials in Code (CWE-798) ─────────────────────────
|
||||
{
|
||||
ID: "hardcoded-password-assignment", Title: "Hardcoded Password in Source",
|
||||
Severity: security_model.SeverityHigh, CWE: "CWE-798",
|
||||
Pattern: regexp.MustCompile(`(?i)(?:password|passwd|pwd)\s*(?::=|=)\s*["'][^"']{8,}["']`),
|
||||
Description: "Hardcoded password in source code — use environment variables or config",
|
||||
Languages: []string{".go", ".py", ".js", ".ts", ".php", ".rb", ".java"},
|
||||
},
|
||||
|
||||
// ── Weak Cryptography (CWE-327) ─────────────────────────────────────
|
||||
{
|
||||
ID: "weak-crypto-md5", Title: "Weak Cryptography: MD5",
|
||||
Severity: security_model.SeverityLow, CWE: "CWE-327",
|
||||
Pattern: regexp.MustCompile(`(?i)(?:md5\.New|hashlib\.md5|MD5\.Create|MessageDigest\.getInstance\s*\(\s*["']MD5)`),
|
||||
Description: "MD5 is cryptographically broken — use SHA-256 or stronger for security purposes",
|
||||
Languages: []string{".go", ".py", ".cs", ".java"},
|
||||
},
|
||||
{
|
||||
ID: "weak-crypto-sha1", Title: "Weak Cryptography: SHA-1 for Security",
|
||||
Severity: security_model.SeverityLow, CWE: "CWE-327",
|
||||
Pattern: regexp.MustCompile(`(?i)(?:sha1\.New|hashlib\.sha1|SHA1\.Create|MessageDigest\.getInstance\s*\(\s*["']SHA-?1)`),
|
||||
Description: "SHA-1 is deprecated for security — use SHA-256 or stronger",
|
||||
Languages: []string{".go", ".py", ".cs", ".java"},
|
||||
},
|
||||
}
|
||||
|
||||
// Language extensions for file filtering.
|
||||
var codeFileExtensions = map[string]bool{
|
||||
".go": true, ".py": true, ".js": true, ".ts": true, ".jsx": true, ".tsx": true,
|
||||
".php": true, ".rb": true, ".java": true, ".cs": true, ".rs": true,
|
||||
".vue": true, ".svelte": true, ".mjs": true, ".cjs": true,
|
||||
}
|
||||
|
||||
// CodeScanner implements the Scanner interface for code pattern analysis.
|
||||
type CodeScanner struct {
|
||||
Rules []CodeRule
|
||||
}
|
||||
|
||||
// NewCodeScanner creates a scanner with default code analysis rules.
|
||||
func NewCodeScanner() *CodeScanner {
|
||||
return &CodeScanner{Rules: DefaultCodeRules}
|
||||
}
|
||||
|
||||
func (s *CodeScanner) Type() security_model.ScannerType {
|
||||
return security_model.ScannerCode
|
||||
}
|
||||
|
||||
func (s *CodeScanner) ScanCommit(commit *git.Commit) ([]Finding, error) {
|
||||
return s.ScanTree(commit)
|
||||
}
|
||||
|
||||
func (s *CodeScanner) ScanTree(commit *git.Commit) ([]Finding, error) {
|
||||
if commit == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
entries, err := commit.ListEntriesRecursiveFast()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ListEntriesRecursiveFast: %w", err)
|
||||
}
|
||||
|
||||
var findings []Finding
|
||||
for _, entry := range entries {
|
||||
if !entry.IsRegular() {
|
||||
continue
|
||||
}
|
||||
|
||||
path := entry.Name()
|
||||
if shouldSkipFile(path) {
|
||||
continue
|
||||
}
|
||||
|
||||
ext := fileExtension(path)
|
||||
if !codeFileExtensions[ext] {
|
||||
continue
|
||||
}
|
||||
|
||||
if entry.Blob().Size() > 1024*1024 {
|
||||
continue
|
||||
}
|
||||
|
||||
applicableRules := s.rulesForExtension(ext)
|
||||
if len(applicableRules) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
reader, err := entry.Blob().DataAsync()
|
||||
if err != nil {
|
||||
log.Trace("CodeScanner: skip %s: %v", path, err)
|
||||
continue
|
||||
}
|
||||
|
||||
fileFindings := s.scanReader(reader, path, commit.ID.String(), applicableRules)
|
||||
reader.Close()
|
||||
findings = append(findings, fileFindings...)
|
||||
}
|
||||
|
||||
return findings, nil
|
||||
}
|
||||
|
||||
func (s *CodeScanner) rulesForExtension(ext string) []CodeRule {
|
||||
var rules []CodeRule
|
||||
for _, rule := range s.Rules {
|
||||
if len(rule.Languages) == 0 {
|
||||
rules = append(rules, rule)
|
||||
continue
|
||||
}
|
||||
for _, lang := range rule.Languages {
|
||||
if lang == ext {
|
||||
rules = append(rules, rule)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
func (s *CodeScanner) scanReader(r io.Reader, filePath, commitSHA string, rules []CodeRule) []Finding {
|
||||
var findings []Finding
|
||||
scanner := bufio.NewScanner(r)
|
||||
lineNum := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
line := scanner.Text()
|
||||
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if isCommentLine(trimmed) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if rule.Pattern.MatchString(line) {
|
||||
fingerprint := fmt.Sprintf("%x", sha256.Sum256([]byte(rule.ID+":"+filePath+":"+line)))
|
||||
findings = append(findings, Finding{
|
||||
Scanner: security_model.ScannerCode,
|
||||
Severity: rule.Severity,
|
||||
RuleID: rule.ID,
|
||||
Title: rule.Title,
|
||||
Description: rule.Description + " [" + rule.CWE + "]",
|
||||
FilePath: filePath,
|
||||
LineNumber: lineNum,
|
||||
CommitSHA: commitSHA,
|
||||
Fingerprint: fingerprint[:32],
|
||||
Metadata: fmt.Sprintf(`{"cwe":"%s"}`, rule.CWE),
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
func fileExtension(path string) string {
|
||||
lower := strings.ToLower(path)
|
||||
if i := strings.LastIndex(lower, "."); i >= 0 {
|
||||
return lower[i:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isCommentLine(line string) bool {
|
||||
return strings.HasPrefix(line, "//") ||
|
||||
strings.HasPrefix(line, "#") ||
|
||||
strings.HasPrefix(line, "*") ||
|
||||
strings.HasPrefix(line, "/*") ||
|
||||
strings.HasPrefix(line, "<!--")
|
||||
}
|
||||
@@ -56,8 +56,9 @@ func ScanOnPush(ctx context.Context, repo *repo_model.Repository, commit *git.Co
|
||||
if cfg.DependScanner {
|
||||
scanners = append(scanners, NewDependencyScanner())
|
||||
}
|
||||
// Future scanners added here:
|
||||
// if cfg.CodeScanner { scanners = append(scanners, NewCodeScanner()) }
|
||||
if cfg.CodeScanner {
|
||||
scanners = append(scanners, NewCodeScanner())
|
||||
}
|
||||
|
||||
if len(scanners) == 0 {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user