Skip to content

Commit de610a0

Browse files
committed
Add GHA & linting (missing files)
1 parent 7b52cd0 commit de610a0

File tree

2 files changed

+175
-0
lines changed

2 files changed

+175
-0
lines changed

ratelimit.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Package main - ratelimit.go provides rate limiting functionality.
2+
package main
3+
4+
import (
5+
"sync"
6+
"time"
7+
)
8+
9+
// RateLimiter implements a simple token bucket rate limiter.
10+
type RateLimiter struct {
11+
lastRefill time.Time
12+
mu sync.Mutex
13+
refillRate time.Duration
14+
tokens int
15+
maxTokens int
16+
}
17+
18+
// NewRateLimiter creates a new rate limiter.
19+
func NewRateLimiter(maxTokens int, refillRate time.Duration) *RateLimiter {
20+
return &RateLimiter{
21+
tokens: maxTokens,
22+
maxTokens: maxTokens,
23+
refillRate: refillRate,
24+
lastRefill: time.Now(),
25+
}
26+
}
27+
28+
// Allow checks if an operation is allowed under the rate limit.
29+
func (r *RateLimiter) Allow() bool {
30+
r.mu.Lock()
31+
defer r.mu.Unlock()
32+
33+
// Refill tokens based on elapsed time
34+
now := time.Now()
35+
elapsed := now.Sub(r.lastRefill)
36+
tokensToAdd := int(elapsed / r.refillRate)
37+
38+
if tokensToAdd > 0 {
39+
r.tokens = minInt(r.tokens+tokensToAdd, r.maxTokens)
40+
r.lastRefill = now
41+
}
42+
43+
// Check if we have tokens available
44+
if r.tokens > 0 {
45+
r.tokens--
46+
return true
47+
}
48+
49+
return false
50+
}
51+
52+
// minInt returns the minimum of two integers.
53+
func minInt(a, b int) int {
54+
if a < b {
55+
return a
56+
}
57+
return b
58+
}

security.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Package main - security.go provides security utilities and validation functions.
2+
package main
3+
4+
import (
5+
"errors"
6+
"fmt"
7+
"regexp"
8+
"strings"
9+
)
10+
11+
const (
12+
// Security constants.
13+
minTokenLength = 40 // GitHub tokens are at least 40 chars
14+
maxTokenLength = 255 // Reasonable upper bound
15+
maxUsernameLen = 39 // GitHub username max length
16+
maxURLLength = 2048 // Maximum URL length
17+
minPrintableChar = 0x20 // Minimum printable character
18+
deleteChar = 0x7F // Delete character
19+
)
20+
21+
var (
22+
// githubUsernameRegex validates GitHub usernames.
23+
// GitHub usernames can only contain alphanumeric characters and hyphens,
24+
// cannot start or end with hyphen, and max 39 characters.
25+
githubUsernameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]{0,37}[a-zA-Z0-9]$|^[a-zA-Z0-9]$`)
26+
27+
// githubTokenRegex validates GitHub token format.
28+
// Classic tokens: 40 hex chars.
29+
// New tokens: ghp_ (personal), ghs_ (server), ghr_ (refresh), gho_ (OAuth), ghu_ (user-to-server) followed by base62 chars.
30+
// Fine-grained tokens: github_pat_ followed by base62 chars.
31+
githubTokenRegex = regexp.MustCompile(`^[a-f0-9]{40}$|^gh[psoru]_[A-Za-z0-9]{36,251}$|^github_pat_[A-Za-z0-9]{82}$`)
32+
)
33+
34+
// validateGitHubUsername validates a GitHub username.
35+
func validateGitHubUsername(username string) error {
36+
if username == "" {
37+
return errors.New("username cannot be empty")
38+
}
39+
if len(username) > maxUsernameLen {
40+
return fmt.Errorf("username too long: %d > %d", len(username), maxUsernameLen)
41+
}
42+
if !githubUsernameRegex.MatchString(username) {
43+
return fmt.Errorf("invalid GitHub username format: %s", username)
44+
}
45+
return nil
46+
}
47+
48+
// validateGitHubToken performs basic validation on a GitHub token.
49+
func validateGitHubToken(token string) error {
50+
if token == "" {
51+
return errors.New("token cannot be empty")
52+
}
53+
54+
tokenLen := len(token)
55+
if tokenLen < minTokenLength {
56+
return fmt.Errorf("token too short: %d < %d", tokenLen, minTokenLength)
57+
}
58+
if tokenLen > maxTokenLength {
59+
return fmt.Errorf("token too long: %d > %d", tokenLen, maxTokenLength)
60+
}
61+
62+
// Check for common placeholder values
63+
if strings.Contains(strings.ToLower(token), "your_token") ||
64+
strings.Contains(strings.ToLower(token), "xxx") ||
65+
strings.Contains(token, "...") {
66+
return errors.New("token appears to be a placeholder")
67+
}
68+
69+
if !githubTokenRegex.MatchString(token) {
70+
return errors.New("token does not match expected GitHub token format")
71+
}
72+
73+
return nil
74+
}
75+
76+
// sanitizeForLog removes sensitive information from strings before logging.
77+
func sanitizeForLog(s string) string {
78+
// Redact tokens (both classic 40-char hex and new format)
79+
// Classic tokens
80+
s = regexp.MustCompile(`\b[a-f0-9]{40}\b`).ReplaceAllString(s, "[REDACTED-TOKEN]")
81+
// New format tokens (ghp_, ghs_, ghr_, gho_, ghu_)
82+
s = regexp.MustCompile(`\bgh[psoru]_[A-Za-z0-9]{36,251}\b`).ReplaceAllString(s, "[REDACTED-TOKEN]")
83+
// Fine-grained personal access tokens
84+
s = regexp.MustCompile(`\bgithub_pat_[A-Za-z0-9]{82}\b`).ReplaceAllString(s, "[REDACTED-TOKEN]")
85+
// Bearer tokens in headers
86+
s = regexp.MustCompile(`Bearer [A-Za-z0-9_\-.]+`).ReplaceAllString(s, "Bearer [REDACTED]")
87+
// Authorization headers
88+
s = regexp.MustCompile(`Authorization: \S+`).ReplaceAllString(s, "Authorization: [REDACTED]")
89+
90+
return s
91+
}
92+
93+
// validateURL performs strict validation on URLs.
94+
func validateURL(rawURL string) error {
95+
if rawURL == "" {
96+
return errors.New("URL cannot be empty")
97+
}
98+
99+
// Check for null bytes or control characters
100+
for _, r := range rawURL {
101+
if r < minPrintableChar || r == deleteChar {
102+
return errors.New("URL contains control characters")
103+
}
104+
}
105+
106+
// Ensure URL starts with https://
107+
if !strings.HasPrefix(rawURL, "https://") {
108+
return errors.New("URL must use HTTPS")
109+
}
110+
111+
// Check for URL length limits
112+
if len(rawURL) > maxURLLength {
113+
return errors.New("URL too long")
114+
}
115+
116+
return nil
117+
}

0 commit comments

Comments
 (0)