Skip to content

Commit dafcb0b

Browse files
ericfitzclaude
andauthored
feat(api): add cwe_id, cvss arrays to threats and alias array to threat models (#108)
* feat(api): add cwe_id, cvss arrays to threats and alias array to threat models Implements GitHub issue #86: - Add cwe_id string array to Threat objects (CWE identifiers, pattern: CWE-[0-9]+) - Add cvss object array to Threat objects (vector string + score float pairs) - Add alias string array to ThreatModel objects (alternative names/identifiers) - Add CVSSScore schema component to OpenAPI specification - Add CVSSArray custom GORM type for cross-database compatibility All arrays are optional but require at least one element when present (minItems: 1). Validation constraints: - cwe_id: 5-16 chars, pattern ^CWE-[0-9]+$, max 50 items - cvss.vector: 26-250 chars, cvss.score: 0.0-10.0, max 10 items - alias: 3-30 chars, pattern ^[a-zA-Z0-9_-]+$, max 20 items Closes #86 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(deps): update integration test dependencies Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c8d5037 commit dafcb0b

File tree

10 files changed

+1424
-845
lines changed

10 files changed

+1424
-845
lines changed

.version

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"major": 0,
3-
"minor": 269,
4-
"patch": 2
3+
"minor": 270,
4+
"patch": 1
55
}

api/api.go

Lines changed: 863 additions & 829 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/database_store_gorm.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@ func (s *GormThreatModelStore) convertToAPIModel(tm *models.ThreatModel) (Threat
199199
framework = "STRIDE"
200200
}
201201

202+
// Convert alias array
203+
var alias *[]string
204+
if len(tm.Alias) > 0 {
205+
aliasSlice := []string(tm.Alias)
206+
alias = &aliasSlice
207+
}
208+
202209
return ThreatModel{
203210
Id: &tmUUID,
204211
Name: tm.Name,
@@ -215,6 +222,7 @@ func (s *GormThreatModelStore) convertToAPIModel(tm *models.ThreatModel) (Threat
215222
Metadata: &metadata,
216223
Threats: &threats,
217224
Diagrams: diagrams,
225+
Alias: alias,
218226
}, nil
219227
}
220228

