feat: add backend for jfrog xray support (#11829)

This commit is contained in:
Jon Ayers 2024-01-29 19:30:02 -06:00 committed by GitHub
parent 46d92dac57
commit 4f5a2f0a9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 944 additions and 2 deletions

103
coderd/apidoc/docs.go generated
View File

@ -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": [

View File

@ -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"],

View File

@ -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

View File

@ -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() {

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1 @@
DROP TABLE jfrog_xray_scans;

View File

@ -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)
);

View File

@ -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'
);

View File

@ -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"`

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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);

50
codersdk/jfrog.go Normal file
View File

@ -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)
}

View File

@ -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 {

101
docs/api/enterprise.md generated
View File

@ -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

24
docs/api/schemas.md generated
View File

@ -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

View File

@ -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 {

121
enterprise/coderd/jfrog.go Normal file
View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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;