Skip to content
This repository has been archived by the owner on Aug 16, 2024. It is now read-only.

Commit

Permalink
Merge pull request #34 from Creativice-Oy/feature/findings
Browse files Browse the repository at this point in the history
INT-5777 - Ingest findings and account
MadOx710 authored Jan 25, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents c0ee632 + 63bfdf6 commit 6237a99
Showing 26 changed files with 2,750 additions and 2,104 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [12.x]
node-version: [14.x]
os: [ubuntu-latest]

steps:
@@ -34,17 +34,17 @@ jobs:
release:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
if: github.ref == 'refs/heads/main'
strategy:
fail-fast: false
matrix:
node: [12]
node: [14]

steps:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 12.x
node-version: 14.x

- name: Check out repo
uses: actions/checkout@v2
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -8,6 +8,26 @@ and this project adheres to

## [Unreleased]

### Added

- Updated SDK versions to v8

- Entities:

| Resources | Entity `_type` | Entity `_class` |
| --------- | ------------------- | --------------- |
| Account | `sonarqube_account` | `Account` |
| Finding | `sonarqube_finding` | `Finding` |

- Relationships:

| Source Entity `_type` | Relationship `_class` | Target Entity `_type` |
| --------------------- | --------------------- | ---------------------- |
| `sonarqube_account` | **HAS** | `sonarqube_project` |
| `sonarqube_account` | **HAS** | `sonarqube_user` |
| `sonarqube_account` | **HAS** | `sonarqube_user_group` |
| `sonarqube_project` | **HAS** | `sonarqube_finding` |

## 0.1.0 2021-04-29

### Added
12 changes: 9 additions & 3 deletions docs/jupiterone.md
Original file line number Diff line number Diff line change
@@ -85,6 +85,8 @@ The following entities are created:

| Resources | Entity `_type` | Entity `_class` |
| --------- | ---------------------- | --------------- |
| Account | `sonarqube_account` | `Account` |
| Finding | `sonarqube_finding` | `Finding` |
| Project | `sonarqube_project` | `Project` |
| User | `sonarqube_user` | `User` |
| UserGroup | `sonarqube_user_group` | `UserGroup` |
@@ -93,9 +95,13 @@ The following entities are created:

The following relationships are created:

| Source Entity `_type` | Relationship `_class` | Target Entity `_type` |
| ---------------------- | --------------------- | --------------------- |
| `sonarqube_user_group` | **HAS** | `sonarqube_user` |
| Source Entity `_type` | Relationship `_class` | Target Entity `_type` |
| ---------------------- | --------------------- | ---------------------- |
| `sonarqube_account` | **HAS** | `sonarqube_project` |
| `sonarqube_account` | **HAS** | `sonarqube_user` |
| `sonarqube_account` | **HAS** | `sonarqube_user_group` |
| `sonarqube_project` | **HAS** | `sonarqube_finding` |
| `sonarqube_user_group` | **HAS** | `sonarqube_user` |

<!--
********************************************************************************
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -21,16 +21,16 @@
"test": "jest",
"test:ci": "yarn lint && yarn type-check && yarn test",
"build": "tsc -p tsconfig.dist.json --declaration",
"prepush": "yarn lint && yarn type-check && jest --changedSince master",
"prepush": "yarn lint && yarn type-check && jest --changedSince main",
"prepack": "yarn build"
},
"peerDependencies": {
"@jupiterone/integration-sdk-core": "^6.0.0"
"@jupiterone/integration-sdk-core": "^8.30.0"
},
"devDependencies": {
"@jupiterone/integration-sdk-core": "^6.0.0",
"@jupiterone/integration-sdk-dev-tools": "^6.0.0",
"@jupiterone/integration-sdk-testing": "^6.0.0",
"@jupiterone/integration-sdk-core": "^8.30.0",
"@jupiterone/integration-sdk-dev-tools": "^8.30.0",
"@jupiterone/integration-sdk-testing": "^8.30.0",
"@types/node-fetch": "^2.5.10"
},
"dependencies": {
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@
},
"entries": [
{
"_id": "5ea0703e163d12c3dbab7a6a760de85c",
"_id": "1a1a0b0103438ff790fea2cc0dba1da4",
"_order": 0,
"cache": {},
"request": {
@@ -45,7 +45,7 @@
"value": "example.com"
}
],
"headersSize": 292,
"headersSize": 296,
"httpVersion": "HTTP/1.1",
"method": "GET",
"queryString": [],
@@ -60,13 +60,9 @@
},
"cookies": [],
"headers": [
{
"name": "accept-ranges",
"value": "bytes"
},
{
"name": "age",
"value": "16484"
"value": "1205"
},
{
"name": "cache-control",
@@ -78,19 +74,19 @@
},
{
"name": "date",
"value": "Wed, 28 Apr 2021 17:40:33 GMT"
"value": "Wed, 21 Dec 2022 04:10:52 GMT"
},
{
"name": "expires",
"value": "Wed, 05 May 2021 17:40:33 GMT"
"value": "Wed, 28 Dec 2022 04:10:52 GMT"
},
{
"name": "last-modified",
"value": "Wed, 28 Apr 2021 13:05:49 GMT"
"value": "Wed, 21 Dec 2022 03:50:47 GMT"
},
{
"name": "server",
"value": "ECS (agb/52B2)"
"value": "ECS (oxr/830D)"
},
{
"name": "vary",
@@ -109,22 +105,22 @@
"value": "close"
}
],
"headersSize": 359,
"headersSize": 336,
"httpVersion": "HTTP/1.1",
"redirectURL": "",
"status": 404,
"statusText": "Not Found"
},
"startedDateTime": "2021-04-28T17:40:33.678Z",
"time": 41,
"startedDateTime": "2022-12-21T04:10:52.586Z",
"time": 385,
"timings": {
"blocked": -1,
"connect": -1,
"dns": -1,
"receive": 0,
"send": 0,
"ssl": -1,
"wait": 41
"wait": 385
}
}
],
13 changes: 13 additions & 0 deletions src/provider/SonarqubeClient.ts
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import {
ValidationResponse,
SonarqubeUserGroup,
SonarqubeUser,
SonarqubeFinding,
} from './types';

