diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 098ea767e4..15b52df7bf 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1182,6 +1182,84 @@ const docTemplate = `{ } } }, + "/integrations/jfrog/xray-scan": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Get JFrog XRay scan by workspace agent ID.", + "operationId": "get-jfrog-xray-scan-by-workspace-agent-id", + "parameters": [ + { + "type": "string", + "description": "Workspace ID", + "name": "workspace_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Agent ID", + "name": "agent_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.JFrogXrayScan" + } + } + } + }, + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Post JFrog XRay scan by workspace agent ID.", + "operationId": "post-jfrog-xray-scan-by-workspace-agent-id", + "parameters": [ + { + "description": "Post JFrog XRay scan request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.JFrogXrayScan" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.Response" + } + } + } + } + }, "/licenses": { "get": { "security": [ @@ -9581,6 +9659,31 @@ const docTemplate = `{ } } }, + "codersdk.JFrogXrayScan": { + "type": "object", + "properties": { + "agent_id": { + "type": "string", + "format": "uuid" + }, + "critical": { + "type": "integer" + }, + "high": { + "type": "integer" + }, + "medium": { + "type": "integer" + }, + "results_url": { + "type": "string" + }, + "workspace_id": { + "type": "string", + "format": "uuid" + } + } + }, "codersdk.JobErrorCode": { "type": "string", "enum": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 24bc5e29cc..e8872f103d 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1018,6 +1018,74 @@ } } }, + "/integrations/jfrog/xray-scan": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Get JFrog XRay scan by workspace agent ID.", + "operationId": "get-jfrog-xray-scan-by-workspace-agent-id", + "parameters": [ + { + "type": "string", + "description": "Workspace ID", + "name": "workspace_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Agent ID", + "name": "agent_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.JFrogXrayScan" + } + } + } + }, + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Post JFrog XRay scan by workspace agent ID.", + "operationId": "post-jfrog-xray-scan-by-workspace-agent-id", + "parameters": [ + { + "description": "Post JFrog XRay scan request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.JFrogXrayScan" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.Response" + } + } + } + } + }, "/licenses": { "get": { "security": [ @@ -8607,6 +8675,31 @@ } } }, + "codersdk.JFrogXrayScan": { + "type": "object", + "properties": { + "agent_id": { + "type": "string", + "format": "uuid" + }, + "critical": { + "type": "integer" + }, + "high": { + "type": "integer" + }, + "medium": { + "type": "integer" + }, + "results_url": { + "type": "string" + }, + "workspace_id": { + "type": "string", + "format": "uuid" + } + } + }, "codersdk.JobErrorCode": { "type": "string", "enum": ["REQUIRED_TEMPLATE_VARIABLES"], diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 97743186f1..257e12ebf4 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1111,6 +1111,13 @@ func (q *querier) GetHungProvisionerJobs(ctx context.Context, hungSince time.Tim return q.db.GetHungProvisionerJobs(ctx, hungSince) } +func (q *querier) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg database.GetJFrogXrayScanByWorkspaceAndAgentIDParams) (database.JfrogXrayScan, error) { + if _, err := fetch(q.log, q.auth, q.db.GetWorkspaceByID)(ctx, arg.WorkspaceID); err != nil { + return database.JfrogXrayScan{}, err + } + return q.db.GetJFrogXrayScanByWorkspaceAndAgentID(ctx, arg) +} + func (q *querier) GetLastUpdateCheck(ctx context.Context) (string, error) { if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil { return "", err @@ -3153,6 +3160,27 @@ func (q *querier) UpsertHealthSettings(ctx context.Context, value string) error return q.db.UpsertHealthSettings(ctx, value) } +func (q *querier) UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg database.UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error { + // TODO: Having to do all this extra querying makes me a sad panda. + workspace, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID) + if err != nil { + return xerrors.Errorf("get workspace by id: %w", err) + } + + template, err := q.db.GetTemplateByID(ctx, workspace.TemplateID) + if err != nil { + return xerrors.Errorf("get template by id: %w", err) + } + + // Only template admins should be able to write JFrog Xray scans to a workspace. + // We don't want this to be a workspace-level permission because then users + // could overwrite their own results. + if err := q.authorizeContext(ctx, rbac.ActionCreate, template); err != nil { + return err + } + return q.db.UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx, arg) +} + func (q *querier) UpsertLastUpdateCheck(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceSystem); err != nil { return err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 9668497dbf..34b3c7ddc0 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -364,7 +364,7 @@ func (s *MethodTestSuite) TestGroup() { })) } -func (s *MethodTestSuite) TestProvsionerJob() { +func (s *MethodTestSuite) TestProvisionerJob() { s.Run("ArchiveUnusedTemplateVersions", s.Subtest(func(db database.Store, check *expects) { j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ Type: database.ProvisionerJobTypeTemplateVersionImport, @@ -2216,6 +2216,44 @@ func (s *MethodTestSuite) TestSystemFunctions() { s.Run("GetUserLinksByUserID", s.Subtest(func(db database.Store, check *expects) { check.Args(uuid.New()).Asserts(rbac.ResourceSystem, rbac.ActionRead) })) + s.Run("GetJFrogXrayScanByWorkspaceAndAgentID", s.Subtest(func(db database.Store, check *expects) { + ws := dbgen.Workspace(s.T(), db, database.Workspace{}) + agent := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{}) + + err := db.UpsertJFrogXrayScanByWorkspaceAndAgentID(context.Background(), database.UpsertJFrogXrayScanByWorkspaceAndAgentIDParams{ + AgentID: agent.ID, + WorkspaceID: ws.ID, + Critical: 1, + High: 12, + Medium: 14, + ResultsUrl: "http://hello", + }) + require.NoError(s.T(), err) + + expect := database.JfrogXrayScan{ + WorkspaceID: ws.ID, + AgentID: agent.ID, + Critical: 1, + High: 12, + Medium: 14, + ResultsUrl: "http://hello", + } + + check.Args(database.GetJFrogXrayScanByWorkspaceAndAgentIDParams{ + WorkspaceID: ws.ID, + AgentID: agent.ID, + }).Asserts(ws, rbac.ActionRead).Returns(expect) + })) + s.Run("UpsertJFrogXrayScanByWorkspaceAndAgentID", s.Subtest(func(db database.Store, check *expects) { + tpl := dbgen.Template(s.T(), db, database.Template{}) + ws := dbgen.Workspace(s.T(), db, database.Workspace{ + TemplateID: tpl.ID, + }) + check.Args(database.UpsertJFrogXrayScanByWorkspaceAndAgentIDParams{ + WorkspaceID: ws.ID, + AgentID: uuid.New(), + }).Asserts(tpl, rbac.ActionCreate) + })) } func (s *MethodTestSuite) TestOAuth2ProviderApps() { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index fa07cf01da..80c80547f4 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -129,6 +129,7 @@ type data struct { gitSSHKey []database.GitSSHKey groupMembers []database.GroupMember groups []database.Group + jfrogXRayScans []database.JfrogXrayScan licenses []database.License oauth2ProviderApps []database.OAuth2ProviderApp oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret @@ -1986,6 +1987,24 @@ func (q *FakeQuerier) GetHungProvisionerJobs(_ context.Context, hungSince time.T return hungJobs, nil } +func (q *FakeQuerier) GetJFrogXrayScanByWorkspaceAndAgentID(_ context.Context, arg database.GetJFrogXrayScanByWorkspaceAndAgentIDParams) (database.JfrogXrayScan, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.JfrogXrayScan{}, err + } + + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, scan := range q.jfrogXRayScans { + if scan.AgentID == arg.AgentID && scan.WorkspaceID == arg.WorkspaceID { + return scan, nil + } + } + + return database.JfrogXrayScan{}, sql.ErrNoRows +} + func (q *FakeQuerier) GetLastUpdateCheck(_ context.Context) (string, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -7292,6 +7311,39 @@ func (q *FakeQuerier) UpsertHealthSettings(_ context.Context, data string) error return nil } +func (q *FakeQuerier) UpsertJFrogXrayScanByWorkspaceAndAgentID(_ context.Context, arg database.UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, scan := range q.jfrogXRayScans { + if scan.AgentID == arg.AgentID && scan.WorkspaceID == arg.WorkspaceID { + scan.Critical = arg.Critical + scan.High = arg.High + scan.Medium = arg.Medium + scan.ResultsUrl = arg.ResultsUrl + q.jfrogXRayScans[i] = scan + return nil + } + } + + //nolint:gosimple + q.jfrogXRayScans = append(q.jfrogXRayScans, database.JfrogXrayScan{ + WorkspaceID: arg.WorkspaceID, + AgentID: arg.AgentID, + Critical: arg.Critical, + High: arg.High, + Medium: arg.Medium, + ResultsUrl: arg.ResultsUrl, + }) + + return nil +} + func (q *FakeQuerier) UpsertLastUpdateCheck(_ context.Context, data string) error { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 15c7492fb9..d53bc484ef 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -545,6 +545,13 @@ func (m metricsStore) GetHungProvisionerJobs(ctx context.Context, hungSince time return jobs, err } +func (m metricsStore) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg database.GetJFrogXrayScanByWorkspaceAndAgentIDParams) (database.JfrogXrayScan, error) { + start := time.Now() + r0, r1 := m.s.GetJFrogXrayScanByWorkspaceAndAgentID(ctx, arg) + m.queryLatencies.WithLabelValues("GetJFrogXrayScanByWorkspaceAndAgentID").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) GetLastUpdateCheck(ctx context.Context) (string, error) { start := time.Now() version, err := m.s.GetLastUpdateCheck(ctx) @@ -2027,6 +2034,13 @@ func (m metricsStore) UpsertHealthSettings(ctx context.Context, value string) er return r0 } +func (m metricsStore) UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg database.UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error { + start := time.Now() + r0 := m.s.UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx, arg) + m.queryLatencies.WithLabelValues("UpsertJFrogXrayScanByWorkspaceAndAgentID").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) UpsertLastUpdateCheck(ctx context.Context, value string) error { start := time.Now() r0 := m.s.UpsertLastUpdateCheck(ctx, value) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 2b1c864b5a..ebc171a09e 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1069,6 +1069,21 @@ func (mr *MockStoreMockRecorder) GetHungProvisionerJobs(arg0, arg1 any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHungProvisionerJobs", reflect.TypeOf((*MockStore)(nil).GetHungProvisionerJobs), arg0, arg1) } +// GetJFrogXrayScanByWorkspaceAndAgentID mocks base method. +func (m *MockStore) GetJFrogXrayScanByWorkspaceAndAgentID(arg0 context.Context, arg1 database.GetJFrogXrayScanByWorkspaceAndAgentIDParams) (database.JfrogXrayScan, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetJFrogXrayScanByWorkspaceAndAgentID", arg0, arg1) + ret0, _ := ret[0].(database.JfrogXrayScan) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetJFrogXrayScanByWorkspaceAndAgentID indicates an expected call of GetJFrogXrayScanByWorkspaceAndAgentID. +func (mr *MockStoreMockRecorder) GetJFrogXrayScanByWorkspaceAndAgentID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetJFrogXrayScanByWorkspaceAndAgentID", reflect.TypeOf((*MockStore)(nil).GetJFrogXrayScanByWorkspaceAndAgentID), arg0, arg1) +} + // GetLastUpdateCheck mocks base method. func (m *MockStore) GetLastUpdateCheck(arg0 context.Context) (string, error) { m.ctrl.T.Helper() @@ -4256,6 +4271,20 @@ func (mr *MockStoreMockRecorder) UpsertHealthSettings(arg0, arg1 any) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertHealthSettings", reflect.TypeOf((*MockStore)(nil).UpsertHealthSettings), arg0, arg1) } +// UpsertJFrogXrayScanByWorkspaceAndAgentID mocks base method. +func (m *MockStore) UpsertJFrogXrayScanByWorkspaceAndAgentID(arg0 context.Context, arg1 database.UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertJFrogXrayScanByWorkspaceAndAgentID", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertJFrogXrayScanByWorkspaceAndAgentID indicates an expected call of UpsertJFrogXrayScanByWorkspaceAndAgentID. +func (mr *MockStoreMockRecorder) UpsertJFrogXrayScanByWorkspaceAndAgentID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertJFrogXrayScanByWorkspaceAndAgentID", reflect.TypeOf((*MockStore)(nil).UpsertJFrogXrayScanByWorkspaceAndAgentID), arg0, arg1) +} + // UpsertLastUpdateCheck mocks base method. func (m *MockStore) UpsertLastUpdateCheck(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index d13d615f9f..cc6f3c59b3 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -438,6 +438,15 @@ COMMENT ON COLUMN groups.display_name IS 'Display name is a custom, human-friend COMMENT ON COLUMN groups.source IS 'Source indicates how the group was created. It can be created by a user manually, or through some system process like OIDC group sync.'; +CREATE TABLE jfrog_xray_scans ( + agent_id uuid NOT NULL, + workspace_id uuid NOT NULL, + critical integer DEFAULT 0 NOT NULL, + high integer DEFAULT 0 NOT NULL, + medium integer DEFAULT 0 NOT NULL, + results_url text DEFAULT ''::text NOT NULL +); + CREATE TABLE licenses ( id integer NOT NULL, uploaded_at timestamp with time zone NOT NULL, @@ -1292,6 +1301,9 @@ ALTER TABLE ONLY groups ALTER TABLE ONLY groups ADD CONSTRAINT groups_pkey PRIMARY KEY (id); +ALTER TABLE ONLY jfrog_xray_scans + ADD CONSTRAINT jfrog_xray_scans_pkey PRIMARY KEY (agent_id, workspace_id); + ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt); @@ -1536,6 +1548,12 @@ ALTER TABLE ONLY group_members ALTER TABLE ONLY groups ADD CONSTRAINT groups_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; +ALTER TABLE ONLY jfrog_xray_scans + ADD CONSTRAINT jfrog_xray_scans_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + +ALTER TABLE ONLY jfrog_xray_scans + ADD CONSTRAINT jfrog_xray_scans_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 5dc75af93f..f5ecbe0d15 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -13,6 +13,8 @@ const ( ForeignKeyGroupMembersGroupID ForeignKeyConstraint = "group_members_group_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE; ForeignKeyGroupMembersUserID ForeignKeyConstraint = "group_members_user_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ForeignKeyGroupsOrganizationID ForeignKeyConstraint = "groups_organization_id_fkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyJfrogXrayScansAgentID ForeignKeyConstraint = "jfrog_xray_scans_agent_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyJfrogXrayScansWorkspaceID ForeignKeyConstraint = "jfrog_xray_scans_workspace_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; ForeignKeyOauth2ProviderAppSecretsAppID ForeignKeyConstraint = "oauth2_provider_app_secrets_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; ForeignKeyOrganizationMembersOrganizationIDUUID ForeignKeyConstraint = "organization_members_organization_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_organization_id_uuid_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; ForeignKeyOrganizationMembersUserIDUUID ForeignKeyConstraint = "organization_members_user_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000187_jfrog_xray.down.sql b/coderd/database/migrations/000187_jfrog_xray.down.sql new file mode 100644 index 0000000000..8fa8f99f47 --- /dev/null +++ b/coderd/database/migrations/000187_jfrog_xray.down.sql @@ -0,0 +1 @@ +DROP TABLE jfrog_xray_scans; diff --git a/coderd/database/migrations/000187_jfrog_xray.up.sql b/coderd/database/migrations/000187_jfrog_xray.up.sql new file mode 100644 index 0000000000..8143dac49d --- /dev/null +++ b/coderd/database/migrations/000187_jfrog_xray.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE jfrog_xray_scans ( + agent_id uuid NOT NULL REFERENCES workspace_agents(id) ON DELETE CASCADE, + workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + critical integer NOT NULL DEFAULT 0, + high integer NOT NULL DEFAULT 0, + medium integer NOT NULL DEFAULT 0, + results_url text NOT NULL DEFAULT '', + PRIMARY KEY (agent_id, workspace_id) +); diff --git a/coderd/database/migrations/testdata/fixtures/000187_jfrog_xray.up.sql b/coderd/database/migrations/testdata/fixtures/000187_jfrog_xray.up.sql new file mode 100644 index 0000000000..3dc664242c --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000187_jfrog_xray.up.sql @@ -0,0 +1,11 @@ +INSERT INTO jfrog_xray_scans + (workspace_id, agent_id, critical, high, medium, results_url) +VALUES ( + 'b90547be-8870-4d68-8184-e8b2242b7c01', + '8fa17bbd-c48c-44c7-91ae-d4acbc755fad', + 10, + 5, + 2, + 'https://hello-world' +); + diff --git a/coderd/database/models.go b/coderd/database/models.go index e0c310b2a7..dd12b08682 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1779,6 +1779,15 @@ type GroupMember struct { GroupID uuid.UUID `db:"group_id" json:"group_id"` } +type JfrogXrayScan struct { + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + Critical int32 `db:"critical" json:"critical"` + High int32 `db:"high" json:"high"` + Medium int32 `db:"medium" json:"medium"` + ResultsUrl string `db:"results_url" json:"results_url"` +} + type License struct { ID int32 `db:"id" json:"id"` UploadedAt time.Time `db:"uploaded_at" json:"uploaded_at"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 7996e3ca22..dc53934227 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -119,6 +119,7 @@ type sqlcQuerier interface { GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]Group, error) GetHealthSettings(ctx context.Context) (string, error) GetHungProvisionerJobs(ctx context.Context, updatedAt time.Time) ([]ProvisionerJob, error) + GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg GetJFrogXrayScanByWorkspaceAndAgentIDParams) (JfrogXrayScan, error) GetLastUpdateCheck(ctx context.Context) (string, error) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) @@ -384,6 +385,7 @@ type sqlcQuerier interface { // The functional values are immutable and controlled implicitly. UpsertDefaultProxy(ctx context.Context, arg UpsertDefaultProxyParams) error UpsertHealthSettings(ctx context.Context, value string) error + UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error UpsertLastUpdateCheck(ctx context.Context, value string) error UpsertLogoURL(ctx context.Context, value string) error UpsertOAuthSigningKey(ctx context.Context, value string) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 55ee373540..e3725ebdf5 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2438,6 +2438,75 @@ func (q *sqlQuerier) GetUserLatencyInsights(ctx context.Context, arg GetUserLate return items, nil } +const getJFrogXrayScanByWorkspaceAndAgentID = `-- name: GetJFrogXrayScanByWorkspaceAndAgentID :one +SELECT + agent_id, workspace_id, critical, high, medium, results_url +FROM + jfrog_xray_scans +WHERE + agent_id = $1 +AND + workspace_id = $2 +LIMIT + 1 +` + +type GetJFrogXrayScanByWorkspaceAndAgentIDParams struct { + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` +} + +func (q *sqlQuerier) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg GetJFrogXrayScanByWorkspaceAndAgentIDParams) (JfrogXrayScan, error) { + row := q.db.QueryRowContext(ctx, getJFrogXrayScanByWorkspaceAndAgentID, arg.AgentID, arg.WorkspaceID) + var i JfrogXrayScan + err := row.Scan( + &i.AgentID, + &i.WorkspaceID, + &i.Critical, + &i.High, + &i.Medium, + &i.ResultsUrl, + ) + return i, err +} + +const upsertJFrogXrayScanByWorkspaceAndAgentID = `-- name: UpsertJFrogXrayScanByWorkspaceAndAgentID :exec +INSERT INTO + jfrog_xray_scans ( + agent_id, + workspace_id, + critical, + high, + medium, + results_url + ) +VALUES + ($1, $2, $3, $4, $5, $6) +ON CONFLICT (agent_id, workspace_id) +DO UPDATE SET critical = $3, high = $4, medium = $5, results_url = $6 +` + +type UpsertJFrogXrayScanByWorkspaceAndAgentIDParams struct { + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + Critical int32 `db:"critical" json:"critical"` + High int32 `db:"high" json:"high"` + Medium int32 `db:"medium" json:"medium"` + ResultsUrl string `db:"results_url" json:"results_url"` +} + +func (q *sqlQuerier) UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error { + _, err := q.db.ExecContext(ctx, upsertJFrogXrayScanByWorkspaceAndAgentID, + arg.AgentID, + arg.WorkspaceID, + arg.Critical, + arg.High, + arg.Medium, + arg.ResultsUrl, + ) + return err +} + const deleteLicense = `-- name: DeleteLicense :one DELETE FROM licenses diff --git a/coderd/database/queries/jfrog.sql b/coderd/database/queries/jfrog.sql new file mode 100644 index 0000000000..de9467c532 --- /dev/null +++ b/coderd/database/queries/jfrog.sql @@ -0,0 +1,26 @@ +-- name: GetJFrogXrayScanByWorkspaceAndAgentID :one +SELECT + * +FROM + jfrog_xray_scans +WHERE + agent_id = $1 +AND + workspace_id = $2 +LIMIT + 1; + +-- name: UpsertJFrogXrayScanByWorkspaceAndAgentID :exec +INSERT INTO + jfrog_xray_scans ( + agent_id, + workspace_id, + critical, + high, + medium, + results_url + ) +VALUES + ($1, $2, $3, $4, $5, $6) +ON CONFLICT (agent_id, workspace_id) +DO UPDATE SET critical = $3, high = $4, medium = $5, results_url = $6; diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index f397692f1d..af9e7b3cbf 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -19,6 +19,7 @@ const ( UniqueGroupMembersUserIDGroupIDKey UniqueConstraint = "group_members_user_id_group_id_key" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_group_id_key UNIQUE (user_id, group_id); UniqueGroupsNameOrganizationIDKey UniqueConstraint = "groups_name_organization_id_key" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_name_organization_id_key UNIQUE (name, organization_id); UniqueGroupsPkey UniqueConstraint = "groups_pkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_pkey PRIMARY KEY (id); + UniqueJfrogXrayScansPkey UniqueConstraint = "jfrog_xray_scans_pkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_pkey PRIMARY KEY (agent_id, workspace_id); UniqueLicensesJWTKey UniqueConstraint = "licenses_jwt_key" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt); UniqueLicensesPkey UniqueConstraint = "licenses_pkey" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_pkey PRIMARY KEY (id); UniqueOauth2ProviderAppSecretsAppIDHashedSecretKey UniqueConstraint = "oauth2_provider_app_secrets_app_id_hashed_secret_key" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_hashed_secret_key UNIQUE (app_id, hashed_secret); diff --git a/codersdk/jfrog.go b/codersdk/jfrog.go new file mode 100644 index 0000000000..aa7fec2572 --- /dev/null +++ b/codersdk/jfrog.go @@ -0,0 +1,50 @@ +package codersdk + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/google/uuid" + "golang.org/x/xerrors" +) + +type JFrogXrayScan struct { + WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"` + AgentID uuid.UUID `json:"agent_id" format:"uuid"` + Critical int `json:"critical"` + High int `json:"high"` + Medium int `json:"medium"` + ResultsURL string `json:"results_url"` +} + +func (c *Client) PostJFrogXrayScan(ctx context.Context, req JFrogXrayScan) error { + res, err := c.Request(ctx, http.MethodPost, "/api/v2/integrations/jfrog/xray-scan", req) + if err != nil { + return xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusCreated { + return ReadBodyAsError(res) + } + return nil +} + +func (c *Client) JFrogXRayScan(ctx context.Context, workspaceID, agentID uuid.UUID) (JFrogXrayScan, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/integrations/jfrog/xray-scan", nil, + WithQueryParam("workspace_id", workspaceID.String()), + WithQueryParam("agent_id", agentID.String()), + ) + if err != nil { + return JFrogXrayScan{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return JFrogXrayScan{}, ReadBodyAsError(res) + } + + var resp JFrogXrayScan + return resp, json.NewDecoder(res.Body).Decode(&resp) +} diff --git a/codersdk/templates.go b/codersdk/templates.go index 1be4d931ad..f60a66a80e 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -263,7 +263,7 @@ type TemplateExample struct { func (c *Client) Template(ctx context.Context, template uuid.UUID) (Template, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s", template), nil) if err != nil { - return Template{}, nil + return Template{}, xerrors.Errorf("do request: %w", err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { diff --git a/docs/api/enterprise.md b/docs/api/enterprise.md index 1ae77d4b7e..cb100f346f 100644 --- a/docs/api/enterprise.md +++ b/docs/api/enterprise.md @@ -359,6 +359,107 @@ curl -X PATCH http://coder-server:8080/api/v2/groups/{group} \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get JFrog XRay scan by workspace agent ID. + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/integrations/jfrog/xray-scan?workspace_id=string&agent_id=string \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /integrations/jfrog/xray-scan` + +### Parameters + +| Name | In | Type | Required | Description | +| -------------- | ----- | ------ | -------- | ------------ | +| `workspace_id` | query | string | true | Workspace ID | +| `agent_id` | query | string | true | Agent ID | + +### Example responses + +> 200 Response + +```json +{ + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "critical": 0, + "high": 0, + "medium": 0, + "results_url": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.JFrogXrayScan](schemas.md#codersdkjfrogxrayscan) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Post JFrog XRay scan by workspace agent ID. + +### Code samples + +```shell +# Example request using curl +curl -X POST http://coder-server:8080/api/v2/integrations/jfrog/xray-scan \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`POST /integrations/jfrog/xray-scan` + +> Body parameter + +```json +{ + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "critical": 0, + "high": 0, + "medium": 0, + "results_url": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +| ------ | ---- | ---------------------------------------------------------- | -------- | ---------------------------- | +| `body` | body | [codersdk.JFrogXrayScan](schemas.md#codersdkjfrogxrayscan) | true | Post JFrog XRay scan request | + +### Example responses + +> 200 Response + +```json +{ + "detail": "string", + "message": "string", + "validations": [ + { + "detail": "string", + "field": "string" + } + ] +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get licenses ### Code samples diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 8114d0750b..0ec54af432 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -3339,6 +3339,30 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | -------------- | ------ | -------- | ------------ | ----------- | | `signed_token` | string | false | | | +## codersdk.JFrogXrayScan + +```json +{ + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "critical": 0, + "high": 0, + "medium": 0, + "results_url": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| -------------- | ------- | -------- | ------------ | ----------- | +| `agent_id` | string | false | | | +| `critical` | integer | false | | | +| `high` | integer | false | | | +| `medium` | integer | false | | | +| `results_url` | string | false | | | +| `workspace_id` | string | false | | | + ## codersdk.JobErrorCode ```json diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 584f4627b8..925b5b9229 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -348,6 +348,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { }) }) }) + r.Route("/integrations", func(r chi.Router) { + r.Use( + apiKeyMiddleware, + api.jfrogEnabledMW, + ) + + r.Post("/jfrog/xray-scan", api.postJFrogXrayScan) + r.Get("/jfrog/xray-scan", api.jFrogXrayScan) + }) }) if len(options.SCIMAPIKey) != 0 { diff --git a/enterprise/coderd/jfrog.go b/enterprise/coderd/jfrog.go new file mode 100644 index 0000000000..7195aee908 --- /dev/null +++ b/enterprise/coderd/jfrog.go @@ -0,0 +1,121 @@ +package coderd + +import ( + "net/http" + + "github.com/google/uuid" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/codersdk" +) + +// Post workspace agent results for a JFrog XRay scan. +// +// @Summary Post JFrog XRay scan by workspace agent ID. +// @ID post-jfrog-xray-scan-by-workspace-agent-id +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Enterprise +// @Param request body codersdk.JFrogXrayScan true "Post JFrog XRay scan request" +// @Success 200 {object} codersdk.Response +// @Router /integrations/jfrog/xray-scan [post] +func (api *API) postJFrogXrayScan(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var req codersdk.JFrogXrayScan + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + err := api.Database.UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx, database.UpsertJFrogXrayScanByWorkspaceAndAgentIDParams{ + WorkspaceID: req.WorkspaceID, + AgentID: req.AgentID, + Critical: int32(req.Critical), + High: int32(req.High), + Medium: int32(req.Medium), + ResultsUrl: req.ResultsURL, + }) + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + httpapi.Write(ctx, rw, http.StatusCreated, codersdk.Response{ + Message: "Successfully inserted JFrog XRay scan!", + }) +} + +// Get workspace agent results for a JFrog XRay scan. +// +// @Summary Get JFrog XRay scan by workspace agent ID. +// @ID get-jfrog-xray-scan-by-workspace-agent-id +// @Security CoderSessionToken +// @Produce json +// @Tags Enterprise +// @Param workspace_id query string true "Workspace ID" +// @Param agent_id query string true "Agent ID" +// @Success 200 {object} codersdk.JFrogXrayScan +// @Router /integrations/jfrog/xray-scan [get] +func (api *API) jFrogXrayScan(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + vals = r.URL.Query() + p = httpapi.NewQueryParamParser() + wsID = p.Required("workspace_id").UUID(vals, uuid.UUID{}, "workspace_id") + agentID = p.Required("agent_id").UUID(vals, uuid.UUID{}, "agent_id") + ) + + if len(p.Errors) > 0 { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid query params.", + Validations: p.Errors, + }) + return + } + + scan, err := api.Database.GetJFrogXrayScanByWorkspaceAndAgentID(ctx, database.GetJFrogXrayScanByWorkspaceAndAgentIDParams{ + WorkspaceID: wsID, + AgentID: agentID, + }) + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, codersdk.JFrogXrayScan{ + WorkspaceID: scan.WorkspaceID, + AgentID: scan.AgentID, + Critical: int(scan.Critical), + High: int(scan.High), + Medium: int(scan.Medium), + ResultsURL: scan.ResultsUrl, + }) +} + +func (api *API) jfrogEnabledMW(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + api.entitlementsMu.RLock() + // This doesn't actually use the external auth feature but we want + // to lock this behind an enterprise license and it's somewhat + // related to external auth (in that it is JFrog integration). + enabled := api.entitlements.Features[codersdk.FeatureMultipleExternalAuth].Enabled + api.entitlementsMu.RUnlock() + + if !enabled { + httpapi.RouteNotFound(rw) + return + } + + next.ServeHTTP(rw, r) + }) +} diff --git a/enterprise/coderd/jfrog_test.go b/enterprise/coderd/jfrog_test.go new file mode 100644 index 0000000000..fd47f80b3e --- /dev/null +++ b/enterprise/coderd/jfrog_test.go @@ -0,0 +1,122 @@ +package coderd_test + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbfake" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" + "github.com/coder/coder/v2/enterprise/coderd/license" + "github.com/coder/coder/v2/testutil" +) + +func TestJFrogXrayScan(t *testing.T) { + t.Parallel() + + t.Run("Post/Get", func(t *testing.T) { + t.Parallel() + ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{codersdk.FeatureMultipleExternalAuth: 1}, + }, + }) + + tac, ta := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin()) + + wsResp := dbfake.WorkspaceBuild(t, db, database.Workspace{ + OrganizationID: owner.OrganizationID, + OwnerID: ta.ID, + }).WithAgent().Do() + + ws := coderdtest.MustWorkspace(t, tac, wsResp.Workspace.ID) + require.Len(t, ws.LatestBuild.Resources, 1) + require.Len(t, ws.LatestBuild.Resources[0].Agents, 1) + + agentID := ws.LatestBuild.Resources[0].Agents[0].ID + expectedPayload := codersdk.JFrogXrayScan{ + WorkspaceID: ws.ID, + AgentID: agentID, + Critical: 19, + High: 5, + Medium: 3, + ResultsURL: "https://hello-world", + } + + ctx := testutil.Context(t, testutil.WaitMedium) + err := tac.PostJFrogXrayScan(ctx, expectedPayload) + require.NoError(t, err) + + resp1, err := tac.JFrogXRayScan(ctx, ws.ID, agentID) + require.NoError(t, err) + require.Equal(t, expectedPayload, resp1) + + // Can update again without error. + expectedPayload = codersdk.JFrogXrayScan{ + WorkspaceID: ws.ID, + AgentID: agentID, + Critical: 20, + High: 22, + Medium: 8, + ResultsURL: "https://goodbye-world", + } + err = tac.PostJFrogXrayScan(ctx, expectedPayload) + require.NoError(t, err) + + resp2, err := tac.JFrogXRayScan(ctx, ws.ID, agentID) + require.NoError(t, err) + require.NotEqual(t, expectedPayload, resp1) + require.Equal(t, expectedPayload, resp2) + }) + + t.Run("MemberPostUnauthorized", func(t *testing.T) { + t.Parallel() + + ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{codersdk.FeatureMultipleExternalAuth: 1}, + }, + }) + + memberClient, member := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID) + + wsResp := dbfake.WorkspaceBuild(t, db, database.Workspace{ + OrganizationID: owner.OrganizationID, + OwnerID: member.ID, + }).WithAgent().Do() + + ws := coderdtest.MustWorkspace(t, memberClient, wsResp.Workspace.ID) + require.Len(t, ws.LatestBuild.Resources, 1) + require.Len(t, ws.LatestBuild.Resources[0].Agents, 1) + + agentID := ws.LatestBuild.Resources[0].Agents[0].ID + expectedPayload := codersdk.JFrogXrayScan{ + WorkspaceID: ws.ID, + AgentID: agentID, + Critical: 19, + High: 5, + Medium: 3, + ResultsURL: "https://hello-world", + } + + ctx := testutil.Context(t, testutil.WaitMedium) + err := memberClient.PostJFrogXrayScan(ctx, expectedPayload) + require.Error(t, err) + cerr, ok := codersdk.AsError(err) + require.True(t, ok) + require.Equal(t, http.StatusNotFound, cerr.StatusCode()) + + err = ownerClient.PostJFrogXrayScan(ctx, expectedPayload) + require.NoError(t, err) + + // We should still be able to fetch. + resp1, err := memberClient.JFrogXRayScan(ctx, ws.ID, agentID) + require.NoError(t, err) + require.Equal(t, expectedPayload, resp1) + }) +} diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index eeab0f373b..5770e3b6fe 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -607,6 +607,16 @@ export interface IssueReconnectingPTYSignedTokenResponse { readonly signed_token: string; } +// From codersdk/jfrog.go +export interface JFrogXrayScan { + readonly workspace_id: string; + readonly agent_id: string; + readonly critical: number; + readonly high: number; + readonly medium: number; + readonly results_url: string; +} + // From codersdk/licenses.go export interface License { readonly id: number;