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 @@
-
+
\ 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);
}