-
Notifications
You must be signed in to change notification settings - Fork 0
feat: support all CAEP event types #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
d155af6
feat: support `risk-level-change` events
jtanios b740d6f
refactor: revert changes to log sink pending discussion
tjhorner d248d2d
test: add E2E test for risk-level-change event type
tjhorner 2e1382c
chore: minor docker-compose changes
tjhorner 44f119a
chore: run goimports
tjhorner bdabb9b
feat: add validation to risk-level-change events
tjhorner 3b5d804
feat: add support for session-established and session-presented CAEP …
tjhorner 47df6c5
docs: add supported events section
tjhorner 01d7f27
refactor: switch base type for CAEP events to caep.BaseCAEPEvent
tjhorner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,4 @@ | ||
| config.yaml | ||
| .claude/settings.local.json | ||
| .claude/settings.local.json | ||
|
|
||
| .idea/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
|
tjhorner marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| // Package caepext registers custom CAEP event type parsers not yet included | ||
| // in the upstream caep.dev/secevent library. | ||
| package caepext | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
|
|
||
| "github.com/sgnl-ai/caep.dev/secevent/pkg/event" | ||
| "github.com/sgnl-ai/caep.dev/secevent/pkg/schemes/caep" | ||
| ) | ||
|
|
||
| const EventTypeRiskLevelChange event.EventType = "https://schemas.openid.net/secevent/caep/event-type/risk-level-change" | ||
|
|
||
| // RiskLevel represents the risk level values defined in Section 3.8.1 of the CAEP specification. | ||
| type RiskLevel string | ||
|
|
||
| const ( | ||
| RiskLevelLow RiskLevel = "LOW" | ||
| RiskLevelMedium RiskLevel = "MEDIUM" | ||
| RiskLevelHigh RiskLevel = "HIGH" | ||
| ) | ||
|
|
||
| // Principal identifies the type of entity involved in a risk event. | ||
| // The spec defines well-known values but permits any string. | ||
| type Principal string | ||
|
|
||
| const ( | ||
| PrincipalUser Principal = "USER" | ||
| PrincipalDevice Principal = "DEVICE" | ||
| PrincipalSession Principal = "SESSION" | ||
| PrincipalTenant Principal = "TENANT" | ||
| PrincipalOrgUnit Principal = "ORG_UNIT" | ||
| PrincipalGroup Principal = "GROUP" | ||
| ) | ||
|
|
||
| // RiskLevelChangePayload holds the event-specific claims for a risk-level-change event. | ||
| type RiskLevelChangePayload struct { | ||
| CurrentLevel RiskLevel `json:"current_level"` | ||
| PreviousLevel *RiskLevel `json:"previous_level,omitempty"` | ||
| Principal Principal `json:"principal"` | ||
| RiskReason string `json:"risk_reason,omitempty"` | ||
| } | ||
|
|
||
| // RiskLevelChangeEvent represents a CAEP risk-level-change event. | ||
| type RiskLevelChangeEvent struct { | ||
| caep.BaseCAEPEvent | ||
| RiskLevelChangePayload | ||
| } | ||
|
|
||
| var validRiskLevels = map[RiskLevel]bool{ | ||
| RiskLevelLow: true, | ||
| RiskLevelMedium: true, | ||
| RiskLevelHigh: true, | ||
| } | ||
|
|
||
| func (e *RiskLevelChangeEvent) Validate() error { | ||
| if err := e.ValidateMetadata(); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if e.Principal == "" { | ||
| return event.NewError(event.ErrCodeMissingField, "missing required claim: principal", "principal", "") | ||
| } | ||
|
|
||
| if e.CurrentLevel == "" { | ||
| return event.NewError(event.ErrCodeMissingField, "missing required claim: current_level", "current_level", "") | ||
| } | ||
|
|
||
| if !validRiskLevels[e.CurrentLevel] { | ||
| return event.NewError(event.ErrCodeInvalidValue, "current_level must be LOW, MEDIUM, or HIGH", "current_level", string(e.CurrentLevel)) | ||
| } | ||
|
|
||
| if e.PreviousLevel != nil && !validRiskLevels[*e.PreviousLevel] { | ||
| return event.NewError(event.ErrCodeInvalidValue, "previous_level must be LOW, MEDIUM, or HIGH", "previous_level", string(*e.PreviousLevel)) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (e *RiskLevelChangeEvent) Payload() any { | ||
| payload := e.RiskLevelChangePayload | ||
|
|
||
| if e.Metadata != nil { | ||
| return struct { | ||
| RiskLevelChangePayload | ||
| *caep.EventMetadata | ||
| }{ | ||
| RiskLevelChangePayload: payload, | ||
| EventMetadata: e.Metadata, | ||
| } | ||
| } | ||
|
|
||
| return payload | ||
| } | ||
|
|
||
| func (e *RiskLevelChangeEvent) MarshalJSON() ([]byte, error) { | ||
| return json.Marshal(e.Payload()) | ||
| } | ||
|
|
||
| func (e *RiskLevelChangeEvent) UnmarshalJSON(data []byte) error { | ||
| var payload struct { | ||
| RiskLevelChangePayload | ||
| *caep.EventMetadata | ||
| } | ||
|
|
||
| if err := json.Unmarshal(data, &payload); err != nil { | ||
| return event.NewError(event.ErrCodeParseError, "failed to parse risk-level-change event data", "", err.Error()) | ||
| } | ||
|
|
||
| e.SetType(EventTypeRiskLevelChange) | ||
| e.RiskLevelChangePayload = payload.RiskLevelChangePayload | ||
| e.Metadata = payload.EventMetadata | ||
|
|
||
| return e.Validate() | ||
| } | ||
|
|
||
| func parseRiskLevelChangeEvent(data []byte) (event.Event, error) { | ||
| var e RiskLevelChangeEvent | ||
| if err := json.Unmarshal(data, &e); err != nil { | ||
| return nil, event.NewError(event.ErrCodeParseError, "failed to parse risk-level-change event", "", err.Error()) | ||
| } | ||
|
|
||
| return &e, nil | ||
| } | ||
|
|
||
| func init() { | ||
| event.RegisterEventParser(EventTypeRiskLevelChange, parseRiskLevelChangeEvent) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| package caepext | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "testing" | ||
|
|
||
| "github.com/sgnl-ai/caep.dev/secevent/pkg/schemes/caep" | ||
| ) | ||
|
|
||
| func TestRiskLevelChangeValidate(t *testing.T) { | ||
| prevLow := RiskLevelLow | ||
| prevInvalid := RiskLevel("medium") | ||
| negTS := int64(-1) | ||
|
|
||
| meta := caep.NewEventMetadata() | ||
| meta.EventTimestamp = &negTS | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| event RiskLevelChangeEvent | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "valid minimal", | ||
| event: RiskLevelChangeEvent{RiskLevelChangePayload: RiskLevelChangePayload{CurrentLevel: RiskLevelLow, Principal: PrincipalUser}}, | ||
| }, | ||
| { | ||
| name: "valid with optional fields", | ||
| event: RiskLevelChangeEvent{RiskLevelChangePayload: RiskLevelChangePayload{ | ||
| CurrentLevel: RiskLevelHigh, | ||
| PreviousLevel: &prevLow, | ||
| Principal: PrincipalDevice, | ||
| RiskReason: "PASSWORD_FOUND_IN_DATA_BREACH", | ||
| }}, | ||
| }, | ||
| { | ||
| name: "missing principal", | ||
| event: RiskLevelChangeEvent{RiskLevelChangePayload: RiskLevelChangePayload{CurrentLevel: RiskLevelLow}}, | ||
| wantErr: true, | ||
| }, | ||
| { | ||
| name: "missing current_level", | ||
| event: RiskLevelChangeEvent{RiskLevelChangePayload: RiskLevelChangePayload{Principal: PrincipalUser}}, | ||
| wantErr: true, | ||
| }, | ||
| { | ||
| name: "invalid current_level", | ||
| event: RiskLevelChangeEvent{RiskLevelChangePayload: RiskLevelChangePayload{CurrentLevel: "medium", Principal: PrincipalUser}}, | ||
| wantErr: true, | ||
| }, | ||
| { | ||
| name: "invalid previous_level", | ||
| event: RiskLevelChangeEvent{RiskLevelChangePayload: RiskLevelChangePayload{ | ||
| CurrentLevel: RiskLevelLow, | ||
| PreviousLevel: &prevInvalid, | ||
| Principal: PrincipalUser, | ||
| }}, | ||
| wantErr: true, | ||
| }, | ||
| { | ||
| name: "invalid metadata timestamp", | ||
| event: func() RiskLevelChangeEvent { | ||
| e := RiskLevelChangeEvent{RiskLevelChangePayload: RiskLevelChangePayload{CurrentLevel: RiskLevelLow, Principal: PrincipalUser}} | ||
| e.Metadata = meta | ||
| return e | ||
| }(), | ||
| wantErr: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| err := tt.event.Validate() | ||
| if (err != nil) != tt.wantErr { | ||
| t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestRiskLevelChangeRoundtrip(t *testing.T) { | ||
| data, err := json.Marshal(map[string]any{ | ||
| "current_level": "HIGH", | ||
| "previous_level": "LOW", | ||
| "principal": "DEVICE", | ||
| "risk_reason": "SUSPICIOUS_ACTIVITY", | ||
| "event_timestamp": int64(1615304991), | ||
| }) | ||
| if err != nil { | ||
| t.Fatalf("marshaling input: %v", err) | ||
| } | ||
|
|
||
| var e RiskLevelChangeEvent | ||
| if err := json.Unmarshal(data, &e); err != nil { | ||
| t.Fatalf("UnmarshalJSON: %v", err) | ||
| } | ||
|
|
||
| if e.Type() != EventTypeRiskLevelChange { | ||
| t.Errorf("Type() = %q, want %q", e.Type(), EventTypeRiskLevelChange) | ||
| } | ||
| if e.CurrentLevel != RiskLevelHigh { | ||
| t.Errorf("CurrentLevel = %q, want %q", e.CurrentLevel, RiskLevelHigh) | ||
| } | ||
| if e.PreviousLevel == nil || *e.PreviousLevel != RiskLevelLow { | ||
| t.Errorf("PreviousLevel = %v, want %q", e.PreviousLevel, RiskLevelLow) | ||
| } | ||
| if e.Principal != PrincipalDevice { | ||
| t.Errorf("Principal = %q, want %q", e.Principal, PrincipalDevice) | ||
| } | ||
| if e.RiskReason != "SUSPICIOUS_ACTIVITY" { | ||
| t.Errorf("RiskReason = %q, want %q", e.RiskReason, "SUSPICIOUS_ACTIVITY") | ||
| } | ||
| } | ||
|
|
||
| func TestRiskLevelChangeUnmarshalValidates(t *testing.T) { | ||
| invalid, _ := json.Marshal(map[string]any{"current_level": "medium", "principal": "USER"}) | ||
| var e RiskLevelChangeEvent | ||
| if err := json.Unmarshal(invalid, &e); err == nil { | ||
| t.Error("expected error for invalid current_level, got nil") | ||
| } | ||
| } | ||
|
|
||
| func TestRiskLevelChangeParser(t *testing.T) { | ||
| data, _ := json.Marshal(map[string]any{ | ||
| "current_level": "LOW", | ||
| "principal": "USER", | ||
| }) | ||
|
|
||
| e, err := parseRiskLevelChangeEvent(data) | ||
| if err != nil { | ||
| t.Fatalf("parseRiskLevelChangeEvent: %v", err) | ||
| } | ||
| if e.Type() != EventTypeRiskLevelChange { | ||
| t.Errorf("Type() = %q, want %q", e.Type(), EventTypeRiskLevelChange) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.