Skip to content
This repository has been archived by the owner on Apr 29, 2022. It is now read-only.

Commit

Permalink
feat: Add new scanners
Browse files Browse the repository at this point in the history
  • Loading branch information
kgilpin committed Sep 8, 2021
1 parent 209e2aa commit 99430f2
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 15 deletions.
9 changes: 8 additions & 1 deletion src/assertionChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@ import { AbortError } from './errors';
import { AppMap } from '@appland/models';
import SqlQueryStrategy from './strategy/sqlQueryStrategy';
import { AssertionFailure } from './types';
import EventStrategy from './strategy/eventStrategy';
import FunctionStrategy from './strategy/functionStrategy';

export default class AssertionChecker {
private strategies: Strategy[] = [new HttpRequestStrategy(), new SqlQueryStrategy()];
private strategies: Strategy[] = [
new EventStrategy(),
new FunctionStrategy(),
new HttpRequestStrategy(),
new SqlQueryStrategy(),
];

check(appMapData: AppMap, assertion: Assertion, failures: AssertionFailure[]): void {
for (const strategy of this.strategies) {
Expand Down
8 changes: 7 additions & 1 deletion src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,14 @@ export default {
if (failure.event.elapsedTime !== undefined) {
eventMsg += ` (${failure.event.elapsedTime}ms)`;
}

console.log(eventMsg);
if (failure.event.parent) {
console.log(
`\tParent:\t${
failure.event.parent.id
} - ${failure.event.parent.toString()}`
);
}
console.log(`\tCondition:\t${failure.condition}`);
console.log('\n');
});
Expand Down
10 changes: 9 additions & 1 deletion src/defaultAssertions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import Assertion from './assertion';
import hasContentType from './scanner/hasContentType';
import missingAuthentication from './scanner/missingAuthentication';
import queryFromView from './scanner/queryFromView';
import slowHttpServerRequest from './scanner/slowHttpServerRequest';
import slowQuery from './scanner/slowQuery';

const assertions: Assertion[] = [slowHttpServerRequest(), slowQuery(), queryFromView()];
const assertions: Assertion[] = [
slowHttpServerRequest(),
slowQuery(),
queryFromView(),
hasContentType(),
missingAuthentication(),
];

export default function (): Assertion[] {
return assertions;
Expand Down
20 changes: 20 additions & 0 deletions src/scanner/hasContentType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @ts-ignore
import { Event } from '@appland/models';
import Assertion from '../assertion';
import { contentType } from './util';

const isRedirect = (status: number) =>
[301, 302, 303, 307, 308].includes(status);
const isNoContent = (status: number) => status != 204;

export default function () {
return Assertion.assert(
'http_server_request',
(e: Event) => contentType(e) !== undefined,
(assertion: Assertion): void => {
assertion.where = (e: Event) =>
e.httpServerResponse !== undefined && !isRedirect(e.httpServerResponse!.status_code) && !isNoContent(e.httpServerResponse!.status_code);
assertion.description = `HTTP server request must have a Content-Type header`;
}
);
}
40 changes: 40 additions & 0 deletions src/scanner/missingAuthentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// @ts-ignore
import { Event, EventNavigator } from '@appland/models';
import Assertion from '../assertion';
import { contentType, isFalsey } from './util';

function isPublic(event: Event): boolean {
return event.labels.has('public');
}

function providesAuthentication(event: Event) {
return (
event.labels.has('security.authentication') && !isFalsey(event.returnValue)
);
}

export default function (
routes: RegExp[] = [/.*/],
contentTypes: RegExp[] = [/.*/]
) {
return Assertion.assert(
'http_server_request',
(event: Event) =>
new EventNavigator(event)
.descendants((e: Event) => isPublic(e) || providesAuthentication(e))
.next()?.value,
(assertion: Assertion): void => {
assertion.where = (e: Event) => {
return (
e.route !== undefined &&
e.httpServerResponse !== undefined &&
e.httpServerResponse!.status_code < 300 &&
contentType(e) !== undefined &&
routes.some((pattern) => pattern.test(e.route!)) &&
contentTypes.some((pattern) => pattern.test(contentType(e)!))
);
};
assertion.description = `HTTP server request must be authenticated`;
}
);
}
23 changes: 23 additions & 0 deletions src/scanner/queryFromConstraint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @ts-ignore
import { Event } from '@appland/models';
import Assertion from '../assertion';

const Whitelist = [/BEGIN/, /COMMIT/, /ROLLBACK/, /RELEASE/, /SAVEPOINT/];

export default function (parentPackages: string[], whitelist: RegExp[] = []) {
return Assertion.assert(
'event',
(e: Event) => parentPackages.includes(e.parent!.codeObject.packageOf),
(assertion: Assertion): void => {
assertion.where = (e: Event) =>
e.sqlQuery !== undefined &&
e.parent !== undefined &&
!whitelist
.concat(Whitelist)
.some((pattern) => pattern.test(e.sqlQuery!));
assertion.description = `Query must be invoked from one of (${parentPackages.join(
','
)})`;
}
);
}
2 changes: 1 addition & 1 deletion src/scanner/slowQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default function (
): Assertion {
return Assertion.assert(
'sql_query',
(e: Event) => e.elapsedTime !== undefined && e.elapsedTime < timeAllowed,
(e: Event) => e.elapsedTime! < timeAllowed,
(assertion: Assertion): void => {
assertion.where = (e: Event) =>
e.elapsedTime !== undefined &&
Expand Down
31 changes: 31 additions & 0 deletions src/scanner/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @ts-ignore
import { Event } from '@appland/models';

/*
const responseHeaders = (event: Event): any => {
return event.httpServerResponse?.headers || event.httpClientResponse?.headers || {};
};
*/

// TODO: Why is mime_type still defined on httpServerResponse? It should be "headers".
const contentType = (event: Event): string | undefined => event.httpServerResponse?.mime_type;
// responseHeaders(event)['Content-Type'] ||

function isFalsey(valueObj: any): boolean {
if (!valueObj) {
return true;
}
if (valueObj.class === 'FalseClass') {
return true;
}
if (valueObj.class === 'Array' && valueObj.value === '[]') {
return true;
}
if (valueObj.value === '') {
return true;
}

return false;
}

export { contentType, isFalsey };
10 changes: 0 additions & 10 deletions src/strategy/appMapStrategy.ts

This file was deleted.

12 changes: 12 additions & 0 deletions src/strategy/eventStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @ts-ignore
import { Event } from '@appland/models';
import Strategy from './strategy';
import { Scope } from '../types';

export default class EventStrategy extends Strategy {
protected scope: Scope = 'event';

protected isEventApplicable(event: Event): boolean {
return true;
}
}
2 changes: 1 addition & 1 deletion src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Event } from '@appland/models';

export type Scope = 'appmap' | 'http_server_request' | 'sql_query' | 'function';
export type Scope = 'event' | 'http_server_request' | 'sql_query' | 'function';

export interface AssertionFailure {
appMapName: string;
Expand Down

0 comments on commit 99430f2

Please sign in to comment.