Skip to content

Commit 01a049c

Browse files
authored
Merge pull request #513 from GSA/1780-tooling-scan-prototype
1780 tooling scan prototype
2 parents 6b61694 + 0e11339 commit 01a049c

File tree

11 files changed

+229
-41
lines changed

11 files changed

+229
-41
lines changed

apt.yml

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,17 @@
11
---
22
packages:
3-
- gconf-service
4-
- libasound2
3+
- libnss3
4+
- libnspr4
55
- libatk1.0-0
6-
- libc6
7-
- libcairo2
6+
- libatk-bridge2.0-0
87
- libcups2
9-
- libdbus-1-3
10-
- libexpat1
11-
- libfontconfig1
12-
- libgcc1
13-
- libgconf-2-4
14-
- libgdk-pixbuf2.0-0
15-
- libglib2.0-0
16-
- libgtk-3-0
17-
- libnspr4
18-
- libpango-1.0-0
19-
- libpangocairo-1.0-0
20-
- libstdc++6
21-
- libx11-6
22-
- libx11-xcb1
23-
- libxcb1
248
- libxcomposite1
25-
- libxcursor1
269
- libxdamage1
27-
- libxext6
28-
- libxfixes3
29-
- libxi6
3010
- libxrandr2
31-
- libxrender1
11+
- libgbm-dev
12+
- libpango-1.0-0
13+
- libcairo2
14+
- libasound2
3215
- libxss1
3316
- libxtst6
34-
- ca-certificates
3517
- fonts-liberation
36-
- libappindicator1
37-
- libnss3
38-
- lsb-release
39-
- xdg-utils
40-
- wget
41-
- libgbm-dev

