bugnitor-security-scanner
Version:
AI-Era Security Scanner: Intelligent automated security review agent specializing in AI-generated vulnerability patterns
514 lines • 22.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CICDAnalyzer = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class CICDAnalyzer {
async analyzeCICD(projectPath) {
const findings = [];
const workflows = [];
// Analyze GitHub Actions
const githubWorkflowsPath = path.join(projectPath, '.github', 'workflows');
if (fs.existsSync(githubWorkflowsPath)) {
const result = await this.analyzeGitHubActions(githubWorkflowsPath);
findings.push(...result.findings);
workflows.push(...result.workflows);
}
// Analyze GitLab CI
const gitlabCIPath = path.join(projectPath, '.gitlab-ci.yml');
if (fs.existsSync(gitlabCIPath)) {
const result = await this.analyzeGitLabCI(gitlabCIPath);
findings.push(...result.findings);
workflows.push(result.workflow);
}
// Analyze Jenkins
const jenkinsfilePath = path.join(projectPath, 'Jenkinsfile');
if (fs.existsSync(jenkinsfilePath)) {
const result = await this.analyzeJenkinsfile(jenkinsfilePath);
findings.push(...result.findings);
workflows.push(result.workflow);
}
// Analyze Dockerfiles
const dockerFiles = this.findDockerfiles(projectPath);
for (const dockerFile of dockerFiles) {
const result = await this.analyzeDockerfile(dockerFile);
findings.push(...result.findings);
workflows.push(result.workflow);
}
return {
findings,
workflows
};
}
async analyzeGitHubActions(workflowsPath) {
const findings = [];
const workflows = [];
try {
const files = fs.readdirSync(workflowsPath);
for (const file of files) {
if (file.endsWith('.yml') || file.endsWith('.yaml')) {
const filePath = path.join(workflowsPath, file);
const content = fs.readFileSync(filePath, 'utf-8');
const workflowFindings = this.analyzeGitHubWorkflow(content, file);
findings.push(...workflowFindings);
workflows.push({
file: path.relative(path.dirname(workflowsPath), filePath),
type: 'github',
findings: workflowFindings
});
}
}
}
catch (error) {
findings.push({
type: 'cicd',
category: 'File Access Error',
severity: 'low',
title: 'Cannot analyze GitHub Actions',
description: `Error reading GitHub workflows: ${error}`,
file: '.github/workflows/',
recommendation: 'Ensure workflow files are accessible',
confidence: 0.9,
impact: 'CI/CD security analysis incomplete',
effort: 'low'
});
}
return { findings, workflows };
}
analyzeGitHubWorkflow(content, filename) {
const findings = [];
// Check for secrets in workflow files
const secretPatterns = [
/password\s*:\s*["'][^"']*["']/gi,
/token\s*:\s*["'][^"']*["']/gi,
/api_key\s*:\s*["'][^"']*["']/gi,
/secret\s*:\s*["'][^"']*["']/gi
];
for (const pattern of secretPatterns) {
let match;
while ((match = pattern.exec(content)) !== null) {
findings.push({
type: 'cicd',
category: 'Hardcoded Secret in CI/CD',
severity: 'critical',
title: 'Secret exposed in GitHub workflow',
description: 'Sensitive information hardcoded in workflow file',
file: filename,
line: this.getLineNumber(content, match.index || 0),
code: match[0],
recommendation: 'Use GitHub secrets instead of hardcoding sensitive values',
confidence: 0.8,
impact: 'Secret exposure in version control',
effort: 'low'
});
}
}
// Check for pull request write permissions
if (content.includes('pull-requests: write') && content.includes('on:') && content.includes('pull_request')) {
findings.push({
type: 'cicd',
category: 'Excessive Permissions',
severity: 'medium',
title: 'Excessive pull request permissions',
description: 'Workflow has write access to pull requests',
file: filename,
recommendation: 'Use minimal required permissions. Consider using read-only access.',
confidence: 0.7,
impact: 'Potential unauthorized modifications to pull requests',
effort: 'low'
});
}
// Check for dangerous shell commands
const dangerousCommands = [
/curl.*\|.*sh/gi,
/wget.*\|.*bash/gi,
/sudo\s+/gi,
/rm\s+-rf\s+\//gi,
/chmod\s+777/gi
];
for (const pattern of dangerousCommands) {
let match;
while ((match = pattern.exec(content)) !== null) {
findings.push({
type: 'cicd',
category: 'Dangerous Command',
severity: 'high',
title: 'Dangerous shell command in workflow',
description: 'Potentially unsafe command execution in CI/CD pipeline',
file: filename,
line: this.getLineNumber(content, match.index || 0),
code: match[0],
recommendation: 'Avoid piping downloads to shell or using dangerous file operations',
confidence: 0.8,
impact: 'Code execution vulnerabilities in CI/CD environment',
effort: 'medium'
});
}
}
// Check for third-party actions without version pinning
const actionPattern = /uses:\s*([^@\s]+)@([^\s]+)/gi;
let match;
while ((match = actionPattern.exec(content)) !== null) {
const actionRef = match[2];
if (actionRef === 'main' || actionRef === 'master' || actionRef === 'latest') {
findings.push({
type: 'cicd',
category: 'Unpinned Action Version',
severity: 'medium',
title: 'Unpinned third-party action',
description: 'Third-party GitHub action not pinned to specific version',
file: filename,
line: this.getLineNumber(content, match.index || 0),
code: match[0],
recommendation: 'Pin actions to specific commit SHA or version tag',
confidence: 0.7,
impact: 'Supply chain attacks via action updates',
effort: 'low'
});
}
}
// Check for missing security scanning
const hasSecurityScan = content.includes('security') ||
content.includes('vulnerability') ||
content.includes('codeql') ||
content.includes('snyk');
if (!hasSecurityScan && content.includes('on:') && (content.includes('push') || content.includes('pull_request'))) {
findings.push({
type: 'cicd',
category: 'Missing Security Controls',
severity: 'medium',
title: 'No security scanning in CI/CD',
description: 'Workflow lacks security vulnerability scanning',
file: filename,
recommendation: 'Add security scanning tools like CodeQL, Snyk, or similar',
confidence: 0.6,
impact: 'Vulnerabilities may go undetected',
effort: 'medium'
});
}
return findings;
}
async analyzeGitLabCI(filePath) {
const findings = [];
try {
const content = fs.readFileSync(filePath, 'utf-8');
const filename = path.basename(filePath);
// Check for secrets in variables
const secretPattern = /variables:\s*\n([\s\S]*?)(?=\n\S|\n$)/gi;
let match;
while ((match = secretPattern.exec(content)) !== null) {
const variables = match[1];
if (variables.includes('password') || variables.includes('token') || variables.includes('key')) {
findings.push({
type: 'cicd',
category: 'Hardcoded Secret in CI/CD',
severity: 'high',
title: 'Potential secret in GitLab CI variables',
description: 'Sensitive variables may be exposed in CI configuration',
file: filename,
line: this.getLineNumber(content, match.index || 0),
recommendation: 'Use GitLab CI/CD variables or secrets instead',
confidence: 0.6,
impact: 'Secret exposure in CI/CD configuration',
effort: 'low'
});
}
}
// Check for image security
const imagePattern = /image:\s*([^\s]+)/gi;
while ((match = imagePattern.exec(content)) !== null) {
const image = match[1];
if (image.includes(':latest')) {
findings.push({
type: 'cicd',
category: 'Unpinned Docker Image',
severity: 'low',
title: 'Unpinned Docker image version',
description: 'CI/CD uses latest Docker image tag',
file: filename,
line: this.getLineNumber(content, match.index || 0),
code: match[0],
recommendation: 'Pin Docker images to specific versions',
confidence: 0.7,
impact: 'Build inconsistencies, potential supply chain attacks',
effort: 'low'
});
}
}
}
catch (error) {
findings.push({
type: 'cicd',
category: 'File Access Error',
severity: 'low',
title: 'Cannot analyze GitLab CI',
description: `Error reading .gitlab-ci.yml: ${error}`,
file: path.basename(filePath),
recommendation: 'Ensure GitLab CI file is accessible',
confidence: 0.9,
impact: 'CI/CD security analysis incomplete',
effort: 'low'
});
}
return {
findings,
workflow: {
file: path.basename(filePath),
type: 'gitlab',
findings
}
};
}
async analyzeJenkinsfile(filePath) {
const findings = [];
try {
const content = fs.readFileSync(filePath, 'utf-8');
const filename = path.basename(filePath);
// Check for shell command injection
const shellPattern = /sh\s+['"`]([^'"`]*)['"]/gi;
let match;
while ((match = shellPattern.exec(content)) !== null) {
const command = match[1];
if (command.includes('$') && (command.includes('env.') || command.includes('params.'))) {
findings.push({
type: 'cicd',
category: 'Command Injection Risk',
severity: 'high',
title: 'Potential command injection in Jenkinsfile',
description: 'Shell command uses environment variables without proper escaping',
file: filename,
line: this.getLineNumber(content, match.index || 0),
code: match[0],
recommendation: 'Properly escape or validate environment variables in shell commands',
confidence: 0.7,
impact: 'Command injection in CI/CD pipeline',
effort: 'medium'
});
}
}
// Check for credentials usage
if (content.includes('withCredentials') && content.includes('usernamePassword')) {
const credPattern = /usernamePassword.*passwordVariable:\s*['"]([^'"]*)['"]/gi;
while ((match = credPattern.exec(content)) !== null) {
findings.push({
type: 'cicd',
category: 'Credential Exposure Risk',
severity: 'medium',
title: 'Credential usage in pipeline',
description: 'Pipeline uses credentials that may be logged',
file: filename,
line: this.getLineNumber(content, match.index || 0),
recommendation: 'Ensure credentials are not logged or exposed in pipeline output',
confidence: 0.6,
impact: 'Potential credential exposure in logs',
effort: 'low'
});
}
}
}
catch (error) {
findings.push({
type: 'cicd',
category: 'File Access Error',
severity: 'low',
title: 'Cannot analyze Jenkinsfile',
description: `Error reading Jenkinsfile: ${error}`,
file: path.basename(filePath),
recommendation: 'Ensure Jenkinsfile is accessible',
confidence: 0.9,
impact: 'CI/CD security analysis incomplete',
effort: 'low'
});
}
return {
findings,
workflow: {
file: path.basename(filePath),
type: 'jenkins',
findings
}
};
}
async analyzeDockerfile(filePath) {
const findings = [];
try {
const content = fs.readFileSync(filePath, 'utf-8');
const filename = path.basename(filePath);
// Check for root user
if (!content.includes('USER ') || content.includes('USER root')) {
findings.push({
type: 'config',
category: 'Insecure Docker Configuration',
severity: 'medium',
title: 'Container runs as root',
description: 'Dockerfile does not specify non-root user',
file: filename,
recommendation: 'Add USER directive to run container as non-root user',
confidence: 0.8,
impact: 'Privilege escalation if container is compromised',
effort: 'low'
});
}
// Check for latest tag usage
const fromPattern = /FROM\s+([^:\s]+)(?::([^\s]+))?/gi;
let match;
while ((match = fromPattern.exec(content)) !== null) {
const tag = match[2];
if (!tag || tag === 'latest') {
findings.push({
type: 'config',
category: 'Unpinned Docker Image',
severity: 'low',
title: 'Unpinned base image version',
description: 'Dockerfile uses latest or no tag for base image',
file: filename,
line: this.getLineNumber(content, match.index || 0),
code: match[0],
recommendation: 'Pin base image to specific version',
confidence: 0.8,
impact: 'Build inconsistencies, potential supply chain attacks',
effort: 'low'
});
}
}
// Check for ADD instead of COPY
if (content.includes('ADD ')) {
const addPattern = /ADD\s+/gi;
while ((match = addPattern.exec(content)) !== null) {
findings.push({
type: 'config',
category: 'Insecure Docker Practice',
severity: 'low',
title: 'Use of ADD instead of COPY',
description: 'ADD has additional functionality that may be unsafe',
file: filename,
line: this.getLineNumber(content, match.index || 0),
recommendation: 'Use COPY instead of ADD unless auto-extraction is needed',
confidence: 0.6,
impact: 'Potential unintended file extraction or URL downloads',
effort: 'low'
});
}
}
// Check for secrets in build args
const argPattern = /ARG\s+([^=\s]+)(?:=([^\s]+))?/gi;
while ((match = argPattern.exec(content)) !== null) {
const argName = match[1].toLowerCase();
if (argName.includes('password') || argName.includes('token') || argName.includes('key')) {
findings.push({
type: 'config',
category: 'Secret in Build Args',
severity: 'medium',
title: 'Potential secret in build argument',
description: 'Build argument may contain sensitive information',
file: filename,
line: this.getLineNumber(content, match.index || 0),
code: match[0],
recommendation: 'Avoid passing secrets as build arguments. Use multi-stage builds or runtime secrets.',
confidence: 0.7,
impact: 'Secrets visible in image layers and build cache',
effort: 'medium'
});
}
}
// Check for package manager cache
if (content.includes('apt-get update') && !content.includes('rm -rf /var/lib/apt/lists/*')) {
findings.push({
type: 'config',
category: 'Docker Image Bloat',
severity: 'low',
title: 'Package manager cache not cleaned',
description: 'APT cache not removed, increasing image size',
file: filename,
recommendation: 'Add "rm -rf /var/lib/apt/lists/*" after apt-get commands',
confidence: 0.8,
impact: 'Larger image size, potential security metadata exposure',
effort: 'low'
});
}
}
catch (error) {
findings.push({
type: 'config',
category: 'File Access Error',
severity: 'low',
title: 'Cannot analyze Dockerfile',
description: `Error reading Dockerfile: ${error}`,
file: path.basename(filePath),
recommendation: 'Ensure Dockerfile is accessible',
confidence: 0.9,
impact: 'Docker security analysis incomplete',
effort: 'low'
});
}
return {
findings,
workflow: {
file: path.basename(filePath),
type: 'docker',
findings
}
};
}
findDockerfiles(projectPath) {
const dockerfiles = [];
try {
const findDockerfilesRecursive = (dir) => {
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory() && !item.startsWith('.') && item !== 'node_modules') {
findDockerfilesRecursive(fullPath);
}
else if (stat.isFile() && (item === 'Dockerfile' || item.startsWith('Dockerfile.'))) {
dockerfiles.push(fullPath);
}
}
};
findDockerfilesRecursive(projectPath);
}
catch (error) {
// Silently handle errors in recursive search
}
return dockerfiles;
}
getLineNumber(content, index) {
return content.substring(0, index).split('\n').length;
}
}
exports.CICDAnalyzer = CICDAnalyzer;
//# sourceMappingURL=cicd-analyzer.js.map