diff --git a/cmd/dashboards/dashboards.go b/cmd/dashboards/dashboards.go index cf76f685..4bb018e4 100644 --- a/cmd/dashboards/dashboards.go +++ b/cmd/dashboards/dashboards.go @@ -15,6 +15,7 @@ type controlRef struct { type dashboardJSON struct { ID *uuid.UUID `json:"id,omitempty"` Name string `json:"name,omitempty"` + SSPID *uuid.UUID `json:"sspId,omitempty"` Filter *labelfilter.Filter `json:"filter"` Controls []controlRef `json:"controls,omitempty"` } diff --git a/cmd/dashboards/export.go b/cmd/dashboards/export.go index c3189fc1..1f2449e0 100644 --- a/cmd/dashboards/export.go +++ b/cmd/dashboards/export.go @@ -52,7 +52,7 @@ func exportDashboards(cmd *cobra.Command, args []string) { var dashboards []relational.Filter if err := db. - Select("id", "name", "filter"). + Select("id", "name", "ssp_id", "filter"). Preload("Controls", func(db *gorm.DB) *gorm.DB { return db.Select("id", "catalog_id") }). @@ -75,7 +75,8 @@ func exportDashboards(cmd *cobra.Command, args []string) { exports := make([]dashboardJSON, 0, len(dashboards)) for _, f := range dashboards { fe := dashboardJSON{ - Name: f.Name, + Name: f.Name, + SSPID: f.SSPID, } // Extract underlying filter and omit if empty lf := f.Filter.Data() diff --git a/cmd/dashboards/import.go b/cmd/dashboards/import.go index bbed14b7..8b0084ce 100644 --- a/cmd/dashboards/import.go +++ b/cmd/dashboards/import.go @@ -76,7 +76,8 @@ func importDashboards(cmd *cobra.Command, args []string) { created := 0 for _, in := range inputs { rec := relational.Filter{ - Name: in.Name, + Name: in.Name, + SSPID: in.SSPID, } if in.ID != nil { rec.ID = in.ID diff --git a/docs/docs.go b/docs/docs.go index 5dc58a05..12a3ccb9 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1819,6 +1819,26 @@ const docTemplate = `{ } } }, + "/dashboard-suggestions/config": { + "get": { + "description": "Returns whether AI dashboard suggestions are enabled.", + "produces": [ + "application/json" + ], + "tags": [ + "Dashboard Suggestions" + ], + "summary": "Get dashboard suggestions feature configuration", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionConfigResponse" + } + } + } + } + }, "/evidence": { "post": { "description": "Creates a new Evidence record including activities, inventory items, components, and subjects.", @@ -1887,6 +1907,12 @@ const docTemplate = `{ "name": "id", "in": "path", "required": true + }, + { + "type": "string", + "description": "System Security Plan ID; limits filters to global + same-SSP", + "name": "sspId", + "in": "query" } ], "responses": { @@ -1896,6 +1922,12 @@ const docTemplate = `{ "$ref": "#/definitions/handler.GenericDataListResponse-evidence_StatusCount" } }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -1969,6 +2001,12 @@ const docTemplate = `{ "name": "id", "in": "path", "required": true + }, + { + "type": "string", + "description": "System Security Plan ID; limits filters to global + same-SSP", + "name": "sspId", + "in": "query" } ], "responses": { @@ -2463,7 +2501,7 @@ const docTemplate = `{ }, "/filters": { "get": { - "description": "Retrieves all filters, optionally filtered by controlId or componentId.", + "description": "Retrieves filters, optionally filtered by controlId, componentId, sspId, or global scope.", "produces": [ "application/json" ], @@ -2471,6 +2509,32 @@ const docTemplate = `{ "Filters" ], "summary": "List filters", + "parameters": [ + { + "type": "string", + "description": "Control ID", + "name": "controlId", + "in": "query" + }, + { + "type": "string", + "description": "Component ID", + "name": "componentId", + "in": "query" + }, + { + "type": "string", + "description": "System Security Plan ID; returns global + same-SSP filters", + "name": "sspId", + "in": "query" + }, + { + "type": "string", + "description": "Filter scope. Use 'global' for global filters only", + "name": "scope", + "in": "query" + } + ], "responses": { "200": { "description": "OK", @@ -2478,6 +2542,18 @@ const docTemplate = `{ "$ref": "#/definitions/handler.GenericDataListResponse-handler_FilterWithAssociations" } }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -15943,224 +16019,16 @@ const docTemplate = `{ ] } }, - "/oscal/system-security-plans/{id}/import-profile": { + "/oscal/system-security-plans/{id}/dashboard-suggestion-runs/latest": { "get": { - "description": "Retrieves import-profile for a given SSP.", - "produces": [ - "application/json" - ], - "tags": [ - "System Security Plans" - ], - "summary": "Get SSP import-profile", - "parameters": [ - { - "type": "string", - "description": "SSP ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_ImportProfile" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/api.Error" - } - } - } - }, - "put": { - "description": "Updates import-profile for a given SSP.", - "consumes": [ - "application/json" - ], + "description": "Returns the latest dashboard suggestion run with cell progress.", "produces": [ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Update SSP import-profile", - "parameters": [ - { - "type": "string", - "description": "SSP ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Import Profile data", - "name": "import-profile", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.ImportProfile" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_ImportProfile" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/api.Error" - } - } - } - } - }, - "/oscal/system-security-plans/{id}/metadata": { - "get": { - "description": "Retrieves metadata for a given SSP.", - "produces": [ - "application/json" - ], - "tags": [ - "System Security Plans" - ], - "summary": "Get SSP metadata", - "parameters": [ - { - "type": "string", - "description": "SSP ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Metadata" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/api.Error" - } - } - } - }, - "put": { - "description": "Updates metadata for a given SSP.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "System Security Plans" - ], - "summary": "Update SSP metadata", - "parameters": [ - { - "type": "string", - "description": "SSP ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Metadata data", - "name": "metadata", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.Metadata" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Metadata" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/api.Error" - } - } - } - } - }, - "/oscal/system-security-plans/{id}/profile": { - "get": { - "description": "Retrieves the Profile attached to the specified System Security Plan.", - "produces": [ - "application/json" - ], - "tags": [ - "System Security Plans" - ], - "summary": "Get Profile for a System Security Plan", + "summary": "Get latest dashboard suggestion run for an SSP", "parameters": [ { "type": "string", @@ -16174,7 +16042,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Profile" + "$ref": "#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionRunResponse" } }, "400": { @@ -16207,42 +16075,39 @@ const docTemplate = `{ "OAuth2Password": [] } ] - }, - "put": { - "description": "Associates a given Profile with a System Security Plan.", - "consumes": [ - "application/json" - ], + } + }, + "/oscal/system-security-plans/{id}/dashboard-suggestion-runs/{runId}": { + "get": { + "description": "Returns a dashboard suggestion run with cell progress.", "produces": [ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Attach a Profile to a System Security Plan", + "summary": "Get a dashboard suggestion run", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { - "description": "Profile binding request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscal.addProfileRequest" - } + "type": "string", + "description": "Dashboard suggestion run ID", + "name": "runId", + "in": "path", + "required": true } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemSecurityPlan" + "$ref": "#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionRunResponse" } }, "400": { @@ -16251,6 +16116,12 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -16263,33 +16134,44 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] } }, - "/oscal/system-security-plans/{id}/profiles": { + "/oscal/system-security-plans/{id}/dashboard-suggestions": { "get": { - "description": "Returns all profiles associated with a System Security Plan via the ssp_profiles join table.", + "description": "Lists dashboard suggestions joined with control title and target filter name.", "produces": [ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "List Profiles bound to an SSP", + "summary": "List dashboard suggestions for an SSP", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true + }, + { + "type": "string", + "description": "Suggestion status (default pending)", + "name": "status", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataListResponse-oscal_profileSummary" + "$ref": "#/definitions/handler.GenericDataListResponse-oscal_dashboardSuggestionResponse" } }, "400": { @@ -16298,8 +16180,8 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", + "401": { + "description": "Unauthorized", "schema": { "$ref": "#/definitions/api.Error" } @@ -16316,9 +16198,11 @@ const docTemplate = `{ "OAuth2Password": [] } ] - }, + } + }, + "/oscal/system-security-plans/{id}/dashboard-suggestions/accept": { "post": { - "description": "Associates an additional Profile with a System Security Plan. Creates ImplementedRequirements for any new controls.", + "description": "Accepts pending dashboard suggestions and creates or extends SSP-bound filters.", "consumes": [ "application/json" ], @@ -16326,24 +16210,24 @@ const docTemplate = `{ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Add a Profile binding to an SSP", + "summary": "Accept dashboard suggestions", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { - "description": "Profile binding request", + "description": "Suggestion IDs", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscal.addProfileRequest" + "$ref": "#/definitions/oscal.dashboardSuggestionDecisionRequest" } } ], @@ -16351,7 +16235,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataListResponse-oscal_profileSummary" + "$ref": "#/definitions/handler.GenericDataResponse-oscal_acceptDashboardSuggestionsResponse" } }, "400": { @@ -16360,8 +16244,8 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", + "401": { + "description": "Unauthorized", "schema": { "$ref": "#/definitions/api.Error" } @@ -16386,37 +16270,41 @@ const docTemplate = `{ ] } }, - "/oscal/system-security-plans/{id}/profiles/{profileId}": { - "delete": { - "description": "Removes a profile association from a System Security Plan. Enqueues orphaned risk cleanup.", + "/oscal/system-security-plans/{id}/dashboard-suggestions/generate": { + "post": { + "description": "Creates a dashboard suggestion run, snapshots the resolved scope, creates run cells, and enqueues cell processing.", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Remove a Profile binding from an SSP", + "summary": "Generate dashboard suggestions for an SSP", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { - "type": "string", - "description": "Profile ID to remove", - "name": "profileId", - "in": "path", - "required": true + "description": "Generation request", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/oscal.generateDashboardSuggestionsRequest" + } } ], "responses": { - "200": { - "description": "OK", + "202": { + "description": "Accepted", "schema": { - "$ref": "#/definitions/handler.GenericDataListResponse-oscal_profileSummary" + "$ref": "#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionRunResponse" } }, "400": { @@ -16425,8 +16313,20 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "422": { + "description": "Unprocessable Entity", "schema": { "$ref": "#/definitions/api.Error" } @@ -16436,6 +16336,12 @@ const docTemplate = `{ "schema": { "$ref": "#/definitions/api.Error" } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "$ref": "#/definitions/api.Error" + } } }, "security": [ @@ -16445,16 +16351,16 @@ const docTemplate = `{ ] } }, - "/oscal/system-security-plans/{id}/system-characteristics": { + "/oscal/system-security-plans/{id}/dashboard-suggestions/label-sets": { "get": { - "description": "Retrieves the System Characteristics for a given System Security Plan.", + "description": "Returns canonical evidence label sets for dashboard suggestion scope selection.", "produces": [ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Get System Characteristics", + "summary": "List dashboard suggestion label sets", "parameters": [ { "type": "string", @@ -16468,7 +16374,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemCharacteristics" + "$ref": "#/definitions/handler.GenericDataListResponse-suggestions_LabelSetInput" } }, "400": { @@ -16483,12 +16389,6 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -16501,9 +16401,11 @@ const docTemplate = `{ "OAuth2Password": [] } ] - }, - "put": { - "description": "Updates the System Characteristics for a given System Security Plan.", + } + }, + "/oscal/system-security-plans/{id}/dashboard-suggestions/preview": { + "post": { + "description": "Resolves the requested dashboard suggestion scope and returns planned call counts without creating runs or enqueueing work.", "consumes": [ "application/json" ], @@ -16511,9 +16413,9 @@ const docTemplate = `{ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Update System Characteristics", + "summary": "Preview dashboard suggestion generation for an SSP", "parameters": [ { "type": "string", @@ -16523,12 +16425,11 @@ const docTemplate = `{ "required": true }, { - "description": "Updated System Characteristics object", - "name": "characteristics", + "description": "Preview request", + "name": "request", "in": "body", - "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.SystemCharacteristics" + "$ref": "#/definitions/oscal.generateDashboardSuggestionsRequest" } } ], @@ -16536,7 +16437,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemCharacteristics" + "$ref": "#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionPreviewResponse" } }, "400": { @@ -16551,8 +16452,8 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", + "422": { + "description": "Unprocessable Entity", "schema": { "$ref": "#/definitions/api.Error" } @@ -16571,16 +16472,19 @@ const docTemplate = `{ ] } }, - "/oscal/system-security-plans/{id}/system-characteristics/authorization-boundary": { - "get": { - "description": "Retrieves the Authorization Boundary for a given System Security Plan.", + "/oscal/system-security-plans/{id}/dashboard-suggestions/reject": { + "post": { + "description": "Rejects pending dashboard suggestions.", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Get Authorization Boundary", + "summary": "Reject dashboard suggestions", "parameters": [ { "type": "string", @@ -16588,13 +16492,22 @@ const docTemplate = `{ "name": "id", "in": "path", "required": true + }, + { + "description": "Suggestion IDs and reason", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscal.dashboardSuggestionDecisionRequest" + } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_AuthorizationBoundary" + "$ref": "#/definitions/handler.GenericDataListResponse-oscal_dashboardSuggestionResponse" } }, "400": { @@ -16609,8 +16522,8 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", + "409": { + "description": "Conflict", "schema": { "$ref": "#/definitions/api.Error" } @@ -16629,19 +16542,16 @@ const docTemplate = `{ ] } }, - "/oscal/system-security-plans/{id}/system-characteristics/authorization-boundary/diagrams": { - "post": { - "description": "Creates a new Diagram under the Authorization Boundary of a System Security Plan. Creates the Authorization Boundary grouping if it does not exist yet.", - "consumes": [ - "application/json" - ], + "/oscal/system-security-plans/{id}/dashboard-suggestions/{suggestionId}/events": { + "get": { + "description": "Returns audit events for one dashboard suggestion.", "produces": [ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Create an Authorization Boundary Diagram", + "summary": "List dashboard suggestion events", "parameters": [ { "type": "string", @@ -16651,20 +16561,18 @@ const docTemplate = `{ "required": true }, { - "description": "Diagram object to create", - "name": "diagram", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" - } + "type": "string", + "description": "Dashboard suggestion ID", + "name": "suggestionId", + "in": "path", + "required": true } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" + "$ref": "#/definitions/handler.GenericDataListResponse-suggestions_DashboardSuggestionEvent" } }, "400": { @@ -16699,49 +16607,30 @@ const docTemplate = `{ ] } }, - "/oscal/system-security-plans/{id}/system-characteristics/authorization-boundary/diagrams/{diagram}": { - "put": { - "description": "Updates a specific Diagram under the Authorization Boundary of a System Security Plan.", - "consumes": [ - "application/json" - ], + "/oscal/system-security-plans/{id}/import-profile": { + "get": { + "description": "Retrieves import-profile for a given SSP.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Update an Authorization Boundary Diagram", + "summary": "Get SSP import-profile", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true - }, - { - "type": "string", - "description": "Diagram ID", - "name": "diagram", - "in": "path", - "required": true - }, - { - "description": "Updated Diagram object", - "name": "diagram", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" - } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_ImportProfile" } }, "400": { @@ -16750,12 +16639,6 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, "404": { "description": "Not Found", "schema": { @@ -16768,50 +16651,47 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } } - }, - "security": [ - { - "OAuth2Password": [] - } - ] + } }, - "delete": { - "description": "Deletes a specific Diagram under the Authorization Boundary of a System Security Plan.", + "put": { + "description": "Updates import-profile for a given SSP.", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Delete an Authorization Boundary Diagram", + "summary": "Update SSP import-profile", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true }, { - "type": "string", - "description": "Diagram ID", - "name": "diagram", - "in": "path", - "required": true + "description": "Import Profile data", + "name": "import-profile", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscalTypes_1_1_3.ImportProfile" + } } ], "responses": { - "204": { - "description": "No Content" - }, - "400": { - "description": "Bad Request", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/api.Error" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_ImportProfile" } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad Request", "schema": { "$ref": "#/definitions/api.Error" } @@ -16828,28 +16708,23 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } } - }, - "security": [ - { - "OAuth2Password": [] - } - ] + } } }, - "/oscal/system-security-plans/{id}/system-characteristics/data-flow": { + "/oscal/system-security-plans/{id}/metadata": { "get": { - "description": "Retrieves the Data Flow for a given System Security Plan.", + "description": "Retrieves metadata for a given SSP.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Get Data Flow", + "summary": "Get SSP metadata", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true @@ -16859,7 +16734,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_DataFlow" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Metadata" } }, "400": { @@ -16868,12 +16743,6 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, "404": { "description": "Not Found", "schema": { @@ -16886,17 +16755,10 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } } - }, - "security": [ - { - "OAuth2Password": [] - } - ] - } - }, - "/oscal/system-security-plans/{id}/system-characteristics/data-flow/diagrams": { - "post": { - "description": "Creates a new Diagram under the Data Flow of a System Security Plan. Creates the Data Flow grouping if it does not exist yet.", + } + }, + "put": { + "description": "Updates metadata for a given SSP.", "consumes": [ "application/json" ], @@ -16906,30 +16768,30 @@ const docTemplate = `{ "tags": [ "System Security Plans" ], - "summary": "Create a Data Flow Diagram", + "summary": "Update SSP metadata", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true }, { - "description": "Diagram object to create", - "name": "diagram", + "description": "Metadata data", + "name": "metadata", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" + "$ref": "#/definitions/oscalTypes_1_1_3.Metadata" } } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Metadata" } }, "400": { @@ -16938,12 +16800,6 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, "404": { "description": "Not Found", "schema": { @@ -16956,27 +16812,19 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } } - }, - "security": [ - { - "OAuth2Password": [] - } - ] + } } }, - "/oscal/system-security-plans/{id}/system-characteristics/data-flow/diagrams/{diagram}": { - "put": { - "description": "Updates a specific Diagram under the Data Flow of a System Security Plan.", - "consumes": [ - "application/json" - ], + "/oscal/system-security-plans/{id}/profile": { + "get": { + "description": "Retrieves the Profile attached to the specified System Security Plan.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Update a Data Flow Diagram", + "summary": "Get Profile for a System Security Plan", "parameters": [ { "type": "string", @@ -16984,29 +16832,13 @@ const docTemplate = `{ "name": "id", "in": "path", "required": true - }, - { - "type": "string", - "description": "Diagram ID", - "name": "diagram", - "in": "path", - "required": true - }, - { - "description": "Updated Diagram object", - "name": "diagram", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" - } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Profile" } }, "400": { @@ -17040,43 +16872,45 @@ const docTemplate = `{ } ] }, - "delete": { - "description": "Deletes a specific Diagram under the Data Flow of a System Security Plan.", + "put": { + "description": "Associates a given Profile with a System Security Plan.", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Delete a Data Flow Diagram", + "summary": "Attach a Profile to a System Security Plan", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true }, { - "type": "string", - "description": "Diagram ID", - "name": "diagram", - "in": "path", - "required": true + "description": "Profile binding request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscal.addProfileRequest" + } } ], "responses": { - "204": { - "description": "No Content" - }, - "400": { - "description": "Bad Request", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/api.Error" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemSecurityPlan" } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad Request", "schema": { "$ref": "#/definitions/api.Error" } @@ -17093,28 +16927,23 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } } - }, - "security": [ - { - "OAuth2Password": [] - } - ] + } } }, - "/oscal/system-security-plans/{id}/system-characteristics/network-architecture": { + "/oscal/system-security-plans/{id}/profiles": { "get": { - "description": "Retrieves the Network Architecture for a given System Security Plan.", + "description": "Returns all profiles associated with a System Security Plan via the ssp_profiles join table.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Get Network Architecture", + "summary": "List Profiles bound to an SSP", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true @@ -17124,7 +16953,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_NetworkArchitecture" + "$ref": "#/definitions/handler.GenericDataListResponse-oscal_profileSummary" } }, "400": { @@ -17133,12 +16962,6 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, "404": { "description": "Not Found", "schema": { @@ -17157,11 +16980,9 @@ const docTemplate = `{ "OAuth2Password": [] } ] - } - }, - "/oscal/system-security-plans/{id}/system-characteristics/network-architecture/diagrams": { + }, "post": { - "description": "Creates a new Diagram under the Network Architecture of a System Security Plan. Creates the Network Architecture grouping if it does not exist yet.", + "description": "Associates an additional Profile with a System Security Plan. Creates ImplementedRequirements for any new controls.", "consumes": [ "application/json" ], @@ -17171,30 +16992,30 @@ const docTemplate = `{ "tags": [ "System Security Plans" ], - "summary": "Create a Network Architecture Diagram", + "summary": "Add a Profile binding to an SSP", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true }, { - "description": "Diagram object to create", - "name": "diagram", + "description": "Profile binding request", + "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" + "$ref": "#/definitions/oscal.addProfileRequest" } } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" + "$ref": "#/definitions/handler.GenericDataListResponse-oscal_profileSummary" } }, "400": { @@ -17203,14 +17024,14 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, - "401": { - "description": "Unauthorized", + "404": { + "description": "Not Found", "schema": { "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", + "409": { + "description": "Conflict", "schema": { "$ref": "#/definitions/api.Error" } @@ -17229,110 +17050,38 @@ const docTemplate = `{ ] } }, - "/oscal/system-security-plans/{id}/system-characteristics/network-architecture/diagrams/{diagram}": { - "put": { - "description": "Updates a specific Diagram under the Network Architecture of a System Security Plan.", - "consumes": [ - "application/json" - ], + "/oscal/system-security-plans/{id}/profiles/{profileId}": { + "delete": { + "description": "Removes a profile association from a System Security Plan. Enqueues orphaned risk cleanup.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Update a Network Architecture Diagram", + "summary": "Remove a Profile binding from an SSP", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true }, { "type": "string", - "description": "Diagram ID", - "name": "diagram", + "description": "Profile ID to remove", + "name": "profileId", "in": "path", "required": true - }, - { - "description": "Updated Diagram object", - "name": "diagram", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" - } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/api.Error" + "$ref": "#/definitions/handler.GenericDataListResponse-oscal_profileSummary" } - } - }, - "security": [ - { - "OAuth2Password": [] - } - ] - }, - "delete": { - "description": "Deletes a specific Diagram under the Network Architecture of a System Security Plan.", - "produces": [ - "application/json" - ], - "tags": [ - "System Security Plans" - ], - "summary": "Delete a Network Architecture Diagram", - "parameters": [ - { - "type": "string", - "description": "System Security Plan ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Diagram ID", - "name": "diagram", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" }, "400": { "description": "Bad Request", @@ -17340,12 +17089,6 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, "404": { "description": "Not Found", "schema": { @@ -17366,16 +17109,16 @@ const docTemplate = `{ ] } }, - "/oscal/system-security-plans/{id}/system-implementation": { + "/oscal/system-security-plans/{id}/system-characteristics": { "get": { - "description": "Retrieves the System Implementation for a given System Security Plan.", + "description": "Retrieves the System Characteristics for a given System Security Plan.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Get System Implementation", + "summary": "Get System Characteristics", "parameters": [ { "type": "string", @@ -17389,7 +17132,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemImplementation" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemCharacteristics" } }, "400": { @@ -17424,7 +17167,7 @@ const docTemplate = `{ ] }, "put": { - "description": "Updates the System Implementation for a given System Security Plan.", + "description": "Updates the System Characteristics for a given System Security Plan.", "consumes": [ "application/json" ], @@ -17434,7 +17177,7 @@ const docTemplate = `{ "tags": [ "System Security Plans" ], - "summary": "Update System Implementation", + "summary": "Update System Characteristics", "parameters": [ { "type": "string", @@ -17444,12 +17187,12 @@ const docTemplate = `{ "required": true }, { - "description": "Updated System Implementation object", - "name": "system-implementation", + "description": "Updated System Characteristics object", + "name": "characteristics", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.SystemImplementation" + "$ref": "#/definitions/oscalTypes_1_1_3.SystemCharacteristics" } } ], @@ -17457,7 +17200,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemImplementation" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemCharacteristics" } }, "400": { @@ -17492,16 +17235,16 @@ const docTemplate = `{ ] } }, - "/oscal/system-security-plans/{id}/system-implementation/components": { + "/oscal/system-security-plans/{id}/system-characteristics/authorization-boundary": { "get": { - "description": "Retrieves components in the System Implementation for a given System Security Plan.", + "description": "Retrieves the Authorization Boundary for a given System Security Plan.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "List System Implementation Components", + "summary": "Get Authorization Boundary", "parameters": [ { "type": "string", @@ -17515,7 +17258,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_SystemComponent" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_AuthorizationBoundary" } }, "400": { @@ -17548,9 +17291,11 @@ const docTemplate = `{ "OAuth2Password": [] } ] - }, + } + }, + "/oscal/system-security-plans/{id}/system-characteristics/authorization-boundary/diagrams": { "post": { - "description": "Creates a new system component for a given SSP. Accepts an optional definedComponentId field to link to a DefinedComponent.", + "description": "Creates a new Diagram under the Authorization Boundary of a System Security Plan. Creates the Authorization Boundary grouping if it does not exist yet.", "consumes": [ "application/json" ], @@ -17560,22 +17305,22 @@ const docTemplate = `{ "tags": [ "System Security Plans" ], - "summary": "Create a new system component", + "summary": "Create an Authorization Boundary Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { - "description": "System Component data with optional definedComponentId field", - "name": "component", + "description": "Diagram object to create", + "name": "diagram", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscal.SystemComponentRequest" + "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" } } ], @@ -17583,7 +17328,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemComponent" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" } }, "400": { @@ -17592,6 +17337,12 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -17604,19 +17355,27 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] } }, - "/oscal/system-security-plans/{id}/system-implementation/components/{componentId}": { - "get": { - "description": "Retrieves component in the System Implementation for a given System Security Plan.", + "/oscal/system-security-plans/{id}/system-characteristics/authorization-boundary/diagrams/{diagram}": { + "put": { + "description": "Updates a specific Diagram under the Authorization Boundary of a System Security Plan.", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Get System Implementation Component", + "summary": "Update an Authorization Boundary Diagram", "parameters": [ { "type": "string", @@ -17627,17 +17386,26 @@ const docTemplate = `{ }, { "type": "string", - "description": "Component ID", - "name": "componentId", + "description": "Diagram ID", + "name": "diagram", "in": "path", "required": true + }, + { + "description": "Updated Diagram object", + "name": "diagram", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" + } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemComponent" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" } }, "400": { @@ -17671,49 +17439,34 @@ const docTemplate = `{ } ] }, - "put": { - "description": "Updates an existing system component for a given SSP. Accepts an optional definedComponentId field to link to a DefinedComponent.", - "consumes": [ - "application/json" - ], + "delete": { + "description": "Deletes a specific Diagram under the Authorization Boundary of a System Security Plan.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Update a system component", + "summary": "Delete an Authorization Boundary Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { "type": "string", - "description": "Component ID", - "name": "componentId", + "description": "Diagram ID", + "name": "diagram", "in": "path", "required": true - }, - { - "description": "System Component data with optional definedComponentId field", - "name": "component", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscal.SystemComponentRequest" - } } ], "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemComponent" - } + "204": { + "description": "No Content" }, "400": { "description": "Bad Request", @@ -17721,48 +17474,8 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/api.Error" - } - } - } - }, - "delete": { - "description": "Deletes an existing system component for a given SSP.", - "tags": [ - "System Security Plans" - ], - "summary": "Delete a system component", - "parameters": [ - { - "type": "string", - "description": "SSP ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Component ID", - "name": "componentId", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - }, - "400": { - "description": "Bad Request", + "401": { + "description": "Unauthorized", "schema": { "$ref": "#/definitions/api.Error" } @@ -17779,19 +17492,24 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] } }, - "/oscal/system-security-plans/{id}/system-implementation/inventory-items": { + "/oscal/system-security-plans/{id}/system-characteristics/data-flow": { "get": { - "description": "Retrieves inventory items in the System Implementation for a given System Security Plan.", + "description": "Retrieves the Data Flow for a given System Security Plan.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "List System Implementation Inventory Items", + "summary": "Get Data Flow", "parameters": [ { "type": "string", @@ -17805,7 +17523,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_InventoryItem" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_DataFlow" } }, "400": { @@ -17838,9 +17556,11 @@ const docTemplate = `{ "OAuth2Password": [] } ] - }, + } + }, + "/oscal/system-security-plans/{id}/system-characteristics/data-flow/diagrams": { "post": { - "description": "Creates a new inventory item for a given SSP.", + "description": "Creates a new Diagram under the Data Flow of a System Security Plan. Creates the Data Flow grouping if it does not exist yet.", "consumes": [ "application/json" ], @@ -17850,22 +17570,22 @@ const docTemplate = `{ "tags": [ "System Security Plans" ], - "summary": "Create a new inventory item", + "summary": "Create a Data Flow Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { - "description": "Inventory Item data", - "name": "item", + "description": "Diagram object to create", + "name": "diagram", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.InventoryItem" + "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" } } ], @@ -17873,7 +17593,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_InventoryItem" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" } }, "400": { @@ -17882,6 +17602,12 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -17894,12 +17620,17 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] } }, - "/oscal/system-security-plans/{id}/system-implementation/inventory-items/{itemId}": { + "/oscal/system-security-plans/{id}/system-characteristics/data-flow/diagrams/{diagram}": { "put": { - "description": "Updates an existing inventory item for a given SSP.", + "description": "Updates a specific Diagram under the Data Flow of a System Security Plan.", "consumes": [ "application/json" ], @@ -17909,29 +17640,29 @@ const docTemplate = `{ "tags": [ "System Security Plans" ], - "summary": "Update an inventory item", + "summary": "Update a Data Flow Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { "type": "string", - "description": "Item ID", - "name": "itemId", + "description": "Diagram ID", + "name": "diagram", "in": "path", "required": true }, { - "description": "Inventory Item data", - "name": "item", + "description": "Updated Diagram object", + "name": "diagram", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.InventoryItem" + "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" } } ], @@ -17939,7 +17670,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_InventoryItem" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" } }, "400": { @@ -17948,6 +17679,12 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -17960,26 +17697,34 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] }, "delete": { - "description": "Deletes an existing inventory item for a given SSP.", + "description": "Deletes a specific Diagram under the Data Flow of a System Security Plan.", + "produces": [ + "application/json" + ], "tags": [ "System Security Plans" ], - "summary": "Delete an inventory item", + "summary": "Delete a Data Flow Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { "type": "string", - "description": "Item ID", - "name": "itemId", + "description": "Diagram ID", + "name": "diagram", "in": "path", "required": true } @@ -17994,6 +17739,12 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -18006,19 +17757,24 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] } }, - "/oscal/system-security-plans/{id}/system-implementation/leveraged-authorizations": { + "/oscal/system-security-plans/{id}/system-characteristics/network-architecture": { "get": { - "description": "Retrieves leveraged authorizations in the System Implementation for a given System Security Plan.", + "description": "Retrieves the Network Architecture for a given System Security Plan.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "List System Implementation Leveraged Authorizations", + "summary": "Get Network Architecture", "parameters": [ { "type": "string", @@ -18032,7 +17788,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_LeveragedAuthorization" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_NetworkArchitecture" } }, "400": { @@ -18065,9 +17821,11 @@ const docTemplate = `{ "OAuth2Password": [] } ] - }, + } + }, + "/oscal/system-security-plans/{id}/system-characteristics/network-architecture/diagrams": { "post": { - "description": "Creates a new leveraged authorization for a given SSP.", + "description": "Creates a new Diagram under the Network Architecture of a System Security Plan. Creates the Network Architecture grouping if it does not exist yet.", "consumes": [ "application/json" ], @@ -18077,22 +17835,22 @@ const docTemplate = `{ "tags": [ "System Security Plans" ], - "summary": "Create a new leveraged authorization", + "summary": "Create a Network Architecture Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { - "description": "Leveraged Authorization data", - "name": "auth", + "description": "Diagram object to create", + "name": "diagram", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.LeveragedAuthorization" + "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" } } ], @@ -18100,7 +17858,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_LeveragedAuthorization" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" } }, "400": { @@ -18109,6 +17867,12 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -18121,12 +17885,17 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] } }, - "/oscal/system-security-plans/{id}/system-implementation/leveraged-authorizations/{authId}": { + "/oscal/system-security-plans/{id}/system-characteristics/network-architecture/diagrams/{diagram}": { "put": { - "description": "Updates an existing leveraged authorization for a given SSP.", + "description": "Updates a specific Diagram under the Network Architecture of a System Security Plan.", "consumes": [ "application/json" ], @@ -18136,29 +17905,29 @@ const docTemplate = `{ "tags": [ "System Security Plans" ], - "summary": "Update a leveraged authorization", + "summary": "Update a Network Architecture Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { "type": "string", - "description": "Authorization ID", - "name": "authId", + "description": "Diagram ID", + "name": "diagram", "in": "path", "required": true }, { - "description": "Leveraged Authorization data", - "name": "auth", + "description": "Updated Diagram object", + "name": "diagram", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.LeveragedAuthorization" + "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" } } ], @@ -18166,7 +17935,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_LeveragedAuthorization" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" } }, "400": { @@ -18175,6 +17944,12 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -18187,26 +17962,34 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] }, "delete": { - "description": "Deletes an existing leveraged authorization for a given SSP.", + "description": "Deletes a specific Diagram under the Network Architecture of a System Security Plan.", + "produces": [ + "application/json" + ], "tags": [ "System Security Plans" ], - "summary": "Delete a leveraged authorization", + "summary": "Delete a Network Architecture Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { "type": "string", - "description": "Authorization ID", - "name": "authId", + "description": "Diagram ID", + "name": "diagram", "in": "path", "required": true } @@ -18221,6 +18004,12 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -18233,19 +18022,24 @@ const docTemplate = `{ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] } }, - "/oscal/system-security-plans/{id}/system-implementation/users": { + "/oscal/system-security-plans/{id}/system-implementation": { "get": { - "description": "Retrieves users in the System Implementation for a given System Security Plan.", + "description": "Retrieves the System Implementation for a given System Security Plan.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "List System Implementation Users", + "summary": "Get System Implementation", "parameters": [ { "type": "string", @@ -18259,7 +18053,877 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_SystemUser" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemImplementation" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + }, + "security": [ + { + "OAuth2Password": [] + } + ] + }, + "put": { + "description": "Updates the System Implementation for a given System Security Plan.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Update System Implementation", + "parameters": [ + { + "type": "string", + "description": "System Security Plan ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated System Implementation object", + "name": "system-implementation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscalTypes_1_1_3.SystemImplementation" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemImplementation" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + }, + "security": [ + { + "OAuth2Password": [] + } + ] + } + }, + "/oscal/system-security-plans/{id}/system-implementation/components": { + "get": { + "description": "Retrieves components in the System Implementation for a given System Security Plan.", + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "List System Implementation Components", + "parameters": [ + { + "type": "string", + "description": "System Security Plan ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_SystemComponent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + }, + "security": [ + { + "OAuth2Password": [] + } + ] + }, + "post": { + "description": "Creates a new system component for a given SSP. Accepts an optional definedComponentId field to link to a DefinedComponent.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Create a new system component", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "System Component data with optional definedComponentId field", + "name": "component", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscal.SystemComponentRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemComponent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/oscal/system-security-plans/{id}/system-implementation/components/{componentId}": { + "get": { + "description": "Retrieves component in the System Implementation for a given System Security Plan.", + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Get System Implementation Component", + "parameters": [ + { + "type": "string", + "description": "System Security Plan ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Component ID", + "name": "componentId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemComponent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + }, + "security": [ + { + "OAuth2Password": [] + } + ] + }, + "put": { + "description": "Updates an existing system component for a given SSP. Accepts an optional definedComponentId field to link to a DefinedComponent.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Update a system component", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Component ID", + "name": "componentId", + "in": "path", + "required": true + }, + { + "description": "System Component data with optional definedComponentId field", + "name": "component", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscal.SystemComponentRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemComponent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + }, + "delete": { + "description": "Deletes an existing system component for a given SSP.", + "tags": [ + "System Security Plans" + ], + "summary": "Delete a system component", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Component ID", + "name": "componentId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/oscal/system-security-plans/{id}/system-implementation/inventory-items": { + "get": { + "description": "Retrieves inventory items in the System Implementation for a given System Security Plan.", + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "List System Implementation Inventory Items", + "parameters": [ + { + "type": "string", + "description": "System Security Plan ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_InventoryItem" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + }, + "security": [ + { + "OAuth2Password": [] + } + ] + }, + "post": { + "description": "Creates a new inventory item for a given SSP.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Create a new inventory item", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Inventory Item data", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscalTypes_1_1_3.InventoryItem" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_InventoryItem" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/oscal/system-security-plans/{id}/system-implementation/inventory-items/{itemId}": { + "put": { + "description": "Updates an existing inventory item for a given SSP.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Update an inventory item", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Item ID", + "name": "itemId", + "in": "path", + "required": true + }, + { + "description": "Inventory Item data", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscalTypes_1_1_3.InventoryItem" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_InventoryItem" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + }, + "delete": { + "description": "Deletes an existing inventory item for a given SSP.", + "tags": [ + "System Security Plans" + ], + "summary": "Delete an inventory item", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Item ID", + "name": "itemId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/oscal/system-security-plans/{id}/system-implementation/leveraged-authorizations": { + "get": { + "description": "Retrieves leveraged authorizations in the System Implementation for a given System Security Plan.", + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "List System Implementation Leveraged Authorizations", + "parameters": [ + { + "type": "string", + "description": "System Security Plan ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_LeveragedAuthorization" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + }, + "security": [ + { + "OAuth2Password": [] + } + ] + }, + "post": { + "description": "Creates a new leveraged authorization for a given SSP.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Create a new leveraged authorization", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Leveraged Authorization data", + "name": "auth", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscalTypes_1_1_3.LeveragedAuthorization" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_LeveragedAuthorization" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/oscal/system-security-plans/{id}/system-implementation/leveraged-authorizations/{authId}": { + "put": { + "description": "Updates an existing leveraged authorization for a given SSP.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Update a leveraged authorization", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Authorization ID", + "name": "authId", + "in": "path", + "required": true + }, + { + "description": "Leveraged Authorization data", + "name": "auth", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscalTypes_1_1_3.LeveragedAuthorization" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_LeveragedAuthorization" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + }, + "delete": { + "description": "Deletes an existing leveraged authorization for a given SSP.", + "tags": [ + "System Security Plans" + ], + "summary": "Delete a leveraged authorization", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Authorization ID", + "name": "authId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/oscal/system-security-plans/{id}/system-implementation/users": { + "get": { + "description": "Retrieves users in the System Implementation for a given System Security Plan.", + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "List System Implementation Users", + "parameters": [ + { + "type": "string", + "description": "System Security Plan ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_SystemUser" } }, "400": { @@ -28283,6 +28947,18 @@ const docTemplate = `{ } } }, + "handler.GenericDataListResponse-oscal_dashboardSuggestionResponse": { + "type": "object", + "properties": { + "data": { + "description": "Items from the list response", + "type": "array", + "items": { + "$ref": "#/definitions/oscal.dashboardSuggestionResponse" + } + } + } + }, "handler.GenericDataListResponse-oscal_profileSummary": { "type": "object", "properties": { @@ -28379,6 +29055,30 @@ const docTemplate = `{ } } }, + "handler.GenericDataListResponse-suggestions_DashboardSuggestionEvent": { + "type": "object", + "properties": { + "data": { + "description": "Items from the list response", + "type": "array", + "items": { + "$ref": "#/definitions/suggestions.DashboardSuggestionEvent" + } + } + } + }, + "handler.GenericDataListResponse-suggestions_LabelSetInput": { + "type": "object", + "properties": { + "data": { + "description": "Items from the list response", + "type": "array", + "items": { + "$ref": "#/definitions/suggestions.LabelSetInput" + } + } + } + }, "handler.GenericDataResponse-array_oscalTypes_1_1_3_AssessmentAssets": { "type": "object", "properties": { @@ -29377,6 +30077,58 @@ const docTemplate = `{ } } }, + "handler.GenericDataResponse-oscal_acceptDashboardSuggestionsResponse": { + "type": "object", + "properties": { + "data": { + "description": "Wrapped response data", + "allOf": [ + { + "$ref": "#/definitions/oscal.acceptDashboardSuggestionsResponse" + } + ] + } + } + }, + "handler.GenericDataResponse-oscal_dashboardSuggestionConfigResponse": { + "type": "object", + "properties": { + "data": { + "description": "Wrapped response data", + "allOf": [ + { + "$ref": "#/definitions/oscal.dashboardSuggestionConfigResponse" + } + ] + } + } + }, + "handler.GenericDataResponse-oscal_dashboardSuggestionPreviewResponse": { + "type": "object", + "properties": { + "data": { + "description": "Wrapped response data", + "allOf": [ + { + "$ref": "#/definitions/oscal.dashboardSuggestionPreviewResponse" + } + ] + } + } + }, + "handler.GenericDataResponse-oscal_dashboardSuggestionRunResponse": { + "type": "object", + "properties": { + "data": { + "description": "Wrapped response data", + "allOf": [ + { + "$ref": "#/definitions/oscal.dashboardSuggestionRunResponse" + } + ] + } + } + }, "handler.GenericDataResponse-poam_PoamItemControlLink": { "type": "object", "properties": { @@ -29807,6 +30559,13 @@ const docTemplate = `{ }, "name": { "type": "string" + }, + "sspId": { + "description": "System Security Plan ID. On PUT, omitted or null clears the binding to global.", + "type": "string", + "format": "uuid", + "x-nullable": true, + "example": "00000000-0000-0000-0000-000000000000" } } }, @@ -31633,6 +32392,23 @@ const docTemplate = `{ } } }, + "oscal.acceptDashboardSuggestionsResponse": { + "type": "object", + "properties": { + "acceptedFilterIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "suggestions": { + "type": "array", + "items": { + "$ref": "#/definitions/oscal.dashboardSuggestionResponse" + } + } + } + }, "oscal.addProfileRequest": { "type": "object", "properties": { @@ -31641,6 +32417,206 @@ const docTemplate = `{ } } }, + "oscal.dashboardSuggestionConfigResponse": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "oscal.dashboardSuggestionDecisionRequest": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "reason": { + "type": "string" + } + } + }, + "oscal.dashboardSuggestionPreviewResponse": { + "type": "object", + "properties": { + "controlCount": { + "type": "integer" + }, + "exceedsLimit": { + "type": "boolean" + }, + "labelSetCount": { + "type": "integer" + }, + "maxCallsPerRun": { + "type": "integer" + }, + "plannedCalls": { + "type": "integer" + } + } + }, + "oscal.dashboardSuggestionResponse": { + "type": "object", + "properties": { + "acceptedFilterId": { + "type": "string" + }, + "confidence": { + "type": "number" + }, + "controlCatalogId": { + "type": "string" + }, + "controlId": { + "type": "string" + }, + "controlTitle": { + "type": "string" + }, + "decidedAt": { + "type": "string" + }, + "decidedByUserId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "labelSet": { + "$ref": "#/definitions/datatypes.JSONMap" + }, + "labelSetHash": { + "type": "string" + }, + "proposedFilterName": { + "type": "string" + }, + "reasoning": { + "type": "string" + }, + "rejectReason": { + "type": "string" + }, + "runId": { + "type": "string" + }, + "sspId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "targetFilterId": { + "type": "string" + }, + "targetFilterName": { + "type": "string" + } + } + }, + "oscal.dashboardSuggestionRunResponse": { + "type": "object", + "properties": { + "cells": { + "type": "array", + "items": { + "$ref": "#/definitions/suggestions.DashboardSuggestionRunCell" + } + }, + "completedAt": { + "type": "string" + }, + "completedCells": { + "type": "integer" + }, + "error": { + "type": "string" + }, + "failedCells": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "inputTokens": { + "type": "integer" + }, + "model": { + "type": "string" + }, + "outputTokens": { + "type": "integer" + }, + "plannedCalls": { + "type": "integer" + }, + "promptVersion": { + "type": "string" + }, + "scope": { + "$ref": "#/definitions/datatypes.JSONMap" + }, + "sspId": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "stats": { + "$ref": "#/definitions/datatypes.JSONMap" + }, + "status": { + "type": "string" + }, + "suggestionCount": { + "type": "integer" + }, + "suggestions": { + "type": "array", + "items": { + "$ref": "#/definitions/suggestions.DashboardSuggestion" + } + }, + "triggeredByUserId": { + "type": "string" + } + } + }, + "oscal.dashboardSuggestionScopeRequest": { + "type": "object", + "properties": { + "controlKeys": { + "type": "array", + "items": { + "type": "string" + } + }, + "labelSetHashes": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "oscal.generateDashboardSuggestionsRequest": { + "type": "object", + "properties": { + "scope": { + "$ref": "#/definitions/oscal.dashboardSuggestionScopeRequest" + }, + "supersedePending": { + "type": "boolean" + } + } + }, "oscal.profileSummary": { "type": "object", "properties": { @@ -39475,6 +40451,158 @@ const docTemplate = `{ } } }, + "suggestions.DashboardSuggestion": { + "type": "object", + "properties": { + "acceptedFilterId": { + "type": "string" + }, + "confidence": { + "type": "number" + }, + "controlCatalogId": { + "type": "string" + }, + "controlId": { + "type": "string" + }, + "decidedAt": { + "type": "string" + }, + "decidedByUserId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "labelSet": { + "$ref": "#/definitions/datatypes.JSONMap" + }, + "labelSetHash": { + "type": "string" + }, + "proposedFilterName": { + "type": "string" + }, + "reasoning": { + "type": "string" + }, + "rejectReason": { + "type": "string" + }, + "runId": { + "type": "string" + }, + "sspId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "targetFilterId": { + "type": "string" + } + } + }, + "suggestions.DashboardSuggestionEvent": { + "type": "object", + "properties": { + "actorUserId": { + "type": "string" + }, + "details": { + "type": "string" + }, + "eventType": { + "type": "string" + }, + "id": { + "type": "string" + }, + "occurredAt": { + "type": "string" + }, + "payload": { + "$ref": "#/definitions/datatypes.JSONMap" + }, + "runId": { + "type": "string" + }, + "snapshot": { + "$ref": "#/definitions/datatypes.JSONMap" + }, + "suggestionId": { + "type": "string" + } + } + }, + "suggestions.DashboardSuggestionRunCell": { + "type": "object", + "properties": { + "cellIndex": { + "type": "integer" + }, + "completedAt": { + "type": "string" + }, + "controlKeys": { + "type": "array", + "items": { + "type": "string" + } + }, + "error": { + "type": "string" + }, + "inputTokens": { + "type": "integer" + }, + "labelSetHashes": { + "type": "array", + "items": { + "type": "string" + } + }, + "mappingsRejected": { + "type": "integer" + }, + "mappingsReturned": { + "type": "integer" + }, + "outputTokens": { + "type": "integer" + }, + "runId": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "suggestions.LabelSetInput": { + "type": "object", + "properties": { + "evidence_count": { + "type": "integer" + }, + "hash": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "sample_titles": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "templates.batchRiskTemplateItem": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index ce98b4f1..4d5d5193 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1813,6 +1813,26 @@ } } }, + "/dashboard-suggestions/config": { + "get": { + "description": "Returns whether AI dashboard suggestions are enabled.", + "produces": [ + "application/json" + ], + "tags": [ + "Dashboard Suggestions" + ], + "summary": "Get dashboard suggestions feature configuration", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionConfigResponse" + } + } + } + } + }, "/evidence": { "post": { "description": "Creates a new Evidence record including activities, inventory items, components, and subjects.", @@ -1881,6 +1901,12 @@ "name": "id", "in": "path", "required": true + }, + { + "type": "string", + "description": "System Security Plan ID; limits filters to global + same-SSP", + "name": "sspId", + "in": "query" } ], "responses": { @@ -1890,6 +1916,12 @@ "$ref": "#/definitions/handler.GenericDataListResponse-evidence_StatusCount" } }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -1963,6 +1995,12 @@ "name": "id", "in": "path", "required": true + }, + { + "type": "string", + "description": "System Security Plan ID; limits filters to global + same-SSP", + "name": "sspId", + "in": "query" } ], "responses": { @@ -2457,7 +2495,7 @@ }, "/filters": { "get": { - "description": "Retrieves all filters, optionally filtered by controlId or componentId.", + "description": "Retrieves filters, optionally filtered by controlId, componentId, sspId, or global scope.", "produces": [ "application/json" ], @@ -2465,6 +2503,32 @@ "Filters" ], "summary": "List filters", + "parameters": [ + { + "type": "string", + "description": "Control ID", + "name": "controlId", + "in": "query" + }, + { + "type": "string", + "description": "Component ID", + "name": "componentId", + "in": "query" + }, + { + "type": "string", + "description": "System Security Plan ID; returns global + same-SSP filters", + "name": "sspId", + "in": "query" + }, + { + "type": "string", + "description": "Filter scope. Use 'global' for global filters only", + "name": "scope", + "in": "query" + } + ], "responses": { "200": { "description": "OK", @@ -2472,6 +2536,18 @@ "$ref": "#/definitions/handler.GenericDataListResponse-handler_FilterWithAssociations" } }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -15937,224 +16013,16 @@ ] } }, - "/oscal/system-security-plans/{id}/import-profile": { + "/oscal/system-security-plans/{id}/dashboard-suggestion-runs/latest": { "get": { - "description": "Retrieves import-profile for a given SSP.", - "produces": [ - "application/json" - ], - "tags": [ - "System Security Plans" - ], - "summary": "Get SSP import-profile", - "parameters": [ - { - "type": "string", - "description": "SSP ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_ImportProfile" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/api.Error" - } - } - } - }, - "put": { - "description": "Updates import-profile for a given SSP.", - "consumes": [ - "application/json" - ], + "description": "Returns the latest dashboard suggestion run with cell progress.", "produces": [ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Update SSP import-profile", - "parameters": [ - { - "type": "string", - "description": "SSP ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Import Profile data", - "name": "import-profile", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.ImportProfile" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_ImportProfile" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/api.Error" - } - } - } - } - }, - "/oscal/system-security-plans/{id}/metadata": { - "get": { - "description": "Retrieves metadata for a given SSP.", - "produces": [ - "application/json" - ], - "tags": [ - "System Security Plans" - ], - "summary": "Get SSP metadata", - "parameters": [ - { - "type": "string", - "description": "SSP ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Metadata" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/api.Error" - } - } - } - }, - "put": { - "description": "Updates metadata for a given SSP.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "System Security Plans" - ], - "summary": "Update SSP metadata", - "parameters": [ - { - "type": "string", - "description": "SSP ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Metadata data", - "name": "metadata", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.Metadata" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Metadata" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/api.Error" - } - } - } - } - }, - "/oscal/system-security-plans/{id}/profile": { - "get": { - "description": "Retrieves the Profile attached to the specified System Security Plan.", - "produces": [ - "application/json" - ], - "tags": [ - "System Security Plans" - ], - "summary": "Get Profile for a System Security Plan", + "summary": "Get latest dashboard suggestion run for an SSP", "parameters": [ { "type": "string", @@ -16168,7 +16036,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Profile" + "$ref": "#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionRunResponse" } }, "400": { @@ -16201,42 +16069,39 @@ "OAuth2Password": [] } ] - }, - "put": { - "description": "Associates a given Profile with a System Security Plan.", - "consumes": [ - "application/json" - ], + } + }, + "/oscal/system-security-plans/{id}/dashboard-suggestion-runs/{runId}": { + "get": { + "description": "Returns a dashboard suggestion run with cell progress.", "produces": [ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Attach a Profile to a System Security Plan", + "summary": "Get a dashboard suggestion run", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { - "description": "Profile binding request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscal.addProfileRequest" - } + "type": "string", + "description": "Dashboard suggestion run ID", + "name": "runId", + "in": "path", + "required": true } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemSecurityPlan" + "$ref": "#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionRunResponse" } }, "400": { @@ -16245,6 +16110,12 @@ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -16257,33 +16128,44 @@ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] } }, - "/oscal/system-security-plans/{id}/profiles": { + "/oscal/system-security-plans/{id}/dashboard-suggestions": { "get": { - "description": "Returns all profiles associated with a System Security Plan via the ssp_profiles join table.", + "description": "Lists dashboard suggestions joined with control title and target filter name.", "produces": [ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "List Profiles bound to an SSP", + "summary": "List dashboard suggestions for an SSP", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true + }, + { + "type": "string", + "description": "Suggestion status (default pending)", + "name": "status", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataListResponse-oscal_profileSummary" + "$ref": "#/definitions/handler.GenericDataListResponse-oscal_dashboardSuggestionResponse" } }, "400": { @@ -16292,8 +16174,8 @@ "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", + "401": { + "description": "Unauthorized", "schema": { "$ref": "#/definitions/api.Error" } @@ -16310,9 +16192,11 @@ "OAuth2Password": [] } ] - }, + } + }, + "/oscal/system-security-plans/{id}/dashboard-suggestions/accept": { "post": { - "description": "Associates an additional Profile with a System Security Plan. Creates ImplementedRequirements for any new controls.", + "description": "Accepts pending dashboard suggestions and creates or extends SSP-bound filters.", "consumes": [ "application/json" ], @@ -16320,24 +16204,24 @@ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Add a Profile binding to an SSP", + "summary": "Accept dashboard suggestions", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { - "description": "Profile binding request", + "description": "Suggestion IDs", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscal.addProfileRequest" + "$ref": "#/definitions/oscal.dashboardSuggestionDecisionRequest" } } ], @@ -16345,7 +16229,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataListResponse-oscal_profileSummary" + "$ref": "#/definitions/handler.GenericDataResponse-oscal_acceptDashboardSuggestionsResponse" } }, "400": { @@ -16354,8 +16238,8 @@ "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", + "401": { + "description": "Unauthorized", "schema": { "$ref": "#/definitions/api.Error" } @@ -16380,37 +16264,41 @@ ] } }, - "/oscal/system-security-plans/{id}/profiles/{profileId}": { - "delete": { - "description": "Removes a profile association from a System Security Plan. Enqueues orphaned risk cleanup.", + "/oscal/system-security-plans/{id}/dashboard-suggestions/generate": { + "post": { + "description": "Creates a dashboard suggestion run, snapshots the resolved scope, creates run cells, and enqueues cell processing.", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Remove a Profile binding from an SSP", + "summary": "Generate dashboard suggestions for an SSP", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { - "type": "string", - "description": "Profile ID to remove", - "name": "profileId", - "in": "path", - "required": true + "description": "Generation request", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/oscal.generateDashboardSuggestionsRequest" + } } ], "responses": { - "200": { - "description": "OK", + "202": { + "description": "Accepted", "schema": { - "$ref": "#/definitions/handler.GenericDataListResponse-oscal_profileSummary" + "$ref": "#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionRunResponse" } }, "400": { @@ -16419,8 +16307,20 @@ "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "422": { + "description": "Unprocessable Entity", "schema": { "$ref": "#/definitions/api.Error" } @@ -16430,6 +16330,12 @@ "schema": { "$ref": "#/definitions/api.Error" } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "$ref": "#/definitions/api.Error" + } } }, "security": [ @@ -16439,16 +16345,16 @@ ] } }, - "/oscal/system-security-plans/{id}/system-characteristics": { + "/oscal/system-security-plans/{id}/dashboard-suggestions/label-sets": { "get": { - "description": "Retrieves the System Characteristics for a given System Security Plan.", + "description": "Returns canonical evidence label sets for dashboard suggestion scope selection.", "produces": [ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Get System Characteristics", + "summary": "List dashboard suggestion label sets", "parameters": [ { "type": "string", @@ -16462,7 +16368,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemCharacteristics" + "$ref": "#/definitions/handler.GenericDataListResponse-suggestions_LabelSetInput" } }, "400": { @@ -16477,12 +16383,6 @@ "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -16495,9 +16395,11 @@ "OAuth2Password": [] } ] - }, - "put": { - "description": "Updates the System Characteristics for a given System Security Plan.", + } + }, + "/oscal/system-security-plans/{id}/dashboard-suggestions/preview": { + "post": { + "description": "Resolves the requested dashboard suggestion scope and returns planned call counts without creating runs or enqueueing work.", "consumes": [ "application/json" ], @@ -16505,9 +16407,9 @@ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Update System Characteristics", + "summary": "Preview dashboard suggestion generation for an SSP", "parameters": [ { "type": "string", @@ -16517,12 +16419,11 @@ "required": true }, { - "description": "Updated System Characteristics object", - "name": "characteristics", + "description": "Preview request", + "name": "request", "in": "body", - "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.SystemCharacteristics" + "$ref": "#/definitions/oscal.generateDashboardSuggestionsRequest" } } ], @@ -16530,7 +16431,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemCharacteristics" + "$ref": "#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionPreviewResponse" } }, "400": { @@ -16545,8 +16446,8 @@ "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", + "422": { + "description": "Unprocessable Entity", "schema": { "$ref": "#/definitions/api.Error" } @@ -16565,16 +16466,19 @@ ] } }, - "/oscal/system-security-plans/{id}/system-characteristics/authorization-boundary": { - "get": { - "description": "Retrieves the Authorization Boundary for a given System Security Plan.", + "/oscal/system-security-plans/{id}/dashboard-suggestions/reject": { + "post": { + "description": "Rejects pending dashboard suggestions.", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Get Authorization Boundary", + "summary": "Reject dashboard suggestions", "parameters": [ { "type": "string", @@ -16582,13 +16486,22 @@ "name": "id", "in": "path", "required": true + }, + { + "description": "Suggestion IDs and reason", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscal.dashboardSuggestionDecisionRequest" + } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_AuthorizationBoundary" + "$ref": "#/definitions/handler.GenericDataListResponse-oscal_dashboardSuggestionResponse" } }, "400": { @@ -16603,8 +16516,8 @@ "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", + "409": { + "description": "Conflict", "schema": { "$ref": "#/definitions/api.Error" } @@ -16623,19 +16536,16 @@ ] } }, - "/oscal/system-security-plans/{id}/system-characteristics/authorization-boundary/diagrams": { - "post": { - "description": "Creates a new Diagram under the Authorization Boundary of a System Security Plan. Creates the Authorization Boundary grouping if it does not exist yet.", - "consumes": [ - "application/json" - ], + "/oscal/system-security-plans/{id}/dashboard-suggestions/{suggestionId}/events": { + "get": { + "description": "Returns audit events for one dashboard suggestion.", "produces": [ "application/json" ], "tags": [ - "System Security Plans" + "Dashboard Suggestions" ], - "summary": "Create an Authorization Boundary Diagram", + "summary": "List dashboard suggestion events", "parameters": [ { "type": "string", @@ -16645,20 +16555,18 @@ "required": true }, { - "description": "Diagram object to create", - "name": "diagram", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" - } + "type": "string", + "description": "Dashboard suggestion ID", + "name": "suggestionId", + "in": "path", + "required": true } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" + "$ref": "#/definitions/handler.GenericDataListResponse-suggestions_DashboardSuggestionEvent" } }, "400": { @@ -16693,49 +16601,30 @@ ] } }, - "/oscal/system-security-plans/{id}/system-characteristics/authorization-boundary/diagrams/{diagram}": { - "put": { - "description": "Updates a specific Diagram under the Authorization Boundary of a System Security Plan.", - "consumes": [ - "application/json" - ], + "/oscal/system-security-plans/{id}/import-profile": { + "get": { + "description": "Retrieves import-profile for a given SSP.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Update an Authorization Boundary Diagram", + "summary": "Get SSP import-profile", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true - }, - { - "type": "string", - "description": "Diagram ID", - "name": "diagram", - "in": "path", - "required": true - }, - { - "description": "Updated Diagram object", - "name": "diagram", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" - } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_ImportProfile" } }, "400": { @@ -16744,12 +16633,6 @@ "$ref": "#/definitions/api.Error" } }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, "404": { "description": "Not Found", "schema": { @@ -16762,50 +16645,47 @@ "$ref": "#/definitions/api.Error" } } - }, - "security": [ - { - "OAuth2Password": [] - } - ] + } }, - "delete": { - "description": "Deletes a specific Diagram under the Authorization Boundary of a System Security Plan.", + "put": { + "description": "Updates import-profile for a given SSP.", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Delete an Authorization Boundary Diagram", + "summary": "Update SSP import-profile", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true }, { - "type": "string", - "description": "Diagram ID", - "name": "diagram", - "in": "path", - "required": true + "description": "Import Profile data", + "name": "import-profile", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscalTypes_1_1_3.ImportProfile" + } } ], "responses": { - "204": { - "description": "No Content" - }, - "400": { - "description": "Bad Request", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/api.Error" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_ImportProfile" } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad Request", "schema": { "$ref": "#/definitions/api.Error" } @@ -16822,28 +16702,23 @@ "$ref": "#/definitions/api.Error" } } - }, - "security": [ - { - "OAuth2Password": [] - } - ] + } } }, - "/oscal/system-security-plans/{id}/system-characteristics/data-flow": { + "/oscal/system-security-plans/{id}/metadata": { "get": { - "description": "Retrieves the Data Flow for a given System Security Plan.", + "description": "Retrieves metadata for a given SSP.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Get Data Flow", + "summary": "Get SSP metadata", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true @@ -16853,7 +16728,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_DataFlow" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Metadata" } }, "400": { @@ -16862,12 +16737,6 @@ "$ref": "#/definitions/api.Error" } }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, "404": { "description": "Not Found", "schema": { @@ -16880,17 +16749,10 @@ "$ref": "#/definitions/api.Error" } } - }, - "security": [ - { - "OAuth2Password": [] - } - ] - } - }, - "/oscal/system-security-plans/{id}/system-characteristics/data-flow/diagrams": { - "post": { - "description": "Creates a new Diagram under the Data Flow of a System Security Plan. Creates the Data Flow grouping if it does not exist yet.", + } + }, + "put": { + "description": "Updates metadata for a given SSP.", "consumes": [ "application/json" ], @@ -16900,30 +16762,30 @@ "tags": [ "System Security Plans" ], - "summary": "Create a Data Flow Diagram", + "summary": "Update SSP metadata", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true }, { - "description": "Diagram object to create", - "name": "diagram", + "description": "Metadata data", + "name": "metadata", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" + "$ref": "#/definitions/oscalTypes_1_1_3.Metadata" } } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Metadata" } }, "400": { @@ -16932,12 +16794,6 @@ "$ref": "#/definitions/api.Error" } }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, "404": { "description": "Not Found", "schema": { @@ -16950,27 +16806,19 @@ "$ref": "#/definitions/api.Error" } } - }, - "security": [ - { - "OAuth2Password": [] - } - ] + } } }, - "/oscal/system-security-plans/{id}/system-characteristics/data-flow/diagrams/{diagram}": { - "put": { - "description": "Updates a specific Diagram under the Data Flow of a System Security Plan.", - "consumes": [ - "application/json" - ], + "/oscal/system-security-plans/{id}/profile": { + "get": { + "description": "Retrieves the Profile attached to the specified System Security Plan.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Update a Data Flow Diagram", + "summary": "Get Profile for a System Security Plan", "parameters": [ { "type": "string", @@ -16978,29 +16826,13 @@ "name": "id", "in": "path", "required": true - }, - { - "type": "string", - "description": "Diagram ID", - "name": "diagram", - "in": "path", - "required": true - }, - { - "description": "Updated Diagram object", - "name": "diagram", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" - } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Profile" } }, "400": { @@ -17034,43 +16866,45 @@ } ] }, - "delete": { - "description": "Deletes a specific Diagram under the Data Flow of a System Security Plan.", + "put": { + "description": "Associates a given Profile with a System Security Plan.", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Delete a Data Flow Diagram", + "summary": "Attach a Profile to a System Security Plan", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true }, { - "type": "string", - "description": "Diagram ID", - "name": "diagram", - "in": "path", - "required": true + "description": "Profile binding request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscal.addProfileRequest" + } } ], "responses": { - "204": { - "description": "No Content" - }, - "400": { - "description": "Bad Request", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/api.Error" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemSecurityPlan" } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad Request", "schema": { "$ref": "#/definitions/api.Error" } @@ -17087,28 +16921,23 @@ "$ref": "#/definitions/api.Error" } } - }, - "security": [ - { - "OAuth2Password": [] - } - ] + } } }, - "/oscal/system-security-plans/{id}/system-characteristics/network-architecture": { + "/oscal/system-security-plans/{id}/profiles": { "get": { - "description": "Retrieves the Network Architecture for a given System Security Plan.", + "description": "Returns all profiles associated with a System Security Plan via the ssp_profiles join table.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Get Network Architecture", + "summary": "List Profiles bound to an SSP", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true @@ -17118,7 +16947,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_NetworkArchitecture" + "$ref": "#/definitions/handler.GenericDataListResponse-oscal_profileSummary" } }, "400": { @@ -17127,12 +16956,6 @@ "$ref": "#/definitions/api.Error" } }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, "404": { "description": "Not Found", "schema": { @@ -17151,11 +16974,9 @@ "OAuth2Password": [] } ] - } - }, - "/oscal/system-security-plans/{id}/system-characteristics/network-architecture/diagrams": { + }, "post": { - "description": "Creates a new Diagram under the Network Architecture of a System Security Plan. Creates the Network Architecture grouping if it does not exist yet.", + "description": "Associates an additional Profile with a System Security Plan. Creates ImplementedRequirements for any new controls.", "consumes": [ "application/json" ], @@ -17165,30 +16986,30 @@ "tags": [ "System Security Plans" ], - "summary": "Create a Network Architecture Diagram", + "summary": "Add a Profile binding to an SSP", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true }, { - "description": "Diagram object to create", - "name": "diagram", + "description": "Profile binding request", + "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" + "$ref": "#/definitions/oscal.addProfileRequest" } } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" + "$ref": "#/definitions/handler.GenericDataListResponse-oscal_profileSummary" } }, "400": { @@ -17197,14 +17018,14 @@ "$ref": "#/definitions/api.Error" } }, - "401": { - "description": "Unauthorized", + "404": { + "description": "Not Found", "schema": { "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", + "409": { + "description": "Conflict", "schema": { "$ref": "#/definitions/api.Error" } @@ -17223,110 +17044,38 @@ ] } }, - "/oscal/system-security-plans/{id}/system-characteristics/network-architecture/diagrams/{diagram}": { - "put": { - "description": "Updates a specific Diagram under the Network Architecture of a System Security Plan.", - "consumes": [ - "application/json" - ], + "/oscal/system-security-plans/{id}/profiles/{profileId}": { + "delete": { + "description": "Removes a profile association from a System Security Plan. Enqueues orphaned risk cleanup.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Update a Network Architecture Diagram", + "summary": "Remove a Profile binding from an SSP", "parameters": [ { "type": "string", - "description": "System Security Plan ID", + "description": "SSP ID", "name": "id", "in": "path", "required": true }, { "type": "string", - "description": "Diagram ID", - "name": "diagram", + "description": "Profile ID to remove", + "name": "profileId", "in": "path", "required": true - }, - { - "description": "Updated Diagram object", - "name": "diagram", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" - } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/api.Error" + "$ref": "#/definitions/handler.GenericDataListResponse-oscal_profileSummary" } - } - }, - "security": [ - { - "OAuth2Password": [] - } - ] - }, - "delete": { - "description": "Deletes a specific Diagram under the Network Architecture of a System Security Plan.", - "produces": [ - "application/json" - ], - "tags": [ - "System Security Plans" - ], - "summary": "Delete a Network Architecture Diagram", - "parameters": [ - { - "type": "string", - "description": "System Security Plan ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Diagram ID", - "name": "diagram", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" }, "400": { "description": "Bad Request", @@ -17334,12 +17083,6 @@ "$ref": "#/definitions/api.Error" } }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, "404": { "description": "Not Found", "schema": { @@ -17360,16 +17103,16 @@ ] } }, - "/oscal/system-security-plans/{id}/system-implementation": { + "/oscal/system-security-plans/{id}/system-characteristics": { "get": { - "description": "Retrieves the System Implementation for a given System Security Plan.", + "description": "Retrieves the System Characteristics for a given System Security Plan.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Get System Implementation", + "summary": "Get System Characteristics", "parameters": [ { "type": "string", @@ -17383,7 +17126,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemImplementation" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemCharacteristics" } }, "400": { @@ -17418,7 +17161,7 @@ ] }, "put": { - "description": "Updates the System Implementation for a given System Security Plan.", + "description": "Updates the System Characteristics for a given System Security Plan.", "consumes": [ "application/json" ], @@ -17428,7 +17171,7 @@ "tags": [ "System Security Plans" ], - "summary": "Update System Implementation", + "summary": "Update System Characteristics", "parameters": [ { "type": "string", @@ -17438,12 +17181,12 @@ "required": true }, { - "description": "Updated System Implementation object", - "name": "system-implementation", + "description": "Updated System Characteristics object", + "name": "characteristics", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.SystemImplementation" + "$ref": "#/definitions/oscalTypes_1_1_3.SystemCharacteristics" } } ], @@ -17451,7 +17194,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemImplementation" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemCharacteristics" } }, "400": { @@ -17486,16 +17229,16 @@ ] } }, - "/oscal/system-security-plans/{id}/system-implementation/components": { + "/oscal/system-security-plans/{id}/system-characteristics/authorization-boundary": { "get": { - "description": "Retrieves components in the System Implementation for a given System Security Plan.", + "description": "Retrieves the Authorization Boundary for a given System Security Plan.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "List System Implementation Components", + "summary": "Get Authorization Boundary", "parameters": [ { "type": "string", @@ -17509,7 +17252,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_SystemComponent" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_AuthorizationBoundary" } }, "400": { @@ -17542,9 +17285,11 @@ "OAuth2Password": [] } ] - }, + } + }, + "/oscal/system-security-plans/{id}/system-characteristics/authorization-boundary/diagrams": { "post": { - "description": "Creates a new system component for a given SSP. Accepts an optional definedComponentId field to link to a DefinedComponent.", + "description": "Creates a new Diagram under the Authorization Boundary of a System Security Plan. Creates the Authorization Boundary grouping if it does not exist yet.", "consumes": [ "application/json" ], @@ -17554,22 +17299,22 @@ "tags": [ "System Security Plans" ], - "summary": "Create a new system component", + "summary": "Create an Authorization Boundary Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { - "description": "System Component data with optional definedComponentId field", - "name": "component", + "description": "Diagram object to create", + "name": "diagram", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscal.SystemComponentRequest" + "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" } } ], @@ -17577,7 +17322,7 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemComponent" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" } }, "400": { @@ -17586,6 +17331,12 @@ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -17598,19 +17349,27 @@ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] } }, - "/oscal/system-security-plans/{id}/system-implementation/components/{componentId}": { - "get": { - "description": "Retrieves component in the System Implementation for a given System Security Plan.", + "/oscal/system-security-plans/{id}/system-characteristics/authorization-boundary/diagrams/{diagram}": { + "put": { + "description": "Updates a specific Diagram under the Authorization Boundary of a System Security Plan.", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Get System Implementation Component", + "summary": "Update an Authorization Boundary Diagram", "parameters": [ { "type": "string", @@ -17621,17 +17380,26 @@ }, { "type": "string", - "description": "Component ID", - "name": "componentId", + "description": "Diagram ID", + "name": "diagram", "in": "path", "required": true + }, + { + "description": "Updated Diagram object", + "name": "diagram", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" + } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemComponent" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" } }, "400": { @@ -17665,49 +17433,34 @@ } ] }, - "put": { - "description": "Updates an existing system component for a given SSP. Accepts an optional definedComponentId field to link to a DefinedComponent.", - "consumes": [ - "application/json" - ], + "delete": { + "description": "Deletes a specific Diagram under the Authorization Boundary of a System Security Plan.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "Update a system component", + "summary": "Delete an Authorization Boundary Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { "type": "string", - "description": "Component ID", - "name": "componentId", + "description": "Diagram ID", + "name": "diagram", "in": "path", "required": true - }, - { - "description": "System Component data with optional definedComponentId field", - "name": "component", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/oscal.SystemComponentRequest" - } } ], "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemComponent" - } + "204": { + "description": "No Content" }, "400": { "description": "Bad Request", @@ -17715,48 +17468,8 @@ "$ref": "#/definitions/api.Error" } }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/api.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/api.Error" - } - } - } - }, - "delete": { - "description": "Deletes an existing system component for a given SSP.", - "tags": [ - "System Security Plans" - ], - "summary": "Delete a system component", - "parameters": [ - { - "type": "string", - "description": "SSP ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Component ID", - "name": "componentId", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - }, - "400": { - "description": "Bad Request", + "401": { + "description": "Unauthorized", "schema": { "$ref": "#/definitions/api.Error" } @@ -17773,19 +17486,24 @@ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] } }, - "/oscal/system-security-plans/{id}/system-implementation/inventory-items": { + "/oscal/system-security-plans/{id}/system-characteristics/data-flow": { "get": { - "description": "Retrieves inventory items in the System Implementation for a given System Security Plan.", + "description": "Retrieves the Data Flow for a given System Security Plan.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "List System Implementation Inventory Items", + "summary": "Get Data Flow", "parameters": [ { "type": "string", @@ -17799,7 +17517,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_InventoryItem" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_DataFlow" } }, "400": { @@ -17832,9 +17550,11 @@ "OAuth2Password": [] } ] - }, + } + }, + "/oscal/system-security-plans/{id}/system-characteristics/data-flow/diagrams": { "post": { - "description": "Creates a new inventory item for a given SSP.", + "description": "Creates a new Diagram under the Data Flow of a System Security Plan. Creates the Data Flow grouping if it does not exist yet.", "consumes": [ "application/json" ], @@ -17844,22 +17564,22 @@ "tags": [ "System Security Plans" ], - "summary": "Create a new inventory item", + "summary": "Create a Data Flow Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { - "description": "Inventory Item data", - "name": "item", + "description": "Diagram object to create", + "name": "diagram", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.InventoryItem" + "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" } } ], @@ -17867,7 +17587,7 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_InventoryItem" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" } }, "400": { @@ -17876,6 +17596,12 @@ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -17888,12 +17614,17 @@ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] } }, - "/oscal/system-security-plans/{id}/system-implementation/inventory-items/{itemId}": { + "/oscal/system-security-plans/{id}/system-characteristics/data-flow/diagrams/{diagram}": { "put": { - "description": "Updates an existing inventory item for a given SSP.", + "description": "Updates a specific Diagram under the Data Flow of a System Security Plan.", "consumes": [ "application/json" ], @@ -17903,29 +17634,29 @@ "tags": [ "System Security Plans" ], - "summary": "Update an inventory item", + "summary": "Update a Data Flow Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { "type": "string", - "description": "Item ID", - "name": "itemId", + "description": "Diagram ID", + "name": "diagram", "in": "path", "required": true }, { - "description": "Inventory Item data", - "name": "item", + "description": "Updated Diagram object", + "name": "diagram", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.InventoryItem" + "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" } } ], @@ -17933,7 +17664,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_InventoryItem" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" } }, "400": { @@ -17942,6 +17673,12 @@ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -17954,26 +17691,34 @@ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] }, "delete": { - "description": "Deletes an existing inventory item for a given SSP.", + "description": "Deletes a specific Diagram under the Data Flow of a System Security Plan.", + "produces": [ + "application/json" + ], "tags": [ "System Security Plans" ], - "summary": "Delete an inventory item", + "summary": "Delete a Data Flow Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { "type": "string", - "description": "Item ID", - "name": "itemId", + "description": "Diagram ID", + "name": "diagram", "in": "path", "required": true } @@ -17988,6 +17733,12 @@ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -18000,19 +17751,24 @@ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] } }, - "/oscal/system-security-plans/{id}/system-implementation/leveraged-authorizations": { + "/oscal/system-security-plans/{id}/system-characteristics/network-architecture": { "get": { - "description": "Retrieves leveraged authorizations in the System Implementation for a given System Security Plan.", + "description": "Retrieves the Network Architecture for a given System Security Plan.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "List System Implementation Leveraged Authorizations", + "summary": "Get Network Architecture", "parameters": [ { "type": "string", @@ -18026,7 +17782,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_LeveragedAuthorization" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_NetworkArchitecture" } }, "400": { @@ -18059,9 +17815,11 @@ "OAuth2Password": [] } ] - }, + } + }, + "/oscal/system-security-plans/{id}/system-characteristics/network-architecture/diagrams": { "post": { - "description": "Creates a new leveraged authorization for a given SSP.", + "description": "Creates a new Diagram under the Network Architecture of a System Security Plan. Creates the Network Architecture grouping if it does not exist yet.", "consumes": [ "application/json" ], @@ -18071,22 +17829,22 @@ "tags": [ "System Security Plans" ], - "summary": "Create a new leveraged authorization", + "summary": "Create a Network Architecture Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { - "description": "Leveraged Authorization data", - "name": "auth", + "description": "Diagram object to create", + "name": "diagram", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.LeveragedAuthorization" + "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" } } ], @@ -18094,7 +17852,7 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_LeveragedAuthorization" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" } }, "400": { @@ -18103,6 +17861,12 @@ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -18115,12 +17879,17 @@ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] } }, - "/oscal/system-security-plans/{id}/system-implementation/leveraged-authorizations/{authId}": { + "/oscal/system-security-plans/{id}/system-characteristics/network-architecture/diagrams/{diagram}": { "put": { - "description": "Updates an existing leveraged authorization for a given SSP.", + "description": "Updates a specific Diagram under the Network Architecture of a System Security Plan.", "consumes": [ "application/json" ], @@ -18130,29 +17899,29 @@ "tags": [ "System Security Plans" ], - "summary": "Update a leveraged authorization", + "summary": "Update a Network Architecture Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { "type": "string", - "description": "Authorization ID", - "name": "authId", + "description": "Diagram ID", + "name": "diagram", "in": "path", "required": true }, { - "description": "Leveraged Authorization data", - "name": "auth", + "description": "Updated Diagram object", + "name": "diagram", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oscalTypes_1_1_3.LeveragedAuthorization" + "$ref": "#/definitions/oscalTypes_1_1_3.Diagram" } } ], @@ -18160,7 +17929,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_LeveragedAuthorization" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_Diagram" } }, "400": { @@ -18169,6 +17938,12 @@ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -18181,26 +17956,34 @@ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] }, "delete": { - "description": "Deletes an existing leveraged authorization for a given SSP.", + "description": "Deletes a specific Diagram under the Network Architecture of a System Security Plan.", + "produces": [ + "application/json" + ], "tags": [ "System Security Plans" ], - "summary": "Delete a leveraged authorization", + "summary": "Delete a Network Architecture Diagram", "parameters": [ { "type": "string", - "description": "SSP ID", + "description": "System Security Plan ID", "name": "id", "in": "path", "required": true }, { "type": "string", - "description": "Authorization ID", - "name": "authId", + "description": "Diagram ID", + "name": "diagram", "in": "path", "required": true } @@ -18215,6 +17998,12 @@ "$ref": "#/definitions/api.Error" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, "404": { "description": "Not Found", "schema": { @@ -18227,19 +18016,24 @@ "$ref": "#/definitions/api.Error" } } - } + }, + "security": [ + { + "OAuth2Password": [] + } + ] } }, - "/oscal/system-security-plans/{id}/system-implementation/users": { + "/oscal/system-security-plans/{id}/system-implementation": { "get": { - "description": "Retrieves users in the System Implementation for a given System Security Plan.", + "description": "Retrieves the System Implementation for a given System Security Plan.", "produces": [ "application/json" ], "tags": [ "System Security Plans" ], - "summary": "List System Implementation Users", + "summary": "Get System Implementation", "parameters": [ { "type": "string", @@ -18253,7 +18047,877 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_SystemUser" + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemImplementation" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + }, + "security": [ + { + "OAuth2Password": [] + } + ] + }, + "put": { + "description": "Updates the System Implementation for a given System Security Plan.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Update System Implementation", + "parameters": [ + { + "type": "string", + "description": "System Security Plan ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated System Implementation object", + "name": "system-implementation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscalTypes_1_1_3.SystemImplementation" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemImplementation" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + }, + "security": [ + { + "OAuth2Password": [] + } + ] + } + }, + "/oscal/system-security-plans/{id}/system-implementation/components": { + "get": { + "description": "Retrieves components in the System Implementation for a given System Security Plan.", + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "List System Implementation Components", + "parameters": [ + { + "type": "string", + "description": "System Security Plan ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_SystemComponent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + }, + "security": [ + { + "OAuth2Password": [] + } + ] + }, + "post": { + "description": "Creates a new system component for a given SSP. Accepts an optional definedComponentId field to link to a DefinedComponent.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Create a new system component", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "System Component data with optional definedComponentId field", + "name": "component", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscal.SystemComponentRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemComponent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/oscal/system-security-plans/{id}/system-implementation/components/{componentId}": { + "get": { + "description": "Retrieves component in the System Implementation for a given System Security Plan.", + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Get System Implementation Component", + "parameters": [ + { + "type": "string", + "description": "System Security Plan ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Component ID", + "name": "componentId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemComponent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + }, + "security": [ + { + "OAuth2Password": [] + } + ] + }, + "put": { + "description": "Updates an existing system component for a given SSP. Accepts an optional definedComponentId field to link to a DefinedComponent.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Update a system component", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Component ID", + "name": "componentId", + "in": "path", + "required": true + }, + { + "description": "System Component data with optional definedComponentId field", + "name": "component", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscal.SystemComponentRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_SystemComponent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + }, + "delete": { + "description": "Deletes an existing system component for a given SSP.", + "tags": [ + "System Security Plans" + ], + "summary": "Delete a system component", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Component ID", + "name": "componentId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/oscal/system-security-plans/{id}/system-implementation/inventory-items": { + "get": { + "description": "Retrieves inventory items in the System Implementation for a given System Security Plan.", + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "List System Implementation Inventory Items", + "parameters": [ + { + "type": "string", + "description": "System Security Plan ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_InventoryItem" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + }, + "security": [ + { + "OAuth2Password": [] + } + ] + }, + "post": { + "description": "Creates a new inventory item for a given SSP.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Create a new inventory item", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Inventory Item data", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscalTypes_1_1_3.InventoryItem" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_InventoryItem" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/oscal/system-security-plans/{id}/system-implementation/inventory-items/{itemId}": { + "put": { + "description": "Updates an existing inventory item for a given SSP.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Update an inventory item", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Item ID", + "name": "itemId", + "in": "path", + "required": true + }, + { + "description": "Inventory Item data", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscalTypes_1_1_3.InventoryItem" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_InventoryItem" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + }, + "delete": { + "description": "Deletes an existing inventory item for a given SSP.", + "tags": [ + "System Security Plans" + ], + "summary": "Delete an inventory item", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Item ID", + "name": "itemId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/oscal/system-security-plans/{id}/system-implementation/leveraged-authorizations": { + "get": { + "description": "Retrieves leveraged authorizations in the System Implementation for a given System Security Plan.", + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "List System Implementation Leveraged Authorizations", + "parameters": [ + { + "type": "string", + "description": "System Security Plan ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_LeveragedAuthorization" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + }, + "security": [ + { + "OAuth2Password": [] + } + ] + }, + "post": { + "description": "Creates a new leveraged authorization for a given SSP.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Create a new leveraged authorization", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Leveraged Authorization data", + "name": "auth", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscalTypes_1_1_3.LeveragedAuthorization" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_LeveragedAuthorization" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/oscal/system-security-plans/{id}/system-implementation/leveraged-authorizations/{authId}": { + "put": { + "description": "Updates an existing leveraged authorization for a given SSP.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "Update a leveraged authorization", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Authorization ID", + "name": "authId", + "in": "path", + "required": true + }, + { + "description": "Leveraged Authorization data", + "name": "auth", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oscalTypes_1_1_3.LeveragedAuthorization" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataResponse-oscalTypes_1_1_3_LeveragedAuthorization" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + }, + "delete": { + "description": "Deletes an existing leveraged authorization for a given SSP.", + "tags": [ + "System Security Plans" + ], + "summary": "Delete a leveraged authorization", + "parameters": [ + { + "type": "string", + "description": "SSP ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Authorization ID", + "name": "authId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/oscal/system-security-plans/{id}/system-implementation/users": { + "get": { + "description": "Retrieves users in the System Implementation for a given System Security Plan.", + "produces": [ + "application/json" + ], + "tags": [ + "System Security Plans" + ], + "summary": "List System Implementation Users", + "parameters": [ + { + "type": "string", + "description": "System Security Plan ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GenericDataListResponse-oscalTypes_1_1_3_SystemUser" } }, "400": { @@ -28277,6 +28941,18 @@ } } }, + "handler.GenericDataListResponse-oscal_dashboardSuggestionResponse": { + "type": "object", + "properties": { + "data": { + "description": "Items from the list response", + "type": "array", + "items": { + "$ref": "#/definitions/oscal.dashboardSuggestionResponse" + } + } + } + }, "handler.GenericDataListResponse-oscal_profileSummary": { "type": "object", "properties": { @@ -28373,6 +29049,30 @@ } } }, + "handler.GenericDataListResponse-suggestions_DashboardSuggestionEvent": { + "type": "object", + "properties": { + "data": { + "description": "Items from the list response", + "type": "array", + "items": { + "$ref": "#/definitions/suggestions.DashboardSuggestionEvent" + } + } + } + }, + "handler.GenericDataListResponse-suggestions_LabelSetInput": { + "type": "object", + "properties": { + "data": { + "description": "Items from the list response", + "type": "array", + "items": { + "$ref": "#/definitions/suggestions.LabelSetInput" + } + } + } + }, "handler.GenericDataResponse-array_oscalTypes_1_1_3_AssessmentAssets": { "type": "object", "properties": { @@ -29371,6 +30071,58 @@ } } }, + "handler.GenericDataResponse-oscal_acceptDashboardSuggestionsResponse": { + "type": "object", + "properties": { + "data": { + "description": "Wrapped response data", + "allOf": [ + { + "$ref": "#/definitions/oscal.acceptDashboardSuggestionsResponse" + } + ] + } + } + }, + "handler.GenericDataResponse-oscal_dashboardSuggestionConfigResponse": { + "type": "object", + "properties": { + "data": { + "description": "Wrapped response data", + "allOf": [ + { + "$ref": "#/definitions/oscal.dashboardSuggestionConfigResponse" + } + ] + } + } + }, + "handler.GenericDataResponse-oscal_dashboardSuggestionPreviewResponse": { + "type": "object", + "properties": { + "data": { + "description": "Wrapped response data", + "allOf": [ + { + "$ref": "#/definitions/oscal.dashboardSuggestionPreviewResponse" + } + ] + } + } + }, + "handler.GenericDataResponse-oscal_dashboardSuggestionRunResponse": { + "type": "object", + "properties": { + "data": { + "description": "Wrapped response data", + "allOf": [ + { + "$ref": "#/definitions/oscal.dashboardSuggestionRunResponse" + } + ] + } + } + }, "handler.GenericDataResponse-poam_PoamItemControlLink": { "type": "object", "properties": { @@ -29801,6 +30553,13 @@ }, "name": { "type": "string" + }, + "sspId": { + "description": "System Security Plan ID. On PUT, omitted or null clears the binding to global.", + "type": "string", + "format": "uuid", + "x-nullable": true, + "example": "00000000-0000-0000-0000-000000000000" } } }, @@ -31627,6 +32386,23 @@ } } }, + "oscal.acceptDashboardSuggestionsResponse": { + "type": "object", + "properties": { + "acceptedFilterIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "suggestions": { + "type": "array", + "items": { + "$ref": "#/definitions/oscal.dashboardSuggestionResponse" + } + } + } + }, "oscal.addProfileRequest": { "type": "object", "properties": { @@ -31635,6 +32411,206 @@ } } }, + "oscal.dashboardSuggestionConfigResponse": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "oscal.dashboardSuggestionDecisionRequest": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "reason": { + "type": "string" + } + } + }, + "oscal.dashboardSuggestionPreviewResponse": { + "type": "object", + "properties": { + "controlCount": { + "type": "integer" + }, + "exceedsLimit": { + "type": "boolean" + }, + "labelSetCount": { + "type": "integer" + }, + "maxCallsPerRun": { + "type": "integer" + }, + "plannedCalls": { + "type": "integer" + } + } + }, + "oscal.dashboardSuggestionResponse": { + "type": "object", + "properties": { + "acceptedFilterId": { + "type": "string" + }, + "confidence": { + "type": "number" + }, + "controlCatalogId": { + "type": "string" + }, + "controlId": { + "type": "string" + }, + "controlTitle": { + "type": "string" + }, + "decidedAt": { + "type": "string" + }, + "decidedByUserId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "labelSet": { + "$ref": "#/definitions/datatypes.JSONMap" + }, + "labelSetHash": { + "type": "string" + }, + "proposedFilterName": { + "type": "string" + }, + "reasoning": { + "type": "string" + }, + "rejectReason": { + "type": "string" + }, + "runId": { + "type": "string" + }, + "sspId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "targetFilterId": { + "type": "string" + }, + "targetFilterName": { + "type": "string" + } + } + }, + "oscal.dashboardSuggestionRunResponse": { + "type": "object", + "properties": { + "cells": { + "type": "array", + "items": { + "$ref": "#/definitions/suggestions.DashboardSuggestionRunCell" + } + }, + "completedAt": { + "type": "string" + }, + "completedCells": { + "type": "integer" + }, + "error": { + "type": "string" + }, + "failedCells": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "inputTokens": { + "type": "integer" + }, + "model": { + "type": "string" + }, + "outputTokens": { + "type": "integer" + }, + "plannedCalls": { + "type": "integer" + }, + "promptVersion": { + "type": "string" + }, + "scope": { + "$ref": "#/definitions/datatypes.JSONMap" + }, + "sspId": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "stats": { + "$ref": "#/definitions/datatypes.JSONMap" + }, + "status": { + "type": "string" + }, + "suggestionCount": { + "type": "integer" + }, + "suggestions": { + "type": "array", + "items": { + "$ref": "#/definitions/suggestions.DashboardSuggestion" + } + }, + "triggeredByUserId": { + "type": "string" + } + } + }, + "oscal.dashboardSuggestionScopeRequest": { + "type": "object", + "properties": { + "controlKeys": { + "type": "array", + "items": { + "type": "string" + } + }, + "labelSetHashes": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "oscal.generateDashboardSuggestionsRequest": { + "type": "object", + "properties": { + "scope": { + "$ref": "#/definitions/oscal.dashboardSuggestionScopeRequest" + }, + "supersedePending": { + "type": "boolean" + } + } + }, "oscal.profileSummary": { "type": "object", "properties": { @@ -39469,6 +40445,158 @@ } } }, + "suggestions.DashboardSuggestion": { + "type": "object", + "properties": { + "acceptedFilterId": { + "type": "string" + }, + "confidence": { + "type": "number" + }, + "controlCatalogId": { + "type": "string" + }, + "controlId": { + "type": "string" + }, + "decidedAt": { + "type": "string" + }, + "decidedByUserId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "labelSet": { + "$ref": "#/definitions/datatypes.JSONMap" + }, + "labelSetHash": { + "type": "string" + }, + "proposedFilterName": { + "type": "string" + }, + "reasoning": { + "type": "string" + }, + "rejectReason": { + "type": "string" + }, + "runId": { + "type": "string" + }, + "sspId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "targetFilterId": { + "type": "string" + } + } + }, + "suggestions.DashboardSuggestionEvent": { + "type": "object", + "properties": { + "actorUserId": { + "type": "string" + }, + "details": { + "type": "string" + }, + "eventType": { + "type": "string" + }, + "id": { + "type": "string" + }, + "occurredAt": { + "type": "string" + }, + "payload": { + "$ref": "#/definitions/datatypes.JSONMap" + }, + "runId": { + "type": "string" + }, + "snapshot": { + "$ref": "#/definitions/datatypes.JSONMap" + }, + "suggestionId": { + "type": "string" + } + } + }, + "suggestions.DashboardSuggestionRunCell": { + "type": "object", + "properties": { + "cellIndex": { + "type": "integer" + }, + "completedAt": { + "type": "string" + }, + "controlKeys": { + "type": "array", + "items": { + "type": "string" + } + }, + "error": { + "type": "string" + }, + "inputTokens": { + "type": "integer" + }, + "labelSetHashes": { + "type": "array", + "items": { + "type": "string" + } + }, + "mappingsRejected": { + "type": "integer" + }, + "mappingsReturned": { + "type": "integer" + }, + "outputTokens": { + "type": "integer" + }, + "runId": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "suggestions.LabelSetInput": { + "type": "object", + "properties": { + "evidence_count": { + "type": "integer" + }, + "hash": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "sample_titles": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "templates.batchRiskTemplateItem": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e3e4f08d..a40f6be0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -629,6 +629,14 @@ definitions: $ref: '#/definitions/oscal.ProfileHandler' type: array type: object + handler.GenericDataListResponse-oscal_dashboardSuggestionResponse: + properties: + data: + description: Items from the list response + items: + $ref: '#/definitions/oscal.dashboardSuggestionResponse' + type: array + type: object handler.GenericDataListResponse-oscal_profileSummary: properties: data: @@ -941,6 +949,22 @@ definitions: $ref: '#/definitions/relational.User' type: array type: object + handler.GenericDataListResponse-suggestions_DashboardSuggestionEvent: + properties: + data: + description: Items from the list response + items: + $ref: '#/definitions/suggestions.DashboardSuggestionEvent' + type: array + type: object + handler.GenericDataListResponse-suggestions_LabelSetInput: + properties: + data: + description: Items from the list response + items: + $ref: '#/definitions/suggestions.LabelSetInput' + type: array + type: object handler.GenericDataResponse-array_oscalTypes_1_1_3_AssessmentAssets: properties: data: @@ -1126,6 +1150,34 @@ definitions: - $ref: '#/definitions/oscal.ProfileHandler' description: Wrapped response data type: object + handler.GenericDataResponse-oscal_acceptDashboardSuggestionsResponse: + properties: + data: + allOf: + - $ref: '#/definitions/oscal.acceptDashboardSuggestionsResponse' + description: Wrapped response data + type: object + handler.GenericDataResponse-oscal_dashboardSuggestionConfigResponse: + properties: + data: + allOf: + - $ref: '#/definitions/oscal.dashboardSuggestionConfigResponse' + description: Wrapped response data + type: object + handler.GenericDataResponse-oscal_dashboardSuggestionPreviewResponse: + properties: + data: + allOf: + - $ref: '#/definitions/oscal.dashboardSuggestionPreviewResponse' + description: Wrapped response data + type: object + handler.GenericDataResponse-oscal_dashboardSuggestionRunResponse: + properties: + data: + allOf: + - $ref: '#/definitions/oscal.dashboardSuggestionRunResponse' + description: Wrapped response data + type: object handler.GenericDataResponse-oscalTypes_1_1_3_Activity: properties: data: @@ -1751,6 +1803,13 @@ definitions: $ref: '#/definitions/labelfilter.Filter' name: type: string + sspId: + description: System Security Plan ID. On PUT, omitted or null clears the binding + to global. + example: 00000000-0000-0000-0000-000000000000 + format: uuid + type: string + x-nullable: true required: - filter - name @@ -2967,11 +3026,153 @@ definitions: uuid: type: string type: object + oscal.acceptDashboardSuggestionsResponse: + properties: + acceptedFilterIds: + items: + type: string + type: array + suggestions: + items: + $ref: '#/definitions/oscal.dashboardSuggestionResponse' + type: array + type: object oscal.addProfileRequest: properties: profileId: type: string type: object + oscal.dashboardSuggestionConfigResponse: + properties: + enabled: + type: boolean + type: object + oscal.dashboardSuggestionDecisionRequest: + properties: + ids: + items: + type: string + type: array + reason: + type: string + required: + - ids + type: object + oscal.dashboardSuggestionPreviewResponse: + properties: + controlCount: + type: integer + exceedsLimit: + type: boolean + labelSetCount: + type: integer + maxCallsPerRun: + type: integer + plannedCalls: + type: integer + type: object + oscal.dashboardSuggestionResponse: + properties: + acceptedFilterId: + type: string + confidence: + type: number + controlCatalogId: + type: string + controlId: + type: string + controlTitle: + type: string + decidedAt: + type: string + decidedByUserId: + type: string + id: + type: string + labelSet: + $ref: '#/definitions/datatypes.JSONMap' + labelSetHash: + type: string + proposedFilterName: + type: string + reasoning: + type: string + rejectReason: + type: string + runId: + type: string + sspId: + type: string + status: + type: string + targetFilterId: + type: string + targetFilterName: + type: string + type: object + oscal.dashboardSuggestionRunResponse: + properties: + cells: + items: + $ref: '#/definitions/suggestions.DashboardSuggestionRunCell' + type: array + completedAt: + type: string + completedCells: + type: integer + error: + type: string + failedCells: + type: integer + id: + type: string + inputTokens: + type: integer + model: + type: string + outputTokens: + type: integer + plannedCalls: + type: integer + promptVersion: + type: string + scope: + $ref: '#/definitions/datatypes.JSONMap' + sspId: + type: string + startedAt: + type: string + stats: + $ref: '#/definitions/datatypes.JSONMap' + status: + type: string + suggestionCount: + type: integer + suggestions: + items: + $ref: '#/definitions/suggestions.DashboardSuggestion' + type: array + triggeredByUserId: + type: string + type: object + oscal.dashboardSuggestionScopeRequest: + properties: + controlKeys: + items: + type: string + type: array + labelSetHashes: + items: + type: string + type: array + type: object + oscal.generateDashboardSuggestionsRequest: + properties: + scope: + $ref: '#/definitions/oscal.dashboardSuggestionScopeRequest' + supersedePending: + type: boolean + type: object oscal.profileSummary: properties: id: @@ -8151,6 +8352,106 @@ definitions: totalPages: type: integer type: object + suggestions.DashboardSuggestion: + properties: + acceptedFilterId: + type: string + confidence: + type: number + controlCatalogId: + type: string + controlId: + type: string + decidedAt: + type: string + decidedByUserId: + type: string + id: + type: string + labelSet: + $ref: '#/definitions/datatypes.JSONMap' + labelSetHash: + type: string + proposedFilterName: + type: string + reasoning: + type: string + rejectReason: + type: string + runId: + type: string + sspId: + type: string + status: + type: string + targetFilterId: + type: string + type: object + suggestions.DashboardSuggestionEvent: + properties: + actorUserId: + type: string + details: + type: string + eventType: + type: string + id: + type: string + occurredAt: + type: string + payload: + $ref: '#/definitions/datatypes.JSONMap' + runId: + type: string + snapshot: + $ref: '#/definitions/datatypes.JSONMap' + suggestionId: + type: string + type: object + suggestions.DashboardSuggestionRunCell: + properties: + cellIndex: + type: integer + completedAt: + type: string + controlKeys: + items: + type: string + type: array + error: + type: string + inputTokens: + type: integer + labelSetHashes: + items: + type: string + type: array + mappingsRejected: + type: integer + mappingsReturned: + type: integer + outputTokens: + type: integer + runId: + type: string + status: + type: string + type: object + suggestions.LabelSetInput: + properties: + evidence_count: + type: integer + hash: + type: string + labels: + additionalProperties: + type: string + type: object + sample_titles: + items: + type: string + type: array + type: object templates.batchRiskTemplateItem: properties: dedupe-label-keys: @@ -10740,6 +11041,19 @@ paths: summary: Get OAuth2 token tags: - Auth + /dashboard-suggestions/config: + get: + description: Returns whether AI dashboard suggestions are enabled. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionConfigResponse' + summary: Get dashboard suggestions feature configuration + tags: + - Dashboard Suggestions /evidence: post: consumes: @@ -10890,6 +11204,10 @@ paths: name: id required: true type: string + - description: System Security Plan ID; limits filters to global + same-SSP + in: query + name: sspId + type: string produces: - application/json responses: @@ -10897,6 +11215,10 @@ paths: description: OK schema: $ref: '#/definitions/handler.GenericDataListResponse-evidence_StatusCount' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' "500": description: Internal Server Error schema: @@ -10945,6 +11267,10 @@ paths: name: id required: true type: string + - description: System Security Plan ID; limits filters to global + same-SSP + in: query + name: sspId + type: string produces: - application/json responses: @@ -11171,7 +11497,25 @@ paths: - Evidence /filters: get: - description: Retrieves all filters, optionally filtered by controlId or componentId. + description: Retrieves filters, optionally filtered by controlId, componentId, + sspId, or global scope. + parameters: + - description: Control ID + in: query + name: controlId + type: string + - description: Component ID + in: query + name: componentId + type: string + - description: System Security Plan ID; returns global + same-SSP filters + in: query + name: sspId + type: string + - description: Filter scope. Use 'global' for global filters only + in: query + name: scope + type: string produces: - application/json responses: @@ -11179,6 +11523,14 @@ paths: description: OK schema: $ref: '#/definitions/handler.GenericDataListResponse-handler_FilterWithAssociations' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/api.Error' "500": description: Internal Server Error schema: @@ -19979,6 +20331,388 @@ paths: summary: Suggest system components for an implemented requirement tags: - System Security Plans + /oscal/system-security-plans/{id}/dashboard-suggestion-runs/{runId}: + get: + description: Returns a dashboard suggestion run with cell progress. + parameters: + - description: System Security Plan ID + in: path + name: id + required: true + type: string + - description: Dashboard suggestion run ID + in: path + name: runId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionRunResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/api.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/api.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.Error' + security: + - OAuth2Password: [] + summary: Get a dashboard suggestion run + tags: + - Dashboard Suggestions + /oscal/system-security-plans/{id}/dashboard-suggestion-runs/latest: + get: + description: Returns the latest dashboard suggestion run with cell progress. + parameters: + - description: System Security Plan ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionRunResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/api.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/api.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.Error' + security: + - OAuth2Password: [] + summary: Get latest dashboard suggestion run for an SSP + tags: + - Dashboard Suggestions + /oscal/system-security-plans/{id}/dashboard-suggestions: + get: + description: Lists dashboard suggestions joined with control title and target + filter name. + parameters: + - description: System Security Plan ID + in: path + name: id + required: true + type: string + - description: Suggestion status (default pending) + in: query + name: status + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.GenericDataListResponse-oscal_dashboardSuggestionResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/api.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.Error' + security: + - OAuth2Password: [] + summary: List dashboard suggestions for an SSP + tags: + - Dashboard Suggestions + /oscal/system-security-plans/{id}/dashboard-suggestions/{suggestionId}/events: + get: + description: Returns audit events for one dashboard suggestion. + parameters: + - description: System Security Plan ID + in: path + name: id + required: true + type: string + - description: Dashboard suggestion ID + in: path + name: suggestionId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.GenericDataListResponse-suggestions_DashboardSuggestionEvent' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/api.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/api.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.Error' + security: + - OAuth2Password: [] + summary: List dashboard suggestion events + tags: + - Dashboard Suggestions + /oscal/system-security-plans/{id}/dashboard-suggestions/accept: + post: + consumes: + - application/json + description: Accepts pending dashboard suggestions and creates or extends SSP-bound + filters. + parameters: + - description: System Security Plan ID + in: path + name: id + required: true + type: string + - description: Suggestion IDs + in: body + name: request + required: true + schema: + $ref: '#/definitions/oscal.dashboardSuggestionDecisionRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.GenericDataResponse-oscal_acceptDashboardSuggestionsResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/api.Error' + "409": + description: Conflict + schema: + $ref: '#/definitions/api.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.Error' + security: + - OAuth2Password: [] + summary: Accept dashboard suggestions + tags: + - Dashboard Suggestions + /oscal/system-security-plans/{id}/dashboard-suggestions/generate: + post: + consumes: + - application/json + description: Creates a dashboard suggestion run, snapshots the resolved scope, + creates run cells, and enqueues cell processing. + parameters: + - description: System Security Plan ID + in: path + name: id + required: true + type: string + - description: Generation request + in: body + name: request + schema: + $ref: '#/definitions/oscal.generateDashboardSuggestionsRequest' + produces: + - application/json + responses: + "202": + description: Accepted + schema: + $ref: '#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionRunResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/api.Error' + "409": + description: Conflict + schema: + $ref: '#/definitions/api.Error' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/api.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.Error' + "503": + description: Service Unavailable + schema: + $ref: '#/definitions/api.Error' + security: + - OAuth2Password: [] + summary: Generate dashboard suggestions for an SSP + tags: + - Dashboard Suggestions + /oscal/system-security-plans/{id}/dashboard-suggestions/label-sets: + get: + description: Returns canonical evidence label sets for dashboard suggestion + scope selection. + parameters: + - description: System Security Plan ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.GenericDataListResponse-suggestions_LabelSetInput' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/api.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.Error' + security: + - OAuth2Password: [] + summary: List dashboard suggestion label sets + tags: + - Dashboard Suggestions + /oscal/system-security-plans/{id}/dashboard-suggestions/preview: + post: + consumes: + - application/json + description: Resolves the requested dashboard suggestion scope and returns planned + call counts without creating runs or enqueueing work. + parameters: + - description: System Security Plan ID + in: path + name: id + required: true + type: string + - description: Preview request + in: body + name: request + schema: + $ref: '#/definitions/oscal.generateDashboardSuggestionsRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.GenericDataResponse-oscal_dashboardSuggestionPreviewResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/api.Error' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/api.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.Error' + security: + - OAuth2Password: [] + summary: Preview dashboard suggestion generation for an SSP + tags: + - Dashboard Suggestions + /oscal/system-security-plans/{id}/dashboard-suggestions/reject: + post: + consumes: + - application/json + description: Rejects pending dashboard suggestions. + parameters: + - description: System Security Plan ID + in: path + name: id + required: true + type: string + - description: Suggestion IDs and reason + in: body + name: request + required: true + schema: + $ref: '#/definitions/oscal.dashboardSuggestionDecisionRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.GenericDataListResponse-oscal_dashboardSuggestionResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/api.Error' + "409": + description: Conflict + schema: + $ref: '#/definitions/api.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.Error' + security: + - OAuth2Password: [] + summary: Reject dashboard suggestions + tags: + - Dashboard Suggestions /oscal/system-security-plans/{id}/import-profile: get: description: Retrieves import-profile for a given SSP. diff --git a/internal/api/handler/api.go b/internal/api/handler/api.go index c495e761..ae8569bb 100644 --- a/internal/api/handler/api.go +++ b/internal/api/handler/api.go @@ -45,7 +45,9 @@ func RegisterHandlers(server *api.Server, logger *zap.SugaredLogger, db *gorm.DB healthHandler.Register(server.API().Group("/health")) filterHandler := NewFilterHandler(logger, db) - filterHandler.Register(server.API().Group("/filters")) + filterGroup := server.API().Group("/filters") + filterGroup.Use(middleware.JWTMiddleware(config.JWTPublicKey)) + filterHandler.Register(filterGroup) heartbeatHandler := NewHeartbeatHandler(logger, db) agentIngestMiddleware := middleware.AgentJWTOrPublicMiddleware(db, config.JWTPublicKey, !config.StrictDisablePublicAgentEndpoints) diff --git a/internal/api/handler/evidence.go b/internal/api/handler/evidence.go index 25fa5a62..3100d4a0 100644 --- a/internal/api/handler/evidence.go +++ b/internal/api/handler/evidence.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "strings" "time" "github.com/compliance-framework/api/internal" @@ -778,11 +779,12 @@ func (h *EvidenceHandler) VerifySignature(ctx echo.Context) error { // @Description Retrieves Evidence records associated with a specific Control ID, including related activities, inventory items, components, subjects, and labels. // @Tags Evidence // @Produce json -// @Param id path string true "Control ID" -// @Success 200 {object} handler.ForControl.EvidenceDataListResponse -// @Failure 400 {object} api.Error -// @Failure 404 {object} api.Error -// @Failure 500 {object} api.Error +// @Param id path string true "Control ID" +// @Param sspId query string false "System Security Plan ID; limits filters to global + same-SSP" +// @Success 200 {object} handler.ForControl.EvidenceDataListResponse +// @Failure 400 {object} api.Error +// @Failure 404 {object} api.Error +// @Failure 500 {object} api.Error // @Router /evidence/for-control/{id} [get] func (h *EvidenceHandler) ForControl(ctx echo.Context) error { type responseMetadata struct { @@ -809,13 +811,14 @@ func (h *EvidenceHandler) ForControl(ctx echo.Context) error { }, } - filters := []labelfilter.Filter{} - for _, filter := range control.Filters { - filters = append(filters, filter.Filter.Data()) + filters, filterErr := visibleControlFilters(ctx, control.Filters) + if filterErr != nil { + return ctx.JSON(http.StatusBadRequest, api.NewError(filterErr)) } if len(filters) == 0 { - return ctx.JSON(http.StatusOK, GenericDataListResponse[evidencesvc.StatusCount]{Data: []evidencesvc.StatusCount{}}) + response.Data = []PublicEvidenceResponse{} + return ctx.JSON(http.StatusOK, response) } evidenceList, err := h.evidenceService.GetLatestForFilters(filters...) @@ -981,9 +984,11 @@ func (h *EvidenceHandler) StatusOverTimeByUUID(ctx echo.Context) error { // @Description Retrieves the count of evidence statuses for filters associated with a specific Control ID. // @Tags Evidence // @Produce json -// @Param id path string true "Control ID" -// @Success 200 {object} GenericDataListResponse[evidence.StatusCount] -// @Failure 500 {object} api.Error +// @Param id path string true "Control ID" +// @Param sspId query string false "System Security Plan ID; limits filters to global + same-SSP" +// @Success 200 {object} GenericDataListResponse[evidence.StatusCount] +// @Failure 400 {object} api.Error +// @Failure 500 {object} api.Error // @Router /evidence/compliance-by-control/{id} [get] func (h *EvidenceHandler) ComplianceByControl(ctx echo.Context) error { id := ctx.Param("id") @@ -995,9 +1000,9 @@ func (h *EvidenceHandler) ComplianceByControl(ctx echo.Context) error { return ctx.JSON(http.StatusInternalServerError, api.NewError(err)) } - filters := []labelfilter.Filter{} - for _, filter := range control.Filters { - filters = append(filters, filter.Filter.Data()) + filters, filterErr := visibleControlFilters(ctx, control.Filters) + if filterErr != nil { + return ctx.JSON(http.StatusBadRequest, api.NewError(filterErr)) } if len(filters) == 0 { @@ -1012,6 +1017,26 @@ func (h *EvidenceHandler) ComplianceByControl(ctx echo.Context) error { return ctx.JSON(http.StatusOK, GenericDataListResponse[evidencesvc.StatusCount]{Data: rows}) } +func visibleControlFilters(ctx echo.Context, filterModels []relational.Filter) ([]labelfilter.Filter, error) { + sspIDParam := strings.TrimSpace(ctx.QueryParam("sspId")) + var sspID *uuid.UUID + if sspIDParam != "" { + parsed, err := uuid.Parse(sspIDParam) + if err != nil { + return nil, err + } + sspID = &parsed + } + filters := make([]labelfilter.Filter, 0, len(filterModels)) + for _, filter := range filterModels { + if sspID != nil && filter.SSPID != nil && *filter.SSPID != *sspID { + continue + } + filters = append(filters, filter.Filter.Data()) + } + return filters, nil +} + // ComplianceByFilter godoc // // @Summary Get compliance status counts by filter/dashboard ID diff --git a/internal/api/handler/evidence_integration_test.go b/internal/api/handler/evidence_integration_test.go index f7dbd384..de291d85 100644 --- a/internal/api/handler/evidence_integration_test.go +++ b/internal/api/handler/evidence_integration_test.go @@ -49,6 +49,49 @@ func (suite *EvidenceApiIntegrationSuite) setupServer() *api.Server { return server } +func (suite *EvidenceApiIntegrationSuite) TestForControlWithScopedOutFiltersKeepsResponseShape() { + err := suite.Migrator.Refresh() + suite.Require().NoError(err) + + visibleSSPID := uuid.New() + hiddenSSPID := uuid.New() + suite.Require().NoError(suite.DB.Create(&relational.SystemSecurityPlan{UUIDModel: relational.UUIDModel{ID: &visibleSSPID}}).Error) + suite.Require().NoError(suite.DB.Create(&relational.SystemSecurityPlan{UUIDModel: relational.UUIDModel{ID: &hiddenSSPID}}).Error) + + catalog := relational.Catalog{} + suite.Require().NoError(suite.DB.Create(&catalog).Error) + control := relational.Control{CatalogID: *catalog.ID, ID: "AC-1", Title: "Access Control 1"} + filter := relational.Filter{ + Name: "Hidden SSP Filter", + SSPID: &hiddenSSPID, + Filter: datatypes.NewJSONType(labelfilter.Filter{ + Scope: &labelfilter.Scope{ + Condition: &labelfilter.Condition{Label: "provider", Operator: "=", Value: "aws"}, + }, + }), + } + suite.Require().NoError(suite.DB.Create(&filter).Error) + control.Filters = []relational.Filter{filter} + suite.Require().NoError(suite.DB.Create(&control).Error) + + server := suite.setupServer() + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/evidence/for-control/%s?sspId=%s", control.ID, visibleSSPID), nil) + server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusOK, rec.Code, rec.Body.String()) + + var response map[string]any + suite.Require().NoError(json.Unmarshal(rec.Body.Bytes(), &response)) + metadata, ok := response["metadata"].(map[string]any) + suite.Require().True(ok, rec.Body.String()) + controlMetadata, ok := metadata["control"].(map[string]any) + suite.Require().True(ok, rec.Body.String()) + suite.Equal("AC-1", controlMetadata["id"]) + data, ok := response["data"].([]any) + suite.Require().True(ok, rec.Body.String()) + suite.Empty(data) +} + func (suite *EvidenceApiIntegrationSuite) TestCreate() { err := suite.Migrator.Refresh() suite.Require().NoError(err) diff --git a/internal/api/handler/filter.go b/internal/api/handler/filter.go index 5870d5cb..577eac91 100644 --- a/internal/api/handler/filter.go +++ b/internal/api/handler/filter.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "strings" "github.com/compliance-framework/api/internal/api" "github.com/compliance-framework/api/internal/service/relational" @@ -96,15 +97,23 @@ func (h *FilterHandler) Get(ctx echo.Context) error { // List godoc // // @Summary List filters -// @Description Retrieves all filters, optionally filtered by controlId or componentId. +// @Description Retrieves filters, optionally filtered by controlId, componentId, sspId, or global scope. // @Tags Filters // @Produce json -// @Success 200 {object} GenericDataListResponse[FilterWithAssociations] -// @Failure 500 {object} api.Error +// @Param controlId query string false "Control ID" +// @Param componentId query string false "Component ID" +// @Param sspId query string false "System Security Plan ID; returns global + same-SSP filters" +// @Param scope query string false "Filter scope. Use 'global' for global filters only" +// @Success 200 {object} GenericDataListResponse[FilterWithAssociations] +// @Failure 400 {object} api.Error +// @Failure 401 {object} api.Error +// @Failure 500 {object} api.Error // @Router /filters [get] func (h *FilterHandler) List(ctx echo.Context) error { controlID := ctx.QueryParam("controlId") componentID := ctx.QueryParam("componentId") + scope := strings.TrimSpace(ctx.QueryParam("scope")) + sspIDParam := strings.TrimSpace(ctx.QueryParam("sspId")) if controlID != "" && componentID != "" { return ctx.JSON(http.StatusBadRequest, api.NewError(fmt.Errorf("controlId and componentId are mutually exclusive"))) @@ -128,6 +137,19 @@ func (h *FilterHandler) List(ctx echo.Context) error { Distinct() } + switch { + case scope == "global": + query = query.Where("filters.ssp_id IS NULL") + case scope != "": + return ctx.JSON(http.StatusBadRequest, api.NewError(fmt.Errorf("unsupported scope %q", scope))) + case sspIDParam != "": + sspID, err := uuid.Parse(sspIDParam) + if err != nil { + return ctx.JSON(http.StatusBadRequest, api.NewError(err)) + } + query = query.Where("filters.ssp_id IS NULL OR filters.ssp_id = ?", sspID) + } + var filters []relational.Filter if err := query.Find(&filters).Error; err != nil { return ctx.JSON(http.StatusInternalServerError, api.NewError(err)) @@ -196,6 +218,7 @@ func (h *FilterHandler) Create(ctx echo.Context) error { filter := relational.Filter{ Name: req.Name, + SSPID: req.SSPID, Filter: datatypes.NewJSONType(req.Filter), } @@ -275,6 +298,8 @@ func (h *FilterHandler) Update(ctx echo.Context) error { } filter.Name = req.Name + // PUT is full replacement: omitted or null sspId intentionally clears the SSP binding. + filter.SSPID = req.SSPID filter.Filter = datatypes.NewJSONType(req.Filter) // Note: nil and empty slices are semantically different here. diff --git a/internal/api/handler/filter_integration_test.go b/internal/api/handler/filter_integration_test.go index ee3eda17..e2d06abe 100644 --- a/internal/api/handler/filter_integration_test.go +++ b/internal/api/handler/filter_integration_test.go @@ -31,6 +31,12 @@ type FilterApiIntegrationSuite struct { tests.IntegrationTestSuite } +func (suite *FilterApiIntegrationSuite) authorize(req *http.Request) { + token, err := suite.GetAuthToken() + suite.Require().NoError(err) + req.Header.Set(echo.HeaderAuthorization, fmt.Sprintf("Bearer %s", *token)) +} + func (suite *FilterApiIntegrationSuite) TestCreate() { suite.Run("Simple", func() { err := suite.Migrator.Refresh() @@ -58,6 +64,7 @@ func (suite *FilterApiIntegrationSuite) TestCreate() { reqBody, _ := json.Marshal(createReq) req := httptest.NewRequest(http.MethodPost, "/api/filters", bytes.NewReader(reqBody)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + suite.authorize(req) server.E().ServeHTTP(rec, req) assert.Equal(suite.T(), http.StatusCreated, rec.Code) }) @@ -103,6 +110,7 @@ func (suite *FilterApiIntegrationSuite) TestCreate() { reqBody, _ := json.Marshal(createReq) req := httptest.NewRequest(http.MethodPost, "/api/filters", bytes.NewReader(reqBody)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + suite.authorize(req) server.E().ServeHTTP(rec, req) assert.Equal(suite.T(), http.StatusCreated, rec.Code) }) @@ -145,6 +153,7 @@ func (suite *FilterApiIntegrationSuite) TestCreate() { reqBody, _ := json.Marshal(createReq) req := httptest.NewRequest(http.MethodPost, "/api/filters", bytes.NewReader(reqBody)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + suite.authorize(req) server.E().ServeHTTP(rec, req) assert.Equal(suite.T(), http.StatusCreated, rec.Code) }) @@ -190,6 +199,7 @@ func (suite *FilterApiIntegrationSuite) TestList() { rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/filters", bytes.NewReader(body)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + suite.authorize(req) server.E().ServeHTTP(rec, req) assert.Equal(suite.T(), http.StatusCreated, rec.Code) @@ -210,12 +220,14 @@ func (suite *FilterApiIntegrationSuite) TestList() { rec = httptest.NewRecorder() req = httptest.NewRequest(http.MethodPost, "/api/filters", bytes.NewReader(body)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + suite.authorize(req) server.E().ServeHTTP(rec, req) assert.Equal(suite.T(), http.StatusCreated, rec.Code) // Fetch filters linked to AC-1 rec = httptest.NewRecorder() req = httptest.NewRequest(http.MethodGet, "/api/filters?controlId=AC-1", nil) + suite.authorize(req) server.E().ServeHTTP(rec, req) assert.Equal(suite.T(), http.StatusOK, rec.Code) @@ -269,6 +281,7 @@ func (suite *FilterApiIntegrationSuite) TestList() { rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/filters", bytes.NewReader(body)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + suite.authorize(req) server.E().ServeHTTP(rec, req) assert.Equal(suite.T(), http.StatusCreated, rec.Code) @@ -289,12 +302,14 @@ func (suite *FilterApiIntegrationSuite) TestList() { rec = httptest.NewRecorder() req = httptest.NewRequest(http.MethodPost, "/api/filters", bytes.NewReader(body)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + suite.authorize(req) server.E().ServeHTTP(rec, req) assert.Equal(suite.T(), http.StatusCreated, rec.Code) // Fetch filters linked to our component rec = httptest.NewRecorder() req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/filters?componentId=%s", id.String()), nil) + suite.authorize(req) server.E().ServeHTTP(rec, req) assert.Equal(suite.T(), http.StatusOK, rec.Code) @@ -352,6 +367,7 @@ func (suite *FilterApiIntegrationSuite) TestUpdate() { reqBody, _ := json.Marshal(updateReq) req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/filters/%s", filter.ID), bytes.NewReader(reqBody)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + suite.authorize(req) server.E().ServeHTTP(rec, req) assert.Equal(suite.T(), http.StatusOK, rec.Code) @@ -411,6 +427,7 @@ func (suite *FilterApiIntegrationSuite) TestUpdate() { reqBody, _ := json.Marshal(updateReq) req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/filters/%s", filter.ID), bytes.NewReader(reqBody)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + suite.authorize(req) server.E().ServeHTTP(rec, req) assert.Equal(suite.T(), http.StatusBadRequest, rec.Code) @@ -489,6 +506,7 @@ func (suite *FilterApiIntegrationSuite) TestUpdate() { reqBody, _ := json.Marshal(updateReq) req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/filters/%s", filter.ID), bytes.NewReader(reqBody)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + suite.authorize(req) server.E().ServeHTTP(rec, req) assert.Equal(suite.T(), http.StatusOK, rec.Code) @@ -565,6 +583,7 @@ func (suite *FilterApiIntegrationSuite) TestUpdate() { reqBody, _ := json.Marshal(updateReq) req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/filters/%s", filter.ID), bytes.NewReader(reqBody)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + suite.authorize(req) server.E().ServeHTTP(rec, req) assert.Equal(suite.T(), http.StatusOK, rec.Code) @@ -576,3 +595,62 @@ func (suite *FilterApiIntegrationSuite) TestUpdate() { suite.Equal("AC-1", updatedFilter.Controls[0].ID) }) } + +func (suite *FilterApiIntegrationSuite) TestListSSPScopesAndAuth() { + suite.Require().NoError(suite.Migrator.Refresh()) + + logger, _ := zap.NewDevelopment() + metrics := api.NewMetricsHandler(context.Background(), logger.Sugar()) + server := api.NewServer(context.Background(), logger.Sugar(), suite.Config, metrics) + RegisterHandlers(server, logger.Sugar(), suite.DB, suite.Config, &APIServices{}) + + sspA := uuid.New() + sspB := uuid.New() + suite.Require().NoError(suite.DB.Create(&relational.SystemSecurityPlan{UUIDModel: relational.UUIDModel{ID: &sspA}}).Error) + suite.Require().NoError(suite.DB.Create(&relational.SystemSecurityPlan{UUIDModel: relational.UUIDModel{ID: &sspB}}).Error) + global := relational.Filter{Name: "global", Filter: datatypes.NewJSONType(labelfilter.Filter{})} + aFilter := relational.Filter{Name: "ssp-a", SSPID: &sspA, Filter: datatypes.NewJSONType(labelfilter.Filter{})} + bFilter := relational.Filter{Name: "ssp-b", SSPID: &sspB, Filter: datatypes.NewJSONType(labelfilter.Filter{})} + suite.Require().NoError(suite.DB.Create(&global).Error) + suite.Require().NoError(suite.DB.Create(&aFilter).Error) + suite.Require().NoError(suite.DB.Create(&bFilter).Error) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/filters?sspId=%s", sspA), nil) + suite.authorize(req) + server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusOK, rec.Code, rec.Body.String()) + var scoped GenericDataListResponse[FilterWithAssociations] + suite.Require().NoError(json.Unmarshal(rec.Body.Bytes(), &scoped)) + suite.Len(scoped.Data, 2) + for _, filter := range scoped.Data { + suite.True(filter.SSPID == nil || *filter.SSPID == sspA) + } + + rec = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, "/api/filters?scope=global", nil) + suite.authorize(req) + server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusOK, rec.Code, rec.Body.String()) + var globalOnly GenericDataListResponse[FilterWithAssociations] + suite.Require().NoError(json.Unmarshal(rec.Body.Bytes(), &globalOnly)) + suite.Len(globalOnly.Data, 1) + suite.Nil(globalOnly.Data[0].SSPID) + + rec = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, "/api/filters", nil) + suite.authorize(req) + server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusOK, rec.Code, rec.Body.String()) + var all GenericDataListResponse[FilterWithAssociations] + suite.Require().NoError(json.Unmarshal(rec.Body.Bytes(), &all)) + suite.Len(all.Data, 3) + + createReq := createFilterRequest{Name: "unauth", Filter: labelfilter.Filter{}} + body, _ := json.Marshal(createReq) + rec = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodPost, "/api/filters", bytes.NewReader(body)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusUnauthorized, rec.Code) +} diff --git a/internal/api/handler/oscal/api.go b/internal/api/handler/oscal/api.go index c5394123..5eb050e4 100644 --- a/internal/api/handler/oscal/api.go +++ b/internal/api/handler/oscal/api.go @@ -12,6 +12,10 @@ import ( func RegisterHandlers(server *api.Server, logger *zap.SugaredLogger, db *gorm.DB, config *config.Config, evidenceSvc *evidencesvc.EvidenceService, jobEnqueuer SSPJobEnqueuer) { oscalGroup := server.API().Group("/oscal") oscalGroup.Use(middleware.JWTMiddleware(config.JWTPublicKey)) + jwtMiddleware := middleware.JWTMiddleware(config.JWTPublicKey) + + dashboardSuggestionHandler := NewDashboardSuggestionHandler(logger, db, config.AI, jobEnqueuer) + dashboardSuggestionHandler.RegisterConfig(server.API().Group("/dashboard-suggestions")) catalogHandler := NewCatalogHandler(logger, db) catalogHandler.Register(oscalGroup.Group("/catalogs")) @@ -21,6 +25,9 @@ func RegisterHandlers(server *api.Server, logger *zap.SugaredLogger, db *gorm.DB sspHandler := NewSystemSecurityPlanHandler(logger, db, evidenceSvc, jobEnqueuer) sspHandler.Register(oscalGroup.Group("/system-security-plans")) + if config.AI != nil && config.AI.Enabled { + dashboardSuggestionHandler.Register(oscalGroup.Group("/system-security-plans"), jwtMiddleware) + } partyHandler := NewPartyHandler(logger, db) partyHandler.Register(oscalGroup.Group("/parties")) diff --git a/internal/api/handler/oscal/dashboard_suggestions.go b/internal/api/handler/oscal/dashboard_suggestions.go new file mode 100644 index 00000000..f767dbab --- /dev/null +++ b/internal/api/handler/oscal/dashboard_suggestions.go @@ -0,0 +1,819 @@ +package oscal + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "sort" + "strings" + "time" + + "github.com/compliance-framework/api/internal/api" + "github.com/compliance-framework/api/internal/api/handler" + "github.com/compliance-framework/api/internal/authn" + "github.com/compliance-framework/api/internal/config" + "github.com/compliance-framework/api/internal/service/relational" + suggestionrel "github.com/compliance-framework/api/internal/service/relational/suggestions" + workersvc "github.com/compliance-framework/api/internal/service/worker" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgconn" + "github.com/labstack/echo/v4" + "go.uber.org/zap" + "gorm.io/datatypes" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +var ErrDashboardSuggestionWorkerDisabled = errors.New("dashboard suggestion worker disabled") + +type DashboardSuggestionHandler struct { + sugar *zap.SugaredLogger + db *gorm.DB + cfg *config.AIConfig + jobEnqueuer SSPJobEnqueuer +} + +type dashboardSuggestionScopeRequest struct { + ControlKeys []string `json:"controlKeys"` + LabelSetHashes []string `json:"labelSetHashes"` +} + +type generateDashboardSuggestionsRequest struct { + SupersedePending bool `json:"supersedePending"` + Scope *dashboardSuggestionScopeRequest `json:"scope"` +} + +type dashboardSuggestionDecisionRequest struct { + IDs []uuid.UUID `json:"ids" validate:"required"` + Reason string `json:"reason"` +} + +type dashboardSuggestionRunResponse struct { + suggestionrel.DashboardSuggestionRun + CompletedCells int `json:"completedCells"` + FailedCells int `json:"failedCells"` +} + +type dashboardSuggestionResponse struct { + suggestionrel.DashboardSuggestion + ControlTitle string `json:"controlTitle,omitempty"` + TargetFilterName string `json:"targetFilterName,omitempty"` +} + +type acceptDashboardSuggestionsResponse struct { + AcceptedFilterIDs []uuid.UUID `json:"acceptedFilterIds"` + Suggestions []dashboardSuggestionResponse `json:"suggestions"` +} + +type dashboardSuggestionConfigResponse struct { + Enabled bool `json:"enabled"` +} + +type dashboardSuggestionPreviewResponse struct { + PlannedCalls int `json:"plannedCalls"` + ControlCount int `json:"controlCount"` + LabelSetCount int `json:"labelSetCount"` + MaxCallsPerRun int `json:"maxCallsPerRun"` + ExceedsLimit bool `json:"exceedsLimit"` +} + +type dashboardSuggestionPlan struct { + Snapshot suggestionrel.Snapshot + PlannedCalls int + ControlCount int + LabelSetCount int +} + +func NewDashboardSuggestionHandler(sugar *zap.SugaredLogger, db *gorm.DB, cfg *config.AIConfig, jobEnqueuer SSPJobEnqueuer) *DashboardSuggestionHandler { + return &DashboardSuggestionHandler{sugar: sugar, db: db, cfg: cfg, jobEnqueuer: jobEnqueuer} +} + +func (h *DashboardSuggestionHandler) RegisterConfig(apiGroup *echo.Group) { + apiGroup.GET("/config", h.Config) +} + +func (h *DashboardSuggestionHandler) Register(apiGroup *echo.Group, auth echo.MiddlewareFunc) { + apiGroup.POST("/:id/dashboard-suggestions/generate", h.Generate, auth) + apiGroup.POST("/:id/dashboard-suggestions/preview", h.Preview, auth) + apiGroup.GET("/:id/dashboard-suggestions/label-sets", h.LabelSets, auth) + apiGroup.GET("/:id/dashboard-suggestion-runs/latest", h.LatestRun, auth) + apiGroup.GET("/:id/dashboard-suggestion-runs/:runId", h.GetRun, auth) + apiGroup.GET("/:id/dashboard-suggestions", h.ListSuggestions, auth) + apiGroup.POST("/:id/dashboard-suggestions/accept", h.Accept, auth) + apiGroup.POST("/:id/dashboard-suggestions/reject", h.Reject, auth) + apiGroup.GET("/:id/dashboard-suggestions/:suggestionId/events", h.Events, auth) +} + +// Config godoc +// +// @Summary Get dashboard suggestions feature configuration +// @Description Returns whether AI dashboard suggestions are enabled. +// @Tags Dashboard Suggestions +// @Produce json +// @Success 200 {object} handler.GenericDataResponse[oscal.dashboardSuggestionConfigResponse] +// @Router /dashboard-suggestions/config [get] +func (h *DashboardSuggestionHandler) Config(ctx echo.Context) error { + return ctx.JSON(http.StatusOK, handler.GenericDataResponse[dashboardSuggestionConfigResponse]{ + Data: dashboardSuggestionConfigResponse{Enabled: h.aiEnabled()}, + }) +} + +// Generate godoc +// +// @Summary Generate dashboard suggestions for an SSP +// @Description Creates a dashboard suggestion run, snapshots the resolved scope, creates run cells, and enqueues cell processing. +// @Tags Dashboard Suggestions +// @Accept json +// @Produce json +// @Param id path string true "System Security Plan ID" +// @Param request body generateDashboardSuggestionsRequest false "Generation request" +// @Success 202 {object} handler.GenericDataResponse[oscal.dashboardSuggestionRunResponse] +// @Failure 400 {object} api.Error +// @Failure 401 {object} api.Error +// @Failure 409 {object} api.Error +// @Failure 422 {object} api.Error +// @Failure 503 {object} api.Error +// @Failure 500 {object} api.Error +// @Security OAuth2Password +// @Router /oscal/system-security-plans/{id}/dashboard-suggestions/generate [post] +func (h *DashboardSuggestionHandler) Generate(ctx echo.Context) error { + sspID, err := parseUUIDParam(ctx, "id") + if err != nil { + return ctx.JSON(http.StatusBadRequest, api.NewError(err)) + } + var req generateDashboardSuggestionsRequest + if ctx.Request().Body != nil && ctx.Request().ContentLength != 0 { + if err := ctx.Bind(&req); err != nil { + return ctx.JSON(http.StatusBadRequest, api.NewError(err)) + } + } + actorID, err := h.actorUserID(ctx) + if err != nil { + return err + } + + var createdRun suggestionrel.DashboardSuggestionRun + var cells []suggestionrel.DashboardSuggestionRunCell + err = h.db.WithContext(ctx.Request().Context()).Transaction(func(tx *gorm.DB) error { + plan, resolveErr := h.planDashboardSuggestions(tx, sspID, req.Scope) + if resolveErr != nil { + return resolveErr + } + snapshot := plan.Snapshot + plannedCalls := plan.PlannedCalls + if h.maxCallsPerRun() > 0 && plannedCalls > h.maxCallsPerRun() { + return &plannedCallsExceededError{Planned: plannedCalls, Limit: h.maxCallsPerRun()} + } + + runID := uuid.New() + now := time.Now().UTC() + createdRun = suggestionrel.DashboardSuggestionRun{ + UUIDModel: relational.UUIDModel{ID: &runID}, + SSPID: sspID, + Status: "pending", + Model: h.modelName(), + PromptVersion: suggestionrel.PromptVersion, + Scope: snapshotJSON(snapshot), + PlannedCalls: plannedCalls, + TriggeredByUserID: actorID, + StartedAt: &now, + Stats: datatypes.JSONMap{}, + } + if err := tx.Create(&createdRun).Error; err != nil { + return err + } + if req.SupersedePending { + if err := h.supersedePendingInScope(tx, sspID, runID, actorID, snapshot); err != nil { + return err + } + } + grid := suggestionrel.BuildGrid(snapshot, h.chunkConfig()) + cells = make([]suggestionrel.DashboardSuggestionRunCell, 0, len(grid)) + for _, cell := range grid { + cells = append(cells, suggestionrel.DashboardSuggestionRunCell{ + RunID: runID, + CellIndex: cell.CellIndex, + ControlKeys: datatypes.NewJSONSlice(cell.ControlKeys), + LabelSetHashes: datatypes.NewJSONSlice(cell.LabelSetHashes), + Status: "pending", + }) + } + if len(cells) > 0 { + if err := tx.Create(&cells).Error; err != nil { + return err + } + } + if err := createRunEvent(tx, createdRun, suggestionrel.DashboardSuggestionEventTypeRunStarted, actorID, datatypes.JSONMap{ + "planned_calls": plannedCalls, + }); err != nil { + return err + } + if h.jobEnqueuer == nil { + return ErrDashboardSuggestionWorkerDisabled + } + if err := h.jobEnqueuer.EnqueueDashboardSuggestionCells(ctx.Request().Context(), runID, len(cells)); err != nil { + return err + } + return nil + }) + if err != nil { + return h.generateError(ctx, err) + } + + return ctx.JSON(http.StatusAccepted, handler.GenericDataResponse[dashboardSuggestionRunResponse]{ + Data: dashboardSuggestionRunResponse{DashboardSuggestionRun: createdRun}, + }) +} + +// Preview godoc +// +// @Summary Preview dashboard suggestion generation for an SSP +// @Description Resolves the requested dashboard suggestion scope and returns planned call counts without creating runs or enqueueing work. +// @Tags Dashboard Suggestions +// @Accept json +// @Produce json +// @Param id path string true "System Security Plan ID" +// @Param request body generateDashboardSuggestionsRequest false "Preview request" +// @Success 200 {object} handler.GenericDataResponse[oscal.dashboardSuggestionPreviewResponse] +// @Failure 400 {object} api.Error +// @Failure 401 {object} api.Error +// @Failure 422 {object} api.Error +// @Failure 500 {object} api.Error +// @Security OAuth2Password +// @Router /oscal/system-security-plans/{id}/dashboard-suggestions/preview [post] +func (h *DashboardSuggestionHandler) Preview(ctx echo.Context) error { + sspID, err := parseUUIDParam(ctx, "id") + if err != nil { + return ctx.JSON(http.StatusBadRequest, api.NewError(err)) + } + var req generateDashboardSuggestionsRequest + if ctx.Request().Body != nil && ctx.Request().ContentLength != 0 { + if err := ctx.Bind(&req); err != nil { + return ctx.JSON(http.StatusBadRequest, api.NewError(err)) + } + } + + plan, err := h.planDashboardSuggestions(h.db.WithContext(ctx.Request().Context()), sspID, req.Scope) + if err != nil { + return h.generateError(ctx, err) + } + maxCalls := h.maxCallsPerRun() + return ctx.JSON(http.StatusOK, handler.GenericDataResponse[dashboardSuggestionPreviewResponse]{ + Data: dashboardSuggestionPreviewResponse{ + PlannedCalls: plan.PlannedCalls, + ControlCount: plan.ControlCount, + LabelSetCount: plan.LabelSetCount, + MaxCallsPerRun: maxCalls, + ExceedsLimit: maxCalls > 0 && plan.PlannedCalls > maxCalls, + }, + }) +} + +// LabelSets godoc +// +// @Summary List dashboard suggestion label sets +// @Description Returns canonical evidence label sets for dashboard suggestion scope selection. +// @Tags Dashboard Suggestions +// @Produce json +// @Param id path string true "System Security Plan ID" +// @Success 200 {object} handler.GenericDataListResponse[suggestions.LabelSetInput] +// @Failure 400 {object} api.Error +// @Failure 401 {object} api.Error +// @Failure 500 {object} api.Error +// @Security OAuth2Password +// @Router /oscal/system-security-plans/{id}/dashboard-suggestions/label-sets [get] +func (h *DashboardSuggestionHandler) LabelSets(ctx echo.Context) error { + if _, err := parseUUIDParam(ctx, "id"); err != nil { + return ctx.JSON(http.StatusBadRequest, api.NewError(err)) + } + labelSets, err := suggestionrel.NewSuggestionService(h.db).GatherLabelSets(nil) + if err != nil { + return ctx.JSON(http.StatusInternalServerError, api.NewError(err)) + } + return ctx.JSON(http.StatusOK, handler.GenericDataListResponse[suggestionrel.LabelSetInput]{Data: labelSets}) +} + +// LatestRun godoc +// +// @Summary Get latest dashboard suggestion run for an SSP +// @Description Returns the latest dashboard suggestion run with cell progress. +// @Tags Dashboard Suggestions +// @Produce json +// @Param id path string true "System Security Plan ID" +// @Success 200 {object} handler.GenericDataResponse[oscal.dashboardSuggestionRunResponse] +// @Failure 400 {object} api.Error +// @Failure 401 {object} api.Error +// @Failure 404 {object} api.Error +// @Failure 500 {object} api.Error +// @Security OAuth2Password +// @Router /oscal/system-security-plans/{id}/dashboard-suggestion-runs/latest [get] +func (h *DashboardSuggestionHandler) LatestRun(ctx echo.Context) error { + sspID, err := parseUUIDParam(ctx, "id") + if err != nil { + return ctx.JSON(http.StatusBadRequest, api.NewError(err)) + } + var run suggestionrel.DashboardSuggestionRun + if err := h.db.Where("ssp_id = ?", sspID).Order("started_at DESC NULLS LAST").First(&run).Error; err != nil { + return h.notFoundOrInternal(ctx, err) + } + return h.respondRun(ctx, run) +} + +// GetRun godoc +// +// @Summary Get a dashboard suggestion run +// @Description Returns a dashboard suggestion run with cell progress. +// @Tags Dashboard Suggestions +// @Produce json +// @Param id path string true "System Security Plan ID" +// @Param runId path string true "Dashboard suggestion run ID" +// @Success 200 {object} handler.GenericDataResponse[oscal.dashboardSuggestionRunResponse] +// @Failure 400 {object} api.Error +// @Failure 401 {object} api.Error +// @Failure 404 {object} api.Error +// @Failure 500 {object} api.Error +// @Security OAuth2Password +// @Router /oscal/system-security-plans/{id}/dashboard-suggestion-runs/{runId} [get] +func (h *DashboardSuggestionHandler) GetRun(ctx echo.Context) error { + sspID, err := parseUUIDParam(ctx, "id") + if err != nil { + return ctx.JSON(http.StatusBadRequest, api.NewError(err)) + } + runID, err := parseUUIDParam(ctx, "runId") + if err != nil { + return ctx.JSON(http.StatusBadRequest, api.NewError(err)) + } + var run suggestionrel.DashboardSuggestionRun + if err := h.db.Where("id = ? AND ssp_id = ?", runID, sspID).First(&run).Error; err != nil { + return h.notFoundOrInternal(ctx, err) + } + return h.respondRun(ctx, run) +} + +// ListSuggestions godoc +// +// @Summary List dashboard suggestions for an SSP +// @Description Lists dashboard suggestions joined with control title and target filter name. +// @Tags Dashboard Suggestions +// @Produce json +// @Param id path string true "System Security Plan ID" +// @Param status query string false "Suggestion status (default pending)" +// @Success 200 {object} handler.GenericDataListResponse[oscal.dashboardSuggestionResponse] +// @Failure 400 {object} api.Error +// @Failure 401 {object} api.Error +// @Failure 500 {object} api.Error +// @Security OAuth2Password +// @Router /oscal/system-security-plans/{id}/dashboard-suggestions [get] +func (h *DashboardSuggestionHandler) ListSuggestions(ctx echo.Context) error { + sspID, err := parseUUIDParam(ctx, "id") + if err != nil { + return ctx.JSON(http.StatusBadRequest, api.NewError(err)) + } + status := strings.TrimSpace(ctx.QueryParam("status")) + if status == "" { + status = suggestionrel.DashboardSuggestionStatusPending + } + suggestions, err := h.loadSuggestionResponses(h.db.Where("dashboard_suggestions.ssp_id = ? AND dashboard_suggestions.status = ?", sspID, status)) + if err != nil { + return ctx.JSON(http.StatusInternalServerError, api.NewError(err)) + } + return ctx.JSON(http.StatusOK, handler.GenericDataListResponse[dashboardSuggestionResponse]{Data: suggestions}) +} + +// Accept godoc +// +// @Summary Accept dashboard suggestions +// @Description Accepts pending dashboard suggestions and creates or extends SSP-bound filters. +// @Tags Dashboard Suggestions +// @Accept json +// @Produce json +// @Param id path string true "System Security Plan ID" +// @Param request body dashboardSuggestionDecisionRequest true "Suggestion IDs" +// @Success 200 {object} handler.GenericDataResponse[oscal.acceptDashboardSuggestionsResponse] +// @Failure 400 {object} api.Error +// @Failure 401 {object} api.Error +// @Failure 409 {object} api.Error +// @Failure 500 {object} api.Error +// @Security OAuth2Password +// @Router /oscal/system-security-plans/{id}/dashboard-suggestions/accept [post] +func (h *DashboardSuggestionHandler) Accept(ctx echo.Context) error { + sspID, req, actorID, ok := h.bindDecision(ctx) + if !ok { + return nil + } + if err := suggestionrel.NewSuggestionService(h.db).Accept(sspID, req.IDs, *actorID); err != nil { + return h.decisionError(ctx, err) + } + suggestions, err := h.loadSuggestionResponses(h.db.Where("dashboard_suggestions.id IN ?", req.IDs)) + if err != nil { + return ctx.JSON(http.StatusInternalServerError, api.NewError(err)) + } + accepted := map[uuid.UUID]struct{}{} + for _, suggestion := range suggestions { + if suggestion.AcceptedFilterID != nil { + accepted[*suggestion.AcceptedFilterID] = struct{}{} + } + } + acceptedIDs := make([]uuid.UUID, 0, len(accepted)) + for id := range accepted { + acceptedIDs = append(acceptedIDs, id) + } + sort.Slice(acceptedIDs, func(i, j int) bool { return acceptedIDs[i].String() < acceptedIDs[j].String() }) + return ctx.JSON(http.StatusOK, handler.GenericDataResponse[acceptDashboardSuggestionsResponse]{ + Data: acceptDashboardSuggestionsResponse{AcceptedFilterIDs: acceptedIDs, Suggestions: suggestions}, + }) +} + +// Reject godoc +// +// @Summary Reject dashboard suggestions +// @Description Rejects pending dashboard suggestions. +// @Tags Dashboard Suggestions +// @Accept json +// @Produce json +// @Param id path string true "System Security Plan ID" +// @Param request body dashboardSuggestionDecisionRequest true "Suggestion IDs and reason" +// @Success 200 {object} handler.GenericDataListResponse[oscal.dashboardSuggestionResponse] +// @Failure 400 {object} api.Error +// @Failure 401 {object} api.Error +// @Failure 409 {object} api.Error +// @Failure 500 {object} api.Error +// @Security OAuth2Password +// @Router /oscal/system-security-plans/{id}/dashboard-suggestions/reject [post] +func (h *DashboardSuggestionHandler) Reject(ctx echo.Context) error { + sspID, req, actorID, ok := h.bindDecision(ctx) + if !ok { + return nil + } + if err := suggestionrel.NewSuggestionService(h.db).Reject(sspID, req.IDs, req.Reason, *actorID); err != nil { + return h.decisionError(ctx, err) + } + suggestions, err := h.loadSuggestionResponses(h.db.Where("dashboard_suggestions.id IN ?", req.IDs)) + if err != nil { + return ctx.JSON(http.StatusInternalServerError, api.NewError(err)) + } + return ctx.JSON(http.StatusOK, handler.GenericDataListResponse[dashboardSuggestionResponse]{Data: suggestions}) +} + +// Events godoc +// +// @Summary List dashboard suggestion events +// @Description Returns audit events for one dashboard suggestion. +// @Tags Dashboard Suggestions +// @Produce json +// @Param id path string true "System Security Plan ID" +// @Param suggestionId path string true "Dashboard suggestion ID" +// @Success 200 {object} handler.GenericDataListResponse[suggestions.DashboardSuggestionEvent] +// @Failure 400 {object} api.Error +// @Failure 401 {object} api.Error +// @Failure 404 {object} api.Error +// @Failure 500 {object} api.Error +// @Security OAuth2Password +// @Router /oscal/system-security-plans/{id}/dashboard-suggestions/{suggestionId}/events [get] +func (h *DashboardSuggestionHandler) Events(ctx echo.Context) error { + sspID, err := parseUUIDParam(ctx, "id") + if err != nil { + return ctx.JSON(http.StatusBadRequest, api.NewError(err)) + } + suggestionID, err := parseUUIDParam(ctx, "suggestionId") + if err != nil { + return ctx.JSON(http.StatusBadRequest, api.NewError(err)) + } + var count int64 + if err := h.db.Model(&suggestionrel.DashboardSuggestion{}).Where("id = ? AND ssp_id = ?", suggestionID, sspID).Count(&count).Error; err != nil { + return ctx.JSON(http.StatusInternalServerError, api.NewError(err)) + } + if count == 0 { + return ctx.JSON(http.StatusNotFound, api.NewError(gorm.ErrRecordNotFound)) + } + var events []suggestionrel.DashboardSuggestionEvent + if err := h.db.Where("suggestion_id = ?", suggestionID).Order("occurred_at ASC").Find(&events).Error; err != nil { + return ctx.JSON(http.StatusInternalServerError, api.NewError(err)) + } + return ctx.JSON(http.StatusOK, handler.GenericDataListResponse[suggestionrel.DashboardSuggestionEvent]{Data: events}) +} + +func (h *DashboardSuggestionHandler) aiEnabled() bool { + return h.cfg != nil && h.cfg.Enabled +} + +func (h *DashboardSuggestionHandler) chunkConfig() suggestionrel.ChunkConfig { + if h.cfg == nil { + return suggestionrel.ChunkConfig{} + } + return suggestionrel.ChunkConfig{MaxControlsPerChunk: h.cfg.MaxControlsPerChunk, MaxLabelSetsPerChunk: h.cfg.MaxLabelSetsPerChunk} +} + +func (h *DashboardSuggestionHandler) maxCallsPerRun() int { + if h.cfg == nil { + return 0 + } + return h.cfg.MaxCallsPerRun +} + +func (h *DashboardSuggestionHandler) modelName() string { + if h.cfg == nil || strings.TrimSpace(h.cfg.Model) == "" { + return config.DefaultAIModel + } + return h.cfg.Model +} + +func (h *DashboardSuggestionHandler) planDashboardSuggestions(db *gorm.DB, sspID uuid.UUID, scope *dashboardSuggestionScopeRequest) (dashboardSuggestionPlan, error) { + svc := suggestionrel.NewSuggestionService(db) + snapshot, err := svc.ResolveScope(sspID, scopeFromRequest(scope)) + if err != nil { + return dashboardSuggestionPlan{}, err + } + if len(snapshot.ControlKeys) == 0 { + return dashboardSuggestionPlan{}, &emptyDashboardSuggestionScopeError{message: "no controls resolved for dashboard suggestions"} + } + if len(snapshot.LabelSetHashes) == 0 { + return dashboardSuggestionPlan{}, &emptyDashboardSuggestionScopeError{message: "no evidence label sets resolved for dashboard suggestions"} + } + return dashboardSuggestionPlan{ + Snapshot: snapshot, + PlannedCalls: suggestionrel.PlannedCalls(len(snapshot.ControlKeys), len(snapshot.LabelSetHashes), h.chunkConfig()), + ControlCount: len(snapshot.ControlKeys), + LabelSetCount: len(snapshot.LabelSetHashes), + }, nil +} + +func scopeFromRequest(scope *dashboardSuggestionScopeRequest) suggestionrel.Scope { + if scope == nil { + return suggestionrel.Scope{} + } + return suggestionrel.Scope{ControlKeys: scope.ControlKeys, LabelSetHashes: scope.LabelSetHashes} +} + +func snapshotJSON(snapshot suggestionrel.Snapshot) datatypes.JSONMap { + return datatypes.JSONMap{ + "controlKeys": append([]string(nil), snapshot.ControlKeys...), + "labelSetHashes": append([]string(nil), snapshot.LabelSetHashes...), + } +} + +func parseUUIDParam(ctx echo.Context, name string) (uuid.UUID, error) { + return uuid.Parse(ctx.Param(name)) +} + +func (h *DashboardSuggestionHandler) actorUserID(ctx echo.Context) (*uuid.UUID, error) { + claims, ok := ctx.Get("user").(*authn.UserClaims) + if !ok || claims == nil { + return nil, ctx.JSON(http.StatusUnauthorized, api.NewError(fmt.Errorf("missing authentication claims"))) + } + var user relational.User + if err := h.db.Where("email = ?", claims.Subject).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ctx.JSON(http.StatusNotFound, api.NewError(fmt.Errorf("user not found"))) + } + return nil, ctx.JSON(http.StatusInternalServerError, api.NewError(err)) + } + return user.ID, nil +} + +func (h *DashboardSuggestionHandler) bindDecision(ctx echo.Context) (uuid.UUID, dashboardSuggestionDecisionRequest, *uuid.UUID, bool) { + sspID, err := parseUUIDParam(ctx, "id") + if err != nil { + _ = ctx.JSON(http.StatusBadRequest, api.NewError(err)) + return uuid.Nil, dashboardSuggestionDecisionRequest{}, nil, false + } + var req dashboardSuggestionDecisionRequest + if err := ctx.Bind(&req); err != nil { + _ = ctx.JSON(http.StatusBadRequest, api.NewError(err)) + return uuid.Nil, dashboardSuggestionDecisionRequest{}, nil, false + } + if len(req.IDs) == 0 { + _ = ctx.JSON(http.StatusBadRequest, api.NewError(fmt.Errorf("ids is required"))) + return uuid.Nil, dashboardSuggestionDecisionRequest{}, nil, false + } + actorID, err := h.actorUserID(ctx) + if err != nil { + return uuid.Nil, dashboardSuggestionDecisionRequest{}, nil, false + } + return sspID, req, actorID, true +} + +func (h *DashboardSuggestionHandler) supersedePendingInScope(tx *gorm.DB, sspID, newRunID uuid.UUID, actorID *uuid.UUID, snapshot suggestionrel.Snapshot) error { + query := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Where("ssp_id = ? AND status = ?", sspID, suggestionrel.DashboardSuggestionStatusPending) + if len(snapshot.LabelSetHashes) > 0 { + query = query.Where("label_set_hash IN ?", snapshot.LabelSetHashes) + } + controlFilter := tx.Session(&gorm.Session{}) + for idx, key := range snapshot.ControlKeys { + catalogID, controlID, err := suggestionrel.ParseControlKey(key) + if err != nil { + return err + } + condition := tx.Where("control_catalog_id = ? AND control_id = ?", catalogID, controlID) + if idx == 0 { + controlFilter = controlFilter.Where(condition) + } else { + controlFilter = controlFilter.Or(condition) + } + } + if len(snapshot.ControlKeys) > 0 { + query = query.Where(controlFilter) + } + + var suggestions []suggestionrel.DashboardSuggestion + if err := query.Find(&suggestions).Error; err != nil { + return err + } + now := time.Now().UTC() + for _, suggestion := range suggestions { + if err := tx.Model(&suggestionrel.DashboardSuggestion{}). + Where("id = ?", suggestion.ID). + Updates(map[string]any{ + "status": suggestionrel.DashboardSuggestionStatusSuperseded, + "decided_by_user_id": actorID, + "decided_at": now, + }).Error; err != nil { + return err + } + suggestion.Status = suggestionrel.DashboardSuggestionStatusSuperseded + suggestion.DecidedByUserID = actorID + suggestion.DecidedAt = &now + if err := createSuggestionAuditEvent(tx, suggestion, suggestionrel.DashboardSuggestionEventTypeSuperseded, actorID, datatypes.JSONMap{ + "new_run_id": newRunID.String(), + }); err != nil { + return err + } + } + return nil +} + +func createRunEvent(tx *gorm.DB, run suggestionrel.DashboardSuggestionRun, eventType suggestionrel.DashboardSuggestionEventType, actorID *uuid.UUID, payload datatypes.JSONMap) error { + snapshot, err := jsonMapFrom(run) + if err != nil { + return err + } + event := suggestionrel.DashboardSuggestionEvent{ + RunID: run.ID, + EventType: string(eventType), + ActorUserID: actorID, + OccurredAt: time.Now().UTC(), + Payload: payload, + Snapshot: snapshot, + } + return tx.Create(&event).Error +} + +func createSuggestionAuditEvent(tx *gorm.DB, suggestion suggestionrel.DashboardSuggestion, eventType suggestionrel.DashboardSuggestionEventType, actorID *uuid.UUID, payload datatypes.JSONMap) error { + snapshot, err := jsonMapFrom(suggestion) + if err != nil { + return err + } + event := suggestionrel.DashboardSuggestionEvent{ + RunID: &suggestion.RunID, + SuggestionID: suggestion.ID, + EventType: string(eventType), + ActorUserID: actorID, + OccurredAt: time.Now().UTC(), + Payload: payload, + Snapshot: snapshot, + } + return tx.Create(&event).Error +} + +func jsonMapFrom(value any) (datatypes.JSONMap, error) { + raw, err := json.Marshal(value) + if err != nil { + return nil, err + } + var out datatypes.JSONMap + if err := json.Unmarshal(raw, &out); err != nil { + return nil, err + } + return out, nil +} + +func (h *DashboardSuggestionHandler) respondRun(ctx echo.Context, run suggestionrel.DashboardSuggestionRun) error { + completed, failed, err := h.cellProgress(*run.ID) + if err != nil { + return ctx.JSON(http.StatusInternalServerError, api.NewError(err)) + } + return ctx.JSON(http.StatusOK, handler.GenericDataResponse[dashboardSuggestionRunResponse]{ + Data: dashboardSuggestionRunResponse{DashboardSuggestionRun: run, CompletedCells: completed, FailedCells: failed}, + }) +} + +func (h *DashboardSuggestionHandler) cellProgress(runID uuid.UUID) (int, int, error) { + var rows []struct { + Status string + Count int + } + if err := h.db.Model(&suggestionrel.DashboardSuggestionRunCell{}). + Select("status, count(*) as count"). + Where("run_id = ?", runID). + Group("status"). + Scan(&rows).Error; err != nil { + return 0, 0, err + } + completed := 0 + failed := 0 + for _, row := range rows { + switch row.Status { + case "completed": + completed = row.Count + case "failed": + failed = row.Count + } + } + return completed, failed, nil +} + +func (h *DashboardSuggestionHandler) loadSuggestionResponses(query *gorm.DB) ([]dashboardSuggestionResponse, error) { + type row struct { + suggestionrel.DashboardSuggestion + ControlTitle *string `gorm:"column:control_title"` + TargetFilterName *string `gorm:"column:target_filter_name"` + } + var rows []row + if err := query. + Model(&suggestionrel.DashboardSuggestion{}). + Select("dashboard_suggestions.*, controls.title AS control_title, filters.name AS target_filter_name"). + Joins("LEFT JOIN controls ON controls.catalog_id = dashboard_suggestions.control_catalog_id AND controls.id = dashboard_suggestions.control_id"). + Joins("LEFT JOIN filters ON filters.id = dashboard_suggestions.target_filter_id"). + Order("dashboard_suggestions.control_id ASC, dashboard_suggestions.proposed_filter_name ASC"). + Scan(&rows).Error; err != nil { + return nil, err + } + out := make([]dashboardSuggestionResponse, 0, len(rows)) + for _, row := range rows { + item := dashboardSuggestionResponse{DashboardSuggestion: row.DashboardSuggestion} + if row.ControlTitle != nil { + item.ControlTitle = *row.ControlTitle + } + if row.TargetFilterName != nil { + item.TargetFilterName = *row.TargetFilterName + } + out = append(out, item) + } + return out, nil +} + +type plannedCallsExceededError struct { + Planned int + Limit int +} + +func (e *plannedCallsExceededError) Error() string { + return fmt.Sprintf("planned calls %d exceeds limit %d", e.Planned, e.Limit) +} + +type emptyDashboardSuggestionScopeError struct { + message string +} + +func (e *emptyDashboardSuggestionScopeError) Error() string { + return e.message +} + +func (h *DashboardSuggestionHandler) generateError(ctx echo.Context, err error) error { + var scopeErr *suggestionrel.ScopeError + if errors.As(err, &scopeErr) { + return ctx.JSON(http.StatusUnprocessableEntity, api.NewError(err)) + } + var emptyScopeErr *emptyDashboardSuggestionScopeError + if errors.As(err, &emptyScopeErr) { + return ctx.JSON(http.StatusUnprocessableEntity, api.NewError(err)) + } + var plannedErr *plannedCallsExceededError + if errors.As(err, &plannedErr) { + return ctx.JSON(http.StatusUnprocessableEntity, api.Error{Errors: map[string]any{ + "body": plannedErr.Error(), + "plannedCalls": plannedErr.Planned, + "limit": plannedErr.Limit, + }}) + } + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == "23505" { + return ctx.JSON(http.StatusConflict, api.NewError(fmt.Errorf("active dashboard suggestion run already exists"))) + } + if errors.Is(err, ErrDashboardSuggestionWorkerDisabled) || + errors.Is(err, workersvc.ErrDashboardSuggestionWorkerDisabled) || + strings.Contains(strings.ToLower(err.Error()), "worker service is disabled") { + return ctx.JSON(http.StatusServiceUnavailable, api.NewError(ErrDashboardSuggestionWorkerDisabled)) + } + if errors.Is(err, workersvc.ErrDashboardSuggestionWorkerNotRegistered) { + return ctx.JSON(http.StatusServiceUnavailable, api.NewError(workersvc.ErrDashboardSuggestionWorkerNotRegistered)) + } + return ctx.JSON(http.StatusInternalServerError, api.NewError(err)) +} + +func (h *DashboardSuggestionHandler) decisionError(ctx echo.Context, err error) error { + var conflict *suggestionrel.ConflictError + if errors.As(err, &conflict) { + return ctx.JSON(http.StatusConflict, api.NewError(err)) + } + return ctx.JSON(http.StatusInternalServerError, api.NewError(err)) +} + +func (h *DashboardSuggestionHandler) notFoundOrInternal(ctx echo.Context, err error) error { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ctx.JSON(http.StatusNotFound, api.NewError(err)) + } + return ctx.JSON(http.StatusInternalServerError, api.NewError(err)) +} diff --git a/internal/api/handler/oscal/dashboard_suggestions_integration_test.go b/internal/api/handler/oscal/dashboard_suggestions_integration_test.go new file mode 100644 index 00000000..7ac2d6bd --- /dev/null +++ b/internal/api/handler/oscal/dashboard_suggestions_integration_test.go @@ -0,0 +1,590 @@ +//go:build integration + +package oscal + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/compliance-framework/api/internal/api" + apihandler "github.com/compliance-framework/api/internal/api/handler" + "github.com/compliance-framework/api/internal/config" + "github.com/compliance-framework/api/internal/service/relational" + suggestionrel "github.com/compliance-framework/api/internal/service/relational/suggestions" + workersvc "github.com/compliance-framework/api/internal/service/worker" + "github.com/compliance-framework/api/internal/tests" + oscalTypes_1_1_3 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" + "github.com/google/uuid" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/suite" + "go.uber.org/zap" + "gorm.io/datatypes" +) + +type dashboardSuggestionFakeEnqueuer struct { + runID uuid.UUID + cellCount int + calls int + err error +} + +func (f *dashboardSuggestionFakeEnqueuer) EnqueueOrphanedRiskCleanup(context.Context, uuid.UUID, *uuid.UUID, *uuid.UUID) error { + return nil +} + +func (f *dashboardSuggestionFakeEnqueuer) EnqueueDashboardSuggestionCells(_ context.Context, runID uuid.UUID, cellCount int) error { + f.calls++ + f.runID = runID + f.cellCount = cellCount + return f.err +} + +type DashboardSuggestionsHTTPSuite struct { + tests.IntegrationTestSuite + server *api.Server + enqueuer *dashboardSuggestionFakeEnqueuer +} + +func TestDashboardSuggestionsHTTPSuite(t *testing.T) { + suite.Run(t, new(DashboardSuggestionsHTTPSuite)) +} + +func (suite *DashboardSuggestionsHTTPSuite) SetupTest() { + suite.Require().NoError(suite.Migrator.Refresh()) + suite.enqueuer = &dashboardSuggestionFakeEnqueuer{} + suite.server = suite.newServer(true, suite.enqueuer, 0) +} + +func (suite *DashboardSuggestionsHTTPSuite) newServer(enabled bool, enqueuer SSPJobEnqueuer, maxCalls int) *api.Server { + return suite.newServerWithChunks(enabled, enqueuer, maxCalls, 1, 1) +} + +func (suite *DashboardSuggestionsHTTPSuite) newServerWithChunks(enabled bool, enqueuer SSPJobEnqueuer, maxCalls int, maxControlsPerChunk int, maxLabelSetsPerChunk int) *api.Server { + logConf := zap.NewDevelopmentConfig() + logConf.Level = zap.NewAtomicLevelAt(zap.ErrorLevel) + logger, _ := logConf.Build() + metrics := api.NewMetricsHandler(context.Background(), logger.Sugar()) + cfg := *suite.Config + cfg.AI = config.DefaultAIConfig() + cfg.AI.Enabled = enabled + cfg.AI.MaxControlsPerChunk = maxControlsPerChunk + cfg.AI.MaxLabelSetsPerChunk = maxLabelSetsPerChunk + cfg.AI.MaxCallsPerRun = maxCalls + server := api.NewServer(context.Background(), logger.Sugar(), &cfg, metrics) + RegisterHandlers(server, logger.Sugar(), suite.DB, &cfg, nil, enqueuer) + return server +} + +func (suite *DashboardSuggestionsHTTPSuite) req(method, path string, body any) (*httptest.ResponseRecorder, *http.Request) { + var buf []byte + if body != nil { + var err error + buf, err = json.Marshal(body) + suite.Require().NoError(err) + } + token, err := suite.GetAuthToken() + suite.Require().NoError(err) + rec := httptest.NewRecorder() + req := httptest.NewRequest(method, path, bytes.NewReader(buf)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(echo.HeaderAuthorization, fmt.Sprintf("Bearer %s", *token)) + return rec, req +} + +func (suite *DashboardSuggestionsHTTPSuite) seedScope(controlIDs []string, labelSets []map[string]string) (uuid.UUID, []string, []string) { + sspID := uuid.New() + suite.Require().NoError(suite.DB.Create(&relational.SystemSecurityPlan{UUIDModel: relational.UUIDModel{ID: &sspID}}).Error) + catalog := relational.Catalog{} + suite.Require().NoError(suite.DB.Create(&catalog).Error) + profileID := uuid.New() + suite.Require().NoError(suite.DB.Create(&relational.Profile{UUIDModel: relational.UUIDModel{ID: &profileID}}).Error) + suite.Require().NoError(suite.DB.Exec( + `INSERT INTO ssp_profiles (system_security_plan_id, profile_id) VALUES (?, ?)`, + sspID, profileID, + ).Error) + + controlKeys := make([]string, 0, len(controlIDs)) + for _, controlID := range controlIDs { + suite.Require().NoError(suite.DB.Create(&relational.Control{CatalogID: *catalog.ID, ID: controlID, Title: "Control " + controlID}).Error) + suite.Require().NoError(suite.DB.Exec( + `INSERT INTO profile_controls (profile_id, control_catalog_id, control_id) VALUES (?, ?, ?)`, + profileID, catalog.ID, controlID, + ).Error) + controlKeys = append(controlKeys, suggestionrel.ControlKey(*catalog.ID, controlID)) + } + + hashes := make([]string, 0, len(labelSets)) + for idx, labels := range labelSets { + evidence := relational.Evidence{ + UUID: uuid.New(), + Title: fmt.Sprintf("evidence-%d", idx), + Start: time.Now().UTC(), + End: time.Now().UTC(), + Status: datatypes.NewJSONType(oscalTypes_1_1_3.ObjectiveStatus{State: "satisfied"}), + } + suite.Require().NoError(suite.DB.Create(&evidence).Error) + for key, value := range labels { + suite.Require().NoError(suite.DB.Exec( + `INSERT INTO evidence_labels (evidence_id, labels_name, labels_value) VALUES (?, ?, ?)`, + evidence.ID, key, value, + ).Error) + } + normalized, ok := suggestionrel.NormalizeLabelSet(labels) + suite.Require().True(ok) + hashes = append(hashes, suggestionrel.CanonicalLabelSetHash(normalized)) + } + return sspID, controlKeys, hashes +} + +func (suite *DashboardSuggestionsHTTPSuite) TestGenerateCreatesRunCellsAndEnqueuesThenConflicts() { + sspID, controlKeys, hashes := suite.seedScope([]string{"AC-1", "AC-2"}, []map[string]string{{"env": "prod"}, {"env": "stage"}}) + + rec, req := suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/generate", sspID), nil) + suite.server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusAccepted, rec.Code, rec.Body.String()) + + var response apihandler.GenericDataResponse[dashboardSuggestionRunResponse] + suite.Require().NoError(json.Unmarshal(rec.Body.Bytes(), &response)) + suite.Equal(4, response.Data.PlannedCalls) + suite.Equal(4, suite.enqueuer.cellCount) + suite.Equal(*response.Data.ID, suite.enqueuer.runID) + + var cellCount int64 + suite.Require().NoError(suite.DB.Model(&suggestionrel.DashboardSuggestionRunCell{}).Where("run_id = ?", response.Data.ID).Count(&cellCount).Error) + suite.Equal(int64(4), cellCount) + suite.ElementsMatch(controlKeys, stringSliceFromJSONValue(response.Data.Scope["controlKeys"])) + suite.ElementsMatch(hashes, stringSliceFromJSONValue(response.Data.Scope["labelSetHashes"])) + + rec, req = suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/generate", sspID), nil) + suite.server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusConflict, rec.Code, rec.Body.String()) +} + +func (suite *DashboardSuggestionsHTTPSuite) TestPreviewReturnsChunkedPlanAndLeavesNoGenerationState() { + controlIDs := make([]string, 0, 16) + for idx := 1; idx <= 16; idx++ { + controlIDs = append(controlIDs, fmt.Sprintf("AC-%d", idx)) + } + labelSets := make([]map[string]string, 0, 47) + for idx := 1; idx <= 47; idx++ { + labelSets = append(labelSets, map[string]string{"env": fmt.Sprintf("env-%d", idx)}) + } + sspID, controlKeys, hashes := suite.seedScope(controlIDs, labelSets) + server := suite.newServerWithChunks(true, suite.enqueuer, 0, 40, 200) + + body := generateDashboardSuggestionsRequest{ + Scope: &dashboardSuggestionScopeRequest{ + ControlKeys: controlKeys, + LabelSetHashes: hashes, + }, + } + rec, req := suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/preview", sspID), body) + server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusOK, rec.Code, rec.Body.String()) + + var response apihandler.GenericDataResponse[dashboardSuggestionPreviewResponse] + suite.Require().NoError(json.Unmarshal(rec.Body.Bytes(), &response)) + suite.Equal(1, response.Data.PlannedCalls) + suite.Equal(16, response.Data.ControlCount) + suite.Equal(47, response.Data.LabelSetCount) + suite.Equal(0, response.Data.MaxCallsPerRun) + suite.False(response.Data.ExceedsLimit) + suite.Equal(0, suite.enqueuer.calls) + + var runCount int64 + suite.Require().NoError(suite.DB.Model(&suggestionrel.DashboardSuggestionRun{}).Where("ssp_id = ?", sspID).Count(&runCount).Error) + suite.Equal(int64(0), runCount) + + var cellCount int64 + suite.Require().NoError(suite.DB.Model(&suggestionrel.DashboardSuggestionRunCell{}). + Joins("JOIN dashboard_suggestion_runs ON dashboard_suggestion_runs.id = dashboard_suggestion_run_cells.run_id"). + Where("dashboard_suggestion_runs.ssp_id = ?", sspID). + Count(&cellCount).Error) + suite.Equal(int64(0), cellCount) + + var eventCount int64 + suite.Require().NoError(suite.DB.Model(&suggestionrel.DashboardSuggestionEvent{}).Count(&eventCount).Error) + suite.Equal(int64(0), eventCount) +} + +func (suite *DashboardSuggestionsHTTPSuite) TestPreviewReportsConfiguredMaxCallsLimit() { + sspID, controlKeys, hashes := suite.seedScope([]string{"AC-1", "AC-2"}, []map[string]string{{"env": "prod"}}) + body := generateDashboardSuggestionsRequest{ + Scope: &dashboardSuggestionScopeRequest{ + ControlKeys: controlKeys, + LabelSetHashes: hashes, + }, + } + + limitedServer := suite.newServerWithChunks(true, suite.enqueuer, 1, 1, 1) + rec, req := suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/preview", sspID), body) + limitedServer.E().ServeHTTP(rec, req) + suite.Equal(http.StatusOK, rec.Code, rec.Body.String()) + + var response apihandler.GenericDataResponse[dashboardSuggestionPreviewResponse] + suite.Require().NoError(json.Unmarshal(rec.Body.Bytes(), &response)) + suite.Equal(2, response.Data.PlannedCalls) + suite.Equal(2, response.Data.ControlCount) + suite.Equal(1, response.Data.LabelSetCount) + suite.Equal(1, response.Data.MaxCallsPerRun) + suite.True(response.Data.ExceedsLimit) + + atLimitServer := suite.newServerWithChunks(true, suite.enqueuer, 2, 1, 1) + rec, req = suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/preview", sspID), body) + atLimitServer.E().ServeHTTP(rec, req) + suite.Equal(http.StatusOK, rec.Code, rec.Body.String()) + + suite.Require().NoError(json.Unmarshal(rec.Body.Bytes(), &response)) + suite.Equal(2, response.Data.PlannedCalls) + suite.Equal(2, response.Data.MaxCallsPerRun) + suite.False(response.Data.ExceedsLimit) + suite.Equal(0, suite.enqueuer.calls) +} + +func (suite *DashboardSuggestionsHTTPSuite) TestGenerateValidationAndWorkerDisabledPaths() { + sspID, _, _ := suite.seedScope([]string{"AC-1"}, []map[string]string{{"env": "prod"}}) + + body := generateDashboardSuggestionsRequest{Scope: &dashboardSuggestionScopeRequest{ControlKeys: []string{"missing"}}} + rec, req := suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/generate", sspID), body) + suite.server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusUnprocessableEntity, rec.Code, rec.Body.String()) + + oversizedSSPID, _, _ := suite.seedScope([]string{"AC-2", "AC-3"}, []map[string]string{{"tier": "app"}}) + limitedServer := suite.newServer(true, suite.enqueuer, 1) + rec, req = suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/generate", oversizedSSPID), nil) + limitedServer.E().ServeHTTP(rec, req) + suite.Equal(http.StatusUnprocessableEntity, rec.Code, rec.Body.String()) + + disabledSSPID, _, _ := suite.seedScope([]string{"AC-4"}, []map[string]string{{"tier": "db"}}) + workerDisabledServer := suite.newServer(true, nil, 0) + rec, req = suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/generate", disabledSSPID), nil) + workerDisabledServer.E().ServeHTTP(rec, req) + suite.Equal(http.StatusServiceUnavailable, rec.Code, rec.Body.String()) +} + +func (suite *DashboardSuggestionsHTTPSuite) TestGenerateWorkerNotRegisteredReturnsServiceUnavailableAndRollsBack() { + sspID, _, _ := suite.seedScope([]string{"AC-1"}, []map[string]string{{"env": "prod"}}) + enqueuer := &dashboardSuggestionFakeEnqueuer{err: workersvc.ErrDashboardSuggestionWorkerNotRegistered} + server := suite.newServer(true, enqueuer, 0) + + rec, req := suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/generate", sspID), nil) + server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusServiceUnavailable, rec.Code, rec.Body.String()) + suite.Contains(rec.Body.String(), workersvc.ErrDashboardSuggestionWorkerNotRegistered.Error()) + + var runCount int64 + suite.Require().NoError(suite.DB.Model(&suggestionrel.DashboardSuggestionRun{}).Where("ssp_id = ?", sspID).Count(&runCount).Error) + suite.Equal(int64(0), runCount) + + var cellCount int64 + suite.Require().NoError(suite.DB.Model(&suggestionrel.DashboardSuggestionRunCell{}). + Joins("JOIN dashboard_suggestion_runs ON dashboard_suggestion_runs.id = dashboard_suggestion_run_cells.run_id"). + Where("dashboard_suggestion_runs.ssp_id = ?", sspID). + Count(&cellCount).Error) + suite.Equal(int64(0), cellCount) +} + +func (suite *DashboardSuggestionsHTTPSuite) TestGenerateWorkerDisabledSentinelReturnsServiceUnavailableAndRollsBack() { + sspID, _, _ := suite.seedScope([]string{"AC-1"}, []map[string]string{{"env": "prod"}}) + enqueuer := &dashboardSuggestionFakeEnqueuer{err: workersvc.ErrDashboardSuggestionWorkerDisabled} + server := suite.newServer(true, enqueuer, 0) + + rec, req := suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/generate", sspID), nil) + server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusServiceUnavailable, rec.Code, rec.Body.String()) + suite.Contains(rec.Body.String(), ErrDashboardSuggestionWorkerDisabled.Error()) + + var runCount int64 + suite.Require().NoError(suite.DB.Model(&suggestionrel.DashboardSuggestionRun{}).Where("ssp_id = ?", sspID).Count(&runCount).Error) + suite.Equal(int64(0), runCount) + + var cellCount int64 + suite.Require().NoError(suite.DB.Model(&suggestionrel.DashboardSuggestionRunCell{}). + Joins("JOIN dashboard_suggestion_runs ON dashboard_suggestion_runs.id = dashboard_suggestion_run_cells.run_id"). + Where("dashboard_suggestion_runs.ssp_id = ?", sspID). + Count(&cellCount).Error) + suite.Equal(int64(0), cellCount) +} + +func (suite *DashboardSuggestionsHTTPSuite) TestGenerateRejectsEmptyResolvedScopeWithoutCreatingRun() { + sspID, _, _ := suite.seedScope(nil, nil) + + rec, req := suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/generate", sspID), nil) + suite.server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusUnprocessableEntity, rec.Code, rec.Body.String()) + suite.Contains(rec.Body.String(), "no controls resolved for dashboard suggestions") + suite.Equal(0, suite.enqueuer.calls) + + var runCount int64 + suite.Require().NoError(suite.DB.Model(&suggestionrel.DashboardSuggestionRun{}).Where("ssp_id = ?", sspID).Count(&runCount).Error) + suite.Equal(int64(0), runCount) + + var cellCount int64 + suite.Require().NoError(suite.DB.Model(&suggestionrel.DashboardSuggestionRunCell{}). + Joins("JOIN dashboard_suggestion_runs ON dashboard_suggestion_runs.id = dashboard_suggestion_run_cells.run_id"). + Where("dashboard_suggestion_runs.ssp_id = ?", sspID). + Count(&cellCount).Error) + suite.Equal(int64(0), cellCount) + + var profileIDRaw string + suite.Require().NoError(suite.DB.Raw(`SELECT profile_id FROM ssp_profiles WHERE system_security_plan_id = ? LIMIT 1`, sspID).Scan(&profileIDRaw).Error) + profileID, err := uuid.Parse(profileIDRaw) + suite.Require().NoError(err) + catalog := relational.Catalog{} + suite.Require().NoError(suite.DB.First(&catalog).Error) + suite.Require().NoError(suite.DB.Create(&relational.Control{CatalogID: *catalog.ID, ID: "AC-1", Title: "Control AC-1"}).Error) + suite.Require().NoError(suite.DB.Exec( + `INSERT INTO profile_controls (profile_id, control_catalog_id, control_id) VALUES (?, ?, ?)`, + profileID, catalog.ID, "AC-1", + ).Error) + evidence := relational.Evidence{ + UUID: uuid.New(), + Title: "evidence-valid-after-empty-scope", + Start: time.Now().UTC(), + End: time.Now().UTC(), + Status: datatypes.NewJSONType(oscalTypes_1_1_3.ObjectiveStatus{State: "satisfied"}), + Labels: []relational.Labels{ + {Name: "env", Value: "prod"}, + }, + } + suite.Require().NoError(suite.DB.Create(&evidence).Error) + + rec, req = suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/generate", sspID), nil) + suite.server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusAccepted, rec.Code, rec.Body.String()) + suite.Equal(1, suite.enqueuer.calls) +} + +func (suite *DashboardSuggestionsHTTPSuite) TestFlagOffDoesNotRegisterScopedRoutesAndConfigReportsFalse() { + disabledServer := suite.newServer(false, suite.enqueuer, 0) + sspID, _, _ := suite.seedScope([]string{"AC-1"}, []map[string]string{{"env": "prod"}}) + + rec, req := suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/generate", sspID), nil) + disabledServer.E().ServeHTTP(rec, req) + suite.Equal(http.StatusNotFound, rec.Code) + + rec = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, "/api/dashboard-suggestions/config", nil) + disabledServer.E().ServeHTTP(rec, req) + suite.Equal(http.StatusOK, rec.Code) + var response apihandler.GenericDataResponse[dashboardSuggestionConfigResponse] + suite.Require().NoError(json.Unmarshal(rec.Body.Bytes(), &response)) + suite.False(response.Data.Enabled) +} + +func (suite *DashboardSuggestionsHTTPSuite) TestAcceptCreatesSSPFilterAndWritesEvents() { + sspID, controlKeys, _ := suite.seedScope([]string{"AC-1", "AC-2"}, []map[string]string{{"env": "prod"}}) + runID := suite.seedSuggestionRun(sspID) + catalogID, _ := suite.parseControlKey(controlKeys[0]) + labels := map[string]string{"env": "prod"} + hash := suggestionrel.CanonicalLabelSetHash(labels) + first := suite.seedDashboardSuggestion(runID, sspID, catalogID, "AC-1", labels, hash, "prod evidence", 0.9) + second := suite.seedDashboardSuggestion(runID, sspID, catalogID, "AC-2", labels, hash, "prod evidence", 0.7) + + body := dashboardSuggestionDecisionRequest{IDs: []uuid.UUID{*first.ID, *second.ID}} + rec, req := suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/accept", sspID), body) + suite.server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusOK, rec.Code, rec.Body.String()) + + var response apihandler.GenericDataResponse[acceptDashboardSuggestionsResponse] + suite.Require().NoError(json.Unmarshal(rec.Body.Bytes(), &response)) + suite.Require().Len(response.Data.AcceptedFilterIDs, 1) + filterID := response.Data.AcceptedFilterIDs[0] + + var filter relational.Filter + suite.Require().NoError(suite.DB.First(&filter, "id = ? AND ssp_id = ?", filterID, sspID).Error) + suite.Equal("prod evidence", filter.Name) + + var linkCount int64 + suite.Require().NoError(suite.DB.Table("filter_controls").Where("filter_id = ?", filterID).Count(&linkCount).Error) + suite.Equal(int64(2), linkCount) + + var accepted []suggestionrel.DashboardSuggestion + suite.Require().NoError(suite.DB.Where("id IN ?", []uuid.UUID{*first.ID, *second.ID}).Find(&accepted).Error) + for _, suggestion := range accepted { + suite.Equal(suggestionrel.DashboardSuggestionStatusAccepted, suggestion.Status) + suite.Require().NotNil(suggestion.AcceptedFilterID) + suite.Equal(filterID, *suggestion.AcceptedFilterID) + } + + var eventCount int64 + suite.Require().NoError(suite.DB.Model(&suggestionrel.DashboardSuggestionEvent{}). + Where("event_type = ? AND suggestion_id IN ?", suggestionrel.DashboardSuggestionEventTypeAccepted, []uuid.UUID{*first.ID, *second.ID}). + Count(&eventCount).Error) + suite.Equal(int64(2), eventCount) +} + +func (suite *DashboardSuggestionsHTTPSuite) TestRejectPersistsDecisionAndWritesEvents() { + sspID, controlKeys, _ := suite.seedScope([]string{"AC-1"}, []map[string]string{{"env": "prod"}}) + runID := suite.seedSuggestionRun(sspID) + catalogID, _ := suite.parseControlKey(controlKeys[0]) + suggestion := suite.seedDashboardSuggestion(runID, sspID, catalogID, "AC-1", map[string]string{"env": "prod"}, suggestionrel.CanonicalLabelSetHash(map[string]string{"env": "prod"}), "prod", 0.8) + + body := dashboardSuggestionDecisionRequest{IDs: []uuid.UUID{*suggestion.ID}, Reason: "not applicable"} + rec, req := suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/reject", sspID), body) + suite.server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusOK, rec.Code, rec.Body.String()) + + var reloaded suggestionrel.DashboardSuggestion + suite.Require().NoError(suite.DB.First(&reloaded, "id = ?", suggestion.ID).Error) + suite.Equal(suggestionrel.DashboardSuggestionStatusRejected, reloaded.Status) + suite.Require().NotNil(reloaded.RejectReason) + suite.Equal("not applicable", *reloaded.RejectReason) + suite.Require().NotNil(reloaded.DecidedByUserID) + suite.Require().NotNil(reloaded.DecidedAt) + + var eventCount int64 + suite.Require().NoError(suite.DB.Model(&suggestionrel.DashboardSuggestionEvent{}). + Where("suggestion_id = ? AND event_type = ?", suggestion.ID, suggestionrel.DashboardSuggestionEventTypeRejected). + Count(&eventCount).Error) + suite.Equal(int64(1), eventCount) +} + +func (suite *DashboardSuggestionsHTTPSuite) TestListSuggestionsAndEventsScopeBySSP() { + sspID, controlKeys, _ := suite.seedScope([]string{"AC-1"}, []map[string]string{{"env": "prod"}}) + otherSSPID, _, _ := suite.seedScope([]string{"AC-9"}, []map[string]string{{"env": "stage"}}) + runID := suite.seedSuggestionRun(sspID) + otherRunID := suite.seedSuggestionRun(otherSSPID) + catalogID, _ := suite.parseControlKey(controlKeys[0]) + suggestion := suite.seedDashboardSuggestion(runID, sspID, catalogID, "AC-1", map[string]string{"env": "prod"}, suggestionrel.CanonicalLabelSetHash(map[string]string{"env": "prod"}), "prod", 0.8) + _ = suite.seedDashboardSuggestion(otherRunID, otherSSPID, catalogID, "AC-1", map[string]string{"env": "prod"}, suggestionrel.CanonicalLabelSetHash(map[string]string{"env": "prod"}), "hidden", 0.8) + suite.Require().NoError(suite.DB.Create(&suggestionrel.DashboardSuggestionEvent{ + RunID: &runID, + SuggestionID: suggestion.ID, + EventType: string(suggestionrel.DashboardSuggestionEventTypeSuggestionCreated), + OccurredAt: time.Now().UTC(), + Payload: datatypes.JSONMap{"source": "test"}, + Snapshot: datatypes.JSONMap{}, + }).Error) + + rec, req := suite.req(http.MethodGet, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions", sspID), nil) + suite.server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusOK, rec.Code, rec.Body.String()) + var listResponse apihandler.GenericDataListResponse[dashboardSuggestionResponse] + suite.Require().NoError(json.Unmarshal(rec.Body.Bytes(), &listResponse)) + suite.Require().Len(listResponse.Data, 1) + suite.Equal(*suggestion.ID, *listResponse.Data[0].ID) + suite.Equal("Control AC-1", listResponse.Data[0].ControlTitle) + + rec, req = suite.req(http.MethodGet, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/%s/events", sspID, suggestion.ID), nil) + suite.server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusOK, rec.Code, rec.Body.String()) + var eventsResponse apihandler.GenericDataListResponse[suggestionrel.DashboardSuggestionEvent] + suite.Require().NoError(json.Unmarshal(rec.Body.Bytes(), &eventsResponse)) + suite.Require().Len(eventsResponse.Data, 1) + suite.Equal(string(suggestionrel.DashboardSuggestionEventTypeSuggestionCreated), eventsResponse.Data[0].EventType) + + rec, req = suite.req(http.MethodGet, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/%s/events", otherSSPID, suggestion.ID), nil) + suite.server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusNotFound, rec.Code, rec.Body.String()) +} + +func (suite *DashboardSuggestionsHTTPSuite) TestGenerateSupersedesOnlyPendingSuggestionsInScope() { + sspID, controlKeys, hashes := suite.seedScope([]string{"AC-1", "AC-2"}, []map[string]string{{"env": "prod"}, {"env": "stage"}}) + runID := suite.seedSuggestionRun(sspID) + ac1CatalogID, _ := suite.parseControlKey(controlKeys[0]) + ac2CatalogID, _ := suite.parseControlKey(controlKeys[1]) + prodHash := hashes[0] + stageHash := hashes[1] + + inScope := suite.seedDashboardSuggestion(runID, sspID, ac1CatalogID, "AC-1", map[string]string{"env": "prod"}, prodHash, "in scope", 0.9) + outByControl := suite.seedDashboardSuggestion(runID, sspID, ac2CatalogID, "AC-2", map[string]string{"env": "prod"}, prodHash, "out control", 0.8) + outByLabel := suite.seedDashboardSuggestion(runID, sspID, ac1CatalogID, "AC-1", map[string]string{"env": "stage"}, stageHash, "out label", 0.7) + + body := generateDashboardSuggestionsRequest{ + SupersedePending: true, + Scope: &dashboardSuggestionScopeRequest{ + ControlKeys: []string{controlKeys[0]}, + LabelSetHashes: []string{prodHash}, + }, + } + rec, req := suite.req(http.MethodPost, fmt.Sprintf("/api/oscal/system-security-plans/%s/dashboard-suggestions/generate", sspID), body) + suite.server.E().ServeHTTP(rec, req) + suite.Equal(http.StatusAccepted, rec.Code, rec.Body.String()) + + statuses := map[uuid.UUID]string{} + var suggestions []suggestionrel.DashboardSuggestion + suite.Require().NoError(suite.DB.Where("id IN ?", []uuid.UUID{*inScope.ID, *outByControl.ID, *outByLabel.ID}).Find(&suggestions).Error) + for _, suggestion := range suggestions { + statuses[*suggestion.ID] = suggestion.Status + } + suite.Equal(suggestionrel.DashboardSuggestionStatusSuperseded, statuses[*inScope.ID]) + suite.Equal(suggestionrel.DashboardSuggestionStatusPending, statuses[*outByControl.ID]) + suite.Equal(suggestionrel.DashboardSuggestionStatusPending, statuses[*outByLabel.ID]) + + var eventCount int64 + suite.Require().NoError(suite.DB.Model(&suggestionrel.DashboardSuggestionEvent{}). + Where("suggestion_id = ? AND event_type = ?", inScope.ID, suggestionrel.DashboardSuggestionEventTypeSuperseded). + Count(&eventCount).Error) + suite.Equal(int64(1), eventCount) +} + +func stringSliceFromJSONValue(value any) []string { + raw, ok := value.([]any) + if !ok { + return nil + } + out := make([]string, 0, len(raw)) + for _, item := range raw { + if value, ok := item.(string); ok { + out = append(out, value) + } + } + return out +} + +func (suite *DashboardSuggestionsHTTPSuite) seedSuggestionRun(sspID uuid.UUID) uuid.UUID { + runID := uuid.New() + suite.Require().NoError(suite.DB.Create(&suggestionrel.DashboardSuggestionRun{ + UUIDModel: relational.UUIDModel{ID: &runID}, + SSPID: sspID, + Status: "completed", + Model: "test-model", + PromptVersion: suggestionrel.PromptVersion, + Scope: datatypes.JSONMap{"controlKeys": []string{}, "labelSetHashes": []string{}}, + PlannedCalls: 1, + Stats: datatypes.JSONMap{}, + }).Error) + return runID +} + +func (suite *DashboardSuggestionsHTTPSuite) seedDashboardSuggestion( + runID uuid.UUID, + sspID uuid.UUID, + catalogID uuid.UUID, + controlID string, + labels map[string]string, + hash string, + name string, + confidence float64, +) suggestionrel.DashboardSuggestion { + suggestion := suggestionrel.DashboardSuggestion{ + RunID: runID, + SSPID: sspID, + ControlCatalogID: catalogID, + ControlID: controlID, + LabelSet: datatypes.JSONMap{}, + LabelSetHash: hash, + ProposedFilterName: name, + Reasoning: "test reasoning", + Confidence: confidence, + Status: suggestionrel.DashboardSuggestionStatusPending, + } + for key, value := range labels { + suggestion.LabelSet[key] = value + } + suite.Require().NoError(suite.DB.Create(&suggestion).Error) + return suggestion +} + +func (suite *DashboardSuggestionsHTTPSuite) parseControlKey(key string) (uuid.UUID, string) { + catalogID, controlID, err := suggestionrel.ParseControlKey(key) + suite.Require().NoError(err) + return catalogID, controlID +} diff --git a/internal/api/handler/oscal/profile_compliance.go b/internal/api/handler/oscal/profile_compliance.go index 08b43075..132c8df3 100644 --- a/internal/api/handler/oscal/profile_compliance.go +++ b/internal/api/handler/oscal/profile_compliance.go @@ -163,20 +163,16 @@ func (h *ProfileHandler) ComplianceProgress(ctx echo.Context) error { return ctx.JSON(http.StatusOK, handler.GenericDataResponse[ProfileComplianceProgress]{Data: response}) } - filtersByControl, err := h.loadFiltersByControl(scopeControls) - if err != nil { - h.sugar.Errorw("failed to load filters for profile controls", "profileID", id, "error", err) - return ctx.JSON(http.StatusInternalServerError, api.NewError(err)) - } - sspImplementedControls := map[string]struct{}{} hasImplementationScope := false + var sspIDForFilters *uuid.UUID sspIDParam := strings.TrimSpace(ctx.QueryParam("sspId")) if sspIDParam != "" { sspID, parseErr := uuid.Parse(sspIDParam) if parseErr != nil { return ctx.JSON(http.StatusBadRequest, api.NewError(parseErr)) } + sspIDForFilters = &sspID sspImplementedControls, err = h.loadImplementedControlsForSSP(sspID) if err != nil { @@ -189,6 +185,12 @@ func (h *ProfileHandler) ComplianceProgress(ctx echo.Context) error { hasImplementationScope = true } + filtersByControl, err := h.loadFiltersByControl(scopeControls, sspIDForFilters) + if err != nil { + h.sugar.Errorw("failed to load filters for profile controls", "profileID", id, "error", err) + return ctx.JSON(http.StatusInternalServerError, api.NewError(err)) + } + groups := map[string]*profileComplianceGroupAccumulator{} controls := make([]ProfileComplianceControl, 0, len(scopeControls)) @@ -383,13 +385,18 @@ func flattenProfileComplianceControls(catalog *relational.Catalog) []profileComp return flattened } -func (h *ProfileHandler) loadFiltersByControl(scopeControls []profileComplianceControlScope) (map[profileControlKey][]labelfilter.Filter, error) { +func (h *ProfileHandler) loadFiltersByControl(scopeControls []profileComplianceControlScope, sspID *uuid.UUID) (map[profileControlKey][]labelfilter.Filter, error) { filtersByControl := make(map[profileControlKey][]labelfilter.Filter, len(scopeControls)) if len(scopeControls) == 0 { return filtersByControl, nil } - query := h.db.Model(&relational.Control{}).Preload("Filters") + query := h.db.Model(&relational.Control{}) + if sspID != nil { + query = query.Preload("Filters", "ssp_id IS NULL OR ssp_id = ?", *sspID) + } else { + query = query.Preload("Filters") + } for idx, scopeControl := range scopeControls { condition := h.db.Where("catalog_id = ? AND id = ?", scopeControl.CatalogID, scopeControl.ControlID) if idx == 0 { @@ -430,7 +437,7 @@ func (h *ProfileHandler) getStatusCountsForFilters(filters []labelfilter.Filter) rows := []ProfileComplianceStatusCount{} if err := query.Model(&relational.Evidence{}). - Select("count(*) as count, status->>'state' as status"). + Select("count(DISTINCT uuid) as count, status->>'state' as status"). Group("status->>'state'"). Scan(&rows).Error; err != nil { return nil, err diff --git a/internal/api/handler/request.go b/internal/api/handler/request.go index 908333e5..8ee399fd 100644 --- a/internal/api/handler/request.go +++ b/internal/api/handler/request.go @@ -5,6 +5,7 @@ import ( "time" "github.com/compliance-framework/api/internal/converters/labelfilter" + "github.com/google/uuid" "github.com/labstack/echo/v4" ) @@ -27,7 +28,9 @@ func ParseIntervalListQueryParam(intervalQuery string, def []time.Duration) ([]t // createFilterRequest defines the request payload for method Create type createFilterRequest struct { - Name string `json:"name" yaml:"name" validate:"required"` + Name string `json:"name" yaml:"name" validate:"required"` + // System Security Plan ID. On PUT, omitted or null clears the binding to global. + SSPID *uuid.UUID `json:"sspId" yaml:"ssp_id" extensions:"x-nullable" example:"00000000-0000-0000-0000-000000000000" swaggertype:"string" format:"uuid"` Filter labelfilter.Filter `json:"filter" yaml:"filter" validate:"required"` Controls *[]string `json:"controls" yaml:"controls"` Components *[]string `json:"components" yaml:"components"` diff --git a/internal/service/relational/evidence/search_integration_test.go b/internal/service/relational/evidence/search_integration_test.go index 75a9866d..05bacd10 100644 --- a/internal/service/relational/evidence/search_integration_test.go +++ b/internal/service/relational/evidence/search_integration_test.go @@ -242,6 +242,36 @@ func (suite *EvidenceServiceSearchIntegrationSuite) TestStatusCountsReturnEmptyS suite.Empty(rows) } +func (suite *EvidenceServiceSearchIntegrationSuite) TestStatusCountsByFiltersDedupesOverlappingEvidenceStreams() { + suite.Require().NoError(suite.Migrator.Refresh()) + + now := time.Now().UTC() + evidence := relational.Evidence{ + UUID: uuid.New(), + Title: "overlapping evidence", + Start: now.Add(-time.Minute), + End: now, + Status: datatypes.NewJSONType(oscalTypes_1_1_3.ObjectiveStatus{ + State: "satisfied", + }), + Labels: []relational.Labels{ + {Name: "env", Value: "prod"}, + {Name: "tier", Value: "app"}, + }, + } + suite.Require().NoError(suite.DB.Create(&evidence).Error) + + svc := NewEvidenceService(suite.DB, nil, nil, nil) + rows, err := svc.GetStatusCountsByFilters( + labelfilter.Filter{Scope: &labelfilter.Scope{Condition: &labelfilter.Condition{Label: "env", Operator: "=", Value: "prod"}}}, + labelfilter.Filter{Scope: &labelfilter.Scope{Condition: &labelfilter.Condition{Label: "tier", Operator: "=", Value: "app"}}}, + ) + suite.Require().NoError(err) + suite.Require().Len(rows, 1) + suite.Equal("satisfied", rows[0].Status) + suite.Equal(int64(1), rows[0].Count) +} + func relationalEvidenceTitles(items []relational.Evidence) []string { titles := make([]string, 0, len(items)) for _, item := range items { diff --git a/internal/service/relational/evidence/service.go b/internal/service/relational/evidence/service.go index 03316ce5..bfdc605a 100644 --- a/internal/service/relational/evidence/service.go +++ b/internal/service/relational/evidence/service.go @@ -431,7 +431,7 @@ func (s *EvidenceService) GetStatusCountsAtPoint(filter labelfilter.Filter, endB } var rows []StatusCount if err := q.Model(&relational.Evidence{}). - Select("count(*) as count, status->>'state' as status"). + Select("count(DISTINCT uuid) as count, status->>'state' as status"). Group("status->>'state'"). Scan(&rows).Error; err != nil { return nil, err diff --git a/internal/service/relational/suggestions/core.go b/internal/service/relational/suggestions/core.go index c7588324..d491e819 100644 --- a/internal/service/relational/suggestions/core.go +++ b/internal/service/relational/suggestions/core.go @@ -12,15 +12,16 @@ import ( ) const ( - DefaultMaxControlsPerChunk = 40 - DefaultMaxLabelSetsPerChunk = 200 - DefaultMaxSuggestionsPerRun = 500 - MaxMappingsPerControlPerCell = 10 - MaxReasoningLength = 2000 - ReasoningTruncatedMarker = "\n[truncated]" - DashboardSuggestionStatusPending = "pending" - DashboardSuggestionStatusAccepted = "accepted" - DashboardSuggestionStatusRejected = "rejected" + DefaultMaxControlsPerChunk = 40 + DefaultMaxLabelSetsPerChunk = 200 + DefaultMaxSuggestionsPerRun = 500 + MaxMappingsPerControlPerCell = 10 + MaxReasoningLength = 2000 + ReasoningTruncatedMarker = "\n[truncated]" + DashboardSuggestionStatusPending = "pending" + DashboardSuggestionStatusAccepted = "accepted" + DashboardSuggestionStatusRejected = "rejected" + DashboardSuggestionStatusSuperseded = "superseded" ) type ChunkConfig struct { diff --git a/internal/service/relational/suggestions/service.go b/internal/service/relational/suggestions/service.go index e0ea8678..231b30c6 100644 --- a/internal/service/relational/suggestions/service.go +++ b/internal/service/relational/suggestions/service.go @@ -91,6 +91,13 @@ func (s *SuggestionService) ResolveScope(sspID uuid.UUID, scope Scope) (Snapshot return ResolveSnapshot(scope, controls, labelSets) } +func (s *SuggestionService) GatherLabelSets(hashes []string) ([]LabelSetInput, error) { + if len(hashes) == 0 { + return s.gatherAllLabelSets() + } + return s.gatherLabelSets(hashes) +} + func (s *SuggestionService) GatherCellInput(sspID uuid.UUID, cell GridCell, opts GatherOptions) (GatheredInput, error) { stats := map[string]int{} controls, err := s.gatherControls(sspID, cell.ControlKeys, opts) diff --git a/internal/service/relational/system_component_suggestions.go b/internal/service/relational/system_component_suggestions.go index 577d3a20..2ead30fc 100644 --- a/internal/service/relational/system_component_suggestions.go +++ b/internal/service/relational/system_component_suggestions.go @@ -101,6 +101,7 @@ func (s *SystemComponentSuggestionService) suggestForParent( filterQuery := s.db. Joins("JOIN filter_controls ON filter_controls.filter_id = filters.id"). + Where("filters.ssp_id IS NULL OR filters.ssp_id = ?", sspID). // Normalize on upper case. // We don't need to concern about index hits as for now - these tables will not grow // on a typical CCF usage. diff --git a/internal/service/worker/dashboard_suggestion_job_types.go b/internal/service/worker/dashboard_suggestion_job_types.go index 1350d411..d8c9d4b8 100644 --- a/internal/service/worker/dashboard_suggestion_job_types.go +++ b/internal/service/worker/dashboard_suggestion_job_types.go @@ -14,7 +14,10 @@ const ( DashboardSuggestionMaxAttempts = 3 ) -var ErrDashboardSuggestionWorkerDisabled = errors.New("dashboard suggestion worker is disabled") +var ( + ErrDashboardSuggestionWorkerDisabled = errors.New("dashboard suggestion worker is disabled") + ErrDashboardSuggestionWorkerNotRegistered = errors.New("dashboard suggestion worker is not registered") +) type DashboardSuggestionCellArgs struct { RunID uuid.UUID `json:"run_id" river:"unique"` diff --git a/internal/service/worker/service.go b/internal/service/worker/service.go index e1103d97..79396bd9 100644 --- a/internal/service/worker/service.go +++ b/internal/service/worker/service.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "os" + "strings" "sync" "time" @@ -1128,6 +1129,10 @@ func (s *Service) EnqueueDashboardSuggestionCells(ctx context.Context, runID uui if errors.Is(err, ErrDashboardSuggestionWorkerDisabled) { return err } + if errors.Is(err, ErrDashboardSuggestionWorkerNotRegistered) || + strings.Contains(strings.ToLower(err.Error()), "worker is not registered") { + return ErrDashboardSuggestionWorkerNotRegistered + } return fmt.Errorf("failed to enqueue dashboard suggestion cell jobs for run %s: %w", runID, err) } return nil