| name | sentinel-audit |
| description | Source code security audits, SAST, dependency review. When findings are listed or audit ends, MUST chain to sentinel-report โ read skills/sentinel/sentinel-report/SKILL.md, ask save folder, Write .md + offline .html. Triggers on audit, vulnerability, security review, ๅฎก่ฎก. |
Sentinel: Code Audit
Mandatory handoff: When you present final findings or the userโs audit request is done, you must in the same conversation apply sentinel-report โ open skills/sentinel/sentinel-report/SKILL.md, run its Closing gate, and Write sentinel-security-assessment.md + .html. Do not end with chat-only bullet lists.
Security code review is not about finding syntax errors โ it's about understanding what the code trusts and whether that trust is justified.
Core principle: Follow the data. Every vulnerability is a place where untrusted data reaches a sensitive operation without adequate validation.
Phase 1: Scope & Surface Mapping
Before reading a single line of code:
- Confirm authorization โ Who owns this code? Is review authorized?
- Identify tech stack โ Language, framework, runtime version
- Map entry points โ HTTP endpoints, CLI args, file inputs, IPC, env vars
- Map trust boundaries โ What data comes from users? External APIs? Databases?
- Load the relevant reference โ See references below
Entry points โ trust boundaries โ sensitive sinks โ audit paths between them
Phase 2: Automated Scanning
Run appropriate tools first. Tools find the easy things; your brain finds the subtle things.
By language:
bandit -r . -ll
safety check
semgrep --config=p/python
npm audit
semgrep --config=p/javascript
eslint --plugin security .
semgrep --config=p/java
mvn dependency:check
psalm --taint-analysis
semgrep --config=p/php
gosec ./...
semgrep --config=p/golang
gitleaks detect
trufflehog filesystem .
Record all findings โ do not triage yet. Triage happens in Phase 4.
Phase 3: Manual Review โ The OWASP Path
Walk through these categories in order. Each one has a reference file with language-specific patterns.
A01 โ Broken Access Control
Horizontal Privilege Escalation (IDOR) โ User A accesses User B's resources:
Check every endpoint that accepts an object identifier (ID, UUID, slug):
GET /api/invoice/{id} โ Can user A view user B's invoice?
GET /api/profile/{id} โ Can user A view user B's profile?
POST /api/transfer โ Can user A transfer from someone else's account?
PUT /api/document/{id} โ Can user A edit user B's document?
DELETE /api/resource/{id} โ Can user A delete user B's data?
Audit checklist:
# Test IDOR by comparing responses:
# User A accessing own resource: 200 OK + data
# User A accessing User B's resource with same ID: should be 403 Forbidden, NOT 200 OK
Vertical Privilege Escalation โ Low-privilege user performs admin actions:
Check admin-only endpoints and privilege escalation paths:
POST /api/admin/users โ Can regular user create admin accounts?
GET /api/admin/audit-log โ Can regular user access admin logs?
PUT /api/roles โ Can user escalate their own privileges?
POST /api/settings โ Can user modify system settings?
Audit checklist:
# JWT Privilege Escalation patterns:
# Check if role is in JWT without server-side validation
# Check for "addRole" or "setRole" API that doesn't verify current user is admin
# Check for IDOR in role assignment: PUT /api/users/{id}/role
Access Control Pattern Examples:
def get_invoice(invoice_id):
return Invoice.objects.get(id=invoice_id)
def get_invoice(invoice_id):
return Invoice.objects.get(id=invoice_id, owner=request.user)
def update_user(request):
user.role = request.json['role']
user.save()
@require_admin
def update_user_role(request, user_id):
new_role = request.json['role']
User.objects.filter(id=user_id).update(role=new_role)
const token = jwt.decode(req.headers.authorization);
if (token.role === 'admin') {
}
const user = await getUserFromSession(req);
if (user.role !== 'admin') throw new Forbidden();
A02 โ Cryptographic Failures
- Is sensitive data encrypted at rest and in transit?
- Are passwords hashed with bcrypt/argon2/scrypt (not MD5/SHA1)?
- Are secrets hardcoded or in source control?
- Is TLS properly validated or are cert checks disabled?
A03 โ Injection
- SQL: Are queries parameterized or using ORM? Any string concatenation in queries?
- Command: Is
exec(), system(), subprocess called with user input?
- LDAP, XPath, NoSQL: Same question โ is user input reaching the query engine?
- Template injection: Is user input rendered in a template engine?
A04 โ Insecure Design
- Is rate limiting in place on auth endpoints?
- Are there logical flaws in business workflows (e.g., payment bypass, coupon stacking)?
- Does the system fail securely or does it fail open?
A05 โ Security Misconfiguration
- Debug mode enabled in production?
- Default credentials anywhere?
- Unnecessary features, ports, services enabled?
- Error messages revealing stack traces or internal paths?
A06 โ Vulnerable Components
- Are dependencies up to date?
- Are known-vulnerable versions pinned?
- Cross-reference against CVE database for critical deps
A07 โ Authentication Failures
- Brute force protection on login?
- Session tokens โ are they cryptographically random and sufficient length?
- Password reset flows โ are tokens time-limited and single-use?
- JWT: Is
alg: none accepted? Is the signature actually verified?
A08 โ Software & Data Integrity
- Are software updates verified before installation?
- Is CI/CD pipeline secured against injection?
- Are deserialization paths sanitized? (Java: ObjectInputStream, Python: pickle, Ruby: Marshal)
A09 โ Logging & Monitoring Failures
- Are security events (login attempts, access control failures) logged?
- Are logs protected from tampering?
- Are secrets or PII being logged?
A10 โ SSRF
- Is user-supplied URL fetched by the server?
- Are internal IP ranges blocked?
- Is DNS rebinding considered?
A11 โ Cross-Site Request Forgery (CSRF)
CSRF tricks authenticated users into submitting unintended requests.
Check every state-changing operation (POST, PUT, DELETE):
โ Does every form/action have a CSRF token?
โ Is the token validated server-side?
โ Are sensitive actions requiring additional confirmation?
โ Are CORS headers properly configured?
@app.route('/transfer', methods=['POST'])
def transfer():
amount = request.form['amount']
@app.route('/transfer', methods=['POST'])
def transfer():
if request.form['csrf_token'] != session.get('csrf_token'):
abort(403)
fetch('/api/delete-account', {
method: 'POST',
body: JSON.stringify({ confirm: true })
});
fetch('/api/delete-account', {
method: 'POST',
headers: {
'X-CSRF-Token': getCsrfToken(),
'Content-Type': 'application/json'
},
body: JSON.stringify({ confirm: true })
});
A12 โ Clickjacking
Attackers overlay invisible iframes to trick users into clicking unintended actions.
Check if sensitive pages have proper frame protection:
โ Does the site set X-Frame-Options or Content-Security-Policy: frame-ancestors?
โ Are sensitive actions (login, payment, settings) protected?
โ Is there a CSP frame-ancestors directive?
<meta http-equiv="X-Frame-Options" content="DENY">
<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'none';">
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'self'
A13 โ Session Security
Session fixation, hijacking, and timeout issues.
โ Does session ID regenerate after login (session fixation prevention)?
โ Are sessions expired after inactivity?
โ Are cookies set with httpOnly, secure, sameSite flags?
โ Is session ID in URL? (session fixation risk)
@app.route('/login', methods=['POST'])
def login():
user = authenticate(request.form)
session['user_id'] = user.id
@app.route('/login', methods=['POST'])
def login():
user = authenticate(request.form)
session.clear()
session['user_id'] = user.id
response.set_cookie('session', token)
response.set_cookie('session', token, httponly=True, secure=True, samesite='Strict')
const SESSION_TIMEOUT = 30 * 60 * 1000;
if (Date.now() - lastActivity > SESSION_TIMEOUT) {
logout();
}
A14 โ Unrestricted Resource Consumption
API or application doesn't limit resource usage, allowing DoS or cost amplification.
โ Is there rate limiting on API endpoints?
โ Are file upload sizes limited?
โ Is there pagination without limits?
โ Are expensive operations (regex, crypto) cached or limited?
โ Can attackers trigger unlimited webhook calls?
@app.route('/api/search')
def search():
query = request.args.get('q')
return slow_search(query)
from flask_limiter import Limiter
limiter = Limiter(app, key_func=get_remote_address)
@app.route('/api/search')
@limiter.limit("100/minute")
def search():
query = request.args.get('q')
return slow_search(query)
const upload = multer({ dest: '/uploads' });
const upload = multer({
dest: '/uploads',
limits: { fileSize: 5 * 1024 * 1024, files: 1 }
});
A15 โ OAuth 2.0 / SSO Security
OAuth and SSO implementations have unique attack surfaces.
โ Is redirect_uri strictly validated (exact match, not prefix)?
โ Is the authorization code single-use?
โ Is state parameter validated to prevent CSRF?
โ Are access tokens stored securely?
โ Is PKCE used for public clients?
โ Can authorization codes be leaked via referrer?
@app.route('/callback')
def callback():
redirect_uri = request.args.get('redirect_uri')
if redirect_uri.startswith('https://app.com/'):
return redirect(redirect_uri + '/evil')
ALLOWED_REDIRECTS = ['https://app.com/dashboard', 'https://app.com/callback']
@app.route('/callback')
def callback():
redirect_uri = request.args.get('redirect_uri')
if redirect_uri not in ALLOWED_REDIRECTS:
abort(400)
return redirect(redirect_uri)
const authUrl = `https://auth.example.com/authorize?client_id=...`;
const state = crypto.randomBytes(32).toString('hex');
session.oauthState = state;
const authUrl = `https://auth.example.com/authorize?client_id=...&state=${state}`;
A16 โ WebSocket Security
WebSocket connections lack traditional HTTP security mechanisms.
โ Is WebSocket authentication done properly (not just in first message)?
โ Are sensitive WebSocket endpoints protected by authorization?
โ Is there input validation on WebSocket messages?
โ Can WebSocket be used to bypass CORS?
โ Is TLS used (wss://) in production?
const ws = new WebSocket('wss://api.example.com/ws');
ws.onmessage = (event) => { handleMessage(event.data); };
const ws = new WebSocket('wss://api.example.com/ws?token=' + getAuthToken());
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.action === 'delete') {
deleteItem(msg.id);
}
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.action === 'delete' && hasPermission('delete', msg.resource)) {
deleteItem(msg.id);
}
};
A17 โ CORS Misconfiguration
Overly permissive CORS allows cross-origin attacks.
โ Is Access-Control-Allow-Origin set to '*' for sensitive endpoints?
โ Is Access-Control-Allow-Credentials: true with wildcard origin?
โ Are sensitive headers exposed via CORS?
โ Is OPTIONS preflight properly validated?
โ Are origins validated server-side, not just trusted blindly?
@app.after_request
def add_cors(response):
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
@app.after_request
def add_cors(response):
response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin')
return response
ALLOWED_ORIGINS = ['https://app.example.com', 'https://admin.example.com']
@app.after_request
def add_cors(response):
origin = request.headers.get('Origin')
if origin in ALLOWED_ORIGINS:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
A18 โ GraphQL Security
GraphQL has unique attack surfaces beyond REST.
โ Is introspection disabled or restricted in production?
โ Are batch queries rate limited (mass query)?
โ Are expensive queries blocked (query depth/complexity)?
โ Is authorization checked at resolver level, not just schema?
โ Are directives properly secured?
โ Is aliasๆปฅ็จ prevented?
query {
user(id: "1") {
posts {
comments {
author {
posts {
comments { ... }
}
}
}
}
}
}
MaxQueryDepthInstrumentation maxDepth = new MaxQueryDepthInstrumentation(10);
def resolve_user_info(root, info):
return User.objects.get(id=root.id)
def resolve_user_info(root, info):
if info.context.user.id != root.id and not info.context.user.is_admin:
raise PermissionError()
return User.objects.get(id=root.id)
query { login(username: "admin", password: "pass1") }
query { login(username: "admin", password: "pass2") }
A19 โ Race Conditions (TOCTOU)
Concurrent requests can exploit time-of-check-time-of-use vulnerabilities.
โ Can two simultaneous requests bypass a check?
โ Is there double-spending/withdrawal possible?
โ Can coupon be applied multiple times?
โ Can seat be booked twice?
โ Are financial operations atomic?
@app.route('/transfer', methods=['POST'])
def transfer():
balance = get_balance(current_user)
amount = request.json['amount']
if balance >= amount:
sleep(0.1)
do_transfer(current_user, amount)
return {'success': True}
return {'error': 'Insufficient funds'}
from sqlalchemy import select
@app.route('/transfer', methods=['POST'])
def transfer():
with db.transaction():
balance = db.execute(
select(balance).where(user_id=current_user.id).with_for_update()
).scalar()
amount = request.json['amount']
if balance >= amount:
do_transfer(current_user, amount)
return {'success': True}
return {'error': 'Insufficient funds'}
async function applyCoupon(code) {
const coupon = await getCoupon(code);
if (!coupon.used) {
await setCouponUsed(code);
await applyDiscount(coupon.discount);
}
}
async function applyCoupon(code) {
const result = await db.query(
'UPDATE coupons SET used=true WHERE code=$1 AND used=false RETURNING *',
[code]
);
if (result.rows.length === 0) throw new Error('Coupon already used');
await applyDiscount(result.rows[0].discount);
}
A20 โ Subdomain Takeover
Abandoned subdomains pointing to external services can be claimed.
โ Are there dangling DNS records (CNAME to deleted services)?
โ Do expired Cloudflare/Heroku/GitHub Pages point to your domain?
โ Are there S3 buckets that were deleted but DNS still points there?
โ Is there a vulnerability history check for acquired subdomains?
subfinder -d target.com -o subs.txt
for sub in $(cat subs.txt); do
cname=$(dig +short CNAME $sub)
if echo $cname | grep -qE 'heroku|github|azure|amazonaws'; then
echo "$sub -> $cname (potential takeover)"
fi
done
Phase 4: Triage & Severity Rating
Rate each finding using CVSS v3.1 base score:
| Severity | CVSS Score | Definition |
|---|
| Critical | 9.0โ10.0 | Remote code execution, auth bypass with no conditions |
| High | 7.0โ8.9 | Significant data exposure, privilege escalation |
| Medium | 4.0โ6.9 | Requires auth or complex conditions |
| Low | 0.1โ3.9 | Defense in depth, hardening issues |
| Info | N/A | Best practice violations, no direct security impact |
For each finding, record:
- Title (CWE reference if applicable)
- Location (file:line)
- Description of the vulnerability
- Attack scenario (how would an attacker exploit this?)
- Evidence (code snippet)
- Severity + CVSS rationale
- Remediation recommendation
Phase 5: Verification
Before reporting any finding:
- Can you write a proof-of-concept (even pseudocode)?
- Is this actually reachable from an entry point?
- Is there a defense layer you missed?
False positives waste everyone's time. Verify before reporting.
Phase 6: Report handoff (MANDATORY โ sentinel-report)
Do not skip. After Phase 5, you must:
- Read
skills/sentinel/sentinel-report/SKILL.md (same repo / skill bundle).
- Execute its Closing gate (ask which folder to save; default
.md + offline .html).
Write both files to disk โ not optional โif user asksโ.
If the workspace path differs, use the path where this Sentinel bundle lives. If you cannot read files, still ask the closing-gate questions and Write the report using the template in sentinel-report.
References
Load the relevant reference when auditing:
references/patterns-php.md โ PHP-specific vulnerability patterns
references/patterns-java.md โ Java/Spring/Struts patterns
references/patterns-js.md โ Node.js/Express patterns
references/patterns-python.md โ Django/Flask/FastAPI patterns
Integration
- After audit: follow
sentinel:sentinel-report โ ask which folder to save into; default .md + offline HTML there; opt-out of HTML only if user says so; write files to disk
- For complex vulnerability root cause: invoke
sentinel:sentinel-workflow (systematic root-cause tracing)
- For reviewing audit diffs: invoke
sentinel:sentinel-workflow (security-focused code review)