/**
@@ -77,6 +78,18 @@ export class SonarqubeClient {
);
}

async iterateProjectFindings(
iteratee: ResourceIteratee<SonarqubeFinding>,
params?: NodeJS.Dict<string | string[]>,
): Promise<void> {
return this.iterateResources<'issues', SonarqubeFinding>(
'/issues/search',
'issues',
iteratee,
params,
);
}

async iterateGroupsAssignedToUser(
login: string,
iteratee: ResourceIteratee<SonarqubeUserGroup>,
27 changes: 27 additions & 0 deletions src/provider/types.ts
Original file line number Diff line number Diff line change
@@ -29,6 +29,33 @@ export interface SonarqubeUser {
avatar: string;
}

export interface SonarqubeFinding {
key: string;
rule: string;
severity: string;
component: string;
project: string;
line: number;
hash: string;
textRange: {
startLine: number;
endLine: number;
startOffset: number;
endOffset: number;
};
status: string;
message: string;
effort: string;
debt: string;
author: string;
tags: string[];
creationDate: string;
updateDate: string;
type: string;
scope: string;
quickFixAvailable: boolean;
}

export interface Pagination {
pageIndex: number;
pageSize: number;
23 changes: 23 additions & 0 deletions src/steps/account/converter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createAccountEntity } from './converter';
import { createMockStepExecutionContext } from '@jupiterone/integration-sdk-testing';

describe('#createAccountEntity', () => {
test('should convert to entity', () => {
const context = createMockStepExecutionContext({
instanceConfig: {
baseUrl: process.env.BASE_URL || 'http://localhost:9000',
apiToken: process.env.API_TOKEN || 'string-value',
},
});

const entity = createAccountEntity(context.instance);

expect(entity).toEqual(
expect.objectContaining({
_key: 'sonarqube_account',
_type: 'sonarqube_account',
_class: ['Account'],
}),
);
});
});
28 changes: 28 additions & 0 deletions src/steps/account/converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
createIntegrationEntity,
Entity,
IntegrationInstance,
} from '@jupiterone/integration-sdk-core';
import { SonarqubeIntegrationConfig } from '../../types';

import { Entities } from '../constants';

export function createAccountEntity(
instance: IntegrationInstance<SonarqubeIntegrationConfig>,
): Entity {
const { id, name } = instance;
return createIntegrationEntity({
entityData: {
source: {
name,
},
assign: {
_key: 'sonarqube_account',
_type: Entities.ACCOUNT._type,
_class: Entities.ACCOUNT._class,
id,
name,
},
},
});
}
51 changes: 51 additions & 0 deletions src/steps/account/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
createMockStepExecutionContext,
Recording,
setupRecording,
} from '@jupiterone/integration-sdk-testing';
import { fetchAccount } from '.';

describe('#fetchAccount', () => {
let recording: Recording;

afterEach(async () => {
await recording.stop();
});

test('should collect data', async () => {
recording = setupRecording({
directory: __dirname,
name: 'fetchProjectsShouldCollectData',
options: {
matchRequestsBy: {
url: {
hostname: false,
},
},
},
});

const context = createMockStepExecutionContext({
instanceConfig: {
baseUrl: process.env.BASE_URL || 'http://localhost:9000',
apiToken: process.env.API_TOKEN || 'string-value',
},
});
await fetchAccount(context);

expect(context.jobState.collectedEntities).toHaveLength(1);
expect(context.jobState.collectedRelationships).toHaveLength(0);
expect(context.jobState.collectedEntities).toMatchGraphObjectSchema({
_class: ['Account'],
schema: {
additionalProperties: true,
properties: {
_type: { const: 'sonarqube_account' },
_key: { type: 'string' },
name: { type: 'string' },
id: { type: 'string' },
},
},
});
});
});
26 changes: 26 additions & 0 deletions src/steps/account/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
IntegrationStep,
IntegrationStepExecutionContext,
} from '@jupiterone/integration-sdk-core';

import { ACCOUNT_ENTITY_KEY, Entities, Steps } from '../constants';
import { createAccountEntity } from './converter';
import { SonarqubeIntegrationConfig } from '../../types';

export async function fetchAccount({
instance,
jobState,
}: IntegrationStepExecutionContext<SonarqubeIntegrationConfig>) {
const accountEntity = await jobState.addEntity(createAccountEntity(instance));
await jobState.setData(ACCOUNT_ENTITY_KEY, accountEntity);
}

export const accountSteps: IntegrationStep<SonarqubeIntegrationConfig>[] = [
{
id: Steps.ACCOUNT,
name: 'Fetch Account',
entities: [Entities.ACCOUNT],
executionHandler: fetchAccount,
relationships: [],
},
];
38 changes: 38 additions & 0 deletions src/steps/constants.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { RelationshipClass } from '@jupiterone/integration-sdk-core';

export const ACCOUNT_ENTITY_KEY = 'sonarqube:account';

export const Steps = {
ACCOUNT: 'fetch-account',
PROJECTS: 'fetch-projects',
USER_GROUPS: 'fetch-user-groups',
USERS: 'fetch-users',
BUILD_USER_GROUP_HAS_USER: 'build-user-group-has-user',
FINDINGS: 'fetch-findings',
};

export const Entities = {
ACCOUNT: {
resourceName: 'Account',
_type: 'sonarqube_account',
_class: ['Account'],
},
PROJECT: {
resourceName: 'Project',
_type: 'sonarqube_project',
@@ -23,6 +32,11 @@ export const Entities = {
_type: 'sonarqube_user',
_class: ['User'],
},
FINDING: {
resourceName: 'Finding',
_type: 'sonarqube_finding',
_class: ['Finding'],
},
};

export const Relationships = {
@@ -32,4 +46,28 @@ export const Relationships = {
_class: RelationshipClass.HAS,
targetType: Entities.USER._type,
},
ACCOUNT_HAS_PROJECT: {
_type: 'sonarqube_account_has_project',
sourceType: Entities.ACCOUNT._type,
_class: RelationshipClass.HAS,
targetType: Entities.PROJECT._type,
},
ACCOUNT_HAS_USER: {
_type: 'sonarqube_account_has_user',
sourceType: Entities.ACCOUNT._type,
_class: RelationshipClass.HAS,
targetType: Entities.USER._type,
},
ACCOUNT_HAS_USER_GROUP: {
_type: 'sonarqube_account_has_user_group',
sourceType: Entities.ACCOUNT._type,
_class: RelationshipClass.HAS,
targetType: Entities.USER_GROUP._type,
},
PROJECT_HAS_FINDING: {
_type: 'sonarqube_project_has_finding',
sourceType: Entities.PROJECT._type,
_class: RelationshipClass.HAS,
targetType: Entities.FINDING._type,
},
};

Large diffs are not rendered by default.

101 changes: 101 additions & 0 deletions src/steps/finding/converter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { createFindingEntity } from './converter';
import { SonarqubeFinding } from '../../provider/types';

describe('#createFindingEntity', () => {
test('should convert to entity', () => {
const project = {
key: '12345',
rule: 'javascript:S1234',
severity: 'CRITICAL',
component: '12345',
project: 'Test12345hvhIyiTjXiMI4DI',
line: 235,
hash: '81234598a9eac8',
textRange: {
startLine: 235,
endLine: 235,
startOffset: 2,
endOffset: 110,
},
flows: [],
status: 'OPEN',
message: 'Unexpected var, use let or const instead.',
effort: '5min',
debt: '5min',
author: 'sns@vuln.in',
tags: ['bad-practice', 'es2015'],
creationDate: '2008-07-31T16:52:52+0000',
updateDate: '2012-12-15T02:41:31+0000',
type: 'CODE_SMELL',
scope: 'MAIN',
quickFixAvailable: true,
} as SonarqubeFinding;

const entity = createFindingEntity(project);

expect(entity).toEqual(
expect.objectContaining({
_class: ['Finding'],
_key: 'sonarqube-finding:12345',
_rawData: [
{
name: 'default',
rawData: {
author: 'sns@vuln.in',
component: '12345',
creationDate: '2008-07-31T16:52:52+0000',
debt: '5min',
effort: '5min',
flows: [],
hash: '81234598a9eac8',
key: '12345',
line: 235,
message: 'Unexpected var, use let or const instead.',
project: 'Test12345hvhIyiTjXiMI4DI',
quickFixAvailable: true,
rule: 'javascript:S1234',
scope: 'MAIN',
severity: 'CRITICAL',
status: 'OPEN',
textRange: {
endLine: 235,
endOffset: 110,
startLine: 235,
startOffset: 2,
},
type: 'CODE_SMELL',
updateDate: '2012-12-15T02:41:31+0000',
},
},
],
_type: 'sonarqube_finding',
active: undefined,
author: 'sns@vuln.in',
category: 'CODE_SMELL',
component: '12345',
createdOn: 1217523172000,
debt: '5min',
displayName: '81234598a9eac8',
effort: '5min',
hash: '81234598a9eac8',
key: '12345',
line: 235,
message: 'Unexpected var, use let or const instead.',
name: '81234598a9eac8',
numericSeverity: 8,
open: true,
project: 'Test12345hvhIyiTjXiMI4DI',
quickFixAvailable: true,
rule: 'javascript:S1234',
scope: 'MAIN',
severity: 'critical',
status: 'OPEN',
textRangeEndLine: 235,
textRangeEndOffset: 110,
textRangeStartLine: 235,
textRangeStartOffset: 2,
updatedOn: 1355539291000,
}),
);
});
});
70 changes: 70 additions & 0 deletions src/steps/finding/converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
createIntegrationEntity,
Entity,
parseTimePropertyValue,
} from '@jupiterone/integration-sdk-core';

import { Entities } from '../constants';
import { SonarqubeFinding } from '../../provider/types';

const FINDING_KEY_PREFIX = 'sonarqube-finding';
export function createFindingEntityIdentifier(key: string): string {
return `${FINDING_KEY_PREFIX}:${key}`;
}

const severityToNumericSeverity = (severity: string): number => {
switch (severity) {
case 'BLOCKER':
return 10;
case 'CRITICAL':
return 8;
case 'MAJOR':
return 6;
case 'MINOR':
return 4;
case 'INFO':
return 2;
default:
return 0;
}
};

export function createFindingEntity(finding: SonarqubeFinding): Entity {
// console.log("finding", finding)
const { tags, ...rest } = finding;

return createIntegrationEntity({
entityData: {
source: rest,
assign: {
_key: createFindingEntityIdentifier(finding.key),
_type: Entities.FINDING._type,
_class: Entities.FINDING._class,
key: finding.key,
name: finding.hash,
rule: finding.rule,
severity: finding.severity.toLowerCase(),
component: finding.component,
project: finding.project,
line: finding.line,
hash: finding.hash,
textRangeStartLine: finding.textRange.startLine,
textRangeEndLine: finding.textRange.endLine,
textRangeStartOffset: finding.textRange.startOffset,
textRangeEndOffset: finding.textRange.endOffset,
status: finding.status,
message: finding.message,
effort: finding.effort,
debt: finding.debt,
author: finding.author,
createdOn: parseTimePropertyValue(finding.creationDate),
updatedOn: parseTimePropertyValue(finding.updateDate),
scope: finding.scope,
quickFixAvailable: finding.quickFixAvailable,
category: finding.type,
open: finding.status === 'OPEN',
numericSeverity: severityToNumericSeverity(finding.severity),
},
},
});
}
68 changes: 68 additions & 0 deletions src/steps/finding/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
createMockStepExecutionContext,
Recording,
setupRecording,
} from '@jupiterone/integration-sdk-testing';
import { fetchFindings } from '.';
import { fetchAccount } from '../account';
import { fetchProjects } from '../project';

describe('#fetchFindings', () => {
let recording: Recording;

afterEach(async () => {
await recording.stop();
});

test('should collect data', async () => {
recording = setupRecording({
directory: __dirname,
name: 'fetchFindingsShouldCollectData',
options: {
matchRequestsBy: {
url: {
hostname: false,
},
},
},
});

const context = createMockStepExecutionContext({
instanceConfig: {
baseUrl: process.env.BASE_URL || 'http://localhost:9000',
apiToken: process.env.API_TOKEN || 'string-value',
},
});

await fetchAccount(context);
await fetchProjects(context);
await fetchFindings(context);

const entities = context.jobState.collectedEntities.filter(
(p) => p._type === 'sonarqube_finding',
);
const relationships = context.jobState.collectedRelationships.filter(
(r) => r._type === 'sonarqube_project_has_finding',
);

expect(entities).toMatchGraphObjectSchema({
_class: ['Finding'],
schema: {
additionalProperties: true,
properties: {
_type: { const: 'sonarqube_finding' },
_key: { type: 'string' },
key: { type: 'string' },
name: { type: 'string' },
_rawData: {
type: 'array',
items: { type: 'object' },
},
},
},
});

expect(entities.length).toBeGreaterThan(0);
expect(relationships.length).toBeGreaterThan(0);
});
});
63 changes: 63 additions & 0 deletions src/steps/finding/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
createDirectRelationship,
getRawData,
IntegrationStep,
IntegrationStepExecutionContext,
RelationshipClass,
} from '@jupiterone/integration-sdk-core';

import { Entities, Relationships, Steps } from '../constants';
import { createSonarqubeClient } from '../../provider';
import { SonarqubeIntegrationConfig } from '../../types';
import { SonarqubeProject } from '../../provider/types';
import { createFindingEntity } from './converter';

export async function fetchFindings({
instance,
jobState,
logger,
}: IntegrationStepExecutionContext<SonarqubeIntegrationConfig>) {
const client = createSonarqubeClient(instance.config);

await jobState.iterateEntities(
{ _type: Entities.PROJECT._type },
async (projectEntity) => {
const project = getRawData<SonarqubeProject>(projectEntity);

if (!project) {
logger.warn(`Can not get raw data for entity ${projectEntity._key}`);
return;
}

await client.iterateProjectFindings(
async (finding) => {
const findingEntity = createFindingEntity(finding);

if (!(await jobState.hasKey(findingEntity._key))) {
await jobState.addEntity(findingEntity);
}

await jobState.addRelationship(
createDirectRelationship({
_class: RelationshipClass.HAS,
from: projectEntity,
to: findingEntity,
}),
);
},
{ componentKeys: project.key },
);
},
);
}

export const findingSteps: IntegrationStep<SonarqubeIntegrationConfig>[] = [
{
id: Steps.FINDINGS,
name: 'Fetch Project Findings',
entities: [Entities.FINDING],
executionHandler: fetchFindings,
relationships: [Relationships.PROJECT_HAS_FINDING],
dependsOn: [Steps.PROJECTS],
},
];
10 changes: 9 additions & 1 deletion src/steps/index.ts
Original file line number Diff line number Diff line change
@@ -7,9 +7,17 @@ import { SonarqubeIntegrationConfig } from '../types';
import { projectSteps } from './project';
import { userGroupSteps } from './user-group';
import { userSteps } from './user';
import { accountSteps } from './account';
import { findingSteps } from './finding';

const integrationSteps: Step<
IntegrationStepExecutionContext<SonarqubeIntegrationConfig>
>[] = [...projectSteps, ...userGroupSteps, ...userSteps];
>[] = [
...accountSteps,
...projectSteps,
...userGroupSteps,
...userSteps,
...findingSteps,
];

export { integrationSteps };
Original file line number Diff line number Diff line change
@@ -42,10 +42,10 @@
},
{
"name": "host",
"value": "localhost:9000"
"value": "34.171.196.173:9000"
}
],
"headersSize": 301,
"headersSize": 315,
"httpVersion": "HTTP/1.1",
"method": "GET",
"queryString": [
@@ -58,14 +58,14 @@
"value": "100"
}
],
"url": "http://localhost:9000/api/projects/search?p=1&ps=100"
"url": "http://34.171.196.173:9000/api/projects/search?p=1&ps=100"
},
"response": {
"bodySize": 365,
"bodySize": 369,
"content": {
"mimeType": "application/json",
"size": 365,
"text": "{\"paging\":{\"pageIndex\":1,\"pageSize\":100,\"total\":2},\"components\":[{\"key\":\"escribirio_integration-test-repo\",\"name\":\"integration-test-repo\",\"qualifier\":\"TRK\",\"visibility\":\"public\",\"lastAnalysisDate\":\"2021-04-28T12:19:58+0000\",\"revision\":\"85444305d6f8efa62f3999b09f8a9a85d0fff02d\"},{\"key\":\"test-project\",\"name\":\"test-project\",\"qualifier\":\"TRK\",\"visibility\":\"public\"}]}"
"size": 369,
"text": "{\"paging\":{\"pageIndex\":1,\"pageSize\":100,\"total\":2},\"components\":[{\"key\":\"TestOrganization-Creativice_dvna_AYUTmhvhIyiTjXiMI4DI\",\"name\":\"dvna\",\"qualifier\":\"TRK\",\"visibility\":\"public\",\"lastAnalysisDate\":\"2022-12-15T02:41:31+0000\",\"revision\":\"fce9293474f66351df698bd3feae17eadd2cb06e\"},{\"key\":\"Test-Project\",\"name\":\"Test Project\",\"qualifier\":\"TRK\",\"visibility\":\"public\"}]}"
},
"cookies": [],
"headers": [
@@ -81,6 +81,10 @@
"name": "x-content-type-options",
"value": "nosniff"
},
{
"name": "sonarqube-authentication-token-expiration",
"value": "2023-01-14T00:00:00+0000"
},
{
"name": "cache-control",
"value": "no-cache, no-store, must-revalidate"
@@ -91,33 +95,33 @@
},
{
"name": "content-length",
"value": "365"
"value": "369"
},
{
"name": "date",
"value": "Thu, 29 Apr 2021 14:56:24 GMT"
"value": "Mon, 19 Dec 2022 08:20:17 GMT"
},
{
"name": "connection",
"value": "close"
}
],
"headersSize": 258,
"headersSize": 327,
"httpVersion": "HTTP/1.1",
"redirectURL": "",
"status": 200,
"statusText": "OK"
},
"startedDateTime": "2021-04-29T14:56:24.600Z",
"time": 28,
"startedDateTime": "2022-12-19T08:20:17.403Z",
"time": 517,
"timings": {
"blocked": -1,
"connect": -1,
"dns": -1,
"receive": 0,
"send": 0,
"ssl": -1,
"wait": 28
"wait": 517
}
},
{
@@ -155,10 +159,10 @@
},
{
"name": "host",
"value": "localhost:9000"
"value": "34.171.196.173:9000"
}
],
"headersSize": 301,
"headersSize": 315,
"httpVersion": "HTTP/1.1",
"method": "GET",
"queryString": [
@@ -171,7 +175,7 @@
"value": "100"
}
],
"url": "http://localhost:9000/api/projects/search?p=2&ps=100"
"url": "http://34.171.196.173:9000/api/projects/search?p=2&ps=100"
},
"response": {
"bodySize": 67,
@@ -194,6 +198,10 @@
"name": "x-content-type-options",
"value": "nosniff"
},
{
"name": "sonarqube-authentication-token-expiration",
"value": "2023-01-14T00:00:00+0000"
},
{
"name": "cache-control",
"value": "no-cache, no-store, must-revalidate"
@@ -208,29 +216,29 @@
},
{
"name": "date",
"value": "Thu, 29 Apr 2021 15:00:35 GMT"
"value": "Mon, 19 Dec 2022 08:21:42 GMT"
},
{
"name": "connection",
"value": "close"
}
],
"headersSize": 257,
"headersSize": 326,
"httpVersion": "HTTP/1.1",
"redirectURL": "",
"status": 200,
"statusText": "OK"
},
"startedDateTime": "2021-04-29T15:00:35.190Z",
"time": 23,
"startedDateTime": "2022-12-19T08:21:42.352Z",
"time": 496,
"timings": {
"blocked": -1,
"connect": -1,
"dns": -1,
"receive": 0,
"send": 0,
"ssl": -1,
"wait": 23
"wait": 496
}
}
],
14 changes: 11 additions & 3 deletions src/steps/project/index.test.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import {
setupRecording,
} from '@jupiterone/integration-sdk-testing';
import { fetchProjects } from '.';
import { fetchAccount } from '../account';

describe('#fetchProjects', () => {
let recording: Recording;
@@ -31,11 +32,18 @@ describe('#fetchProjects', () => {
apiToken: process.env.API_TOKEN || 'string-value',
},
});

await fetchAccount(context);
await fetchProjects(context);

expect(context.jobState.collectedEntities).toHaveLength(2);
expect(context.jobState.collectedRelationships).toHaveLength(0);
expect(context.jobState.collectedEntities).toMatchGraphObjectSchema({
expect(context.jobState.collectedEntities.length).toBeGreaterThan(0);
expect(context.jobState.collectedRelationships.length).toBeGreaterThan(0);

const projectEntities = context.jobState.collectedEntities.filter(
(p) => p._type === 'sonarqube_project',
);

expect(projectEntities).toMatchGraphObjectSchema({
_class: ['Project'],
schema: {
additionalProperties: true,
27 changes: 24 additions & 3 deletions src/steps/project/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import {
createDirectRelationship,
Entity,
IntegrationStep,
IntegrationStepExecutionContext,
Relationship,
RelationshipClass,
} from '@jupiterone/integration-sdk-core';

import { Entities, Steps } from '../constants';
import {
ACCOUNT_ENTITY_KEY,
Entities,
Relationships,
Steps,
} from '../constants';
import { createProjectEntity } from './converter';
import { createSonarqubeClient } from '../../provider';
import { SonarqubeIntegrationConfig } from '../../types';
@@ -13,13 +21,25 @@ export async function fetchProjects({
instance,
jobState,
}: IntegrationStepExecutionContext<SonarqubeIntegrationConfig>) {
const accountEntity = (await jobState.getData(ACCOUNT_ENTITY_KEY)) as Entity;
const client = createSonarqubeClient(instance.config);

const convertedProjects: Entity[] = [];
const relationships: Relationship[] = [];
await client.iterateProjects((project) => {
convertedProjects.push(createProjectEntity(project));
const projectEntity = createProjectEntity(project);

convertedProjects.push(projectEntity);
relationships.push(
createDirectRelationship({
_class: RelationshipClass.HAS,
from: accountEntity,
to: projectEntity,
}),
);
});
await jobState.addEntities(convertedProjects);
await jobState.addRelationships(relationships);
}

export const projectSteps: IntegrationStep<SonarqubeIntegrationConfig>[] = [
@@ -28,6 +48,7 @@ export const projectSteps: IntegrationStep<SonarqubeIntegrationConfig>[] = [
name: 'Projects',
entities: [Entities.PROJECT],
executionHandler: fetchProjects,
relationships: [],
relationships: [Relationships.ACCOUNT_HAS_PROJECT],
dependsOn: [Steps.ACCOUNT],
},
];
14 changes: 11 additions & 3 deletions src/steps/user-group/index.test.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import {
setupRecording,
} from '@jupiterone/integration-sdk-testing';
import { fetchUserGroups } from '.';
import { fetchAccount } from '../account';

describe('#fetchUserGroups', () => {
let recording: Recording;
@@ -31,11 +32,18 @@ describe('#fetchUserGroups', () => {
apiToken: process.env.API_TOKEN || 'string-value',
},
});

await fetchAccount(context);
await fetchUserGroups(context);

expect(context.jobState.collectedEntities).toHaveLength(2);
expect(context.jobState.collectedRelationships).toHaveLength(0);
expect(context.jobState.collectedEntities).toMatchGraphObjectSchema({
expect(context.jobState.collectedEntities.length).toBeGreaterThan(0);
expect(context.jobState.collectedRelationships.length).toBeGreaterThan(0);

const userGroupEntities = context.jobState.collectedEntities.filter(
(p) => p._type === 'sonarqube_user_group',
);

expect(userGroupEntities).toMatchGraphObjectSchema({
_class: ['UserGroup'],
schema: {
additionalProperties: true,
29 changes: 23 additions & 6 deletions src/steps/user-group/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import {
createDirectRelationship,
Entity,
IntegrationStep,
IntegrationStepExecutionContext,
RelationshipClass,
} from '@jupiterone/integration-sdk-core';

import { Entities, Steps } from '../constants';
import {
ACCOUNT_ENTITY_KEY,
Entities,
Relationships,
Steps,
} from '../constants';
import { createUserGroupEntity } from './converter';
import { createSonarqubeClient } from '../../provider';
import { SonarqubeIntegrationConfig } from '../../types';
@@ -13,13 +20,22 @@ export async function fetchUserGroups({
instance,
jobState,
}: IntegrationStepExecutionContext<SonarqubeIntegrationConfig>) {
const accountEntity = (await jobState.getData(ACCOUNT_ENTITY_KEY)) as Entity;
const client = createSonarqubeClient(instance.config);

const convertedUserGroups: Entity[] = [];
await client.iterateUserGroups((userGroup) => {
convertedUserGroups.push(createUserGroupEntity(userGroup));
await client.iterateUserGroups(async (userGroup) => {
const userGroupEntity = await jobState.addEntity(
createUserGroupEntity(userGroup),
);

await jobState.addRelationship(
createDirectRelationship({
_class: RelationshipClass.HAS,
from: accountEntity,
to: userGroupEntity,
}),
);
});
await jobState.addEntities(convertedUserGroups);
}

export const userGroupSteps: IntegrationStep<SonarqubeIntegrationConfig>[] = [
@@ -28,6 +44,7 @@ export const userGroupSteps: IntegrationStep<SonarqubeIntegrationConfig>[] = [
name: 'User Groups',
entities: [Entities.USER_GROUP],
executionHandler: fetchUserGroups,
relationships: [],
relationships: [Relationships.ACCOUNT_HAS_USER_GROUP],
dependsOn: [Steps.ACCOUNT],
},
];
28 changes: 19 additions & 9 deletions src/steps/user/index.test.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import {
import { buildUserGroupUserRelationships, fetchUsers } from '.';
import { fetchUserGroups } from '../user-group';
import { Relationships } from '../constants';
import { fetchAccount } from '../account';

describe('#fetchUsers', () => {
let recording: Recording;
@@ -34,11 +35,18 @@ describe('#fetchUsers', () => {
apiToken: process.env.API_TOKEN || 'string-value',
},
});

await fetchAccount(context);
await fetchUsers(context);

expect(context.jobState.collectedEntities).toHaveLength(2);
expect(context.jobState.collectedRelationships).toHaveLength(0);
expect(context.jobState.collectedEntities).toMatchGraphObjectSchema({
expect(context.jobState.collectedEntities.length).toBeGreaterThan(0);
expect(context.jobState.collectedRelationships.length).toBeGreaterThan(0);

const userEntities = context.jobState.collectedEntities.filter(
(p) => p._type === 'sonarqube_user',
);

expect(userEntities).toMatchGraphObjectSchema({
_class: ['User'],
schema: {
additionalProperties: true,
@@ -90,16 +98,18 @@ describe('#buildUserGroupUserRelationships', () => {
apiToken: process.env.API_TOKEN || 'string-value',
},
});

await fetchAccount(context);
await fetchUsers(context);
await fetchUserGroups(context);
await buildUserGroupUserRelationships(context);

expect(context.jobState.collectedRelationships).toHaveLength(3);
expect(
context.jobState.collectedRelationships.filter(
(r) => r._type === Relationships.GROUP_HAS_USER._type,
),
).toMatchDirectRelationshipSchema({
const userUserGroupRelationships = context.jobState.collectedRelationships.filter(
(r) => r._type === Relationships.GROUP_HAS_USER._type,
);

expect(userUserGroupRelationships.length).toBeGreaterThan(0);
expect(userUserGroupRelationships).toMatchDirectRelationshipSchema({
schema: {
properties: {
_class: { const: RelationshipClass.HAS },
23 changes: 20 additions & 3 deletions src/steps/user/index.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,12 @@ import {
RelationshipClass,
} from '@jupiterone/integration-sdk-core';

import { Entities, Steps, Relationships } from '../constants';
import {
Entities,
Steps,
Relationships,
ACCOUNT_ENTITY_KEY,
} from '../constants';
import { createUserEntity } from './converter';
import { createUserGroupEntityIdentifier } from '../user-group/converter';
import { createSonarqubeClient } from '../../provider';
@@ -30,12 +35,23 @@ export async function fetchUsers({
jobState,
}: IntegrationStepExecutionContext<SonarqubeIntegrationConfig>) {
const client = createSonarqubeClient(instance.config);
const accountEntity = (await jobState.getData(ACCOUNT_ENTITY_KEY)) as Entity;

const convertedUsers: Entity[] = [];
const relationships: Relationship[] = [];
await client.iterateUsers((user) => {
convertedUsers.push(createUserEntity(user));
const userEntity = createUserEntity(user);
convertedUsers.push(userEntity);
relationships.push(
createDirectRelationship({
_class: RelationshipClass.HAS,
from: accountEntity,
to: userEntity,
}),
);
});
await jobState.addEntities(convertedUsers);
await jobState.addRelationships(relationships);
}

export async function buildUserGroupUserRelationships({
@@ -79,7 +95,8 @@ export const userSteps: IntegrationStep<SonarqubeIntegrationConfig>[] = [
name: 'Users',
entities: [Entities.USER],
executionHandler: fetchUsers,
relationships: [Relationships.GROUP_HAS_USER],
relationships: [Relationships.ACCOUNT_HAS_USER],
dependsOn: [Steps.ACCOUNT],
},
{
id: Steps.BUILD_USER_GROUP_HAS_USER,
3,487 changes: 1,458 additions & 2,029 deletions yarn.lock

Large diffs are not rendered by default.

0 comments on commit 6237a99

Please sign in to comment.