entities/core-result.entity.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,18 @@ export class CoreResult {
538538
@Exclude()
539539
dateContent?: string;
540540

541+
@Column({ nullable: true })
542+
@Expose({ name: 'tooling' })
543+
@Exclude()
544+
@Transform(({ value }: { value: string }) => {
545+
if (value) {
546+
return value.split(',');
547+
} else {
548+
return null;
549+
}
550+
})
551+
tooling?: string;
552+
541553
static getColumnNames(): string[] {
542554
// return class-transformer version of column names
543555
return Object.keys(classToPlain(new CoreResult()));

entities/scan-data.entity.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,7 @@ export type WwwScan = {
157157
wwwTitle: string;
158158
wwwSame: boolean;
159159
};
160+
161+
export type ToolingScan = {
162+
tooling: string;
163+
};

entities/scan-page.entity.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type PrimaryScans = {
2727
requiredLinksScan: ScanData.RequiredLinksScan;
2828
searchScan: ScanData.SearchScan;
2929
mobileScan: ScanData.MobileScan;
30+
toolingScan: ScanData.ToolingScan;
3031
};
3132
export type PrimaryScan = PageScan<PrimaryScans>;
3233

libs/core-scanner/src/core-scanner.service.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,14 @@ import { HttpService } from '@nestjs/axios';
33
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
44
import { Logger } from 'pino';
55
import { Browser } from 'puppeteer';
6-
76
import { BrowserService } from '@app/browser';
87
import { SecurityDataService } from '@app/security-data';
9-
108
import {
119
AnyFailureStatus,
1210
parseBrowserError,
1311
ScanStatus,
1412
} from 'entities/scan-status';
1513
import { Scanner } from 'libs/scanner.interface';
16-
1714
import { CoreInputDto } from './core.input.dto';
1815
import * as pages from './pages';
1916
import { Page } from './pages';

libs/core-scanner/src/pages/primary.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { Logger } from 'pino';
22
import { Page } from 'puppeteer';
3-
43
import { CoreInputDto } from '@app/core-scanner/core.input.dto';
54
import { buildUrlScanResult } from '@app/core-scanner/scans/url-scan';
65
import { PrimaryScans } from 'entities/scan-page.entity';
7-
86
import { buildDapResult } from '../scans/dap';
97
import { buildSeoResult } from '../scans/seo';
108
import { buildThirdPartyResult } from '../scans/third-party';
@@ -20,9 +18,8 @@ import { buildRequiredLinksResult } from '../scans/required-links';
2018
import { buildCookieResult } from '../scans/cookies';
2119
import { buildSearchResult } from '../scans/search';
2220
import { buildMobileResult } from '../scans/mobile';
23-
21+
import { buildToolingResult } from '../scans/tooling';
2422
import { logCount, logTimer } from '../../../logging/src/metric-utils';
25-
2623
import { createRequestHandlers } from '../util';
2724

2825
export const createPrimaryScanner = (logger: Logger, input: CoreInputDto) => {
@@ -117,6 +114,13 @@ const primaryScan = async (
117114
'MobileScan',
118115
url,
119116
);
117+
const wrappedToolingResult = runScan(
118+
input,
119+
pageLogger,
120+
buildToolingResult,
121+
'ToolingScan',
122+
url,
123+
);
120124

121125
const [
122126
urlScan,
@@ -130,6 +134,7 @@ const primaryScan = async (
130134
requiredLinksScan,
131135
searchScan,
132136
mobileScan,
137+
toolingScan,
133138
] = await promiseAll([
134139
buildUrlScanResult(input, page, response, pageLogger),
135140
wrappedDapResult(getOutboundRequests(), page),
@@ -142,6 +147,7 @@ const primaryScan = async (
142147
wrappedRequiredLinksResult(page),
143148
wrappedSearchResult(page),
144149
wrappedMobileResult(page),
150+
wrappedToolingResult(page),
145151
]);
146152

147153
return {
@@ -156,6 +162,7 @@ const primaryScan = async (
156162
requiredLinksScan,
157163
searchScan,
158164
mobileScan,
165+
toolingScan,
159166
};
160167

161168
function runScan<T>(
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { buildToolingResult } from './tooling';
2+
import { browserInstance, newTestPage } from '../test-helper';
3+
import pino from 'pino';
4+
5+
const mockLogger = pino();
6+
7+
describe('tooling scan', () => {
8+
it('detects Bootstrap via link href', async () => {
9+
await newTestPage(async ({ page }) => {
10+
await page.setContent(`
11+
<html>
12+
<head>
13+
<link rel="stylesheet" href="/css/bootstrap.min.css">
14+
</head>
15+
<body></body>
16+
</html>
17+
`);
18+
expect(await buildToolingResult(mockLogger, page)).toEqual({
19+
tooling: 'bootstrap',
20+
});
21+
});
22+
});
23+
24+
it('detects Bootstrap via characteristic classes', async () => {
25+
await newTestPage(async ({ page }) => {
26+
await page.setContent(`
27+
<html>
28+
<head></head>
29+
<body>
30+
<div class="container-fluid"></div>
31+
</body>
32+
</html>
33+
`);
34+
expect(await buildToolingResult(mockLogger, page)).toEqual({
35+
tooling: 'bootstrap',
36+
});
37+
});
38+
});
39+
40+
it('detects multiple libraries', async () => {
41+
await newTestPage(async ({ page }) => {
42+
await page.setContent(`
43+
<html>
44+
<head>
45+
<link rel="stylesheet" href="/css/bootstrap.min.css">
46+
<link rel="stylesheet" href="/css/animate.min.css">
47+
</head>
48+
<body></body>
49+
</html>
50+
`);
51+
expect(await buildToolingResult(mockLogger, page)).toEqual({
52+
tooling: 'bootstrap,animate.css',
53+
});
54+
});
55+
});
56+
57+
it('returns null when no libraries are detected', async () => {
58+
await newTestPage(async ({ page }) => {
59+
await page.setContent(`
60+
<html>
61+
<head></head>
62+
<body><p>Nothing here</p></body>
63+
</html>
64+
`);
65+
expect(await buildToolingResult(mockLogger, page)).toEqual({
66+
tooling: null,
67+
});
68+
});
69+
});
70+
71+
afterAll(async () => {
72+
if (browserInstance) {
73+
await browserInstance.close();
74+
}
75+
});
76+
});
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { Logger } from 'pino';
2+
import { Page } from 'puppeteer';
3+
import { ToolingScan } from 'entities/scan-data.entity';
4+
5+
export const buildToolingResult = async (
6+
logger: Logger,
7+
page: Page,
8+
): Promise<ToolingScan> => {
9+
const cssLibraryResults = await getCssLibraryResults(page);
10+
11+
return {
12+
tooling: cssLibraryResults.length ? cssLibraryResults : null,
13+
};
14+
};
15+
16+
async function getCssLibraryResults(page: Page): Promise<string> {
17+
const result = await page.evaluate(() => {
18+
const detected: string[] = [];
19+
20+
const linkHrefs = Array.from(
21+
document.querySelectorAll('link[rel="stylesheet"]'),
22+
).map((el) => el.getAttribute('href')?.toLowerCase() ?? '');
23+
24+
const scriptSrcs = Array.from(document.querySelectorAll('script')).map(
25+
(el) => el.getAttribute('src')?.toLowerCase() ?? '',
26+
);
27+
28+
// Bootstrap
29+
if (
30+
linkHrefs.some((href) => href.includes('bootstrap')) ||
31+
document.querySelector('.container-fluid, .navbar-toggler, .btn-primary')
32+
) {
33+
detected.push('bootstrap');
34+
}
35+
36+
// Tailwind
37+
if (
38+
linkHrefs.some((href) => href.includes('tailwind')) ||
39+
scriptSrcs.some((src) => src.includes('tailwind'))
40+
) {
41+
detected.push('tailwind');
42+
}
43+
44+
// Foundation
45+
if (
46+
linkHrefs.some((href) => href.includes('foundation')) ||
47+
document.querySelector('.top-bar, .callout, .orbit-container')
48+
) {
49+
detected.push('foundation');
50+
}
51+
52+
// Animate.css
53+
if (
54+
linkHrefs.some((href) => href.includes('animate')) ||
55+
document.querySelector('.animate__animated, .animated')
56+
) {
57+
detected.push('animate.css');
58+
}
59+
60+
// Bulma
61+
if (
62+
linkHrefs.some((href) => href.includes('bulma')) ||
63+
document.querySelector('.hero-body, .navbar-burger, .is-primary')
64+
) {
65+
detected.push('bulma');
66+
}
67+
68+
// Materialize
69+
if (
70+
linkHrefs.some((href) => href.includes('materialize')) ||
71+
document.querySelector(
72+
'.materialize-red, .waves-effect, .collection-item',
73+
)
74+
) {
75+
detected.push('materialize');
76+
}
77+
78+
// Semantic UI
79+
if (
80+
linkHrefs.some((href) => href.includes('semantic')) ||
81+
document.querySelector('.ui.button, .ui.menu, .ui.container')
82+
) {
83+
detected.push('semantic-ui');
84+
}
85+
86+
// UIkit
87+
if (
88+
linkHrefs.some((href) => href.includes('uikit')) ||
89+
document.querySelector('[uk-grid], [uk-slider], [uk-navbar]')
90+
) {
91+
detected.push('uikit');
92+
}
93+
94+
// Material Design Lite
95+
if (
96+
linkHrefs.some(
97+
(href) => href.includes('material.min') || href.includes('mdl'),
98+
) ||
99+
document.querySelector('.mdl-button, .mdl-layout, .mdl-grid')
100+
) {
101+
detected.push('material-design-lite');
102+
}
103+
104+
return detected;
105+
});
106+
107+
return result.join(',');
108+
}

libs/database/src/core-results/core-result.service.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { Repository } from 'typeorm';
77
import { CoreResultService } from './core-result.service';
88
import { Logger } from '@nestjs/common';
99
import { ScanStatus } from 'entities/scan-status';
10-
import { filter } from 'lodash';
1110

1211
describe('CoreResultService', () => {
1312
let service: CoreResultService;
@@ -177,6 +176,9 @@ describe('CoreResultService', () => {
177176
mobileScan: {
178177
viewportMetaTag: false,
179178
},
179+
toolingScan: {
180+
tooling: null,
181+
},
180182
},
181183
},
182184
robotsTxt: {

libs/database/src/core-results/core-result.service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Injectable, Logger } from '@nestjs/common';
22
import { InjectRepository } from '@nestjs/typeorm';
33
import { Repository } from 'typeorm';
4-
54
import { CoreResult } from 'entities/core-result.entity';
65
import { Website } from 'entities/website.entity';
76
import { ScanStatus } from 'entities/scan-status';
@@ -215,6 +214,9 @@ export class CoreResultService {
215214

216215
// Mobile scan
217216
coreResult.viewportMetaTag = result.mobileScan.viewportMetaTag;
217+
218+
// Tooling scan
219+
coreResult.tooling = result.toolingScan.tooling;
218220
} else {
219221
logger.error({
220222
msg: pages.primary.error,
@@ -268,6 +270,7 @@ export class CoreResultService {
268270
coreResult.ogUrlContent = null;
269271
coreResult.htmlLangContent = null;
270272
coreResult.hrefLangContent = null;
273+
coreResult.tooling = null;
271274
}
272275
}
273276

0 commit comments

Comments
 (0)