Skip to content
Open
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
12 changes: 10 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
FROM node:12-alpine
FROM node:18-alpine
ENV WORKDIR /usr/src/app/
WORKDIR $WORKDIR
COPY package*.json $WORKDIR
RUN npm install --production --no-cache

FROM node:12-alpine
FROM node:18-alpine
ENV USER node
ENV WORKDIR /home/$USER/app

# Install Trivy
RUN apk add --no-cache curl git \
&& curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin \
&& mkdir -p /tmp/scans \
&& chmod 777 /tmp/scans

WORKDIR $WORKDIR
COPY --from=0 /usr/src/app/node_modules node_modules
RUN chown $USER:$USER $WORKDIR
Expand All @@ -16,3 +23,4 @@ COPY --chown=node . $WORKDIR
# Then all further actions including running the containers should be done under non-root user.
USER $USER
EXPOSE 4000
EXPOSE 9229
17 changes: 17 additions & 0 deletions api.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
### Scan API Endpoints

## Create a new scan

POST http://localhost:4000/api/v1/scan
Content-Type: application/json

{
"repoUrl": "https://github.com/OWASP/NodeGoat"
}


### Get scan status by ID
GET http://localhost:4000/api/v1/scan/0bba6a0c-450f-4630-a0ab-92cfc03e750f



8 changes: 8 additions & 0 deletions app/modules/scan/dto/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use strict";

const { VulnerabilityDTO } = require("./vulnerability.dto");

module.exports = {
VulnerabilityDTO
};

70 changes: 70 additions & 0 deletions app/modules/scan/dto/vulnerability.dto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use strict";

/**
* DTO for normalized vulnerability data
* Common format for vulnerabilities from different scanners
*/
class VulnerabilityDTO {
constructor({
vulnerabilityId,
pkgName,
installedVersion,
fixedVersion,
title,
description,
severity,
references = [],
scanner
}) {
this.vulnerabilityId = vulnerabilityId;
this.pkgName = pkgName;
this.installedVersion = installedVersion;
this.fixedVersion = fixedVersion;
this.title = title;
this.description = description;
this.severity = severity;
this.references = references;
this.scanner = scanner;
}

/**
* Create VulnerabilityDTO from Trivy vulnerability object
* @param {Object} trivyVuln - Raw Trivy vulnerability object
* @param {string} scannerName - Name of the scanner
* @returns {VulnerabilityDTO}
*/
static fromTrivy(trivyVuln, scannerName = "trivy") {
return new VulnerabilityDTO({
vulnerabilityId: trivyVuln.VulnerabilityID,
pkgName: trivyVuln.PkgName,
installedVersion: trivyVuln.InstalledVersion,
fixedVersion: trivyVuln.FixedVersion,
title: trivyVuln.Title,
description: trivyVuln.Description,
severity: trivyVuln.Severity,
references: trivyVuln.References || [],
scanner: scannerName
});
}

/**
* Convert to plain object for storage/serialization
* @returns {Object}
*/
toJSON() {
return {
vulnerabilityId: this.vulnerabilityId,
pkgName: this.pkgName,
installedVersion: this.installedVersion,
fixedVersion: this.fixedVersion,
title: this.title,
description: this.description,
severity: this.severity,
references: this.references,
scanner: this.scanner
};
}
}

module.exports = { VulnerabilityDTO };

8 changes: 8 additions & 0 deletions app/modules/scan/enums/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use strict";

const { Severity } = require("./severity.enum");
const { ScanStatus } = require("./scan-status.enum");
const { ScannerType } = require("./scanner-type.enum");

module.exports = { Severity, ScanStatus, ScannerType };

14 changes: 14 additions & 0 deletions app/modules/scan/enums/scan-status.enum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use strict";

/**
* Scan status enum
*/
const ScanStatus = Object.freeze({
QUEUED: "Queued",
SCANNING: "Scanning",
FINISHED: "Finished",
FAILED: "Failed"
});

module.exports = { ScanStatus };

12 changes: 12 additions & 0 deletions app/modules/scan/enums/scanner-type.enum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use strict";

/**
* Scanner type enum
* Supported security scanners
*/
const ScannerType = Object.freeze({
TRIVY: "trivy",
});

module.exports = { ScannerType };

