diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 098ea767e4ffe..15b52df7bf1d8 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 24bc5e29cc05c..e8872f103d360 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 97743186f1356..257e12ebf4444 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 9668497dbfbae..34b3c7ddc0334 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 fa07cf01da11f..80c80547f48e1 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 15c7492fb9b61..d53bc484ef2cc 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 2b1c864b5adbf..ebc171a09e5f7 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 d13d615f9f196..cc6f3c59b3d43 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 5dc75af93f1be..f5ecbe0d156b5 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 0000000000000..8fa8f99f47bb0 --- /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 0000000000000..8143dac49d52f --- /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 0000000000000..3dc664242c46a --- /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 e0c310b2a7baf..dd12b0868249a 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 7996e3ca22f15..dc53934227067 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 55ee373540743..e3725ebdf5941 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 0000000000000..de9467c5323dd --- /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 f397692f1d6d1..af9e7b3cbf5e8 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 0000000000000..aa7fec25727cd --- /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 1be4d931ad7a2..f60a66a80e5dc 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 1ae77d4b7edbb..cb100f346f17b 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 8114d0750b65e..0ec54af432301 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 584f4627b8d50..925b5b9229e46 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 0000000000000..7195aee908dc9 --- /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 0000000000000..fd47f80b3ee92 --- /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 eeab0f373bba6..5770e3b6febb4 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;
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: