Skip to content

Commit a2f5f07

Browse files
authored
Merge pull request #17 from airframesio/copilot/sub-pr-4
Fix golangci-lint issues and add input validation to prevent security vulnerabilities
2 parents 3ffff49 + 3ba5651 commit a2f5f07

10 files changed

Lines changed: 513 additions & 71 deletions

File tree

cmd/archiver.go

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ package cmd
33
import (
44
"bytes"
55
"context"
6-
"crypto/md5"
6+
"crypto/md5" //nolint:gosec // MD5 used for checksums, not cryptography
77
"database/sql"
88
"encoding/hex"
99
"encoding/json"
10+
"errors"
1011
"fmt"
1112
"os"
1213
"strings"
@@ -20,7 +21,12 @@ import (
2021
tea "github.com/charmbracelet/bubbletea"
2122
"github.com/charmbracelet/lipgloss"
2223
"github.com/klauspost/compress/zstd"
23-
_ "github.com/lib/pq"
24+
"github.com/lib/pq"
25+
)
26+
27+
// Stage constants
28+
const (
29+
StageSkipped = "Skipped"
2430
)
2531

2632
type Archiver struct {
@@ -175,12 +181,12 @@ func (a *Archiver) checkTablePermissions(ctx context.Context) error {
175181
`
176182

177183
err := a.db.QueryRowContext(ctx, checkPermissionQuery, a.config.Table, a.config.Table).Scan(&hasPermission)
178-
if err != nil && err != sql.ErrNoRows {
184+
if err != nil && !errors.Is(err, sql.ErrNoRows) {
179185
return fmt.Errorf("failed to check table permissions: %w", err)
180186
}
181187

182188
// If the base table exists and we don't have permission, fail
183-
if err != sql.ErrNoRows && !hasPermission {
189+
if !errors.Is(err, sql.ErrNoRows) && !hasPermission {
184190
return fmt.Errorf("insufficient permissions to read table '%s'", a.config.Table)
185191
}
186192

@@ -197,13 +203,13 @@ func (a *Archiver) checkTablePermissions(ctx context.Context) error {
197203

198204
var samplePartition string
199205
err = a.db.QueryRowContext(ctx, partitionCheckQuery, pattern).Scan(&samplePartition)
200-
if err != nil && err != sql.ErrNoRows {
206+
if err != nil && !errors.Is(err, sql.ErrNoRows) {
201207
// Only fail if it's not a "no rows" error
202208
return fmt.Errorf("failed to check partition table permissions: %w", err)
203209
}
204210

205211
// Check if we found any partitions at all (with or without permissions)
206-
if err == sql.ErrNoRows {
212+
if errors.Is(err, sql.ErrNoRows) {
207213
// Let's see if partitions exist but we can't access them
208214
var partitionExists bool
209215
existsQuery := `
@@ -311,7 +317,7 @@ func (a *Archiver) discoverPartitionsWithUI(program *tea.Program) ([]PartitionIn
311317
// Update count progress
312318
program.Send(updateCount(i+1, len(matchingTables), table.name))
313319
314-
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", table.name)
320+
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", pq.QuoteIdentifier(table.name))
315321
var count int64
316322
if err := a.db.QueryRow(countQuery).Scan(&count); err == nil {
317323
partitions = append(partitions, PartitionInfo{
@@ -420,7 +426,7 @@ func (a *Archiver) discoverPartitions() ([]PartitionInfo, error) {
420426
len(matchingTables),
421427
table.name)
422428
423-
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", table.name)
429+
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", pq.QuoteIdentifier(table.name))
424430
var count int64
425431
if err := a.db.QueryRow(countQuery).Scan(&count); err == nil {
426432
partitions = append(partitions, PartitionInfo{
@@ -551,7 +557,7 @@ func (a *Archiver) ProcessPartitionWithProgress(partition PartitionInfo, index i
551557
// Cached metadata matches S3 - skip without extraction
552558
result.Skipped = true
553559
result.SkipReason = fmt.Sprintf("Cached metadata matches S3 (size=%d, md5=%s)", cachedSize, cachedMD5)
554-
result.Stage = "Skipped"
560+
result.Stage = StageSkipped
555561
result.BytesWritten = cachedSize
556562
if a.config.Debug {
557563
fmt.Printf(" ✅ Skipping based on cache: Size and MD5 match\n")
@@ -620,7 +626,7 @@ func (a *Archiver) ProcessPartitionWithProgress(partition PartitionInfo, index i
620626
}
621627

622628
// Calculate MD5 hash of compressed data
623-
hasher := md5.New()
629+
hasher := md5.New() //nolint:gosec // MD5 used for checksums, not cryptography
624630
hasher.Write(compressed)
625631
localMD5 := hex.EncodeToString(hasher.Sum(nil))
626632
localSize := int64(len(compressed))
@@ -647,7 +653,7 @@ func (a *Archiver) ProcessPartitionWithProgress(partition PartitionInfo, index i
647653
// Single-part upload with matching MD5
648654
result.Skipped = true
649655
result.SkipReason = fmt.Sprintf("Already exists with matching size (%d bytes) and MD5 (%s)", s3Size, s3ETag)
650-
result.Stage = "Skipped"
656+
result.Stage = StageSkipped
651657
if a.config.Debug {
652658
fmt.Printf(" ✅ Skipping: Size and MD5 match\n")
653659
}
@@ -664,7 +670,7 @@ func (a *Archiver) ProcessPartitionWithProgress(partition PartitionInfo, index i
664670
if s3ETag == localMultipartETag {
665671
result.Skipped = true
666672
result.SkipReason = fmt.Sprintf("Already exists with matching size (%d bytes) and multipart ETag (%s)", s3Size, s3ETag)
667-
result.Stage = "Skipped"
673+
result.Stage = StageSkipped
668674
if a.config.Debug {
669675
fmt.Printf(" ✅ Skipping: Size and multipart ETag match\n")
670676
}
@@ -718,7 +724,9 @@ func (a *Archiver) ProcessPartitionWithProgress(partition PartitionInfo, index i
718724
}
719725

720726
func (a *Archiver) extractDataWithProgress(partition PartitionInfo, program *tea.Program) ([]byte, error) {
721-
query := fmt.Sprintf("SELECT row_to_json(t) FROM %s t", partition.TableName)
727+
// Use pq.QuoteIdentifier to safely quote the table name
728+
quotedTable := pq.QuoteIdentifier(partition.TableName)
729+
query := fmt.Sprintf("SELECT row_to_json(t) FROM %s t", quotedTable) //nolint:gosec // Table name is quoted with pq.QuoteIdentifier
722730

723731
rows, err := a.db.Query(query)
724732
if err != nil {
@@ -763,6 +771,11 @@ func (a *Archiver) extractDataWithProgress(partition PartitionInfo, program *tea
763771
}
764772
}
765773

774+
// Check for errors from iterating over rows
775+
if err := rows.Err(); err != nil {
776+
return nil, err
777+
}
778+
766779
// Final update
767780
if program != nil {
768781
if partition.RowCount > 0 {
@@ -848,7 +861,7 @@ func (a *Archiver) calculateMultipartETag(data []byte) string {
848861

849862
// If it would be a single part, just return regular MD5
850863
if numParts == 1 {
851-
hasher := md5.New()
864+
hasher := md5.New() //nolint:gosec // MD5 used for checksums, not cryptography
852865
hasher.Write(data)
853866
return hex.EncodeToString(hasher.Sum(nil))
854867
}
@@ -862,13 +875,13 @@ func (a *Archiver) calculateMultipartETag(data []byte) string {
862875
end = len(data)
863876
}
864877

865-
partHasher := md5.New()
878+
partHasher := md5.New() //nolint:gosec // MD5 used for checksums, not cryptography
866879
partHasher.Write(data[start:end])
867880
partMD5s = append(partMD5s, partHasher.Sum(nil)...)
868881
}
869882

870883
// Calculate MD5 of concatenated MD5s
871-
finalHasher := md5.New()
884+
finalHasher := md5.New() //nolint:gosec // MD5 used for checksums, not cryptography
872885
finalHasher.Write(partMD5s)
873886
finalMD5 := hex.EncodeToString(finalHasher.Sum(nil))
874887

@@ -894,18 +907,18 @@ func (a *Archiver) uploadToS3(key string, data []byte) error {
894907

895908
_, err := a.s3Uploader.Upload(uploadInput)
896909
return err
897-
} else {
898-
// Use simple PutObject for smaller files
899-
putInput := &s3.PutObjectInput{
900-
Bucket: aws.String(a.config.S3.Bucket),
901-
Key: aws.String(key),
902-
Body: bytes.NewReader(data),
903-
ContentType: aws.String("application/zstd"),
904-
}
910+
}
905911

906-
_, err := a.s3Client.PutObject(putInput)
907-
return err
912+
// Use simple PutObject for smaller files
913+
putInput := &s3.PutObjectInput{
914+
Bucket: aws.String(a.config.S3.Bucket),
915+
Key: aws.String(key),
916+
Body: bytes.NewReader(data),
917+
ContentType: aws.String("application/zstd"),
908918
}
919+
920+
_, err := a.s3Client.PutObject(putInput)
921+
return err
909922
}
910923

911924
func (a *Archiver) printSummary(results []ProcessResult) {

cmd/cache.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ type RowCountEntry struct {
4747
func getCachePath(tableName string) string {
4848
homeDir, _ := os.UserHomeDir()
4949
cacheDir := filepath.Join(homeDir, ".postgresql-archiver", "cache")
50-
_ = os.MkdirAll(cacheDir, 0755)
50+
_ = os.MkdirAll(cacheDir, 0o755)
5151
return filepath.Join(cacheDir, fmt.Sprintf("%s_metadata.json", tableName))
5252
}
5353

@@ -155,7 +155,7 @@ func (c *PartitionCache) save(tableName string) error {
155155
return err
156156
}
157157

158-
return os.WriteFile(cachePath, data, 0644)
158+
return os.WriteFile(cachePath, data, 0o600)
159159
}
160160

161161
// Backward compatibility wrapper - kept for potential future use

cmd/cache_server.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919
var (
2020
serverPort int
2121
upgrader = websocket.Upgrader{
22-
CheckOrigin: func(r *http.Request) bool {
22+
CheckOrigin: func(_ *http.Request) bool {
2323
return true // Allow all origins for local development
2424
},
2525
}
@@ -91,7 +91,7 @@ func startBackgroundServices() {
9191
})
9292
}
9393

94-
func runCacheServer(cmd *cobra.Command, args []string) error {
94+
func runCacheServer(_ *cobra.Command, _ []string) error {
9595
// Set up HTTP routes
9696
http.HandleFunc("/", serveCacheViewer)
9797
http.HandleFunc("/api/cache", serveCacheData)
@@ -107,16 +107,22 @@ func runCacheServer(cmd *cobra.Command, args []string) error {
107107
fmt.Printf("🌐 Open your browser to view cache data\n")
108108
fmt.Printf("⌨️ Press Ctrl+C to stop the server\n\n")
109109

110-
return http.ListenAndServe(addr, nil)
110+
server := &http.Server{
111+
Addr: addr,
112+
ReadHeaderTimeout: 10 * time.Second,
113+
ReadTimeout: 30 * time.Second,
114+
WriteTimeout: 30 * time.Second,
115+
}
116+
return server.ListenAndServe()
111117
}
112118

113-
func serveCacheViewer(w http.ResponseWriter, r *http.Request) {
119+
func serveCacheViewer(w http.ResponseWriter, _ *http.Request) {
114120
html := getCacheViewerHTML()
115121
w.Header().Set("Content-Type", "text/html; charset=utf-8")
116122
_, _ = w.Write([]byte(html))
117123
}
118124

119-
func serveCacheData(w http.ResponseWriter, r *http.Request) {
125+
func serveCacheData(w http.ResponseWriter, _ *http.Request) {
120126
// Enable CORS for local development
121127
w.Header().Set("Access-Control-Allow-Origin", "*")
122128
w.Header().Set("Content-Type", "application/json")
@@ -215,7 +221,7 @@ func serveCacheData(w http.ResponseWriter, r *http.Request) {
215221
_ = json.NewEncoder(w).Encode(response)
216222
}
217223

218-
func serveStatusData(w http.ResponseWriter, r *http.Request) {
224+
func serveStatusData(w http.ResponseWriter, _ *http.Request) {
219225
// Enable CORS for local development
220226
w.Header().Set("Access-Control-Allow-Origin", "*")
221227
w.Header().Set("Content-Type", "application/json")
@@ -321,8 +327,8 @@ func dataMonitor() {
321327
taskDir := filepath.Join(homeDir, ".postgresql-archiver")
322328

323329
// Create directories if they don't exist
324-
_ = os.MkdirAll(cacheDir, 0755)
325-
_ = os.MkdirAll(taskDir, 0755)
330+
_ = os.MkdirAll(cacheDir, 0o755)
331+
_ = os.MkdirAll(taskDir, 0o755)
326332

327333
// Watch cache directory
328334
if err := watcher.Add(cacheDir); err != nil {

cmd/cache_test.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"time"
99
)
1010

11+
const testTablePartition = "test_table_20240101"
12+
1113
func TestPartitionCache(t *testing.T) {
1214
// Create a temporary directory for test cache
1315
tempDir, err := os.MkdirTemp("", "cache_test")
@@ -41,7 +43,7 @@ func TestPartitionCache(t *testing.T) {
4143
Entries: make(map[string]PartitionCacheEntry),
4244
}
4345

44-
partition := "test_table_20240101"
46+
partition := testTablePartition
4547
count := int64(1000)
4648
cache.setRowCount(partition, count)
4749

@@ -68,7 +70,7 @@ func TestPartitionCache(t *testing.T) {
6870
Entries: make(map[string]PartitionCacheEntry),
6971
}
7072

71-
partition := "test_table_20240101"
73+
partition := testTablePartition
7274
s3Key := "export/test_table/2024/01/2024-01-01.jsonl.zst"
7375
compressedSize := int64(1024)
7476
uncompressedSize := int64(5120)
@@ -103,7 +105,7 @@ func TestPartitionCache(t *testing.T) {
103105
Entries: make(map[string]PartitionCacheEntry),
104106
}
105107

106-
partition := "test_table_20240101"
108+
partition := testTablePartition
107109
s3Key := "export/test_table/2024/01/2024-01-01.jsonl.zst"
108110

109111
// Set file metadata first
@@ -133,7 +135,7 @@ func TestPartitionCache(t *testing.T) {
133135
Entries: make(map[string]PartitionCacheEntry),
134136
}
135137

136-
partition := "test_table_20240101"
138+
partition := testTablePartition
137139
errMsg := "connection failed"
138140

139141
cache.setError(partition, errMsg)
@@ -243,11 +245,11 @@ func TestPartitionCache(t *testing.T) {
243245

244246
// Save legacy cache
245247
cacheDir := filepath.Join(tempDir, ".postgresql-archiver", "cache")
246-
_ = os.MkdirAll(cacheDir, 0755)
248+
_ = os.MkdirAll(cacheDir, 0o755)
247249
legacyPath := filepath.Join(cacheDir, "legacy_table_counts.json")
248250

249251
data, _ := json.MarshalIndent(legacyCache, "", " ")
250-
_ = os.WriteFile(legacyPath, data, 0644)
252+
_ = os.WriteFile(legacyPath, data, 0o644)
251253

252254
// Load should migrate
253255
cache, err := loadPartitionCache("legacy_table")

0 commit comments

Comments
 (0)