diff --git a/docs/jupiterone.md b/docs/jupiterone.md index 78a721ce..a933cc0e 100644 --- a/docs/jupiterone.md +++ b/docs/jupiterone.md @@ -459,6 +459,7 @@ The following relationships are created: | `internet` | **ALLOWS** | `google_compute_firewall` | | `google_cloud_folder` | **HAS** | `google_cloud_folder` | | `google_cloud_function` | **USES** | `google_iam_service_account` | +| `google_cloud_function` | **USES** | `google_cloud_source_repository` | | `google_cloud_organization` | **HAS** | `google_cloud_folder` | | `google_cloud_project` | **HAS** | `google_cloud_api_service` | | `google_cloud_project` | **HAS** | `google_billing_budget` | diff --git a/docs/spec/src/steps/services/functions/index.ts b/docs/spec/src/steps/services/functions/index.ts index c0d3390c..8dc3c160 100644 --- a/docs/spec/src/steps/services/functions/index.ts +++ b/docs/spec/src/steps/services/functions/index.ts @@ -82,7 +82,7 @@ export const functionSteps: StepSpec[] = [ }, ], dependsOn: ['fetch-cloud-functions', 'fetch-cloud-source-repositories'], - implemented: false, + implemented: true, }, { /** diff --git a/src/getStepStartStates.ts b/src/getStepStartStates.ts index a63d462f..22580212 100644 --- a/src/getStepStartStates.ts +++ b/src/getStepStartStates.ts @@ -112,6 +112,7 @@ import * as enablement from './steps/enablement'; import { STEP_CLOUD_FUNCTIONS, STEP_CLOUD_FUNCTIONS_SERVICE_ACCOUNT_RELATIONSHIPS, + STEP_CLOUD_FUNCTIONS_SOURCE_REPO_RELATIONSHIPS, } from './steps/functions'; import { STEP_IAM_CUSTOM_ROLES, @@ -301,6 +302,7 @@ function getDefaultStepStartStates(params: { [STEP_CREATE_API_SERVICE_ANY_RESOURCE_RELATIONSHIPS]: { disabled: false }, [STEP_CLOUD_FUNCTIONS]: { disabled: false }, [STEP_CLOUD_FUNCTIONS_SERVICE_ACCOUNT_RELATIONSHIPS]: { disabled: false }, + [STEP_CLOUD_FUNCTIONS_SOURCE_REPO_RELATIONSHIPS]: { disabled: false }, [STEP_CLOUD_STORAGE_BUCKETS]: { disabled: false }, [STEP_IAM_CUSTOM_ROLES]: { disabled: false }, [STEP_IAM_CUSTOM_ROLE_SERVICE_API_RELATIONSHIPS]: { disabled: false }, @@ -591,6 +593,9 @@ async function getStepStartStatesUsingServiceEnablements(params: { [STEP_CLOUD_FUNCTIONS_SERVICE_ACCOUNT_RELATIONSHIPS]: createStepStartState( ServiceUsageName.CLOUD_FUNCTIONS, ), + [STEP_CLOUD_FUNCTIONS_SOURCE_REPO_RELATIONSHIPS]: createStepStartState( + ServiceUsageName.CLOUD_FUNCTIONS, + ), [STEP_CLOUD_STORAGE_BUCKETS]: createStepStartState( ServiceUsageName.STORAGE, ServiceUsageName.STORAGE_COMPONENT, diff --git a/src/index.test.ts b/src/index.test.ts index 7e5ae2c1..1c654ad6 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -123,10 +123,6 @@ import { STEP_DNS_MANAGED_ZONES, STEP_DNS_POLICIES, } from './steps/dns/constants'; -import { - STEP_CLOUD_FUNCTIONS, - STEP_CLOUD_FUNCTIONS_SERVICE_ACCOUNT_RELATIONSHIPS, -} from './steps/functions'; import { STEP_IAM_CUSTOM_ROLES, STEP_IAM_CUSTOM_ROLE_SERVICE_API_RELATIONSHIPS, @@ -175,6 +171,11 @@ import { import { SqlAdminSteps, STEP_SQL_ADMIN_INSTANCES } from './steps/sql-admin'; import { STEP_CLOUD_STORAGE_BUCKETS } from './steps/storage'; import { IntegrationConfig } from './types'; +import { + STEP_CLOUD_FUNCTIONS, + STEP_CLOUD_FUNCTIONS_SERVICE_ACCOUNT_RELATIONSHIPS, + STEP_CLOUD_FUNCTIONS_SOURCE_REPO_RELATIONSHIPS, +} from './steps/functions'; interface ValidateInvocationInvalidConfigTestParams { instanceConfig?: Partial; @@ -326,6 +327,9 @@ describe('#getStepStartStates success', () => { [STEP_CLOUD_FUNCTIONS_SERVICE_ACCOUNT_RELATIONSHIPS]: { disabled: false, }, + [STEP_CLOUD_FUNCTIONS_SOURCE_REPO_RELATIONSHIPS]: { + disabled: false, + }, [STEP_CLOUD_STORAGE_BUCKETS]: { disabled: false, }, diff --git a/src/steps/functions/__recordings__/build-cloud-function-source-repo-relationships_912814797/recording.har b/src/steps/functions/__recordings__/build-cloud-function-source-repo-relationships_912814797/recording.har new file mode 100644 index 00000000..31dd981e --- /dev/null +++ b/src/steps/functions/__recordings__/build-cloud-function-source-repo-relationships_912814797/recording.har @@ -0,0 +1,496 @@ +{ + "log": { + "_recordingName": "build-cloud-function-source-repo-relationships", + "creator": { + "comment": "persister:JupiterOneIntegationFSPersister", + "name": "Polly.JS", + "version": "6.0.5" + }, + "entries": [ + { + "_id": "acea721c8193b51ced888cae721cc423", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 709, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-type", + "value": "application/x-www-form-urlencoded" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-length", + "value": "709" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "_fromType": "array", + "name": "connection", + "value": "close" + }, + { + "name": "host", + "value": "www.googleapis.com" + } + ], + "headersSize": 300, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/x-www-form-urlencoded", + "params": [], + "text": "[REDACTED]" + }, + "queryString": [], + "url": "https://www.googleapis.com/oauth2/v4/token" + }, + "response": { + "bodySize": 414, + "content": { + "encoding": "utf-8", + "mimeType": "application/json; charset=UTF-8", + "size": 414, + "text": "{\"access_token\":\"[REDACTED]\",\"expires_in\":9999,\"token_type\":\"Bearer\"}" + }, + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json; charset=UTF-8" + }, + { + "name": "vary", + "value": "Origin, X-Origin, Referer" + }, + { + "name": "date", + "value": "Tue, 01 Nov 2022 15:52:58 GMT" + }, + { + "name": "server", + "value": "scaffolding on HTTPServer2" + }, + { + "name": "cache-control", + "value": "private" + }, + { + "name": "x-xss-protection", + "value": "0" + }, + { + "name": "x-frame-options", + "value": "SAMEORIGIN" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" + }, + { + "name": "connection", + "value": "close" + } + ], + "headersSize": 506, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2022-11-01T15:52:58.699Z", + "time": 145, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 145 + } + }, + { + "_id": "bcff475f691147ea18d4b1c0f20e74e4", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "x-goog-api-client", + "value": "gdcl/5.0.2 gl-node/16.13.2 auth/7.1.1" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "google-api-nodejs-client/5.0.2 (gzip)" + }, + { + "_fromType": "array", + "name": "authorization", + "value": "[REDACTED]" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "connection", + "value": "close" + }, + { + "name": "host", + "value": "cloudfunctions.googleapis.com" + } + ], + "headersSize": 1375, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://cloudfunctions.googleapis.com/v1/projects/j1-gc-integration-dev-v3/locations/-/functions" + }, + "response": { + "bodySize": 1145, + "content": { + "encoding": "utf-8", + "mimeType": "application/json; charset=UTF-8", + "size": 1145, + "text": "{\"functions\":[{\"name\":\"projects/j1-gc-integration-dev-v3/locations/us-central1/functions/gaston-delete\",\"sourceRepository\":{\"url\":\"https://source.developers.google.com/projects/j1-gc-integration-dev-v3/repos/gaston/moveable-aliases/master/paths/\",\"deployedUrl\":\"https://source.developers.google.com/projects/j1-gc-integration-dev-v3/repos/gaston/revisions//paths/\"},\"httpsTrigger\":{\"url\":\"https://us-central1-j1-gc-integration-dev-v3.cloudfunctions.net/gaston-delete\",\"securityLevel\":\"SECURE_ALWAYS\"},\"status\":\"UNKNOWN\",\"entryPoint\":\"helloWorld\",\"timeout\":\"60s\",\"availableMemoryMb\":256,\"serviceAccountEmail\":\"j1-gc-integration-dev-v3@appspot.gserviceaccount.com\",\"updateTime\":\"2022-10-31T12:04:14.816Z\",\"versionId\":\"1\",\"labels\":{\"deployment-tool\":\"console-cloud\"},\"runtime\":\"nodejs16\",\"maxInstances\":3000,\"ingressSettings\":\"ALLOW_ALL\",\"dockerRegistry\":\"CONTAINER_REGISTRY\"},{\"name\":\"projects/j1-gc-integration-dev-v3/locations/us-central1/functions/j1-gc-integration-dev-v3testfunction\",\"description\":\"Test function\",\"sourceArchiveUrl\":\"gs://j1-gc-integration-dev-v3cloudfunctions/http_trigger.zip\",\"httpsTrigger\":{\"url\":\"https://us-central1-j1-gc-integration-dev-v3.cloudfunctions.net/j1-gc-integration-dev-v3testfunction\",\"securityLevel\":\"SECURE_OPTIONAL\"},\"status\":\"ACTIVE\",\"entryPoint\":\"handler\",\"timeout\":\"60s\",\"availableMemoryMb\":128,\"serviceAccountEmail\":\"j1-gc-integration-dev-v3@appspot.gserviceaccount.com\",\"updateTime\":\"2021-05-31T16:36:15.024Z\",\"versionId\":\"1\",\"environmentVariables\":{\"TEST_ENV_VAR\":\"test-env-var-val\"},\"runtime\":\"nodejs10\",\"ingressSettings\":\"ALLOW_ALL\",\"buildId\":\"468a40b5-ba13-41aa-84d8-0ffb43a9c153\",\"buildName\":\"projects/167984947943/locations/us-central1/builds/468a40b5-ba13-41aa-84d8-0ffb43a9c153\",\"dockerRegistry\":\"CONTAINER_REGISTRY\"},{\"name\":\"projects/j1-gc-integration-dev-v3/locations/us-central1/functions/j1-gc-integration-dev-v3testfunctionwithsa\",\"description\":\"Test function with service account\",\"sourceArchiveUrl\":\"gs://j1-gc-integration-dev-v3cloudfunctions/http_trigger.zip\",\"httpsTrigger\":{\"url\":\"https://us-central1-j1-gc-integration-dev-v3.cloudfunctions.net/j1-gc-integration-dev-v3testfunctionwithsa\",\"securityLevel\":\"SECURE_OPTIONAL\"},\"status\":\"ACTIVE\",\"entryPoint\":\"handler\",\"timeout\":\"60s\",\"availableMemoryMb\":128,\"serviceAccountEmail\":\"j1-gc-integration-dev-f-sa@j1-gc-integration-dev-v3.iam.gserviceaccount.com\",\"updateTime\":\"2021-05-31T16:36:14.851Z\",\"versionId\":\"1\",\"environmentVariables\":{\"TEST_ENV_VAR\":\"test-env-var-val\"},\"runtime\":\"nodejs10\",\"ingressSettings\":\"ALLOW_ALL\",\"buildId\":\"f90313dc-1de2-4181-8709-9865b6507e04\",\"buildName\":\"projects/167984947943/locations/us-central1/builds/f90313dc-1de2-4181-8709-9865b6507e04\",\"dockerRegistry\":\"CONTAINER_REGISTRY\"}]}" + }, + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json; charset=UTF-8" + }, + { + "name": "vary", + "value": "Origin, X-Origin, Referer" + }, + { + "name": "date", + "value": "Tue, 01 Nov 2022 15:52:59 GMT" + }, + { + "name": "server", + "value": "ESF" + }, + { + "name": "cache-control", + "value": "private" + }, + { + "name": "x-xss-protection", + "value": "0" + }, + { + "name": "x-frame-options", + "value": "SAMEORIGIN" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" + }, + { + "name": "connection", + "value": "close" + } + ], + "headersSize": 483, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2022-11-01T15:52:58.847Z", + "time": 965, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 965 + } + }, + { + "_id": "acea721c8193b51ced888cae721cc423", + "_order": 1, + "cache": {}, + "request": { + "bodySize": 709, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-type", + "value": "application/x-www-form-urlencoded" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-length", + "value": "709" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "_fromType": "array", + "name": "connection", + "value": "close" + }, + { + "name": "host", + "value": "www.googleapis.com" + } + ], + "headersSize": 300, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/x-www-form-urlencoded", + "params": [], + "text": "[REDACTED]" + }, + "queryString": [], + "url": "https://www.googleapis.com/oauth2/v4/token" + }, + "response": { + "bodySize": 394, + "content": { + "encoding": "utf-8", + "mimeType": "application/json; charset=UTF-8", + "size": 394, + "text": "{\"access_token\":\"[REDACTED]\",\"expires_in\":9999,\"token_type\":\"Bearer\"}" + }, + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json; charset=UTF-8" + }, + { + "name": "vary", + "value": "Origin, X-Origin, Referer" + }, + { + "name": "date", + "value": "Tue, 01 Nov 2022 15:53:00 GMT" + }, + { + "name": "server", + "value": "scaffolding on HTTPServer2" + }, + { + "name": "cache-control", + "value": "private" + }, + { + "name": "x-xss-protection", + "value": "0" + }, + { + "name": "x-frame-options", + "value": "SAMEORIGIN" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" + }, + { + "name": "connection", + "value": "close" + } + ], + "headersSize": 506, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2022-11-01T15:52:59.846Z", + "time": 120, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 120 + } + }, + { + "_id": "53e96d058a7bb7dbd8b3068576294ec7", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "x-goog-api-client", + "value": "gdcl/5.0.2 gl-node/16.13.2 auth/7.1.1" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "google-api-nodejs-client/5.0.2 (gzip)" + }, + { + "_fromType": "array", + "name": "authorization", + "value": "[REDACTED]" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "connection", + "value": "close" + }, + { + "name": "host", + "value": "sourcerepo.googleapis.com" + } + ], + "headersSize": 1351, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://sourcerepo.googleapis.com/v1/projects/j1-gc-integration-dev-v3/repos" + }, + "response": { + "bodySize": 243, + "content": { + "encoding": "utf-8", + "mimeType": "application/json; charset=UTF-8", + "size": 243, + "text": "{\"repos\":[{\"name\":\"projects/j1-gc-integration-dev-v3/repos/gaston\",\"url\":\"https://source.developers.google.com/p/j1-gc-integration-dev-v3/r/gaston\"},{\"name\":\"projects/j1-gc-integration-dev-v3/repos/nacho\",\"url\":\"https://source.developers.google.com/p/j1-gc-integration-dev-v3/r/nacho\"}]}" + }, + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json; charset=UTF-8" + }, + { + "name": "vary", + "value": "Origin, X-Origin, Referer" + }, + { + "name": "date", + "value": "Tue, 01 Nov 2022 15:53:01 GMT" + }, + { + "name": "server", + "value": "ESF" + }, + { + "name": "cache-control", + "value": "private" + }, + { + "name": "x-xss-protection", + "value": "0" + }, + { + "name": "x-frame-options", + "value": "SAMEORIGIN" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" + }, + { + "name": "connection", + "value": "close" + } + ], + "headersSize": 483, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2022-11-01T15:52:59.976Z", + "time": 1066, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 1066 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/steps/functions/__snapshots__/converters.test.ts.snap b/src/steps/functions/__snapshots__/converters.test.ts.snap index 4a2170b1..04efbf86 100644 --- a/src/steps/functions/__snapshots__/converters.test.ts.snap +++ b/src/steps/functions/__snapshots__/converters.test.ts.snap @@ -24,6 +24,9 @@ Object { "runtime": "nodejs10", "serviceAccountEmail": "j1-gc-integration-dev-v2@appspot.gserviceaccount.com", "sourceArchiveUrl": "gs://j1-gc-integration-dev-v2cloudfunctions/http_trigger.zip", + "sourceRepository": Object { + "url": "https://source.developers.google.com/projects/j1-gc-integration-dev-v3/repos/gaston/moveable-aliases/master/paths/", + }, "status": "ACTIVE", "timeout": "60s", "updateTime": "2020-07-28T18:35:52.821Z", @@ -43,6 +46,7 @@ Object { "name": "projects/j1-gc-integration-dev-v2/locations/us-central1/functions/j1-gc-integration-dev-v2testfunction", "runtime": "nodejs10", "serviceAccountEmail": "j1-gc-integration-dev-v2@appspot.gserviceaccount.com", + "sourceRepoUrl": "https://source.developers.google.com/projects/j1-gc-integration-dev-v3/repos/gaston/moveable-aliases/master/paths/", "status": "ACTIVE", "timeout": "60s", "updatedOn": 1595961352821, diff --git a/src/steps/functions/constants.ts b/src/steps/functions/constants.ts index 3571c1f0..67a7fcf7 100644 --- a/src/steps/functions/constants.ts +++ b/src/steps/functions/constants.ts @@ -1,9 +1,49 @@ +import { RelationshipClass } from '@jupiterone/integration-sdk-core'; +import { IAM_SERVICE_ACCOUNT_ENTITY_TYPE } from '../iam'; + export const CLOUD_FUNCTION_ENTITY_CLASS = 'Function'; export const CLOUD_FUNCTION_ENTITY_TYPE = 'google_cloud_function'; export const STEP_CLOUD_FUNCTIONS = 'fetch-cloud-functions'; export const STEP_CLOUD_FUNCTIONS_SERVICE_ACCOUNT_RELATIONSHIPS = 'build-cloud-function-service-account-relationships'; +export const STEP_CLOUD_FUNCTIONS_SOURCE_REPO_RELATIONSHIPS = + 'build-cloud-function-source-repo-relationships'; + +export const FunctionStepsSpec = { + FETCH_CLOUD_FUNCTIONS: { + id: 'fetch-cloud-functions', + name: 'Cloud Functions', + }, + CLOUD_FUNCTIONS_SERVICE_ACCOUNT_RELATIONSHIPS: { + id: 'build-cloud-function-service-account-relationships', + name: 'Cloud Function Service Account Relationships', + }, + CLOUD_FUNCTIONS_SOURCE_REPO_RELATIONSHIP: { + id: 'build-cloud-function-source-repo-relationships', + name: 'Build Cloud Function -> Source Repository Relationships', + }, +}; + +export const FunctionEntitiesSpec = { + CLOUD_FUNCTION: { + resourceName: 'Cloud Function', + _type: 'google_cloud_function', + _class: 'Function', + }, +}; -export const RELATIONSHIP_TYPE_CLOUD_FUNCTION_USES_IAM_SERVICE_ACCOUNT = - 'google_cloud_function_uses_iam_service_account'; +export const FunctionsRelationshipsSpec = { + GOOGLE_CLOUD_FUNCTION_USES_IAM_SERVICE_ACCOUNT: { + _class: RelationshipClass.USES, + _type: 'google_cloud_function_uses_iam_service_account', + sourceType: 'google_cloud_function', + targetType: IAM_SERVICE_ACCOUNT_ENTITY_TYPE, + }, + GOOGLE_CLOUD_FUNCTION_USES_SOURCE_REPOSITORY: { + _type: 'google_cloud_function_uses_source_repository', + _class: RelationshipClass.USES, + sourceType: 'google_cloud_function', + targetType: 'google_cloud_source_repository', + }, +}; diff --git a/src/steps/functions/converters.ts b/src/steps/functions/converters.ts index a6daacf7..34e4fb6a 100644 --- a/src/steps/functions/converters.ts +++ b/src/steps/functions/converters.ts @@ -37,6 +37,7 @@ export function createCloudFunctionEntity( status: cloudFunction.status, active: cloudFunction.status === 'ACTIVE', serviceAccountEmail: cloudFunction.serviceAccountEmail, + sourceRepoUrl: cloudFunction.sourceRepository?.url, }, }, }); diff --git a/src/steps/functions/index.test.ts b/src/steps/functions/index.test.ts index 3b70a278..8a4f6c51 100644 --- a/src/steps/functions/index.test.ts +++ b/src/steps/functions/index.test.ts @@ -1,31 +1,36 @@ import { Recording, createMockStepExecutionContext, + StepTestConfig, + executeStepWithDependencies, } from '@jupiterone/integration-sdk-testing'; -import { setupGoogleCloudRecording } from '../../../test/recording'; +import { + setupGoogleCloudRecording, + getMatchRequestsBy, +} from '../../../test/recording'; import { IntegrationConfig } from '../../types'; import { fetchCloudFunctions } from '.'; import { integrationConfig } from '../../../test/config'; import { CLOUD_FUNCTION_ENTITY_CLASS, CLOUD_FUNCTION_ENTITY_TYPE, + FunctionStepsSpec, } from './constants'; +import { invocationConfig } from '../..'; describe('#fetchCloudFunctions', () => { let recording: Recording; - beforeEach(() => { - recording = setupGoogleCloudRecording({ - directory: __dirname, - name: 'fetchCloudFunctions', - }); - }); - afterEach(async () => { await recording.stop(); }); test('should collect data', async () => { + recording = setupGoogleCloudRecording({ + directory: __dirname, + name: 'fetchCloudFunctions', + }); + const context = createMockStepExecutionContext({ instanceConfig: integrationConfig, }); @@ -56,4 +61,26 @@ describe('#fetchCloudFunctions', () => { ), ); }); + + test( + FunctionStepsSpec.CLOUD_FUNCTIONS_SOURCE_REPO_RELATIONSHIP.id, + async () => { + recording = setupGoogleCloudRecording({ + name: FunctionStepsSpec.CLOUD_FUNCTIONS_SOURCE_REPO_RELATIONSHIP.id, + directory: __dirname, + options: { + matchRequestsBy: getMatchRequestsBy(integrationConfig), + }, + }); + + const stepTestConfig: StepTestConfig = { + stepId: FunctionStepsSpec.CLOUD_FUNCTIONS_SOURCE_REPO_RELATIONSHIP.id, + instanceConfig: integrationConfig, + invocationConfig: invocationConfig as any, + }; + + const result = await executeStepWithDependencies(stepTestConfig); + expect(result).toMatchStepMetadata(stepTestConfig); + }, + ); }); diff --git a/src/steps/functions/index.ts b/src/steps/functions/index.ts index 0de3babb..3bec6901 100644 --- a/src/steps/functions/index.ts +++ b/src/steps/functions/index.ts @@ -1,22 +1,20 @@ import { createDirectRelationship, + getRawData, IntegrationStep, RelationshipClass, } from '@jupiterone/integration-sdk-core'; import { CloudFunctionsClient } from './client'; import { IntegrationConfig, IntegrationStepContext } from '../../types'; import { createCloudFunctionEntity } from './converters'; +import { STEP_IAM_SERVICE_ACCOUNTS } from '../iam'; import { - CLOUD_FUNCTION_ENTITY_TYPE, - STEP_CLOUD_FUNCTIONS, - CLOUD_FUNCTION_ENTITY_CLASS, - RELATIONSHIP_TYPE_CLOUD_FUNCTION_USES_IAM_SERVICE_ACCOUNT, - STEP_CLOUD_FUNCTIONS_SERVICE_ACCOUNT_RELATIONSHIPS, + FunctionEntitiesSpec, + FunctionsRelationshipsSpec, + FunctionStepsSpec, } from './constants'; -import { - IAM_SERVICE_ACCOUNT_ENTITY_TYPE, - STEP_IAM_SERVICE_ACCOUNTS, -} from '../iam'; +import { CloudSourceRepositoriesStepsSpec } from '../cloud-source-repositories/constants'; +import { cloudfunctions_v1 } from 'googleapis'; export * from './constants'; @@ -38,7 +36,7 @@ export async function buildCloudFunctionServiceAccountRelationships( await jobState.iterateEntities( { - _type: CLOUD_FUNCTION_ENTITY_TYPE, + _type: FunctionEntitiesSpec.CLOUD_FUNCTION._type, }, async (cloudFunctionEntity) => { const serviceAccountEmail = cloudFunctionEntity.serviceAccountEmail as @@ -68,34 +66,78 @@ export async function buildCloudFunctionServiceAccountRelationships( ); } +export async function buildCloudFunctionSourceRepoRelationships( + context: IntegrationStepContext, +): Promise { + const { jobState } = context; + + await jobState.iterateEntities( + { + _type: FunctionEntitiesSpec.CLOUD_FUNCTION._type, + }, + async (cloudFunctionEntity) => { + const cloudFunction = + getRawData(cloudFunctionEntity); + const sourceRepoUrl = cloudFunction?.sourceRepository?.url; + + if (!sourceRepoUrl) { + return; + } + + const sourceRepoKey = sourceRepoUrl + ?.replace('https://source.developers.google.com/', '') + .split('/moveable-aliases/')[0]; + + if (!sourceRepoKey) return; + + const sourceRepoEntity = await jobState.findEntity(sourceRepoKey); + + if (sourceRepoEntity) { + await jobState.addRelationship( + createDirectRelationship({ + _class: RelationshipClass.USES, + from: cloudFunctionEntity, + to: sourceRepoEntity, + }), + ); + } + }, + ); +} + export const functionsSteps: IntegrationStep[] = [ { - id: STEP_CLOUD_FUNCTIONS, - name: 'Cloud Functions', + id: FunctionStepsSpec.FETCH_CLOUD_FUNCTIONS.id, + name: FunctionStepsSpec.FETCH_CLOUD_FUNCTIONS.name, dependsOn: [], - entities: [ - { - resourceName: 'Cloud Function', - _type: CLOUD_FUNCTION_ENTITY_TYPE, - _class: CLOUD_FUNCTION_ENTITY_CLASS, - }, - ], + entities: [FunctionEntitiesSpec.CLOUD_FUNCTION], relationships: [], executionHandler: fetchCloudFunctions, }, { - id: STEP_CLOUD_FUNCTIONS_SERVICE_ACCOUNT_RELATIONSHIPS, - name: 'Cloud Function Service Account Relationships', - dependsOn: [STEP_CLOUD_FUNCTIONS, STEP_IAM_SERVICE_ACCOUNTS], + id: FunctionStepsSpec.CLOUD_FUNCTIONS_SERVICE_ACCOUNT_RELATIONSHIPS.id, + name: FunctionStepsSpec.CLOUD_FUNCTIONS_SERVICE_ACCOUNT_RELATIONSHIPS.name, + dependsOn: [ + FunctionStepsSpec.FETCH_CLOUD_FUNCTIONS.id, + STEP_IAM_SERVICE_ACCOUNTS, + ], entities: [], relationships: [ - { - _class: RelationshipClass.USES, - _type: RELATIONSHIP_TYPE_CLOUD_FUNCTION_USES_IAM_SERVICE_ACCOUNT, - sourceType: CLOUD_FUNCTION_ENTITY_TYPE, - targetType: IAM_SERVICE_ACCOUNT_ENTITY_TYPE, - }, + FunctionsRelationshipsSpec.GOOGLE_CLOUD_FUNCTION_USES_IAM_SERVICE_ACCOUNT, ], executionHandler: buildCloudFunctionServiceAccountRelationships, }, + { + id: FunctionStepsSpec.CLOUD_FUNCTIONS_SOURCE_REPO_RELATIONSHIP.id, + name: FunctionStepsSpec.CLOUD_FUNCTIONS_SOURCE_REPO_RELATIONSHIP.name, + dependsOn: [ + FunctionStepsSpec.FETCH_CLOUD_FUNCTIONS.id, + CloudSourceRepositoriesStepsSpec.FETCH_REPOSITORIES.id, + ], + entities: [], + relationships: [ + FunctionsRelationshipsSpec.GOOGLE_CLOUD_FUNCTION_USES_SOURCE_REPOSITORY, + ], + executionHandler: buildCloudFunctionSourceRepoRelationships, + }, ]; diff --git a/test/mocks.ts b/test/mocks.ts index 6a648a2e..adcfb2f4 100644 --- a/test/mocks.ts +++ b/test/mocks.ts @@ -1287,6 +1287,9 @@ export function getMockCloudFunction( httpsTrigger: { url: 'https://us-central1-j1-gc-integration-dev-v2.cloudfunctions.net/j1-gc-integration-dev-v2testfunction', }, + sourceRepository: { + url: 'https://source.developers.google.com/projects/j1-gc-integration-dev-v3/repos/gaston/moveable-aliases/master/paths/', + }, status: 'ACTIVE', entryPoint: 'handler', timeout: '60s',