Software supply chain attacks have emerged as one of the most devastating threat vectors in modern cybersecurity, targeting the foundational trust relationships between organizations and their development tools, dependencies, and deployment pipelines. The SolarWinds breach compromised nearly 18,000 organizations, the Kaseya attack enabled ransomware deployment across 1,500 customers, and the Log4Shell vulnerability exposed hundreds of millions of applications worldwide. These incidents demonstrate that adversaries increasingly target the software development lifecycle rather than production systems directly, recognizing that a single compromise in build pipelines or dependency chains can provide persistent access to thousands of downstream victims simultaneously.
Secure DevOps practices transform traditional security approaches by embedding comprehensive security controls, automated testing, and continuous monitoring into every stage of the development pipeline. Modern software delivery relies on complex CI/CD systems, third-party dependencies, infrastructure-as-code, container images, and automated deployments, each introducing potential attack vectors that sophisticated adversaries actively exploit. By applying Zero Trust principles that assume breach and verify explicitly, combined with defense-in-depth strategies that layer multiple security controls, organizations can ensure code, dependencies, infrastructure configurations, and pipeline processes remain trustworthy and tamper-resistant from initial design through production deployment.
This comprehensive guide explores the complete security architecture required for enterprise DevOps environments, covering software bill of materials generation, dependency vulnerability management, secrets protection, code scanning with static and dynamic analysis, container security, infrastructure-as-code validation, pipeline hardening, and continuous monitoring. We examine how Microsoft Defender for Cloud DevOps Security, GitHub Advanced Security, Azure DevOps security features, and open-source tooling combine to create code-to-cloud security visibility that tracks every artifact from source repository through production deployment, enabling organizations to detect and remediate vulnerabilities before they become exploitable in production systems.
Understanding Software Supply Chain Threat Landscape
Supply chain attacks exploit trust relationships that exist throughout the software development lifecycle. Attackers compromise legitimate development tools, inject malicious code into open-source packages, exploit CI/CD pipeline vulnerabilities, or manipulate build artifacts to establish persistent access to target environments. The sophistication of these attacks continues to evolve, with recent campaigns like the Shai-Hulud npm malware demonstrating multi-cloud credential harvesting, GitHub Actions runner backdoors, and self-propagation capabilities that automatically modify and republish compromised packages.
The attack surface in modern DevOps environments is extensive. Source code repositories contain intellectual property and often inadvertently expose credentials through committed secrets. Build systems execute code with elevated privileges and access to production deployment credentials, making them high-value targets for lateral movement. Package managers and artifact repositories serve as distribution channels where malicious components can reach thousands of downstream consumers. Container registries store images that may contain vulnerable base layers or injected malware. Infrastructure-as-code templates define production configurations that attackers can manipulate to create backdoors or privilege escalation paths.
Dependency confusion attacks exploit package resolution logic in package managers, where attackers publish malicious packages with identical names to internal packages but on public repositories. When build systems retrieve dependencies, they may inadvertently download attacker-controlled packages instead of legitimate internal ones. Typosquatting attacks leverage similar naming conventions where attackers register packages with names similar to popular libraries, hoping developers will mistype package names and install malicious alternatives. These attacks succeed because modern applications typically depend on hundreds of third-party packages, creating expansive attack surfaces that are difficult to monitor comprehensively.
Software Bill of Materials: Foundation for Supply Chain Visibility
Software Bill of Materials represents the foundational component of supply chain security, providing comprehensive inventories of all components, dependencies, and versions that comprise software applications. SBOMs enable organizations to understand exactly what third-party code executes in their environments, identify vulnerable components when new vulnerabilities are disclosed, ensure license compliance for open-source dependencies, and respond rapidly to supply chain incidents by determining which applications are affected by compromised packages.
The two predominant SBOM standards are SPDX (Software Package Data Exchange) and CycloneDX, each serving complementary use cases. SPDX, originally developed by the Linux Foundation and standardized as ISO/IEC 5962:2021, focuses on license compliance and intellectual property management, making it ideal for regulatory compliance and legal documentation. CycloneDX, developed by OWASP specifically for security contexts, emphasizes vulnerability management and security posture assessment, providing detailed vulnerability tracking, exploitability information, and provenance data. Both formats support JSON, XML, and other serialization formats, enabling machine-readable processing and integration with automated toolchains.
The National Telecommunications and Information Administration (NTIA) defines minimum elements for SBOMs that organizations must include to ensure usefulness for security and compliance purposes. These elements include supplier name identifying the entity that created the component, component name providing unique identification, version of the component for precise tracking, unique identifiers such as Package URLs or CPEs enabling automated lookups, dependency relationships showing how components relate to each other, author of the SBOM document for accountability, and timestamp of SBOM generation for currency verification. Organizations selling to federal agencies must ensure their SBOMs contain these minimum elements for regulatory compliance.
Microsoft provides the SBOM Tool for Azure Pipelines that generates SBOMs in SPDX 2.2 format during build processes. The following Python implementation demonstrates comprehensive SBOM generation and management workflows that integrate with both SPDX and CycloneDX formats:
import json
import subprocess
import hashlib
from datetime import datetime
from typing import List, Dict, Optional
import uuid
class SBOMManager:
"""
Comprehensive SBOM generation and management supporting both SPDX and CycloneDX formats.
Provides vulnerability correlation, license analysis, and compliance reporting.
"""
def __init__(self, project_path: str, project_name: str):
"""
Initialize SBOM manager for a specific project.
Args:
project_path: Root path of the project to analyze
project_name: Name of the project for SBOM identification
"""
self.project_path = project_path
self.project_name = project_name
self.sbom_data = {
"metadata": {
"project": project_name,
"generated": datetime.utcnow().isoformat() + "Z",
"tool": "SBOMManager v1.0"
},
"components": [],
"dependencies": [],
"vulnerabilities": []
}
def generate_cyclonedx_sbom(self, output_format: str = "json") -> Dict:
"""
Generate CycloneDX format SBOM for security-focused use cases.
Args:
output_format: Output format (json, xml, or protobuf)
Returns:
CycloneDX SBOM document
"""
cyclonedx_doc = {
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": f"urn:uuid:{uuid.uuid4()}",
"version": 1,
"metadata": {
"timestamp": datetime.utcnow().isoformat() + "Z",
"tools": [{
"vendor": "Custom",
"name": "SBOMManager",
"version": "1.0"
}],
"component": {
"type": "application",
"name": self.project_name,
"version": "1.0.0",
"bom-ref": f"pkg:{self.project_name}@1.0.0"
}
},
"components": [],
"dependencies": [],
"vulnerabilities": []
}
# Analyze dependencies based on package manager
if self._detect_python_project():
cyclonedx_doc["components"] = self._analyze_python_dependencies()
elif self._detect_nodejs_project():
cyclonedx_doc["components"] = self._analyze_nodejs_dependencies()
elif self._detect_dotnet_project():
cyclonedx_doc["components"] = self._analyze_dotnet_dependencies()
# Build dependency graph
cyclonedx_doc["dependencies"] = self._build_dependency_graph(
cyclonedx_doc["components"]
)
return cyclonedx_doc
def generate_spdx_sbom(self) -> Dict:
"""
Generate SPDX format SBOM for license compliance and IP management.
Returns:
SPDX SBOM document
"""
spdx_doc = {
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": f"{self.project_name}-SBOM",
"documentNamespace": f"https://sbom.example.com/{self.project_name}/{uuid.uuid4()}",
"creationInfo": {
"created": datetime.utcnow().isoformat() + "Z",
"creators": ["Tool: SBOMManager-1.0"],
"licenseListVersion": "3.21"
},
"packages": [],
"relationships": []
}
# Add root package
spdx_doc["packages"].append({
"SPDXID": f"SPDXRef-Package-{self.project_name}",
"name": self.project_name,
"versionInfo": "1.0.0",
"supplier": "Organization: Internal",
"downloadLocation": "NOASSERTION",
"filesAnalyzed": False,
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
"copyrightText": "NOASSERTION"
})
# Analyze dependencies
components = self._get_all_dependencies()
for component in components:
package_id = f"SPDXRef-Package-{component['name']}-{component['version']}"
spdx_doc["packages"].append({
"SPDXID": package_id,
"name": component["name"],
"versionInfo": component["version"],
"supplier": f"Organization: {component.get('supplier', 'NOASSERTION')}",
"downloadLocation": component.get("downloadLocation", "NOASSERTION"),
"filesAnalyzed": False,
"licenseConcluded": component.get("license", "NOASSERTION"),
"licenseDeclared": component.get("license", "NOASSERTION"),
"copyrightText": "NOASSERTION",
"externalRefs": [{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": component.get("purl", "")
}]
})
# Add dependency relationship
spdx_doc["relationships"].append({
"spdxElementId": f"SPDXRef-Package-{self.project_name}",
"relationshipType": "DEPENDS_ON",
"relatedSpdxElement": package_id
})
return spdx_doc
def _detect_python_project(self) -> bool:
"""Check if project is Python-based."""
import os
return (os.path.exists(f"{self.project_path}/requirements.txt") or
os.path.exists(f"{self.project_path}/Pipfile") or
os.path.exists(f"{self.project_path}/pyproject.toml"))
def _detect_nodejs_project(self) -> bool:
"""Check if project is Node.js-based."""
import os
return os.path.exists(f"{self.project_path}/package.json")
def _detect_dotnet_project(self) -> bool:
"""Check if project is .NET-based."""
import os
import glob
return len(glob.glob(f"{self.project_path}/**/*.csproj", recursive=True)) > 0
def _analyze_python_dependencies(self) -> List[Dict]:
"""
Analyze Python dependencies using pip and generate component list.
Returns:
List of component dictionaries in CycloneDX format
"""
components = []
try:
# Run pip list with JSON output
result = subprocess.run(
["pip", "list", "--format=json"],
capture_output=True,
text=True,
check=True
)
packages = json.loads(result.stdout)
for package in packages:
component = {
"type": "library",
"bom-ref": f"pkg:pypi/{package['name']}@{package['version']}",
"name": package["name"],
"version": package["version"],
"purl": f"pkg:pypi/{package['name']}@{package['version']}",
"externalReferences": [{
"type": "distribution",
"url": f"https://pypi.org/project/{package['name']}/{package['version']}/"
}]
}
# Add license information if available
license_info = self._get_python_package_license(package["name"])
if license_info:
component["licenses"] = [{
"license": {"id": license_info}
}]
components.append(component)
except subprocess.CalledProcessError as e:
print(f"Error analyzing Python dependencies: {e}")
return components
def _analyze_nodejs_dependencies(self) -> List[Dict]:
"""
Analyze Node.js dependencies from package.json and package-lock.json.
Returns:
List of component dictionaries
"""
components = []
try:
with open(f"{self.project_path}/package.json", 'r') as f:
package_json = json.load(f)
dependencies = package_json.get("dependencies", {})
dev_dependencies = package_json.get("devDependencies", {})
all_deps = {**dependencies, **dev_dependencies}
for name, version in all_deps.items():
component = {
"type": "library",
"bom-ref": f"pkg:npm/{name}@{version}",
"name": name,
"version": version.lstrip("^~"),
"purl": f"pkg:npm/{name}@{version.lstrip('^~')}",
"externalReferences": [{
"type": "distribution",
"url": f"https://www.npmjs.com/package/{name}/v/{version.lstrip('^~')}"
}]
}
components.append(component)
except Exception as e:
print(f"Error analyzing Node.js dependencies: {e}")
return components
def _analyze_dotnet_dependencies(self) -> List[Dict]:
"""
Analyze .NET dependencies using dotnet list package.
Returns:
List of component dictionaries
"""
components = []
try:
result = subprocess.run(
["dotnet", "list", "package", "--format", "json"],
capture_output=True,
text=True,
check=True,
cwd=self.project_path
)
package_data = json.loads(result.stdout)
for project in package_data.get("projects", []):
for framework in project.get("frameworks", []):
for package in framework.get("topLevelPackages", []):
component = {
"type": "library",
"bom-ref": f"pkg:nuget/{package['id']}@{package['resolvedVersion']}",
"name": package["id"],
"version": package["resolvedVersion"],
"purl": f"pkg:nuget/{package['id']}@{package['resolvedVersion']}",
"externalReferences": [{
"type": "distribution",
"url": f"https://www.nuget.org/packages/{package['id']}/{package['resolvedVersion']}"
}]
}
components.append(component)
except subprocess.CalledProcessError as e:
print(f"Error analyzing .NET dependencies: {e}")
return components
def _get_python_package_license(self, package_name: str) -> Optional[str]:
"""Retrieve license information for Python package."""
try:
result = subprocess.run(
["pip", "show", package_name],
capture_output=True,
text=True,
check=True
)
for line in result.stdout.split('\n'):
if line.startswith("License:"):
return line.split(":", 1)[1].strip()
except subprocess.CalledProcessError:
pass
return None
def _get_all_dependencies(self) -> List[Dict]:
"""Get all dependencies regardless of project type."""
if self._detect_python_project():
return self._analyze_python_dependencies()
elif self._detect_nodejs_project():
return self._analyze_nodejs_dependencies()
elif self._detect_dotnet_project():
return self._analyze_dotnet_dependencies()
return []
def _build_dependency_graph(self, components: List[Dict]) -> List[Dict]:
"""
Build dependency relationships between components.
Args:
components: List of component dictionaries
Returns:
List of dependency relationships
"""
dependencies = []
# Add root dependency
dependencies.append({
"ref": f"pkg:{self.project_name}@1.0.0",
"dependsOn": [comp["bom-ref"] for comp in components]
})
# For each component, you would analyze transitive dependencies here
# This is simplified - real implementation would parse lock files
for component in components:
dependencies.append({
"ref": component["bom-ref"],
"dependsOn": []
})
return dependencies
def scan_for_vulnerabilities(self, sbom: Dict) -> List[Dict]:
"""
Scan SBOM components for known vulnerabilities using multiple sources.
Args:
sbom: CycloneDX SBOM document
Returns:
List of vulnerability objects
"""
vulnerabilities = []
for component in sbom.get("components", []):
# Query vulnerability databases (OSV, NVD, GitHub Advisory)
# This is a simplified example - real implementation would use APIs
vuln_results = self._query_vulnerability_databases(
component["name"],
component["version"]
)
vulnerabilities.extend(vuln_results)
return vulnerabilities
def _query_vulnerability_databases(self, package_name: str, version: str) -> List[Dict]:
"""Query vulnerability databases for package."""
# Simplified - real implementation would query actual APIs
return []
def generate_compliance_report(self, sbom: Dict) -> Dict:
"""
Generate compliance report analyzing licenses and security posture.
Args:
sbom: SBOM document (either format)
Returns:
Compliance report dictionary
"""
report = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"project": self.project_name,
"total_components": 0,
"licenses": {},
"security_summary": {
"total_vulnerabilities": 0,
"critical": 0,
"high": 0,
"medium": 0,
"low": 0
},
"compliance_status": "COMPLIANT"
}
# Analyze components
components = sbom.get("components", sbom.get("packages", []))
report["total_components"] = len(components)
# Aggregate license information
for component in components:
license_info = component.get("licenses", component.get("licenseConcluded"))
if license_info:
license_id = str(license_info)
report["licenses"][license_id] = report["licenses"].get(license_id, 0) + 1
return report
def export_sbom(self, sbom: Dict, format_type: str, output_file: str):
"""
Export SBOM to file in specified format.
Args:
sbom: SBOM document
format_type: Format (cyclonedx or spdx)
output_file: Output filename
"""
with open(output_file, 'w') as f:
json.dump(sbom, f, indent=2)
print(f"SBOM exported to {output_file}")
# Example usage for enterprise SBOM generation
if __name__ == "__main__":
# Initialize SBOM manager for a project
sbom_manager = SBOMManager(
project_path="/path/to/project",
project_name="enterprise-application"
)
# Generate CycloneDX SBOM for security analysis
cyclonedx_sbom = sbom_manager.generate_cyclonedx_sbom(output_format="json")
sbom_manager.export_sbom(
cyclonedx_sbom,
"cyclonedx",
"sbom-cyclonedx.json"
)
print(f"Generated CycloneDX SBOM with {len(cyclonedx_sbom['components'])} components")
# Generate SPDX SBOM for compliance
spdx_sbom = sbom_manager.generate_spdx_sbom()
sbom_manager.export_sbom(
spdx_sbom,
"spdx",
"sbom-spdx.json"
)
print(f"Generated SPDX SBOM with {len(spdx_sbom['packages'])} packages")
# Scan for vulnerabilities
vulnerabilities = sbom_manager.scan_for_vulnerabilities(cyclonedx_sbom)
print(f"Identified {len(vulnerabilities)} vulnerabilities")
# Generate compliance report
compliance_report = sbom_manager.generate_compliance_report(cyclonedx_sbom)
print(f"Compliance Report:")
print(f" Total Components: {compliance_report['total_components']}")
print(f" Unique Licenses: {len(compliance_report['licenses'])}")
print(f" Compliance Status: {compliance_report['compliance_status']}")Secrets Management and Detection in DevOps Pipelines
Credential exposure in source code and CI/CD pipelines represents one of the most common and dangerous security vulnerabilities in DevOps environments. Developers inadvertently commit API keys, database passwords, cloud credentials, and private keys to repositories, where they remain in Git history even after being removed from current code. Attackers systematically scan public repositories for exposed credentials, with automated tools detecting and exploiting committed secrets within minutes of exposure. Once attackers obtain valid credentials, they can pivot to production systems, exfiltrate data, deploy cryptocurrency miners, or establish persistent backdoors.
GitHub Advanced Security provides comprehensive secret scanning capabilities that detect over 200 credential patterns from major service providers including AWS, Azure, GCP, GitHub, Stripe, Twilio, and many others. Secret scanning operates in two modes: repository scanning that analyzes entire Git history to identify existing exposures, and push protection that blocks commits containing secrets before they enter repositories. When secrets are detected, GitHub automatically notifies both repository administrators and affected service providers, enabling rapid credential revocation before attackers can exploit the exposure.
Azure DevOps Advanced Security integrates similar secret scanning capabilities directly into Azure Repos, providing the same protection patterns for organizations standardized on Microsoft toolchains. The service scans repositories for exposed credentials, validates whether detected secrets remain active through automated checks with service providers, and provides remediation guidance that walks developers through proper secret rotation and secure storage alternatives. Push protection prevents developers from committing secrets, displaying in-line warnings during push operations with clear instructions for removing sensitive data before code enters version control.
The following Node.js implementation demonstrates building a custom secret scanning service that can be deployed alongside GitHub Advanced Security for additional organizational-specific secret patterns:
const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
const { Octokit } = require('@octokit/rest');
class SecretScanner {
constructor(config) {
this.config = config;
this.octokit = new Octokit({ auth: config.githubToken });
// Define custom secret patterns specific to organization
this.secretPatterns = [
{
name: 'AWS Access Key',
pattern: /AKIA[0-9A-Z]{16}/g,
severity: 'critical',
remediation: 'Revoke the AWS access key immediately in IAM console'
},
{
name: 'Azure Storage Account Key',
pattern: /AccountKey=[A-Za-z0-9+/]{88}==/g,
severity: 'critical',
remediation: 'Regenerate the storage account key in Azure portal'
},
{
name: 'GitHub Personal Access Token',
pattern: /ghp_[A-Za-z0-9]{36}/g,
severity: 'critical',
remediation: 'Revoke the token in GitHub settings immediately'
},
{
name: 'Generic API Key',
pattern: /api[_-]?key['"]?\s*[:=]\s*['"]?[A-Za-z0-9]{32,}/gi,
severity: 'high',
remediation: 'Rotate the API key with the service provider'
},
{
name: 'Database Connection String',
pattern: /(?:mongodb|mysql|postgresql|mssql):\/\/[^\s'"]+/gi,
severity: 'critical',
remediation: 'Change database password and update connection string in secrets manager'
},
{
name: 'Private Key',
pattern: /-----BEGIN (?:RSA |OPENSSH |DSA |EC )?PRIVATE KEY-----/g,
severity: 'critical',
remediation: 'Revoke the key and generate a new key pair'
},
{
name: 'JWT Token',
pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
severity: 'high',
remediation: 'Invalidate the JWT and regenerate with shorter expiration'
},
{
name: 'Custom Internal API Key',
pattern: /INTERNAL_API_[A-Z0-9]{24}/g,
severity: 'high',
remediation: 'Contact security team to rotate internal API key'
}
];
}
async scanRepository(owner, repo) {
console.log(`Scanning repository: ${owner}/${repo}`);
const findings = [];
try {
// Get all files in repository
const { data: tree } = await this.octokit.git.getTree({
owner,
repo,
tree_sha: 'HEAD',
recursive: true
});
// Filter for text files (exclude binaries, images, etc.)
const textFiles = tree.tree.filter(item =>
item.type === 'blob' &&
this._isTextFile(item.path)
);
// Scan each file
for (const file of textFiles) {
const fileFindings = await this.scanFile(owner, repo, file.path, file.sha);
findings.push(...fileFindings);
}
// Generate scan report
const report = this.generateScanReport(owner, repo, findings);
// Create GitHub issue for critical findings
if (findings.some(f => f.severity === 'critical')) {
await this.createSecurityIssue(owner, repo, findings);
}
return report;
} catch (error) {
console.error(`Error scanning repository: ${error.message}`);
throw error;
}
}
async scanFile(owner, repo, filePath, sha) {
const findings = [];
try {
// Get file content
const { data: fileData } = await this.octokit.git.getBlob({
owner,
repo,
file_sha: sha
});
// Decode base64 content
const content = Buffer.from(fileData.content, 'base64').toString('utf8');
// Scan content against all patterns
for (const pattern of this.secretPatterns) {
const matches = content.matchAll(pattern.pattern);
for (const match of matches) {
const finding = {
file: filePath,
secretType: pattern.name,
severity: pattern.severity,
matchedValue: this._maskSecret(match[0]),
lineNumber: this._getLineNumber(content, match.index),
remediation: pattern.remediation,
hash: crypto.createHash('sha256')
.update(match[0])
.digest('hex')
.substring(0, 16)
};
findings.push(finding);
}
}
} catch (error) {
// Skip files that can't be decoded as text
if (error.status !== 404) {
console.error(`Error scanning file ${filePath}: ${error.message}`);
}
}
return findings;
}
async scanLocalDirectory(directoryPath) {
console.log(`Scanning local directory: ${directoryPath}`);
const findings = [];
async function scanDir(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
// Skip common directories that shouldn't be scanned
if (entry.isDirectory()) {
if (!['node_modules', '.git', 'dist', 'build', 'vendor'].includes(entry.name)) {
await scanDir(fullPath);
}
} else if (entry.isFile() && this._isTextFile(entry.name)) {
const fileFindings = await this.scanLocalFile(fullPath);
findings.push(...fileFindings);
}
}
}
await scanDir(directoryPath);
return {
scannedPath: directoryPath,
totalFindings: findings.length,
criticalFindings: findings.filter(f => f.severity === 'critical').length,
findings: findings
};
}
async scanLocalFile(filePath) {
const findings = [];
try {
const content = await fs.readFile(filePath, 'utf8');
for (const pattern of this.secretPatterns) {
const matches = content.matchAll(pattern.pattern);
for (const match of matches) {
findings.push({
file: filePath,
secretType: pattern.name,
severity: pattern.severity,
matchedValue: this._maskSecret(match[0]),
lineNumber: this._getLineNumber(content, match.index),
remediation: pattern.remediation
});
}
}
} catch (error) {
// Skip files that can't be read as text
if (error.code !== 'ENOENT') {
console.error(`Error scanning file ${filePath}: ${error.message}`);
}
}
return findings;
}
_isTextFile(filename) {
const textExtensions = [
'.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cs', '.go',
'.rb', '.php', '.json', '.xml', '.yaml', '.yml', '.toml',
'.md', '.txt', '.sh', '.bash', '.env', '.config', '.properties'
];
return textExtensions.some(ext => filename.endsWith(ext));
}
_maskSecret(secret) {
if (secret.length <= 8) {
return '***';
}
return secret.substring(0, 4) + '***' + secret.substring(secret.length - 4);
}
_getLineNumber(content, index) {
return content.substring(0, index).split('\n').length;
}
generateScanReport(owner, repo, findings) {
const report = {
repository: `${owner}/${repo}`,
scanTimestamp: new Date().toISOString(),
totalFindings: findings.length,
bySeverity: {
critical: findings.filter(f => f.severity === 'critical').length,
high: findings.filter(f => f.severity === 'high').length,
medium: findings.filter(f => f.severity === 'medium').length,
low: findings.filter(f => f.severity === 'low').length
},
bySecretType: {},
findings: findings
};
// Aggregate by secret type
for (const finding of findings) {
report.bySecretType[finding.secretType] =
(report.bySecretType[finding.secretType] || 0) + 1;
}
return report;
}
async createSecurityIssue(owner, repo, findings) {
const criticalFindings = findings.filter(f => f.severity === 'critical');
if (criticalFindings.length === 0) return;
const issueBody = `
## Critical Security Alert: Secrets Detected
This repository contains ${criticalFindings.length} critical secret exposure(s) that require immediate attention.
### Summary
${criticalFindings.map(f => `- **${f.secretType}** in \`${f.file}\` (line ${f.lineNumber})`).join('\n')}
### Immediate Actions Required
1. Revoke all exposed credentials immediately
2. Remove secrets from Git history using tools like BFG Repo-Cleaner
3. Implement proper secrets management using Azure Key Vault or GitHub Secrets
4. Enable push protection to prevent future exposures
### Remediation Guidance
${criticalFindings.map(f => `
**${f.secretType}** (\`${f.file}\`):
${f.remediation}
`).join('\n')}
**⚠️ This is an automated security alert. Act immediately to prevent unauthorized access.**
`;
try {
await this.octokit.issues.create({
owner,
repo,
title: `[SECURITY] Critical Secrets Detected - Immediate Action Required`,
body: issueBody,
labels: ['security', 'critical']
});
console.log(`Created security issue for ${owner}/${repo}`);
} catch (error) {
console.error(`Error creating security issue: ${error.message}`);
}
}
async scanPullRequest(owner, repo, pullNumber) {
console.log(`Scanning pull request #${pullNumber}`);
try {
// Get PR files
const { data: files } = await this.octokit.pulls.listFiles({
owner,
repo,
pull_number: pullNumber
});
const findings = [];
// Scan changed files
for (const file of files) {
if (file.status !== 'removed' && this._isTextFile(file.filename)) {
const fileFindings = await this.scanFile(
owner,
repo,
file.filename,
file.sha
);
findings.push(...fileFindings);
}
}
// Comment on PR if secrets found
if (findings.length > 0) {
await this.commentOnPullRequest(owner, repo, pullNumber, findings);
}
return {
pullRequest: pullNumber,
findings: findings,
blocked: findings.some(f => f.severity === 'critical')
};
} catch (error) {
console.error(`Error scanning pull request: ${error.message}`);
throw error;
}
}
async commentOnPullRequest(owner, repo, pullNumber, findings) {
const comment = `
## ⚠️ Secret Scanner Alert
This pull request contains potential secrets that should not be committed:
${findings.map(f => `
### ${f.severity.toUpperCase()}: ${f.secretType}
- **File**: \`${f.file}\`
- **Line**: ${f.lineNumber}
- **Matched**: \`${f.matchedValue}\`
- **Remediation**: ${f.remediation}
`).join('\n')}
${findings.some(f => f.severity === 'critical') ?
'**⚠️ CRITICAL: This PR cannot be merged until these issues are resolved.**' :
'Please review and remediate these findings before merging.'}
`;
try {
await this.octokit.issues.createComment({
owner,
repo,
issue_number: pullNumber,
body: comment
});
} catch (error) {
console.error(`Error commenting on PR: ${error.message}`);
}
}
}
// Example usage
const scanner = new SecretScanner({
githubToken: process.env.GITHUB_TOKEN
});
// Scan a repository
scanner.scanRepository('myorg', 'myrepo')
.then(report => {
console.log('Scan complete:');
console.log(` Total findings: ${report.totalFindings}`);
console.log(` Critical: ${report.bySeverity.critical}`);
console.log(` High: ${report.bySeverity.high}`);
});
// Scan local directory (for pre-commit hooks)
scanner.scanLocalDirectory(process.cwd())
.then(result => {
if (result.criticalFindings > 0) {
console.error(`❌ Commit blocked: ${result.criticalFindings} critical secrets detected`);
process.exit(1);
}
console.log('✅ No critical secrets detected');
});Due to message limits, I’ll continue with the remaining sections in the next part. Should I proceed with the rest of Post 5?
