Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(runner): raise an error with detailed info about a found issue #106

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 1 addition & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,53 +81,7 @@ $ yarn add @sec-tester/runner \

### Usage examples

Here is an example to check your own application for XSS vulnerabilities:

```ts
import { SecRunner, SecScan } from '@sec-tester/runner';
import { Severity, TestType } from '@sec-tester/scan';

describe('/api', () => {
let runner!: SecRunner;
let scan!: SecScan;

beforeEach(async () => {
runner = new SecRunner({ hostname: 'app.neuralegion.com' });

await runner.init();

scan = runner
.createScan({ tests: [TestType.XSS] })
.threshold(Severity.MEDIUM) // i. e. ignore LOW severity issues
.timeout(300000); // i. e. fail if last longer than 5 minutes
});

afterEach(async () => {
await runner.clear();
});

describe('/orders', () => {
it('should not have persistent xss', async () => {
await scan.run({
method: 'POST',
url: 'https://localhost:8000/api/orders',
body: { subject: 'Test', body: "<script>alert('xss')</script>" }
});
});

it('should not have reflective xss', async () => {
await scan.run({
url: 'https://localhost:8000/api/orders',
query: {
q: `<script>alert('xss')</script>`
}
});
});
});
});
```

Full documentation can be found in [**runner**](https://github.com/NeuraLegion/sec-tester-js/tree/master/packages/runner).
Full configuration & usage examples can be found in our [demo project](https://github.com/NeuraLegion/sec-tester-js-demo).

## Documentation & Help

Expand Down
44 changes: 43 additions & 1 deletion packages/reporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ npm i -s @sec-tester/reporter

## Usage

The package provides only one implementation of the `Reporter` that lets to get results to stdout, i.e. `StdReporter`:
The package provides an implementation of the `Reporter` that lets to get results to stdout, i.e. `StdReporter`:

```ts
import { Reporter, StdReporter } from '@sec-tester/reporter';
Expand All @@ -36,6 +36,48 @@ await reporter.report(scan);

</details>

In addition, the package exposes a `PlainTextFormatter` that implements a `Formatter` interface:

```ts
import { Formatter, PlainTextFormatter } from '@sec-tester/reporter';

const formatter: Formatter = new PlainTextFormatter();
```

To convert an issue into text, you just need to call the `format` method:

```ts
formatter.format(issue);
```

<details>
<summary>Sample output</summary>

```
Issue in Bright UI: https://app.neuralegion.com/scans/djoqtSDRJYaR6sH8pfYpDX/issues/8iacauN1FH9vFvDCLoo42v
Name: Missing Strict-Transport-Security Header
Severity: Low
Remediation:
Make sure to proprely set and configure headers on your application - missing strict-transport-security header
Details:
The engine detected a missing strict-transport-security header. Headers are used to outline communication and
improve security of application.
Extra Details:
● Missing Strict-Transport-Security Header
The engine detected a missing Strict-Transport-Security header, which might cause data to be sent insecurely from the client to the server.
Remedy:
- Make sure to set this header to one of the following options:
1. Strict-Transport-Security: max-age=<expire-time>
2. Strict-Transport-Security: max-age=<expire-time>; includeSubDomains
3. Strict-Transport-Security: max-age=<expire-time>; preload
Resources:
- https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#hsts
Issues found on the following URLs:
- [GET] https://qa.brokencrystals.com/
```

</details>

## License

Copyright © 2022 [Bright Security](https://brightsec.com/).
Expand Down
63 changes: 63 additions & 0 deletions packages/reporter/src/__fixtures__/issues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { HttpMethod, Severity } from '@sec-tester/scan';

export const issueWithoutResourcesText = `Issue in Bright UI: http://app.neuralegion.com/scans/pDzxcEXQC8df1fcz1QwPf9/issues/pDzxcEXQC8df1fcz1QwPf9
Name: Database connection crashed
Severity: Medium
Remediation:
The best way to protect against those kind of issues is making sure the Database resources are sufficient
Details:
Cross-site request forgery is a type of malicious website exploit.`;
export const issueWithoutResources = {
id: 'pDzxcEXQC8df1fcz1QwPf9',
order: 1,
details: 'Cross-site request forgery is a type of malicious website exploit.',
name: 'Database connection crashed',
severity: Severity.MEDIUM,
protocol: 'http',
remedy:
'The best way to protect against those kind of issues is making sure the Database resources are sufficient',
cvss: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L',
time: new Date(),
originalRequest: {
method: HttpMethod.GET,
url: 'https://brokencrystals.com/'
},
request: {
method: HttpMethod.GET,
url: 'https://brokencrystals.com/'
},
link: 'http://app.neuralegion.com/scans/pDzxcEXQC8df1fcz1QwPf9/issues/pDzxcEXQC8df1fcz1QwPf9'
};

export const fullyDescribedIssueText = `${issueWithoutResourcesText}
Extra Details:
● Missing Strict-Transport-Security Header
\tThe engine detected a missing Strict-Transport-Security header, which might cause data to be sent insecurely from the client to the server.
\tLinks:
\t● https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#hsts
References:
● https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#hsts`;
export const fullyDescribedIssue = {
...issueWithoutResources,
comments: [
{
headline: 'Missing Strict-Transport-Security Header',
text: 'The engine detected a missing Strict-Transport-Security header, which might cause data to be sent insecurely from the client to the server.',
links: [
'https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#hsts'
]
}
],
resources: [
'https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#hsts'
]
};
export const issueWithoutExtraInfoText = `${issueWithoutResourcesText}
References:
● https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#hsts`;
export const issueWithoutExtraInfo = {
...issueWithoutResources,
resources: [
'https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#hsts'
]
};
44 changes: 44 additions & 0 deletions packages/reporter/src/formatters/PlainTextFormatter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { PlainTextFormatter } from './PlainTextFormatter';
import {
fullyDescribedIssue,
fullyDescribedIssueText,
issueWithoutExtraInfo,
issueWithoutExtraInfoText,
issueWithoutResources,
issueWithoutResourcesText
} from '../__fixtures__/issues';
import { Issue } from '@sec-tester/scan';

describe('PlainTextFormatter', () => {
let formatter!: PlainTextFormatter;

beforeEach(() => {
formatter = new PlainTextFormatter();
});

describe('format', () => {
it.each([
{
input: fullyDescribedIssue,
expected: fullyDescribedIssueText,
title: 'fully described issue'
},
{
input: issueWithoutExtraInfo,
expected: issueWithoutExtraInfoText,
title: 'issue without extra info'
},
{
input: issueWithoutResources,
expected: issueWithoutResourcesText,
title: 'issue without resources'
}
])('should format $title', ({ input, expected }) => {
// act
const result = formatter.format(input as Issue);

// assert
expect(result).toEqual(expected);
});
});
});
84 changes: 84 additions & 0 deletions packages/reporter/src/formatters/PlainTextFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Formatter } from '../lib';
import { Comment, Issue } from '@sec-tester/scan';
import { format } from 'util';

export class PlainTextFormatter implements Formatter {
private readonly BULLET_POINT = '●';
private readonly NEW_LINE = '\n';
private readonly TABULATION = '\t';

public format(issue: Issue): string {
const {
link,
name,
severity,
remedy,
details,
comments = [],
resources = []
} = issue;
const template = this.generateTemplate({
extraInfo: comments.length > 0,
references: resources.length > 0
});

const message = format(
template,
link,
name,
severity,
remedy,
details,
this.formatList(comments, comment => this.formatExtraInfo(comment)),
this.formatList(resources)
);

return message.trim();
}

private generateTemplate(options: {
extraInfo: boolean;
references: boolean;
}): string {
return `
Issue in Bright UI: %s
Name: %s
Severity: %s
Remediation:
%s
Details:
%s${options.extraInfo ? `\nExtra Details:\n%s` : ''}${
options.references ? `\nReferences:\n%s` : ''
}`.trim();
}

private formatExtraInfo({ headline, text = '', links = [] }: Comment) {
const footer = links.length
? this.combineList(['Links:', this.formatList(links)])
: '';
const blocks = [text, footer].map(x => this.indent(x));
const document = this.combineList(blocks);

return this.combineList([headline, document]);
}

private indent(x: string, length: number = 1) {
const lines = x.split(this.NEW_LINE);

return this.combineList(
lines.map(line => `${this.TABULATION.repeat(length)}${line}`)
);
}

private formatList<T>(list: T[], map?: (x: T) => string): string {
const items = list.map(
x => `${this.BULLET_POINT} ${typeof map == 'function' ? map(x) : x}`
);

return this.combineList(items);
}

private combineList(list: string[], sep?: string): string {
return list.join(sep ?? this.NEW_LINE);
}
}
1 change: 1 addition & 0 deletions packages/reporter/src/formatters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './PlainTextFormatter';
3 changes: 2 additions & 1 deletion packages/reporter/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { Reporter } from './lib';
export { PlainTextFormatter } from './formatters';
export { Reporter, Formatter } from './lib';
export { StdReporter } from './std';
7 changes: 7 additions & 0 deletions packages/reporter/src/lib/Formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Issue } from '@sec-tester/scan';

