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

- Name
- Sanjeev Sharma
- @webcoderspeed1
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
- Fix 1: Immediate Security Fixes (Week 1)
- Fix 2: Encryption at Rest for Customer Data
- Fix 3: Audit Logging for Admin and Data Access
- Fix 4: MFA for All Admin Access
- Fix 5: Security Questionnaire Template Answers
- Security Sprint Checklist
- Conclusion
What Enterprise Security Reviews Actually Ask For
Common enterprise security requirements (ranked by frequency):
Tier 1 — Deal 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 2 — Required 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 3 — Nice 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.