DEV Community

Sergio Guadarrama
Sergio Guadarrama

Posted on

From 80% False Positives to 95% Accurate: How We Fixed Architecture Linting

The Starting Point

Two months ago, we built Architect Linter to solve a real problem: teams'
codebases fall apart as they grow
.

v5 used simple pattern matching for security analysis:

  • Any function with "execute" in name → sink
  • All parameters → potential sources
  • Result: False positives everywhere
// Real code from a production NestJS app
// v5 would flag as CRITICAL VULNERABILITY

const executeWithErrorHandling = async (callback) => {
  try {
    return await callback();
  } catch (e) {
    logger.error(e);
    return null;
  }
};

const userInput = req.query.name;
const result = executeWithErrorHandling(async () => {
  // Do something safe with userInput
  return db.prepare("SELECT * FROM users WHERE name = ?").run(userInput);
});

// v5: 🚨 CRITICAL: "executeWithErrorHandling is a sink"
//     🚨 CRITICAL: "executeWithErrorHandling receives user input"
// Reality: ✅ Code is 100% safe (parameterized query)
Enter fullscreen mode Exit fullscreen mode

Developers ignored all findings. Security analysis became useless.


The Rewrite: CFG-Based Analysis

For v6, we completely rewrote the security engine using Control Flow Graphs:

Step 1: Parse code into a CFG

req.query.id (SOURCE)
    ↓
const id = ...
    ↓
escape(id)  (SANITIZER)
    ↓
db.query(id)  (SINK)
    ↓
Result: ✅ SAFE (data was sanitized)
Enter fullscreen mode Exit fullscreen mode

Step 2: Track actual data flow

  • Which variables receive untrusted data?
  • Where does that data go?
  • Is it sanitized before reaching a sink?

Step 3: Only report real issues

// ✅ Safe: Data is parameterized
db.execute("SELECT * FROM users WHERE id = ?", [userId]);

// ⚠️ Unsafe: Direct interpolation
db.execute(`SELECT * FROM users WHERE id = ${userId}`);

// ✅ Safe: Data is escaped
db.execute(`SELECT * FROM users WHERE name = '${escape(userName)}'`);
Enter fullscreen mode Exit fullscreen mode

Result: 95%+ Accuracy

Metric v5.0 v6.0
True Positives 20% 95%
False Positives 80%+ <5%
Developer Trust ❌ None ✅ High
Enterprise Ready ❌ No ✅ Yes

Bonus: Zero-Config Setup

While we were at it, we also fixed the friction of "I have to configure
this for 30 minutes before I can use it":

$ architect init
🔍 Detecting frameworks...
   ✓ NextJS (from package.json)
   ✓ Django (from requirements.txt)

✨ Generating config...
   Created: architect.json (90% auto-complete)

Ready to lint! Run: architect lint .
Enter fullscreen mode Exit fullscreen mode

Now supports many modern frameworks (TypeScript, Python, PHP).


What This Teaches Us

  1. Simple heuristics don't work for security

    • "Contains 'execute'" is a bad signal
    • Need to understand actual control flow
  2. Zero-config adoption beats "perfect but complex"

    • 30-minute setup → Users abandon
    • 5-minute setup → Real usage
  3. Focus beats breadth

    • Supporting 11 languages poorly > supporting 3 languages well
    • Dropped Go/Java, added Vue/Svelte (web-focused)
  4. Tests catch everything

    • We rewrote the core logic (risky!)
    • 432+ tests meant we could refactor confidently
    • Only broke 0 public APIs

Getting Started

cargo install architect-linter-pro
cd your-project
architect init
architect lint .
Enter fullscreen mode Exit fullscreen mode

GitHub: https://github.com/sergiogswv/architect-linter-pro
Crates.io: https://crates.io/crates/architect-linter-pro
Docs: https://github.com/.../docs/MIGRATION_v6.md


What's Next

  • v6.1: Variable tracking (catches injection in loops)
  • v7: Pre-commit hooks + CI/CD templates
  • v8: VS Code extension (if there's interest)

Questions? Hit me in the comments.

Top comments (0)