diff --git a/frontend/src/app/app.service.ts b/frontend/src/app/app.service.ts index 40e0e5e6..43ecdec8 100644 --- a/frontend/src/app/app.service.ts +++ b/frontend/src/app/app.service.ts @@ -267,6 +267,10 @@ export class AppService { ); } + purgeJira(assetId: number) { + return this.http.delete(`${this.api}/asset/jira/${assetId}`); + } + /** * Function is responsible for fetching assets * @param assetId asset ID being requested diff --git a/frontend/src/app/asset-form/asset-form.component.html b/frontend/src/app/asset-form/asset-form.component.html index f0c79860..bf0a9ffb 100644 --- a/frontend/src/app/asset-form/asset-form.component.html +++ b/frontend/src/app/asset-form/asset-form.component.html @@ -3,13 +3,23 @@
- - - - - - +
+
+
+
Jira Integration
+
+
+ + + + + + +
+ +
+
- + \ No newline at end of file diff --git a/frontend/src/app/asset-form/asset-form.component.ts b/frontend/src/app/asset-form/asset-form.component.ts index 3dc4b33e..f3ad1def 100644 --- a/frontend/src/app/asset-form/asset-form.component.ts +++ b/frontend/src/app/asset-form/asset-form.component.ts @@ -63,9 +63,9 @@ export class AssetFormComponent implements OnInit, OnChanges { rebuildForm() { this.assetForm.reset({ name: this.assetModel.name, - jiraApiKey: this.assetModel.jiraApiKey, - jiraHost: this.assetModel.jiraHost, - jiraUsername: this.assetModel.jiraUsername, + jiraApiKey: this.assetModel?.jira?.apiKey, + jiraHost: this.assetModel?.jira?.host, + jiraUsername: this.assetModel?.jira?.username, }); } @@ -87,6 +87,23 @@ export class AssetFormComponent implements OnInit, OnChanges { this.route.navigate([`organization/${this.orgId}`]); } + purgeJiraInfo() { + const r = confirm( + `Purge API Key for username: "${this.assetModel.jira.username}"?` + ); + if (r) { + this.appService.purgeJira(this.assetForm['']).subscribe((res: string) => { + this.alertService.success(res); + this.appService + .getAsset(this.assetId, this.orgId) + .subscribe((asset: Asset) => { + this.assetModel = asset; + this.rebuildForm(); + }); + }); + } + } + /** * Function responsible for creating or updating an asset tied to * an organization diff --git a/src/app.ts b/src/app.ts index 85016cd3..bccbe4fb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -70,6 +70,7 @@ createConnection().then((_) => { app.patch('/api/organization/:id/asset/:assetId', jwtMiddleware.checkToken, assetController.updateAssetById); app.patch('/api/asset/archive/:assetId', jwtMiddleware.checkToken, assetController.archiveAssetById); app.patch('/api/asset/activate/:assetId', jwtMiddleware.checkToken, assetController.activateAssetById); + app.delete('/api/asset/jira/:assetId', jwtMiddleware.checkToken, assetController.purgeJiraInfo); app.get('/api/assessment/:id', jwtMiddleware.checkToken, assessmentController.getAssessmentsByAssetId); app.get('/api/assessment/:id/vulnerability', jwtMiddleware.checkToken, assessmentController.getAssessmentVulns); app.post('/api/assessment', jwtMiddleware.checkToken, assessmentController.createAssessment); diff --git a/src/entity/Asset.ts b/src/entity/Asset.ts index 62ece76f..cc1e70ae 100644 --- a/src/entity/Asset.ts +++ b/src/entity/Asset.ts @@ -1,7 +1,8 @@ -import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, OneToMany } from 'typeorm'; +import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, OneToMany, OneToOne, JoinColumn } from 'typeorm'; import { Organization } from './Organization'; import { Assessment } from './Assessment'; -import { IsIn, IsUrl } from 'class-validator'; +import { IsIn } from 'class-validator'; +import { Jira } from './Jira'; @Entity() export class Asset { @@ -12,15 +13,10 @@ export class Asset { @Column() @IsIn(['A', 'AH']) status: string; - @Column() - @IsUrl() - jiraHost: string; - @Column() - jiraApiKey: string; - @Column() - jiraUsername?: string; @ManyToOne((type) => Organization, (organization) => organization.asset) organization: Organization; @OneToMany((type) => Assessment, (assessment) => assessment.asset) assessment: Assessment[]; + @OneToOne((type) => Jira, (jira) => jira.asset) + jira: Jira; } diff --git a/src/entity/Jira.ts b/src/entity/Jira.ts new file mode 100644 index 00000000..2654ac4d --- /dev/null +++ b/src/entity/Jira.ts @@ -0,0 +1,19 @@ +import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm'; +import { IsUrl } from 'class-validator'; +import { Asset } from './Asset'; + +@Entity() +export class Jira { + @PrimaryGeneratedColumn() + id: number; + @Column() + @IsUrl() + host: string; + @Column() + apiKey: string; + @Column() + username: string; + @OneToOne((type) => Asset, (asset) => asset.jira) + @JoinColumn() + asset: Asset; +} diff --git a/src/entity/Organization.ts b/src/entity/Organization.ts index 3639cd7a..3f9a3311 100644 --- a/src/entity/Organization.ts +++ b/src/entity/Organization.ts @@ -1,7 +1,7 @@ import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn, OneToMany } from 'typeorm'; import { File } from './File'; import { Asset } from './Asset'; -import { IsUrl, IsIn, MaxLength, IsAlpha, IsDecimal } from 'class-validator'; +import { IsIn } from 'class-validator'; @Entity() export class Organization { @@ -12,7 +12,7 @@ export class Organization { @Column() @IsIn(['A', 'AH']) status: string; - @OneToOne((type) => File, { onDelete: 'CASCADE' }) + @OneToOne((type) => File) @JoinColumn() avatar: number; @OneToMany((type) => Asset, (asset) => asset.organization) diff --git a/src/routes/asset.controller.ts b/src/routes/asset.controller.ts index 790f75a3..92892a0f 100644 --- a/src/routes/asset.controller.ts +++ b/src/routes/asset.controller.ts @@ -1,11 +1,12 @@ import { UserRequest } from '../interfaces/user-request.interface'; -import { Response, Request } from 'express'; -import { getConnection } from 'typeorm'; +import { Response, Request, response } from 'express'; +import { getConnection, AdvancedConsoleLogger } from 'typeorm'; import { Asset } from '../entity/Asset'; import { validate } from 'class-validator'; import { Organization } from '../entity/Organization'; import { status } from '../enums/status-enum'; import { encrypt } from '../utilities/crypto.utility'; +import { Jira } from '../entity/Jira'; /** * @description Get organization assets * @param {UserRequest} req @@ -72,23 +73,15 @@ export const createAsset = async (req: UserRequest, res: Response) => { return res.status(400).send('Asset is not valid'); } const asset = new Asset(); - if (req.body.jiraUsername || req.body.jiraHost || req.body.jiraApiKey) { - if (!req.body.jiraUsername || !req.body.jiraHost || !req.body.jiraApiKey) { - return res.status(400).send('JIRA integration requires username, host, and API key.'); - } else { - asset.jiraApiKey = encrypt(req.body.jiraApiKey); - asset.jiraHost = req.body.jiraHost; - asset.jiraUsername = req.body.jiraUsername; - } - } else { - asset.jiraApiKey = null; - asset.jiraHost = null; - asset.jiraUsername = null; + try { + await addJiraIntegration(req.body.jiraUsername, req.body.jiraHost, req.body.jiraApiKey, asset); + } catch (err) { + res.status(400).json('JIRA integration validation failed'); } asset.name = req.body.name; asset.organization = org; asset.status = status.active; - const errors = await validate(asset, { skipMissingProperties: true }); + const errors = await validate(asset); if (errors.length > 0) { res.status(400).send('Asset form validation failed'); } else { @@ -96,6 +89,45 @@ export const createAsset = async (req: UserRequest, res: Response) => { res.status(200).json('Asset saved successfully'); } }; +/** + * @description Purge JIRA by asset ID + * @param {UserRequest} req + * @param {Response} res + * @returns success/error message + */ +export const purgeJiraInfo = async (req: Request, res: Response) => { + if (!req.params.assetId) { + res.status(400).json('Asset ID is not valid'); + } + if (isNaN(+req.params.assetId)) { + res.status(400).json('Asset ID is not valid'); + } + const asset = await getConnection() + .getRepository(Asset) + .findOne(req.params.assetId, { relations: ['jira'] }); + await getConnection().getRepository(Jira).delete(asset.jira); + res.status(200).json('The API Key has been purged successfully'); +}; +const addJiraIntegration = (username: string, host: string, apiKey: string, asset: Asset): Promise => { + return new Promise(async (resolve, reject) => { + console.log(apiKey); + apiKey = encrypt(apiKey); + const jiraInit: Jira = { + id: null, + username, + host, + apiKey, + asset + }; + const errors = await validate(jiraInit); + if (errors.length > 0) { + reject('JIRA integration requires username, host, and API key.'); + } else { + const jiraResult = await getConnection().getRepository(Jira).save(jiraInit); + resolve(jiraResult); + } + }); +}; /** * @description Get asset by ID * @param {UserRequest} req @@ -109,11 +141,15 @@ export const getAssetById = async (req: UserRequest, res: Response) => { if (!req.params.assetId) { return res.status(400).send('Invalid Asset Request'); } - const asset = await getConnection().getRepository(Asset).findOne(req.params.assetId); - delete asset.jiraApiKey; + const asset = await getConnection() + .getRepository(Asset) + .findOne(req.params.assetId, { relations: ['jira'] }); if (!asset) { return res.status(404).send('Asset does not exist'); } + if (asset.jira) { + delete asset.jira.apiKey; + } res.status(200).json(asset); }; @@ -134,18 +170,10 @@ export const updateAssetById = async (req: UserRequest, res: Response) => { if (!req.body.name) { return res.status(400).json('Asset name is not valid'); } - if (req.body.jiraUsername || req.body.jiraHost || req.body.jiraApiKey) { - if (!req.body.jiraUsername || !req.body.jiraHost || !req.body.jiraApiKey) { - return res.status(400).send('JIRA integration requires username, host, and API key.'); - } else { - asset.jiraApiKey = encrypt(req.body.jiraApiKey); - asset.jiraHost = req.body.jiraHost; - asset.jiraUsername = req.body.jiraUsername; - } - } else { - asset.jiraApiKey = null; - asset.jiraHost = null; - asset.jiraUsername = null; + try { + await addJiraIntegration(req.body.jiraUsername, req.body.jiraHost, req.body.jiraApiKey, asset); + } catch (err) { + res.status(400).json('JIRA integration validation failed'); } asset.name = req.body.name; const errors = await validate(asset, { skipMissingProperties: true }); diff --git a/src/routes/vulnerability.controller.ts b/src/routes/vulnerability.controller.ts index 537a1e9c..d0830fe8 100644 --- a/src/routes/vulnerability.controller.ts +++ b/src/routes/vulnerability.controller.ts @@ -287,6 +287,7 @@ export const exportToJira = async (req: UserRequest, res: Response) => { if (!assessment.jiraId) { return res.status(400).json('Unable to create JIRA ticket. Assessment requires JIRA URL.'); } + /* if (!(assessment.asset.jiraApiKey || assessment.asset.jiraHost || assessment.asset.jiraUsername)) { return res.status(400).json('Unable to create JIRA ticket. Please provide JIRA credentials to the parent Asset.'); } @@ -295,11 +296,12 @@ export const exportToJira = async (req: UserRequest, res: Response) => { host: assessment.asset.jiraHost, username: assessment.asset.jiraUsername }; + */ try { - const result = await addNewVulnIssue(vuln, jiraInit); - vuln.jiraId = `https://${process.env.JIRA_HOST}/browse/${result.key}`; + // const result = await addNewVulnIssue(vuln, jiraInit); + // vuln.jiraId = `https://${process.env.JIRA_HOST}/browse/${result.key}`; await getConnection().getRepository(Vulnerability).save(vuln); - return res.status(200).json(result.message); + return res.status(200).json('result.message'); } catch (err) { return res.status(404).json(err); }