Skip to content

Commit e8de889

Browse files
authored
Merge pull request #956 from l3montree-dev/migrations
Migrations
2 parents 75cda99 + 8cb0a57 commit e8de889

56 files changed

Lines changed: 5006 additions & 413 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,127 @@ DevGuard is divided into two projects: A frontend (DevGuard Web) and a backend (
9898

9999
<p align="right">(<a href="#readme-top">back to top</a>)</p>
100100

101+
## Database Migrations
102+
103+
DevGuard uses [golang-migrate](https://github.com/golang-migrate/migrate) for database schema management. All migrations are embedded in the binary and run automatically on startup.
104+
105+
### How Database Migrations Work
106+
107+
1. **Automatic Migration**: By default, migrations run automatically when the application starts
108+
2. **Environment Control**: Set `AUTO_MIGRATE=false` to disable automatic migrations
109+
3. **Embedded Migrations**: Migration files are embedded in the binary for easy deployment
110+
4. **Idempotent**: Migrations can be run multiple times safely
111+
112+
### Creating New Migrations
113+
114+
#### 1. Install the Migration Tool
115+
116+
The project includes golang-migrate as a tool dependency. Install it using:
117+
118+
```bash
119+
go get -tool github.com/golang-migrate/migrate/v4/cmd/migrate
120+
```
121+
122+
#### 2. Create a New Migration
123+
124+
```bash
125+
# Create a new migration file pair (.up.sql and .down.sql)
126+
go tool migrate create -ext sql -dir internal/database/migrations your_migration_name
127+
128+
# Example: Adding a new table
129+
go tool migrate create -ext sql -dir internal/database/migrations add_user_preferences_table
130+
```
131+
132+
This creates two files:
133+
- `internal/database/migrations/YYYYMMDDHHMMSS_your_migration_name.up.sql` - Forward migration
134+
- `internal/database/migrations/YYYYMMDDHHMMSS_your_migration_name.down.sql` - Rollback migration
135+
136+
#### 3. Write Your Migration
137+
138+
**Example: Adding a new table**
139+
140+
`20250801120000_add_user_preferences_table.up.sql`:
141+
```sql
142+
-- Create user preferences table
143+
CREATE TABLE IF NOT EXISTS user_preferences (
144+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
145+
user_id TEXT NOT NULL,
146+
theme TEXT DEFAULT 'light',
147+
notifications_enabled BOOLEAN DEFAULT true,
148+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
149+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
150+
);
151+
152+
-- Add index for faster lookups
153+
CREATE INDEX IF NOT EXISTS idx_user_preferences_user_id ON user_preferences(user_id);
154+
```
155+
156+
`20250801120000_add_user_preferences_table.down.sql`:
157+
```sql
158+
-- Remove the user preferences table
159+
DROP TABLE IF EXISTS user_preferences CASCADE;
160+
```
161+
162+
**Example: Adding a column**
163+
164+
`20250801130000_add_email_to_users.up.sql`:
165+
```sql
166+
-- Add email column to existing users table
167+
ALTER TABLE users ADD COLUMN IF NOT EXISTS email TEXT;
168+
169+
-- Add index for email lookups
170+
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
171+
```
172+
173+
`20250801130000_add_email_to_users.down.sql`:
174+
```sql
175+
-- Remove email column from users table
176+
ALTER TABLE users DROP COLUMN IF EXISTS email CASCADE;
177+
```
178+
179+
**Example: Adding foreign key constraints**
180+
181+
`20250801140000_add_user_organization_fk.up.sql`:
182+
```sql
183+
-- Add foreign key constraint
184+
ALTER TABLE users
185+
ADD CONSTRAINT IF NOT EXISTS fk_users_organization
186+
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
187+
```
188+
189+
`20250801140000_add_user_organization_fk.down.sql`:
190+
```sql
191+
-- Remove foreign key constraint
192+
ALTER TABLE users DROP CONSTRAINT IF EXISTS fk_users_organization;
193+
```
194+
195+
#### 4. Best Practices
196+
197+
- **Always use IF NOT EXISTS/IF EXISTS**: Makes migrations idempotent
198+
- **Include rollback logic**: Always write the down migration
199+
- **Test migrations**: Test both up and down migrations on a copy of production data
200+
- **Small incremental changes**: Keep migrations focused and atomic
201+
- **Use transactions implicitly**: PostgreSQL wraps DDL in transactions automatically
202+
- **Descriptive names**: Use clear, descriptive migration names
203+
204+
#### 5. Manual Migration Commands
205+
206+
```bash
207+
# Check migration status
208+
go tool migrate -database "postgres://user:pass@localhost:5432/devguard?sslmode=disable" -path internal/database/migrations version
209+
210+
# Run migrations manually
211+
go tool migrate -database "postgres://user:pass@localhost:5432/devguard?sslmode=disable" -path internal/database/migrations up
212+
213+
# Rollback one migration
214+
go tool migrate -database "postgres://user:pass@localhost:5432/devguard?sslmode=disable" -path internal/database/migrations down 1
215+
216+
# Rollback to specific version
217+
go tool migrate -database "postgres://user:pass@localhost:5432/devguard?sslmode=disable" -path internal/database/migrations goto 20250801120000
218+
```
219+
220+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
221+
101222
<!-- LICENSE -->
102223
## License
103224

cmd/devguard-cli/commands/migrate.go

Lines changed: 0 additions & 89 deletions
This file was deleted.

cmd/devguard-cli/main.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ func Execute() {
5353

5454
func init() {
5555
rootCmd.AddCommand(commands.NewVulndbCommand())
56-
rootCmd.AddCommand(commands.NewMigrateCommand())
5756
rootCmd.AddCommand(commands.NewComponentsCommand())
5857
rootCmd.AddCommand(commands.NewDaemonCommand())
5958
}

cmd/devguard-scanner/commands/sarif.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"net/http"
2727
"os"
2828
"regexp"
29+
"strconv"
2930
"strings"
3031
"time"
3132

@@ -238,10 +239,11 @@ func obfuscateString(str string) string {
238239
}
239240

240241
// add obfuscation function for snippet
241-
func obfuscateSecret(sarifScan *common.SarifResult) {
242+
func obfuscateSecretAndAddFingerprint(sarifScan *common.SarifResult) {
242243
// obfuscate the snippet
243244
for ru, run := range sarifScan.Runs {
244245
for re, result := range run.Results {
246+
// obfuscate the snippet
245247
for lo, location := range result.Locations {
246248
snippet := location.PhysicalLocation.Region.Snippet.Text
247249
snippetMax := 20
@@ -252,6 +254,10 @@ func obfuscateSecret(sarifScan *common.SarifResult) {
252254
// set the snippet
253255
sarifScan.Runs[ru].Results[re].Locations[lo].PhysicalLocation.Region.Snippet.Text = snippet
254256
}
257+
258+
//set the fingerprint to the calculated fingerprint if it exists
259+
result.Fingerprints.CalculatedFingerprint = result.PartialFingerprints.CommitSha + ":" + result.Locations[0].PhysicalLocation.ArtifactLocation.URI + ":" + result.RuleID + ":" + strconv.Itoa(result.Locations[0].PhysicalLocation.Region.StartLine)
260+
255261
}
256262
}
257263
}

cmd/devguard-scanner/commands/sast.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"os"
99
"os/exec"
1010
"path"
11-
"strconv"
1211

1312
"github.com/jedib0t/go-pretty/v6/table"
1413
"github.com/jedib0t/go-pretty/v6/text"
@@ -26,13 +25,13 @@ func printSastScanResults(firstPartyVulns []vuln.FirstPartyVulnDTO, webUI, asset
2625
green := text.FgGreen
2726
for _, vuln := range firstPartyVulns {
2827
tw.AppendRow(table.Row{"RuleID", vuln.RuleID})
29-
if vuln.Snippet != "" {
30-
tw.AppendRow(table.Row{"Snippet", vuln.Snippet})
28+
for _, snippet := range vuln.SnippetContents {
29+
tw.AppendRow(table.Row{"Snippet", snippet.Snippet})
3130
}
3231
tw.AppendRow(table.Row{"Message", text.WrapText(*vuln.Message, 80)})
3332
if vuln.URI != "" {
34-
tw.AppendRow(table.Row{"File", green.Sprint(vuln.URI + ":" + strconv.Itoa(vuln.StartLine))})
35-
tw.AppendRow(table.Row{"Line", vuln.StartLine})
33+
tw.AppendRow(table.Row{"File", green.Sprint(vuln.URI)})
34+
3635
}
3736
tw.AppendSeparator()
3837
}

cmd/devguard-scanner/commands/sca.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,15 @@ func generateSBOM(path string) (*os.File, error) {
115115

116116
// Function to dynamically change the format of the table row depending on the input parameters
117117
func dependencyVulnToTableRow(pURL packageurl.PackageURL, v vuln.DependencyVulnDTO) table.Row {
118+
var cvss float32 = 0.0
119+
if v.CVE != nil {
120+
cvss = v.CVE.CVSS
121+
}
122+
118123
if pURL.Namespace == "" { //Remove the second slash if the second parameter is empty to avoid double slashes
119-
return table.Row{fmt.Sprintf("pkg:%s/%s", pURL.Type, pURL.Name), utils.SafeDereference(v.CVEID), utils.OrDefault(v.RawRiskAssessment, 0), v.CVE.CVSS, strings.TrimPrefix(pURL.Version, "v"), utils.SafeDereference(v.ComponentFixedVersion), v.State}
124+
return table.Row{fmt.Sprintf("pkg:%s/%s", pURL.Type, pURL.Name), utils.SafeDereference(v.CVEID), utils.OrDefault(v.RawRiskAssessment, 0), cvss, strings.TrimPrefix(pURL.Version, "v"), utils.SafeDereference(v.ComponentFixedVersion), v.State}
120125
} else {
121-
return table.Row{fmt.Sprintf("pkg:%s/%s/%s", pURL.Type, pURL.Namespace, pURL.Name), utils.SafeDereference(v.CVEID), utils.OrDefault(v.RawRiskAssessment, 0), v.CVE.CVSS, strings.TrimPrefix(pURL.Version, "v"), utils.SafeDereference(v.ComponentFixedVersion), v.State}
126+
return table.Row{fmt.Sprintf("pkg:%s/%s/%s", pURL.Type, pURL.Namespace, pURL.Name), utils.SafeDereference(v.CVEID), utils.OrDefault(v.RawRiskAssessment, 0), cvss, strings.TrimPrefix(pURL.Version, "v"), utils.SafeDereference(v.ComponentFixedVersion), v.State}
122127
}
123128
}
124129

@@ -171,6 +176,12 @@ func printScaResults(scanResponse scan.ScanResponse, failOnRisk, failOnCVSS, ass
171176
return utils.OrDefault(f.RawRiskAssessment, 0)
172177
})
173178

179+
openCVSS := utils.Map(utils.Filter(scanResponse.DependencyVulns, func(f vuln.DependencyVulnDTO) bool {
180+
return f.State == "open" && f.CVE != nil
181+
}), func(f vuln.DependencyVulnDTO) float32 {
182+
return f.CVE.CVSS
183+
})
184+
174185
maxRisk := 0.
175186
for _, risk := range openRisks {
176187
if risk > maxRisk {
@@ -179,9 +190,9 @@ func printScaResults(scanResponse scan.ScanResponse, failOnRisk, failOnCVSS, ass
179190
}
180191

181192
var maxCVSS float32
182-
for _, v := range scanResponse.DependencyVulns {
183-
if v.CVE != nil && v.CVE.CVSS > maxCVSS {
184-
maxCVSS = v.CVE.CVSS
193+
for _, v := range openCVSS {
194+
if v > maxCVSS {
195+
maxCVSS = v
185196
}
186197
}
187198

0 commit comments

Comments
 (0)