@@ -358,6 +366,12 @@ func (s *GormThreatModelStore) Create(item ThreatModel, idSetter func(ThreatMode
358366
statusUpdated = &now
359367
}
360368

369+
// Convert alias array if provided
370+
var aliasArray models.StringArray
371+
if item.Alias != nil && len(*item.Alias) > 0 {
372+
aliasArray = models.StringArray(*item.Alias)
373+
}
374+
361375
// Create GORM model
362376
tm := models.ThreatModel{
363377
ID: id,
@@ -369,6 +383,7 @@ func (s *GormThreatModelStore) Create(item ThreatModel, idSetter func(ThreatMode
369383
IssueURI: item.IssueUri,
370384
Status: item.Status,
371385
StatusUpdated: statusUpdated,
386+
Alias: aliasArray,
372387
}
373388

374389
// Set timestamps
@@ -475,6 +490,12 @@ func (s *GormThreatModelStore) Update(id string, item ThreatModel) error {
475490
framework = "STRIDE"
476491
}
477492

493+
// Convert alias array if provided
494+
var aliasValue interface{}
495+
if item.Alias != nil {
496+
aliasValue = models.StringArray(*item.Alias)
497+
}
498+
478499
// Update threat model
479500
// Note: modified_at is handled automatically by GORM's autoUpdateTime tag
480501
updates := map[string]interface{}{
@@ -489,6 +510,9 @@ func (s *GormThreatModelStore) Update(id string, item ThreatModel) error {
489510
if statusUpdated != nil {
490511
updates["status_updated"] = statusUpdated
491512
}
513+
if aliasValue != nil {
514+
updates["alias"] = aliasValue
515+
}
492516

493517
result := tx.Model(&models.ThreatModel{}).Where("id = ?", id).Updates(updates)
494518
if result.Error != nil {

api/models/models.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,18 @@ func (c *ClientCredential) BeforeCreate(tx *gorm.DB) error {
117117
// Note: Explicit column tags removed for Oracle compatibility (Oracle stores column names as UPPERCASE,
118118
// and the Oracle GORM driver doesn't handle case-insensitive matching with explicit column tags)
119119
type ThreatModel struct {
120-
ID string `gorm:"primaryKey;type:varchar(36)"`
121-
OwnerInternalUUID string `gorm:"type:varchar(36);not null;index:idx_tm_owner;index:idx_tm_owner_created,priority:1"`
122-
Name string `gorm:"type:varchar(256);not null"`
123-
Description *string `gorm:"type:varchar(1024)"`
124-
CreatedByInternalUUID string `gorm:"type:varchar(36);not null;index:idx_tm_created_by"`
125-
ThreatModelFramework string `gorm:"type:varchar(30);default:STRIDE;index:idx_tm_framework"`
126-
IssueURI *string `gorm:"type:varchar(1000)"`
127-
Status *string `gorm:"type:varchar(128);index:idx_tm_status"`
128-
StatusUpdated *time.Time `gorm:"index:idx_tm_status_updated"`
129-
CreatedAt time.Time `gorm:"not null;autoCreateTime;index:idx_tm_owner_created,priority:2"`
130-
ModifiedAt time.Time `gorm:"not null;autoUpdateTime"`
120+
ID string `gorm:"primaryKey;type:varchar(36)"`
121+
OwnerInternalUUID string `gorm:"type:varchar(36);not null;index:idx_tm_owner;index:idx_tm_owner_created,priority:1"`
122+
Name string `gorm:"type:varchar(256);not null"`
123+
Description *string `gorm:"type:varchar(1024)"`
124+
CreatedByInternalUUID string `gorm:"type:varchar(36);not null;index:idx_tm_created_by"`
125+
ThreatModelFramework string `gorm:"type:varchar(30);default:STRIDE;index:idx_tm_framework"`
126+
IssueURI *string `gorm:"type:varchar(1000)"`
127+
Status *string `gorm:"type:varchar(128);index:idx_tm_status"`
128+
StatusUpdated *time.Time `gorm:"index:idx_tm_status_updated"`
129+
Alias StringArray `gorm:"column:alias"` // Alternative names/identifiers
130+
CreatedAt time.Time `gorm:"not null;autoCreateTime;index:idx_tm_owner_created,priority:2"`
131+
ModifiedAt time.Time `gorm:"not null;autoUpdateTime"`
131132

132133
// Relationships
133134
Owner User `gorm:"foreignKey:OwnerInternalUUID;references:InternalUUID"`
@@ -232,6 +233,8 @@ type Threat struct {
232233
Mitigated DBBool `gorm:"index:idx_threats_mitigated"`
233234
Status *string `gorm:"type:varchar(128);index:idx_threats_status"`
234235
ThreatType StringArray `gorm:"not null"`
236+
CweID StringArray `gorm:"column:cwe_id"` // CWE identifiers (e.g., CWE-89)
237+
Cvss CVSSArray `gorm:"column:cvss"` // CVSS vector and score pairs
235238
Mitigation *string `gorm:"type:varchar(1024)"`
236239
IssueURI *string `gorm:"type:varchar(1000)"`
237240
// Note: autoCreateTime/autoUpdateTime tags removed for Oracle compatibility.

api/models/types.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,76 @@ func (a *StringArray) Scan(value interface{}) error {
118118
return json.Unmarshal(bytes, a)
119119
}
120120

121+
// CVSSScore represents a CVSS vector and score pair for threat assessment
122+
type CVSSScore struct {
123+
Vector string `json:"vector"`
124+
Score float64 `json:"score"`
125+
}
126+
127+
// CVSSArray is a custom type that stores CVSS score arrays as JSON
128+
// This outputs JSON array format [{"vector":"...","score":9.8}] which works for both
129+
// PostgreSQL JSONB columns and Oracle JSON columns
130+
type CVSSArray []CVSSScore
131+
132+
// GormDBDataType implements the GormDBDataTypeInterface to return
133+
// dialect-specific column types for cross-database compatibility
134+
func (CVSSArray) GormDBDataType(db *gorm.DB, _ *schema.Field) string {
135+
switch db.Name() {
136+
case dialectPostgres:
137+
return "TEXT"
138+
case dialectOracle:
139+
return "CLOB"
140+
case dialectMySQL:
141+
return "LONGTEXT"
142+
case dialectSQLServer:
143+
return "NVARCHAR(MAX)"
144+
case dialectSQLite:
145+
return "TEXT"
146+
default:
147+
return "TEXT"
148+
}
149+
}
150+
151+
// Value implements the driver.Valuer interface for database writes
152+
// Outputs JSON array format: [{"vector":"...","score":9.8}]
153+
func (a CVSSArray) Value() (driver.Value, error) {
154+
if len(a) == 0 {
155+
return "[]", nil
156+
}
157+
bytes, err := json.Marshal(a)
158+
if err != nil {
159+
return nil, err
160+
}
161+
return string(bytes), nil
162+
}
163+
164+
// Scan implements the sql.Scanner interface for database reads
165+
func (a *CVSSArray) Scan(value interface{}) error {
166+
if value == nil {
167+
*a = []CVSSScore{}
168+
return nil
169+
}
170+
171+
var bytes []byte
172+
switch v := value.(type) {
173+
case []byte:
174+
bytes = v
175+
case string:
176+
bytes = []byte(v)
177+
default:
178+
return fmt.Errorf("cannot scan type %T into CVSSArray", value)
179+
}
180+
181+
// Handle empty values
182+
if len(bytes) == 0 || string(bytes) == "[]" {
183+
*a = []CVSSScore{}
184+
return nil
185+
}
186+
187+
// Handle JSON array format
188+
return json.Unmarshal(bytes, a)
189+
}
190+
121191
// JSONMap is a custom type that stores JSON objects
122192
// This works across both PostgreSQL JSONB and Oracle JSON
123193
type JSONMap map[string]interface{}

api/threat_store_gorm.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,19 @@ func (s *GormThreatStore) toGormModelForCreate(threat *Threat) *models.Threat {
887887
assetID := threat.AssetId.String()
888888
gm.AssetID = &assetID
889889
}
890+
if threat.CweId != nil && len(*threat.CweId) > 0 {
891+
gm.CweID = models.StringArray(*threat.CweId)
892+
}
893+
if threat.Cvss != nil && len(*threat.Cvss) > 0 {
894+
cvssArray := make(models.CVSSArray, len(*threat.Cvss))
895+
for i, c := range *threat.Cvss {
896+
cvssArray[i] = models.CVSSScore{
897+
Vector: c.Vector,
898+
Score: float64(c.Score),
899+
}
900+
}
901+
gm.Cvss = cvssArray
902+
}
890903

891904
return gm
892905
}
@@ -962,6 +975,20 @@ func (s *GormThreatStore) toAPIModel(gm *models.Threat) *Threat {
962975
threat.AssetId = &assetID
963976
}
964977
}
978+
if len(gm.CweID) > 0 {
979+
cweSlice := []string(gm.CweID)
980+
threat.CweId = &cweSlice
981+
}
982+
if len(gm.Cvss) > 0 {
983+
cvssSlice := make([]CVSSScore, len(gm.Cvss))
984+
for i, c := range gm.Cvss {
985+
cvssSlice[i] = CVSSScore{
986+
Vector: c.Vector,
987+
Score: float32(c.Score),
988+
}
989+
}
990+
threat.Cvss = &cvssSlice
991+
}
965992

966993
return threat
967994
}

api/version.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ var (
2727
// Major version number
2828
VersionMajor = "0"
2929
// Minor version number
30-
VersionMinor = "269"
30+
VersionMinor = "270"
3131
// Patch version number
32-
VersionPatch = "2"
32+
VersionPatch = "1"
3333
// GitCommit is the git commit hash from build
3434
GitCommit = "development"
3535
// BuildDate is the build timestamp

docs/reference/apis/tmi-openapi.json

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2368,6 +2368,19 @@
23682368
"description": "Status of the threat model in the organization's threat modeling or SDLC process. Examples: \"Not started\", \"In progress\", \"Review\", \"Approved\", \"Closed\"",
23692369
"maxLength": 128,
23702370
"pattern": "^[^\\x00-\\x1F]*$"
2371+
},
2372+
"alias": {
2373+
"type": "array",
2374+
"description": "Alternative names or identifiers for the threat model",
2375+
"items": {
2376+
"type": "string",
2377+
"minLength": 3,
2378+
"maxLength": 30,
2379+
"pattern": "^[a-zA-Z0-9_-]+$"
2380+
},
2381+
"minItems": 1,
2382+
"maxItems": 20,
2383+
"uniqueItems": true
23712384
}
23722385
},
23732386
"required": [
@@ -2397,6 +2410,10 @@
23972410
"email": "alice@example.com",
23982411
"role": "owner"
23992412
}
2413+
],
2414+
"alias": [
2415+
"payment-gateway-v2",
2416+
"PG-TM-2024"
24002417
]
24012418
}
24022419
},
@@ -2576,6 +2593,28 @@
25762593
"nullable": true,
25772594
"maxLength": 36,
25782595
"pattern": "^[0-9a-fA-F]*-[0-9a-fA-F]*-[0-9a-fA-F]*-[0-9a-fA-F]*-[0-9a-fA-F]*$"
2596+
},
2597+
"cwe_id": {
2598+
"type": "array",
2599+
"description": "CWE (Common Weakness Enumeration) identifiers associated with this threat",
2600+
"items": {
2601+
"type": "string",
2602+
"minLength": 5,
2603+
"maxLength": 16,
2604+
"pattern": "^CWE-[0-9]+$"
2605+
},
2606+
"minItems": 1,
2607+
"maxItems": 50,
2608+
"uniqueItems": true
2609+
},
2610+
"cvss": {
2611+
"type": "array",
2612+
"description": "CVSS scoring information for this threat",
2613+
"items": {
2614+
"$ref": "#/components/schemas/CVSSScore"
2615+
},
2616+
"minItems": 1,
2617+
"maxItems": 10
25792618
}
25802619
},
25812620
"required": [
@@ -2588,7 +2627,16 @@
25882627
"Tampering"
25892628
],
25902629
"description": "Attacker could inject malicious SQL through payment form fields",
2591-
"priority": "high"
2630+
"priority": "high",
2631+
"cwe_id": [
2632+
"CWE-89"
2633+
],
2634+
"cvss": [
2635+
{
2636+
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
2637+
"score": 9.8
2638+
}
2639+
]
25922640
}
25932641
},
25942642
"ThreatInput": {
@@ -6028,6 +6076,35 @@
60286076
"status"
60296077
],
60306078
"additionalProperties": false
6079+
},
6080+
"CVSSScore": {
6081+
"type": "object",
6082+
"description": "CVSS vector and score pair",
6083+
"properties": {
6084+
"vector": {
6085+
"type": "string",
6086+
"description": "CVSS vector string (e.g., CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)",
6087+
"minLength": 26,
6088+
"maxLength": 250,
6089+
"pattern": "^[A-Za-z0-9.:/_-]+$"
6090+
},
6091+
"score": {
6092+
"type": "number",
6093+
"description": "CVSS score (0.0-10.0)",
6094+
"minimum": 0.0,
6095+
"maximum": 10.0,
6096+
"multipleOf": 0.1
6097+
}
6098+
},
6099+
"required": [
6100+
"vector",
6101+
"score"
6102+
],
6103+
"additionalProperties": false,
6104+
"example": {
6105+
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
6106+
"score": 9.8
6107+
}
60316108
}
60326109
},
60336110
"responses": {

0 commit comments

Comments
 (0)