diff --git a/example/Nullify Demo Dashboard.json b/example/Nullify Demo Dashboard.json index 999927f..adf0196 100644 --- a/example/Nullify Demo Dashboard.json +++ b/example/Nullify Demo Dashboard.json @@ -360,10 +360,15 @@ }, "endpoint": "sast/events", "queryParameters": { + "eventTypes": [ + "new-branch-summary" + ], "githubRepositoryIdsOrQueries": [ "$repository" ], - "ownerNamesOrQueries": [] + "ownerNamesOrQueries": [ + "$owner" + ] }, "refId": "A" } @@ -727,7 +732,9 @@ "githubRepositoryIdsOrQueries": [ "$repository" ], - "ownerNamesOrQueries": [] + "ownerNamesOrQueries": [ + "$owner" + ] }, "refId": "A" } @@ -1051,12 +1058,15 @@ "queryParameters": { "githubRepositoryIdsOrQueries": [ "$repository" + ], + "ownerNamesOrQueries": [ + "$owner" ] }, "refId": "A" } ], - "title": "Secrets Detection - First committed last 7 days", + "title": "Secrets Detection - First committed", "transformations": [ { "id": "filterByValue", @@ -1481,12 +1491,15 @@ "queryParameters": { "githubRepositoryIdsOrQueries": [ "$repository" + ], + "ownerNamesOrQueries": [ + "$owner" ] }, "refId": "A" } ], - "title": "Secrets Detection - Detections last 7 days", + "title": "Secrets Detection - Detections", "transformations": [ { "id": "filterByValue", @@ -1494,12 +1507,10 @@ "filters": [ { "config": { - "id": "greater", - "options": { - "value": 0 - } + "id": "isNotNull", + "options": {} }, - "fieldName": "finding_timeStamp" + "fieldName": "timestamp" } ], "match": "any", @@ -1510,18 +1521,15 @@ "id": "groupBy", "options": { "fields": { - "finding_firstCommitTimestamp": { - "aggregations": [] - }, - "finding_timeStamp": { - "aggregations": [], - "operation": "groupby" - }, "id": { "aggregations": [ "count" ], "operation": "aggregate" + }, + "timestamp": { + "aggregations": [], + "operation": "groupby" } } } @@ -1567,16 +1575,33 @@ { "color": "green", "value": null - }, - { - "color": "red", - "value": 80 } ] }, "unitScale": true }, - "overrides": [] + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "hidden" + }, + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": false, + "viz": true + } + } + ] + } + ] }, "gridPos": { "h": 10, @@ -1584,11 +1609,11 @@ "x": 0, "y": 40 }, - "id": 5, + "id": 16, "options": { "barRadius": 0, "barWidth": 0.97, - "fullHighlight": false, + "fullHighlight": true, "groupWidth": 0.7, "legend": { "calcs": [], @@ -1597,8 +1622,11 @@ "showLegend": true }, "orientation": "auto", - "showValue": "auto", - "stacking": "none", + "showValue": "never", + "stacking": "normal", + "text": { + "valueSize": 12 + }, "tooltip": { "mode": "single", "sort": "none" @@ -1616,29 +1644,90 @@ "queryParameters": { "githubRepositoryIdsOrQueries": [ "$repository" + ], + "ownerNamesOrQueries": [ + "$owner" ] }, "refId": "A" } ], - "title": "Secret Detection - By Secret Type", + "title": "Secrets - By Type and Repo", "transformations": [ { "id": "groupBy", "options": { "fields": { + "formatted_severity": { + "aggregations": [], + "operation": "groupby" + }, "id": { "aggregations": [ - "distinctCount" + "count" ], "operation": "aggregate" }, + "language": { + "aggregations": [], + "operation": "groupby" + }, + "repository": { + "aggregations": [], + "operation": "groupby" + }, "secretType": { "aggregations": [], "operation": "groupby" } } } + }, + { + "id": "groupingToMatrix", + "options": { + "columnField": "repository", + "emptyValue": "null", + "rowField": "secretType", + "valueField": "id (count)" + } + }, + { + "id": "calculateField", + "options": { + "alias": "", + "mode": "reduceRow", + "reduce": { + "include": [], + "reducer": "sum" + }, + "replaceFields": false + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Total": false + }, + "includeByName": {}, + "indexByName": { + "S1 - CRITICAL": 6, + "S2 - HIGH": 5, + "S3 - MEDIUM": 4, + "S4 - LOW": 3, + "Total": 0, + "UNKNOWN": 2, + "language\\formatted_severity": 1 + }, + "renameByName": { + "S4 - LOW": "", + "Total": "", + "UNKNOWN": "", + "language\\formatted_severity": "Language", + "secretType\\repository": "Repository Name" + } + } } ], "type": "barchart" @@ -1705,6 +1794,6 @@ "timezone": "", "title": "Nullify Demo Dashboard", "uid": "ecf0f5c8-bf81-479a-b72f-94b5c2855d04", - "version": 3, + "version": 12, "weekStart": "" } \ No newline at end of file diff --git a/src/api/sastEvents.ts b/src/api/sastEvents.ts index 22bdb8c..e56eb5b 100644 --- a/src/api/sastEvents.ts +++ b/src/api/sastEvents.ts @@ -216,10 +216,14 @@ export const processSastEvents = async ( events.push(...parseResult.data.events); } - if (!parseResult.data.events || parseResult.data.events.length === 0 || !parseResult.data.nextEventId) { + if (!parseResult.data.nextEventId) { // No more events break; - } else if (parseResult.data.events[0].timestampUnix > range.to.unix()) { + } else if ( + parseResult.data.events && + parseResult.data.events.length > 0 && + parseResult.data.events[0].timestampUnix < range.from.unix() + ) { // No more events required break; } else { @@ -277,14 +281,14 @@ export const processSastEvents = async ( ), }, { - name: 'repositoryId', + name: 'repository', type: FieldType.string, - values: events.map((event) => event.data.provider.github?.repositoryId), + values: events.map((event) => event.data.provider.github?.repositoryName), }, { - name: 'repositoryName', + name: 'branch', type: FieldType.string, - values: events.map((event) => event.data.provider.github?.repositoryName), + values: events.map((event) => event.data.branch), }, { name: 'owners', diff --git a/src/api/scaEvents.ts b/src/api/scaEvents.ts index 5ba64e3..3173945 100644 --- a/src/api/scaEvents.ts +++ b/src/api/scaEvents.ts @@ -180,10 +180,14 @@ export const processScaEvents = async ( if (parseResult.data.events) { events.push(...parseResult.data.events); } - if (!parseResult.data.events || parseResult.data.events.length === 0 || !parseResult.data.nextEventId) { + if (!parseResult.data.nextEventId) { // No more events break; - } else if (parseResult.data.events[0].timestampUnix > range.to.unix()) { + } else if ( + parseResult.data.events && + parseResult.data.events.length > 0 && + parseResult.data.events[0].timestampUnix < range.from.unix() + ) { // No more events required break; } else { @@ -241,14 +245,14 @@ export const processScaEvents = async ( ), }, { - name: 'repositoryId', + name: 'repository', type: FieldType.string, - values: events.map((event) => event.data.provider.github?.repositoryId), + values: events.map((event) => event.data.provider.github?.repositoryName), }, { - name: 'repositoryName', + name: 'branch', type: FieldType.string, - values: events.map((event) => event.data.provider.github?.repositoryName), + values: events.map((event) => event.data.branch), }, { name: 'owners', diff --git a/src/api/secretsCommon.ts b/src/api/secretsCommon.ts index c79434f..f5c01ec 100644 --- a/src/api/secretsCommon.ts +++ b/src/api/secretsCommon.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { FileOwnerSchema } from './common'; export const SecretsScannerFindingEvent = z.object({ id: z.string(), @@ -16,7 +17,9 @@ export const SecretsScannerFindingEvent = z.object({ secretHash: z.string(), hyperlink: z.string(), isBranchHead: z.boolean(), - branches: z.array(z.string()).nullable(), + repository: z.string(), + branch: z.string(), firstCommitTimestamp: z.string(), isAllowlisted: z.boolean(), + fileOwners: z.array(FileOwnerSchema).nullable(), }); diff --git a/src/api/secretsEvents.ts b/src/api/secretsEvents.ts index 98a553c..ca11c8f 100644 --- a/src/api/secretsEvents.ts +++ b/src/api/secretsEvents.ts @@ -3,7 +3,7 @@ import { DataFrame, FieldType, TimeRange, createDataFrame } from '@grafana/data' import { FetchResponse } from '@grafana/runtime'; import { SecretsEventType, SecretsEventsQueryOptions } from 'types'; import { SecretsScannerFindingEvent } from './secretsCommon'; -import { unwrapRepositoryTemplateVariables } from 'utils/utils'; +import { unwrapOwnerTemplateVariables, unwrapRepositoryTemplateVariables } from 'utils/utils'; const MAX_API_REQUESTS = 10; @@ -68,6 +68,7 @@ type SecretsEventsEvent = z.infer; interface SecretsEventsApiRequest { githubRepositoryId?: number[]; + fileOwnerName?: string[]; branch?: string; eventType?: string[]; fromTime?: string; // ISO string @@ -92,6 +93,11 @@ export const processSecretsEvents = async ( ), } : {}), + ...(queryOptions.queryParameters.ownerNamesOrQueries + ? { + fileOwnerName: unwrapOwnerTemplateVariables(queryOptions.queryParameters.ownerNamesOrQueries), + } + : {}), ...(queryOptions.queryParameters.branch ? { branch: queryOptions.queryParameters.branch } : {}), ...(queryOptions.queryParameters.eventTypes && queryOptions.queryParameters.eventTypes.length > 0 ? { eventType: queryOptions.queryParameters.eventTypes } @@ -121,10 +127,15 @@ export const processSecretsEvents = async ( if (parseResult.data.events) { events.push(...parseResult.data.events); } - if (!parseResult.data.events || parseResult.data.events.length === 0 || !parseResult.data.nextEventId) { + + if (!parseResult.data.nextEventId) { // No more events break; - } else if (parseResult.data.events[0].timestampUnix > range.to.unix()) { + } else if ( + parseResult.data.events && + parseResult.data.events.length > 0 && + parseResult.data.events[0].timestampUnix < range.from.unix() + ) { // No more events required break; } else { @@ -140,6 +151,11 @@ export const processSecretsEvents = async ( type: FieldType.string, values: events.map((event) => event.id), }, + { + name: 'timestamp', + type: FieldType.time, + values: events.map((event) => new Date(event.time) ?? undefined), + }, { name: 'type', type: FieldType.string, @@ -156,14 +172,14 @@ export const processSecretsEvents = async ( values: events.map((event) => event.data.commit), }, { - name: 'repository_name', + name: 'repository', type: FieldType.string, - values: events.map((event) => event.data.provider.github?.repositoryName ?? ''), + values: events.map((event) => event.data.finding.repository ?? ''), }, { - name: 'repository_id', - type: FieldType.number, - values: events.map((event) => event.data.provider.github?.repositoryId ?? -1), + name: 'branch', + type: FieldType.string, + values: events.map((event) => event.data.finding.branch ?? ''), }, { name: 'finding_id', @@ -190,11 +206,6 @@ export const processSecretsEvents = async ( type: FieldType.string, values: events.map((event) => event.data.finding.commit ?? ''), }, - { - name: 'finding_timeStamp', - type: FieldType.time, - values: events.map((event) => new Date(event.data.finding.timeStamp) ?? undefined), - }, { name: 'finding_ruleId', type: FieldType.string, diff --git a/src/api/secretsSummary.ts b/src/api/secretsSummary.ts index 3e353d8..8c05ced 100644 --- a/src/api/secretsSummary.ts +++ b/src/api/secretsSummary.ts @@ -3,7 +3,7 @@ import { DataFrame, FieldType, createDataFrame } from '@grafana/data'; import { FetchResponse, getTemplateSrv } from '@grafana/runtime'; import { SecretsSummaryQueryOptions } from 'types'; import { SecretsScannerFindingEvent } from './secretsCommon'; -import { unwrapRepositoryTemplateVariables } from 'utils/utils'; +import { unwrapOwnerTemplateVariables, unwrapRepositoryTemplateVariables } from 'utils/utils'; const SecretsSummaryApiResponseSchema = z.object({ secrets: z.array(SecretsScannerFindingEvent).nullable(), @@ -12,6 +12,7 @@ const SecretsSummaryApiResponseSchema = z.object({ interface SecretsSummaryApiRequest { githubRepositoryId?: number[]; + fileOwnerName?: string[]; branch?: string; secretType?: string; isAllowlisted?: boolean; @@ -29,6 +30,11 @@ export const processSecretsSummary = async ( ), } : {}), + ...(queryOptions.queryParameters.ownerNamesOrQueries + ? { + fileOwnerName: unwrapOwnerTemplateVariables(queryOptions.queryParameters.ownerNamesOrQueries), + } + : {}), ...(queryOptions.queryParameters.branch ? { branch: queryOptions.queryParameters.branch } : {}), ...(queryOptions.queryParameters.secretType ? { secretType: queryOptions.queryParameters.secretType } : {}), ...(queryOptions.queryParameters.isAllowlisted @@ -90,6 +96,16 @@ export const processSecretsSummary = async ( type: FieldType.number, values: parseResult.data.secrets?.map((secret) => secret.entropy), }, + { + name: 'repository', + type: FieldType.string, + values: parseResult.data.secrets?.map((secret) => secret.repository), + }, + { + name: 'branch', + type: FieldType.string, + values: parseResult.data.secrets?.map((secret) => secret.branch), + }, { name: 'isBranchHead', type: FieldType.boolean, diff --git a/src/components/Fields/OwnersField.tsx b/src/components/Fields/OwnersField.tsx index f6d2a9c..410639e 100644 --- a/src/components/Fields/OwnersField.tsx +++ b/src/components/Fields/OwnersField.tsx @@ -1,7 +1,6 @@ import { SelectableValue, TypedVariableModel } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { MultiSelect } from '@grafana/ui'; -import { Organization } from 'api/common'; import React, { useEffect, useRef, useState } from 'react'; import { NullifyVariableQuery, NullifyVariableQueryType, OwnerEntity, OwnerEntityType } from 'types'; diff --git a/src/components/Query/SastEventsSubquery.tsx b/src/components/Query/SastEventsSubquery.tsx index a7ceec5..eec5a70 100644 --- a/src/components/Query/SastEventsSubquery.tsx +++ b/src/components/Query/SastEventsSubquery.tsx @@ -52,16 +52,6 @@ export function SastEventsSubquery(props: Props) { return query.endpoint === 'sast/events' ? ( <> - - - + + + - - - + + + - - - + + + ; @@ -13,9 +14,24 @@ const SecretsEventTypeOptions: Array> = Object.entries(S export function SecretsEventsSubquery(props: Props) { const { query, onChange, onRunQuery } = props; - const [selectedRepositories, setSelectedRepositories] = useState>(query.queryParameters?.githubRepositoryIdsOrQueries || []); + const [selectedRepositories, setSelectedRepositories] = useState>( + query.endpoint === 'secrets/events' ? query.queryParameters?.githubRepositoryIdsOrQueries || [] : [] + ); + const [selectedOwners, setSelectedOwners] = useState( + query.endpoint === 'secrets/events' ? query.queryParameters?.ownerNamesOrQueries || [] : [] + ); - const onRepositoriesChange = (repositories: Array<(number | string)>) => { + const onOwnersChange = (owners: string[]) => { + setSelectedOwners(owners); + onChange({ + ...query, + endpoint: 'secrets/events', + queryParameters: { ...query.queryParameters, ownerNamesOrQueries: owners }, + }); + onRunQuery(); + }; + + const onRepositoriesChange = (repositories: Array) => { setSelectedRepositories(repositories); onChange({ ...query, @@ -36,6 +52,16 @@ export function SecretsEventsSubquery(props: Props) { return query.endpoint === 'secrets/events' ? ( <> + + + ; export function SecretsSummarySubquery(props: Props) { const { query, onChange, onRunQuery } = props; - const [selectedRepositories, setSelectedRepositories] = useState>(query.queryParameters?.githubRepositoryIdsOrQueries || []); + const [selectedRepositories, setSelectedRepositories] = useState>( + query.endpoint === 'secrets/summary' ? query.queryParameters?.githubRepositoryIdsOrQueries || [] : [] + ); + const [selectedOwners, setSelectedOwners] = useState( + query.endpoint === 'secrets/summary' ? query.queryParameters?.ownerNamesOrQueries || [] : [] + ); + + const onOwnersChange = (owners: string[]) => { + setSelectedOwners(owners); + onChange({ + ...query, + endpoint: 'secrets/summary', + queryParameters: { ...query.queryParameters, ownerNamesOrQueries: owners }, + }); + onRunQuery(); + }; - const onRepositoriesChange = (repositories: Array<(number | string)>) => { + const onRepositoriesChange = (repositories: Array) => { setSelectedRepositories(repositories); onChange({ ...query, @@ -47,6 +63,16 @@ export function SecretsSummarySubquery(props: Props) { return query.endpoint === 'secrets/summary' ? ( <> + + + - - + diff --git a/src/types.ts b/src/types.ts index 5f60651..2d74b28 100644 --- a/src/types.ts +++ b/src/types.ts @@ -141,6 +141,7 @@ export interface SecretsSummaryQueryOptions extends BaseQueryOptions { export interface SecretsSummaryQueryParameters { githubRepositoryIdsOrQueries?: Array; + ownerNamesOrQueries?: string[]; branch?: string; secretType?: string; isAllowlisted?: boolean; @@ -155,6 +156,7 @@ export interface SecretsEventsQueryOptions extends BaseQueryOptions { export interface SecretsEventsQueryParameters { githubRepositoryIdsOrQueries?: Array; + ownerNamesOrQueries?: string[]; branch?: string; eventTypes?: string[]; }