export interface Formatter {
format(issue: Issue): string;
}

export const Formatter: unique symbol = Symbol('Formatter');
3 changes: 2 additions & 1 deletion packages/reporter/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { Reporter } from './Reporter';
export * from './Reporter';
export * from './Formatter';
9 changes: 9 additions & 0 deletions packages/runner/src/lib/IssueFound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Formatter } from '@sec-tester/reporter';
import { SecTesterError } from '@sec-tester/core';
import { Issue } from '@sec-tester/scan';

export class IssueFound extends SecTesterError {
constructor(public readonly issue: Issue, formatter: Formatter) {
super(`Target is vulnerable\n\n${formatter.format(issue)}`);
}
}
8 changes: 4 additions & 4 deletions packages/runner/src/lib/SecRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
RepeaterFactory,
RepeatersManager
} from '@sec-tester/repeater';
import { Reporter, StdReporter } from '@sec-tester/reporter';
import { ScanFactory } from '@sec-tester/scan';
import { Formatter, PlainTextFormatter } from '@sec-tester/reporter';

export class SecRunner {
private readonly configuration: Configuration;
Expand Down Expand Up @@ -64,15 +64,15 @@ export class SecRunner {
repeaterId: this.repeater.repeaterId
},
this.configuration.container.resolve<ScanFactory>(ScanFactory),
this.configuration.container.resolve<Reporter>(Reporter)
this.configuration.container.resolve<Formatter>(Formatter)
);
}

private async initConfiguration(configuration: Configuration): Promise<void> {
await configuration.loadCredentials();

configuration.container.register(Reporter, {
useClass: StdReporter
configuration.container.register(Formatter, {
useClass: PlainTextFormatter
});
}
}
Loading