Skip to content

Commit

Permalink
feat(create jira table): broke apart Asset table and moved JIRA infor…
Browse files Browse the repository at this point in the history
…mation to JIRA table

A JIRA table was created to contain all JIRA information.  This table has a bidirectional one-to-one
relation with Asset (parent)

BREAKING CHANGE: Added JIRA table

feat #179
  • Loading branch information
alejandrosaenz117 committed Aug 11, 2020
1 parent ec99949 commit 7d31d45
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 54 deletions.
4 changes: 4 additions & 0 deletions frontend/src/app/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 18 additions & 8 deletions frontend/src/app/asset-form/asset-form.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@
<div class="form-group">
<label for="assetName">Asset Name</label>
<input formControlName="name" type="text" class="form-control" id="assetName" />
<label for="assetName">JIRA Username</label>
<input formControlName="jiraUsername" type="text" class="form-control" id="jiraUsername" />
<label for="assetName">JIRA Host</label>
<input formControlName="jiraHost" type="text" class="form-control" id="jiraHost" />
<label for="assetName">JIRA API Key</label>
<input formControlName="jiraApiKey" type="password" class="form-control" id="jiraApiKey"
[placeholder]="keyPlaceholder" />
<div class="card">
<div class="card-body">
<div class="col-md-12 text-center">
<h5>Jira Integration</h5>
<hr />
</div>
<label for="assetName">JIRA Username</label>
<input formControlName="jiraUsername" type="text" class="form-control" id="jiraUsername" />
<label for="assetName">JIRA Host</label>
<input formControlName="jiraHost" type="text" class="form-control" id="jiraHost" />
<label for="assetName">JIRA API Key</label>
<input formControlName="jiraApiKey" type="password" class="form-control" id="jiraApiKey"
[placeholder]="keyPlaceholder" />
<br>
<button type="button" class="float-right btn btn-warning" (click)="purgeJiraInfo()">Purge</button>
</div>
</div>
</div>
<button [disabled]="!assetForm.valid" class="btn btn-primary float-right" type="submit" data-toggle="tooltip"
data-placement="bottom" title="Submit">
Expand All @@ -20,4 +30,4 @@
Back to Assets
</button>
</form>
</div>
</div>
23 changes: 20 additions & 3 deletions frontend/src/app/asset-form/asset-form.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}

Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 5 additions & 9 deletions src/entity/Asset.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
}
19 changes: 19 additions & 0 deletions src/entity/Jira.ts
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 2 additions & 2 deletions src/entity/Organization.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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)
Expand Down
86 changes: 57 additions & 29 deletions src/routes/asset.controller.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -72,30 +73,61 @@ 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 {
await getConnection().getRepository(Asset).save(asset);
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<Jira> => {
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
Expand All @@ -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);
};

Expand All @@ -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 });
Expand Down
8 changes: 5 additions & 3 deletions src/routes/vulnerability.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}
Expand All @@ -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);
}
Expand Down

0 comments on commit 7d31d45

Please sign in to comment.