Security Scanning: SAST and DAST
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
Why Traditional Security Scanning Fails Modern Teams
The security landscape in 2025 differs fundamentally from even three years ago. Cloud-native applications built on Kubernetes span multiple regions and availability zones. Serverless functions execute in ephemeral environments that disappear before traditional DAST tools can complete their crawls. Supply chain attacks exploit dependencies that change daily through automated updates. AI coding assistants generate thousands of lines of code that may contain subtle vulnerabilities invisible to developers but exploitable at scale.
Traditional security scanning approaches fail because they assume stable, monolithic applications with predictable deployment schedules. Running a SAST scan that takes 45 minutes blocks the entire pipeline when teams need sub-10-minute feedback loops. DAST tools designed to crawl web applications miss API-first architectures where business logic lives in GraphQL resolvers or gRPC services. Point-in-time scans create false confidence—code that was secure yesterday becomes vulnerable today when a new CVE affects a dependency.
The cost implications are severe. A financial services company I worked with discovered their manual security process added 8-12 hours to every release cycle. With 40 releases per week, they were burning 400 engineering hours on security coordination alone. Worse, the delayed feedback meant developers had context-switched to new features by the time security findings arrived, multiplying the cognitive load of fixes.
Modern Architecture for Integrated Security Scanning
Effective SAST and DAST integration requires treating security scanning as a first-class citizen in the development workflow, not a gate at the end. The architecture must support parallel execution, incremental scanning, and intelligent result correlation while maintaining fast feedback loops that don't block developer productivity.
The modern approach uses a multi-stage pipeline where different security tools run at appropriate points based on what they can detect and how long they take. SAST tools analyze code during pull request builds, catching issues before merge. Container scanning runs during image builds. DAST tools execute against ephemeral preview environments created for each feature branch. This distributed model ensures comprehensive coverage without creating bottlenecks.
Here's a production-grade implementation using GitHub Actions with Semgrep for SAST and OWASP ZAP for DAST:
name: Security Scanning Pipeline
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
jobs:
sast-scan:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Semgrep SAST
uses: semgrep/semgrep-action@v1
with:
config: >-
p/security-audit
p/secrets
p/owasp-top-ten
generateSarif: true
env:
SEMGREP_RULES: |
rules:
- id: hardcoded-credentials
severity: ERROR
pattern: |
const $VAR = "$SECRET"
message: Potential hardcoded credential detected
- name: Upload SARIF results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarif
if: always()
build-and-scan:
needs: sast-scan
runs-on: ubuntu-latest
outputs:
preview-url: ${{ steps.deploy.outputs.url }}
steps:
- uses: actions/checkout@v4
- name: Build container
run: |
docker build -t app:${{ github.sha }} .
- name: Scan container with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: app:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
- name: Deploy preview environment
id: deploy
run: |
# Deploy to ephemeral environment
kubectl create namespace preview-${{ github.event.number }}
helm install app-preview-${{ github.event.number }} ./charts \
--namespace preview-${{ github.event.number }} \
--set image.tag=${{ github.sha }}
echo "url=https://preview-${{ github.event.number }}.example.com" >> $GITHUB_OUTPUT
dast-scan:
needs: build-and-scan
runs-on: ubuntu-latest
steps:
- name: Wait for deployment
run: |
timeout 300 bash -c 'until curl -sf ${{ needs.build-and-scan.outputs.preview-url }}/health; do sleep 5; done'
- name: Run OWASP ZAP DAST
uses: zaproxy/action-full-scan@v0.8.0
with:
target: ${{ needs.build-and-scan.outputs.preview-url }}
rules_file_name: .zap/rules.tsv
cmd_options: '-a -j -m 5 -T 30'
allow_issue_writing: true
- name: Process ZAP results
run: |
python3 scripts/process-zap-results.py \
--input zap_report.json \
--output processed-findings.json \
--severity-threshold HIGH
This pipeline architecture provides several critical capabilities. SAST runs early on source code, catching issues like SQL injection patterns, hardcoded secrets, and insecure cryptographic implementations before they reach compiled artifacts. Container scanning catches vulnerable dependencies in the final image. DAST runs against a live preview environment, detecting runtime issues like authentication bypasses, CORS misconfigurations, and API authorization flaws that only manifest in running applications.
Intelligent Result Correlation and Deduplication
The biggest operational challenge with integrated security scanning isn't running the tools—it's managing the flood of findings. A typical application might generate 200+ security findings across SAST, DAST, and dependency scanning. Many are duplicates detected by multiple tools. Others are false positives. Some are real but low-risk in your specific context.
Effective integration requires a correlation layer that deduplicates findings, enriches them with context, and routes them appropriately. Here's a TypeScript implementation of a security finding processor:
import { Octokit } from '@octokit/rest';
import { createHash } from 'crypto';
interface SecurityFinding {
tool: 'sast' | 'dast' | 'sca';
severity: 'critical' | 'high' | 'medium' | 'low';
category: string;
location: {
file?: string;
line?: number;
url?: string;
};
description: string;
cwe?: string;
cvss?: number;
}
interface EnrichedFinding extends SecurityFinding {
fingerprint: string;
isDuplicate: boolean;
businessImpact: 'critical' | 'high' | 'medium' | 'low';
recommendedAction: string;
relatedFindings: string[];
}
class SecurityFindingProcessor {
private findingCache: Map<string, EnrichedFinding> = new Map();
private octokit: Octokit;
constructor(githubToken: string) {
this.octokit = new Octokit({ auth: githubToken });
}
generateFingerprint(finding: SecurityFinding): string {
const normalized = {
category: finding.category,
location: finding.location.file || finding.location.url,
cwe: finding.cwe,
};
return createHash('sha256')
.update(JSON.stringify(normalized))
.digest('hex')
.substring(0, 16);
}
assessBusinessImpact(finding: SecurityFinding): 'critical' | 'high' | 'medium' | 'low' {
// Critical: Authentication/authorization issues in production-facing services
if (finding.category.includes('auth') && finding.location.url?.includes('api')) {
return 'critical';
}
// High: SQL injection, RCE, sensitive data exposure
const highRiskCategories = ['sql-injection', 'command-injection', 'xxe', 'sensitive-data'];
if (highRiskCategories.some(cat => finding.category.includes(cat))) {
return 'high';
}
// Medium: XSS, CSRF in authenticated contexts
if (finding.severity === 'high' && finding.cvss && finding.cvss >= 7.0) {
return 'medium';
}
return 'low';
}
correlateFindings(findings: SecurityFinding[]): EnrichedFinding[] {
const enriched: EnrichedFinding[] = [];
const fingerprintMap = new Map<string, EnrichedFinding[]>();
for (const finding of findings) {
const fingerprint = this.generateFingerprint(finding);
const businessImpact = this.assessBusinessImpact(finding);
const enrichedFinding: EnrichedFinding = {
...finding,
fingerprint,
isDuplicate: this.findingCache.has(fingerprint),
businessImpact,
recommendedAction: this.getRecommendedAction(finding),
relatedFindings: [],
};
if (!fingerprintMap.has(fingerprint)) {
fingerprintMap.set(fingerprint, []);
}
fingerprintMap.get(fingerprint)!.push(enrichedFinding);
enriched.push(enrichedFinding);
}
// Link related findings
for (const [fingerprint, relatedFindings] of fingerprintMap) {
if (relatedFindings.length > 1) {
const tools = relatedFindings.map(f => f.tool).join(', ');
relatedFindings.forEach(f => {
f.relatedFindings = [`Detected by: ${tools}`];
});
}
}
return enriched;
}
getRecommendedAction(finding: SecurityFinding): string {
const actions: Record<string, string> = {
'sql-injection': 'Use parameterized queries or an ORM with prepared statements',
'hardcoded-secret': 'Move to environment variables or secret management service',
'weak-crypto': 'Use AES-256-GCM or ChaCha20-Poly1305 for encryption',
'missing-auth': 'Implement OAuth 2.0 with PKCE or JWT with proper validation',
'cors-misconfiguration': 'Restrict CORS origins to specific trusted domains',
};
for (const [key, action] of Object.entries(actions)) {
if (finding.category.includes(key)) {
return action;
}
}
return 'Review finding details and consult security team';
}
async createSecurityIssues(
findings: EnrichedFinding[],
owner: string,
repo: string,
prNumber: number
): Promise<void> {
const criticalFindings = findings.filter(
f => f.businessImpact === 'critical' && !f.isDuplicate
);
if (criticalFindings.length > 0) {
// Block PR for critical findings
await this.octokit.pulls.createReview({
owner,
repo,
pull_number: prNumber,
event: 'REQUEST_CHANGES',
body: this.formatSecurityComment(criticalFindings),
});
} else {
const highFindings = findings.filter(
f => f.businessImpact === 'high' && !f.isDuplicate
);
if (highFindings.length > 0) {
// Comment but don't block
await this.octokit.pulls.createReview({
owner,
repo,
pull_number: prNumber,
event: 'COMMENT',
body: this.formatSecurityComment(highFindings),
});
}
}
}
formatSecurityComment(findings: EnrichedFinding[]): string {
let comment = '## 🔒 Security Findings\n\n';
for (const finding of findings) {
comment += `### ${finding.severity.toUpperCase()}: ${finding.category}\n`;
comment += `**Location:** ${finding.location.file || finding.location.url}\n`;
comment += `**Impact:** ${finding.businessImpact}\n`;
comment += `**Action:** ${finding.recommendedAction}\n\n`;
if (finding.relatedFindings.length > 0) {
comment += `*${finding.relatedFindings.join(', ')}*\n\n`;
}
}
return comment;
}
}
// Usage in pipeline
async function processPipelineFindings() {
const processor = new SecurityFindingProcessor(process.env.GITHUB_TOKEN!);
const sastFindings = JSON.parse(await fs.readFile('semgrep.json', 'utf-8'));
const dastFindings = JSON.parse(await fs.readFile('zap_report.json', 'utf-8'));
const allFindings = [...sastFindings, ...dastFindings];
const enriched = processor.correlateFindings(allFindings);
await processor.createSecurityIssues(
enriched,
'your-org',
'your-repo',
parseInt(process.env.PR_NUMBER!)
);
}
This correlation engine solves several real problems. It generates stable fingerprints so the same vulnerability detected across multiple scans doesn't create duplicate tickets. It assesses business impact based on context—an authentication bypass in a public API is critical, while a low-severity XSS in an internal admin panel might be medium priority. It provides actionable remediation guidance instead of generic security advice.
Handling Scale and Performance Constraints
As codebases grow and deployment frequency increases, security scanning becomes a performance bottleneck. A monorepo with 500,000 lines of code might take 20 minutes for a full SAST scan. Running DAST against a complex application with 200 API endpoints could take an hour. These timelines are incompatible with modern development velocity.
The solution is incremental scanning that analyzes only what changed. For SAST, this means scanning modified files and their dependencies rather than the entire codebase. For DAST, it means targeting endpoints affected by code changes rather than crawling the entire application.
Implement differential scanning by tracking file changes:
import { execSync } from 'child_process';
import * as fs from 'fs/promises';
interface ScanScope {
changedFiles: string[];
affectedEndpoints: string[];
riskLevel: 'full' | 'targeted' | 'minimal';
}
class IncrementalScanManager {
async determineScanScope(baseBranch: string = 'main'): Promise<ScanScope> {
// Get changed files
const diffOutput = execSync(
`git diff --name-only ${baseBranch}...HEAD`,
{ encoding: 'utf-8' }
);
const changedFiles = diffOutput.trim().split('\n').filter(Boolean);
// Analyze change impact
const riskLevel = this.assessChangeRisk(changedFiles);
const affectedEndpoints = await this.mapFilesToEndpoints(changedFiles);
return {
changedFiles,
affectedEndpoints,
riskLevel,
};
}
assessChangeRisk(files: string[]): 'full' | 'targeted' | 'minimal' {
// Full scan if security-critical files changed
const criticalPatterns = [
/auth/i,
/security/i,
/middleware/i,
/config/i,
/\.env/,
];
if (files.some(f => criticalPatterns.some(p => p.test(f)))) {
return 'full';
}
// Targeted scan for API or service changes
if (files.some(f => f.includes('api/') || f.includes('service/'))) {
return 'targeted';
}
// Minimal scan for isolated changes
return 'minimal';
}
async mapFilesToEndpoints(files: string[]): Promise<string[]> {
// Parse route definitions to find affected endpoints
const endpoints = new Set<string>();
for (const file of files) {
if (!file.endsWith('.ts') && !file.endsWith('.js')) continue;
try {
const content = await fs.readFile(file, 'utf-8');
// Extract route definitions (example for Express)
const routeRegex = /router\.(get|post|put|delete|patch)\(['"]([^'"]+)['"]/g;
let match;
while ((match = routeRegex.exec(content)) !== null) {
endpoints.add(`${match[1].toUpperCase()} ${match[2]}`);
}
} catch (error) {
console.warn(`Could not parse ${file}:`, error);
}
}
return Array.from(endpoints);
}
generateZapConfig(scope: ScanScope): string {
if (scope.riskLevel === 'full') {
return 'full-scan-config.yaml';
}
// Generate targeted scan config
const config = {
env: {
contexts: [
{
name: 'targeted-scan',
urls: scope.affectedEndpoints.map(e =>
`https://preview.example.com${e.split(' ')[1]}`
),
includePaths: scope.affectedEndpoints.map(e => e.split(' ')[1]),
},
],
},
jobs: [
{
type: 'spider',
parameters: {
maxDuration: 5,
maxDepth: 2,
},
},
{
type: 'activeScan',
parameters: {
maxRuleDurationInMins: 10,
maxScanDurationInMins: 15,
},
},
],
};
return JSON.stringify(config, null, 2);
}
}
This incremental approach reduces scan times by 60-80% for typical pull requests while maintaining security coverage. A change to a single API endpoint triggers targeted DAST scanning