Published on

Security Audit Before the Enterprise Deal — Six Weeks to Fix Two Years of Technical Debt

Authors

Introduction

Enterprise sales cycles end with a security review. The prospect's InfoSec team wants penetration test results, evidence of SOC 2 controls, a data processing agreement, and answers to a 200-question security questionnaire. For startups that built fast, this is the moment years of "we'll do security later" debt comes due. The good news: most security fundamentals are fixable in weeks, not months. The bad news: you have to prioritize the right things, fast.

What Enterprise Security Reviews Actually Ask For

Common enterprise security requirements (ranked by frequency):

Tier 1Deal killers if missing:
- Encryption in transit (TLS 1.2+)Usually already done
- Encryption at rest for customer data ← Often missing
- MFA for admin access ← Often missing
- Penetration test results (< 12 months old)Often missing
- Employee background checks ← Process question
- Incident response plan ← Document needed

Tier 2Required for larger deals:
- SOC 2 Type I (process audit) or SOC 2 Type II (6-month operational audit)
- GDPR/CCPA compliance documentation
- Data retention and deletion policies
- Vendor/subprocessor list
- Business continuity plan

Tier 3Nice to have:
- Bug bounty program
- ISO 27001 certification
- Regular security training records

Fix 1: Immediate Security Fixes (Week 1)

// Day 1: Audit for hardcoded secrets
// git log --all -p | grep -E "(password|secret|api_key|token)" | grep "=" | head -50

// Day 2: Ensure all passwords are properly hashed
import bcrypt from 'bcrypt'
import argon2 from 'argon2'

// ❌ Never store plain text or MD5/SHA1 passwords
// ❌ Never log passwords
async function createUser(email: string, password: string) {
  // ✅ Use argon2id (preferred) or bcrypt (widely supported)
  const hash = await argon2.hash(password, {
    type: argon2.argon2id,
    memoryCost: 65536,  // 64 MB
    timeCost: 3,
    parallelism: 4,
  })

  return db.query(
    'INSERT INTO users (email, password_hash) VALUES ($1, $2)',
    [email, hash]
  )
  // Note: "password_hash" not "password" in the column name
}

// Day 3: Ensure sensitive fields are never logged
// Add a log sanitizer that strips passwords, tokens, and secrets

function sanitizeForLogging(obj: Record<string, unknown>): Record<string, unknown> {
  const SENSITIVE_KEYS = new Set([
    'password', 'password_hash', 'token', 'secret', 'api_key',
    'authorization', 'credit_card', 'cvv', 'ssn',
  ])

  return Object.fromEntries(
    Object.entries(obj).map(([key, value]) => {
      if (SENSITIVE_KEYS.has(key.toLowerCase())) {
        return [key, '[REDACTED]']
      }
      if (typeof value === 'object' && value !== null) {
        return [key, sanitizeForLogging(value as Record<string, unknown>)]
      }
      return [key, value]
    })
  )
}

Fix 2: Encryption at Rest for Customer Data

// Encrypt sensitive PII fields at the application layer
// Even if someone gets DB access, they can't read customer data
import crypto from 'crypto'

const ENCRYPTION_KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'base64')
const ALGORITHM = 'aes-256-gcm'