16 changes: 16 additions & 0 deletions app/modules/scan/enums/severity.enum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use strict";

/**
* Vulnerability severity levels
* Matches Trivy severity levels
*/
const Severity = Object.freeze({
CRITICAL: "CRITICAL",
HIGH: "HIGH",
MEDIUM: "MEDIUM",
LOW: "LOW",
UNKNOWN: "UNKNOWN"
});

module.exports = { Severity };

64 changes: 64 additions & 0 deletions app/modules/scan/graphql.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const { buildSchema } = require('graphql');
const ScanService = require('./scan.service');

const schema = buildSchema(`
type Vulnerability {
vulnerabilityId: String!
pkgName: String
installedVersion: String
fixedVersion: String
title: String
description: String
severity: String
references: [String]
scanner: String
}

type Scan {
id: ID!
status: String!
criticalVulnerabilities: [Vulnerability]
}

type Query {
scan(id: ID!): Scan
}

type Mutation {
startScan(repoUrl: String!): Scan
}
`);

function getResolvers(db) {
const scanService = new ScanService(db);

return {
scan: async ({ id }) => {
const scan = await scanService.getScanById(id);
if (!scan) return null;
const resp = scan.toResponse();
return {
id: resp.scanId,
status: resp.status,
criticalVulnerabilities: resp.criticalVulnerabilities || []
};
},
startScan: async ({ repoUrl }) => {
// Validate input
if (!repoUrl || !scanService.isValidGithubUrl(repoUrl)) {
throw new Error('Invalid GitHub repository URL');
}


const result = await scanService.createScan(repoUrl);
return {
id: result.scanId,
status: result.status,
criticalVulnerabilities: []
};
}
};
}

module.exports = { schema, getResolvers };

21 changes: 21 additions & 0 deletions app/modules/scan/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use strict";

const { ScanModel, ScanStatus } = require("./scan.model");
const { ScanWorkerFactory, ScannerType, BaseScanWorker, TrivyScanWorker } = require("./workers");

module.exports = {
// Model
ScanModel,
ScanStatus,

// Service & Repository
ScanService: require("./scan.service"),
ScanRepository: require("./scan.repository"),

// Workers
ScanWorkerFactory,
ScannerType,
BaseScanWorker,
TrivyScanWorker
};

96 changes: 96 additions & 0 deletions app/modules/scan/scan.model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"use strict";

const { ScanStatus, ScannerType } = require("./enums");

/**
* Scan model - represents a security scan entity
*/
class ScanModel {
constructor(data = {}) {
this.scanId = data.scanId || null;
this.repoUrl = data.repoUrl || null;
this.status = data.status || ScanStatus.QUEUED;
this.scanner = data.scanner || ScannerType.TRIVY;
// ...existing code...
this.createdAt = data.createdAt || new Date();
this.updatedAt = data.updatedAt || new Date();
this.criticalVulnerabilities = data.criticalVulnerabilities || [];
this.error = data.error || null;
}

/**
* Check if scan is in active state
* @returns {boolean}
*/
isActive() {
return this.status === ScanStatus.QUEUED || this.status === ScanStatus.SCANNING;
}

/**
* Check if scan is finished
* @returns {boolean}
*/
isFinished() {
return this.status === ScanStatus.FINISHED;
}

/**
* Check if scan has failed
* @returns {boolean}
*/
isFailed() {
return this.status === ScanStatus.FAILED;
}

/**
* Convert to plain object for database storage
* @returns {Object}
*/
toDocument() {
return {
scanId: this.scanId,
repoUrl: this.repoUrl,
status: this.status,
scanner: this.scanner,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
criticalVulnerabilities: this.criticalVulnerabilities,
error: this.error
};
}

/**
* Convert to API response format
* @returns {Object}
*/
toResponse() {
const response = {
scanId: this.scanId,
repoUrl: this.repoUrl,
status: this.status,
scanner: this.scanner,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
criticalVulnerabilities: this.criticalVulnerabilities
};

if (this.error) {
response.error = this.error;
}

return response;
}

/**
* Create ScanModel instance from database document
* @param {Object} doc - MongoDB document
* @returns {ScanModel}
*/
static fromDocument(doc) {
if (!doc) return null;
return new ScanModel(doc);
}
}

module.exports = { ScanModel, ScanStatus, ScannerType };

Loading