function encrypt(plaintext: string): string {
  const iv = crypto.randomBytes(12)
  const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv)

  let encrypted = cipher.update(plaintext, 'utf8', 'base64')
  encrypted += cipher.final('base64')
  const authTag = cipher.getAuthTag()

  // Store: iv + authTag + encrypted data (all base64)
  return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted}`
}

function decrypt(ciphertext: string): string {
  const [ivB64, authTagB64, encrypted] = ciphertext.split(':')
  const iv = Buffer.from(ivB64, 'base64')
  const authTag = Buffer.from(authTagB64, 'base64')

  const decipher = crypto.createDecipheriv(ALGORITHM, ENCRYPTION_KEY, iv)
  decipher.setAuthTag(authTag)

  let decrypted = decipher.update(encrypted, 'base64', 'utf8')
  decrypted += decipher.final('utf8')

  return decrypted
}

// Store encrypted in DB:
await db.query(
  'UPDATE users SET ssn_encrypted = $1 WHERE id = $2',
  [encrypt(ssn), userId]
)

Fix 3: Audit Logging for Admin and Data Access

// Every admin action must be logged — enterprise auditors will ask for this
// "Who accessed customer data, and when?"

interface AuditEvent {
  actorId: string
  actorType: 'user' | 'admin' | 'api_key' | 'system'
  action: string
  resourceType: string
  resourceId: string
  oldValue?: unknown
  newValue?: unknown
  ipAddress: string
  userAgent: string
  timestamp: Date
}

async function logAuditEvent(event: Omit<AuditEvent, 'timestamp'>): Promise<void> {
  await db.query(`
    INSERT INTO audit_log (
      actor_id, actor_type, action, resource_type, resource_id,
      old_value, new_value, ip_address, user_agent, timestamp
    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())
  `, [
    event.actorId, event.actorType, event.action,
    event.resourceType, event.resourceId,
    event.oldValue ? JSON.stringify(event.oldValue) : null,
    event.newValue ? JSON.stringify(event.newValue) : null,
    event.ipAddress, event.userAgent,
  ])
}

// Audit log middleware for admin routes
app.use('/admin', async (req, res, next) => {
  const originalJson = res.json.bind(res)

  res.json = function(body) {
    logAuditEvent({
      actorId: req.admin!.id,
      actorType: 'admin',
      action: `${req.method} ${req.path}`,
      resourceType: 'admin_action',
      resourceId: req.params.id ?? 'unknown',
      ipAddress: req.ip,
      userAgent: req.headers['user-agent'] ?? 'unknown',
    })
    return originalJson(body)
  }

  next()
})

Fix 4: MFA for All Admin Access

// Every admin account must require TOTP MFA
// This is one of the first things enterprise security teams check
import speakeasy from 'speakeasy'
import qrcode from 'qrcode'

async function setupMFA(adminId: string): Promise<{ qrCode: string; secret: string }> {
  const secret = speakeasy.generateSecret({
    name: `MyApp Admin (${adminId})`,
    length: 20,
  })

  // Store secret (encrypted!)
  await db.query(
    'UPDATE admins SET totp_secret = $1, mfa_enabled = false WHERE id = $2',
    [encrypt(secret.base32), adminId]
  )

  const qrCode = await qrcode.toDataURL(secret.otpauth_url!)

  return { qrCode, secret: secret.base32 }
}

async function verifyMFA(adminId: string, token: string): Promise<boolean> {
  const result = await db.query(
    'SELECT totp_secret FROM admins WHERE id = $1',
    [adminId]
  )

  const encryptedSecret = result.rows[0]?.totp_secret
  if (!encryptedSecret) return false

  const secret = decrypt(encryptedSecret)

  return speakeasy.totp.verify({
    secret,
    encoding: 'base32',
    token,
    window: 2,  // Allow ±2 time steps for clock drift
  })
}

// Middleware: require MFA for all admin routes
async function requireMFA(req: Request, res: Response, next: NextFunction) {
  const admin = req.admin!

  if (!admin.mfaEnabled || !admin.mfaVerifiedAt) {
    return res.status(403).json({ error: 'MFA required' })
  }

  // MFA session must be recent (re-verify every 24 hours)
  const mfaAge = Date.now() - new Date(admin.mfaVerifiedAt).getTime()
  if (mfaAge > 24 * 60 * 60 * 1000) {
    return res.status(403).json({ error: 'MFA session expired — re-verify' })
  }

  next()
}

Fix 5: Security Questionnaire Template Answers

# Common Security Questionnaire: Quick Reference Answers

## Data Protection
Q: Is customer data encrypted at rest?
A: Yes — AES-256-GCM encryption at the application layer for all PII fields.
   Database storage encrypted using AWS RDS encryption (AES-256).

Q: Is data encrypted in transit?
A: Yes — TLS 1.3 for all client-server communication. Strict-Transport-Security
   header enforces HTTPS. Internal service communication via TLS.

## Access Control
Q: Is MFA required for administrative access?
A: Yes — TOTP MFA required for all admin accounts.
   SSH access requires key-based auth + bastion host with MFA.

Q: Do employees have least-privilege access?
A: Yes — role-based access control. Engineers have no direct prod DB access
   by default. Elevated access requires manager approval + audit log.

## Incident Response
Q: Do you have an incident response plan?
A: Yes — documented runbook covering detection, triage, containment,
   customer notification within 72 hours (per GDPR), and post-mortem.

## Compliance
Q: Are you SOC 2 certified?
A: SOC 2 Type I certification achieved [date]. Type II audit in progress.
   (Or: We use Vanta/Drata to maintain continuous controls evidence.)

Security Sprint Checklist

  • ✅ No secrets in version control — rotate any that were exposed
  • ✅ Passwords hashed with argon2id or bcrypt — never stored plain
  • ✅ Sensitive fields not appearing in logs (sanitizer in place)
  • ✅ PII encrypted at the application layer (AES-256-GCM)
  • ✅ Audit log for all admin actions and data access
  • ✅ MFA enforced for all admin accounts
  • ✅ Penetration test scheduled (or results from last 12 months)
  • ✅ Security questionnaire template prepared for prospects

Conclusion

Enterprise security reviews feel like a crisis when they're a surprise, but they're really a forcing function for work that should have been done earlier. The majority of deal-blocking security issues — hardcoded secrets, unencrypted PII, missing MFA, no audit log — are fixable in days to weeks. A six-week sprint with clear priorities (secrets → encryption → audit logging → MFA → pen test → documentation) can transform "we don't pass the security review" into "we passed." Start the security hygiene work before the deal is on the table — it's far less stressful.