Audit a repository for credentials and internal detail leaking through print statements, logger calls, raised errors, HTTP error responses, tracebacks and other sinks, then produce a graded markdown report with grounded coverage percentages. Use when asked to check a service for secrets in logs, review print/log/exception hygiene, or produce a "brutal" log audit like the rag and data-query reports.
Resources
3Install
npx skillscat add shreyas-lyzr/code-review-agent/log-review Install via the SkillsCat registry.
log-review
Produce <repo>-print-audit.md: every print-family call, logger call and raise/response sink in a repository, each with a verdict, plus ranked findings, the log-pipeline facts that decide severity, and coverage percentages computed from the same rows. The rag and data-query audits (2026-09-23) are the reference for depth and tone.
Hard rules
- Never print, echo, cat, copy or quote an actual credential, connection string with credentials, token, key or
.envvalue, in tool output, in the report, in commits or in chat. Use placeholders such asmongodb://<user>:<redacted>@<host>/<db>. When you must inspect a secret-bearing file, read only the key names (sed -E 's/=.*//'). - Probes use fake values only (
FAKEPW_9f8e7d,AIzaSyFAKE...,AKIAFAKE...), never real ones, never against production hosts. A probe that needs a host points at127.0.0.1:9orexample.invalid. - Do not rotate credentials, do not change infrastructure, do not edit the audited repo. The deliverable is the report. Fixes are a separate, explicitly requested task.
- Every claim in the report carries a
file:line, a probe result, or a library fact you verified. Rows you did not read stayUNVERIFIED; never inflate a coverage number by guessing. - Bash on this machine is zsh:
set -- $vardoes not word-split (use${=var}); nevercdin parallel calls (usegit -C, absolute paths, subshells); there is notimeoutbinary.
Deliverables
<workspace>/<repo>-print-audit.md(sections listed under "Report structure").- A short chat summary: verdict on prints, on logger calls, on exceptions, the ranked findings with
file:line, and what was not verified. - A memory note if the audit found an open HIGH/MEDIUM item someone will act on later.
Procedure
0. Checkout
git -C <repo> fetch origin && git -C <repo> checkout <default> && git -C <repo> pull --ff-only
git -C <repo> rev-parse --short HEAD ; git -C <repo> log -1 --format=%csRecord commit, branch, date. Note any PR merged since a previous audit (git log --format='%h %s' <old>..HEAD) and what it touched.
1. Inventory (mechanical)
S=<scratch dir>; K="$(dirname "$(find . -path '*skills/log-review/scripts/audit_run.py' 2>/dev/null | head -1)")" # scripts ship in this agent repo at skills/log-review/scripts
python3 $K/audit_run.py <repo_root> <repo_name> $Saudit_run.py runs the three extractors and writes <name>_audit.json (rows + control signals) and <name>_priority.txt (top 160 rows by risk score):
- Python:
audit_extract.py, anastwalk over every.pyoutside tests/venv. Print family =print,pprint,sys.stdout.write,sys.stderr.write,traceback.print_exc,click.echo,typer.echo,rich.print,os.write. Logger =^(logger|logging|log|self\.logger|self\._logger|_logger|LOGGER|_log|LOG|self\.log)\.(debug|info|warning|warn|error|exception|critical)$. Sinks = callee ending inError|Exception|Failure|HTTPException|JSONResponse|Response. Multi-line calls are captured whole, which a line regex cannot do. - Go:
goaudit/(go/ast). Printsfmt.Print*,fmt.Fprint*,print,println,os.Stdout/Stderr.Write*,spew.Dump; loggers = level method on a receiver mentioninglog|logger|zap|slog|logrus|zerolog|sugar|klog|glog, plus stdlog.Print*/Fatal*/Panic*; sinks =errors.New,fmt.Errorf,errors.Wrap*,http.Error,panic,status.Error*, response methods onc|ctx|w|rw|res|resp|writer|json.NewEncoder(), helpers named(respond|write|render|send|reply|handle|abort)(json|error|err|response|...). - TS/JS:
tsaudit.py(regex with paren balancing).console.*,process.stdout/stderr.write,(logger|log|winston|pino|bunyan).(level),throw new *Error(,res.status().json/send,res.json/send,NextResponse.json,new Response,reply.*,next(new Error, and UI sinks (toast*,message.error,notification.*,alert,setError*).
Per-row signals: strong (secret-looking name), weak (url/host/id/data names), exc (exception text per language), interp (interpolation present), script (scripts/tools/examples path), http (response sink). Quick manual re-run for prints only:
grep -rnE '(^|[^A-Za-z_.])(print|println|fmt\.Print[a-z]*|console\.(log|error|warn|info|debug))\(' --include='*.py' --include='*.go' --include='*.ts' --include='*.tsx' . | grep -vE '/(\.venv|node_modules|\.next|dist|vendor|tests?)/'Also grep for sinks the AST pass cannot see: traceback\.print_exc|print_exception|sys\.(stderr|stdout)\.write|click\.echo|typer\.echo|rich\.print|pprint\(|os\.write\(.
2. Establish the log-pipeline facts (these decide every severity)
Python: root logger config (logging.basicConfig, dictConfig, getLogger().setLevel/addHandler) and the LOG_LEVEL default; any forced DEBUG (setLevel(logging.DEBUG), litellm set_verbose/_turn_on_debug, SQLAlchemy echo=True); a builtins.print guard and its bypass env var; central exception_handler; detail=str(e) count; pydantic hide_input_in_errors; uvicorn access log (--no-access-log, access_log=False, uvicorn.access level); httpx logger level (httpx logs every request URL at INFO; litellm sets it to WARNING at import, which is a fragile side effect); Celery worker --loglevel and whether the root logger is hijacked; traceback.format_exc() outside DEBUG; .dockerignore and COPY . .; .env committed or not; where LOG_LEVEL is set in CI/Dockerfiles/env files (keys only).
Go: recovery middleware, structured logger, err.Error() written into responses.
TS: console.* in server code (app/api, pages/api, server).
Also: which HTTP clients are used (httpx, requests, aiohttp; their error text includes the full URL with query string, see facts) and whether any URL can carry a secret (presigned URLs, ?key=).
3. Read the priority rows in context
Open <name>_priority.txt. For each row read the surrounding function and answer: what does the interpolated value actually hold (trace the variable to its source: request body, env, DB document, driver object); at what level does it emit; is it inside a guard; where does a raised error go (grep the exception class to its detail= or handler); does a traceback (exc_info=True, logger.exception, format_exc()) carry an exception whose text embeds a URL or body. Record the verdict and a one-sentence note keyed by file:line. Read the callers of every custom error class that embeds {e} or a response body. For prints, decide CLI (scripts/, __main__, demo helpers) versus service path. Also read the config/settings module, the auth module, and every place a request body with credentials is parsed.
4. Probe libraries with fake values (only when the code path exists)
skills/log-review/probes/ (relative to this agent repo root):
litellm_gemini_key_probe.py,litellm_gemini_key_probe_paths.py: doesstr(e)/repr(e)/cause chain/DEBUG log contain the key on auth-error, unknown-model and timeout paths.http_client_url_in_exception_probe.py,aiohttp_status_error_probe.py: does the client's error text include the URL query string.driver_error_text_probe.py: SQLAlchemymake_urlwith@ : / ?in the password; psycopg connect and bad-conninfo text; oracledb connect text.mongo_public_attribute_graph_probe.py: BFS over underscore-free attributes of a client handle looking for the password (for exec sandboxes).boto3_before_send_hook_probe.py: what abefore-sendevent handler can read from a signed request.
Run them with the repo's own venv (uv sync --frozen --python 3.12; private git deps needGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=url.https://github.com/.insteadOf GIT_CONFIG_VALUE_0=ssh://git@github.com/). Print booleans, never values.
5. Write the overrides and render
Create <name>_overrides.json (schema below) from step 3 and 4, then:
python3 $K/audit_report_generic.py <repo_name> $S <workspace>/<repo_name>-print-audit.mdFor a deep, hand-verified report (every row judged, narrative findings, custom control checklist) copy examples/data_query_report_handverified.py or examples/rag_report_handverified.py and edit its OVERRIDES table and narrative; those generators print the coverage section from the same verdict functions that fill the tables. Re-render after every edit; never hand-edit the markdown.
Overrides schema:
{"rows": {"file:line": {"verdict": "LEAK|LEAK-IF|DISCLOSURE|SAFE|CLI", "note": "one sentence, cites what the value holds"}},
"findings": [{"severity": "HIGH|MEDIUM|LOW|INFO", "title": "...", "detail": "...", "refs": ["file:line"]}],
"controls": {"<control name>": {"present": true, "evidence": "file:line"}},
"pipeline": ["fact about the logging pipeline", "..."],
"summary": "closing summary"}6. Check the output
Scan the report for secret-looking strings before delivering: grep -nE 'AIza[0-9A-Za-z_-]{20,}|sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|mongodb(\+srv)?://[^<*]+:[^<*]+@|X-Amz-Signature=[0-9a-f]{10,}' must return nothing except the documented fake values. Check table rows render (\| inside code spans is fine).
Verdict vocabulary and rules
| Verdict | Meaning |
|---|---|
| LEAK | a real secret reaches the sink on a normal code path (context-verified) |
| LEAK-IF | reaches the sink only under a stated condition: DEBUG level, a character in a password, a URI written with userinfo |
| DISCLOSURE | no secret; internal hosts, user names, internal URLs, error bodies, tracebacks, customer data or PII get out |
| SAFE | static text, ids, counts, key names, redacted values |
| CLI | operator script or __main__/demo block that never runs in the service process |
| UNVERIFIED | regex saw a secret-looking name being interpolated; nobody read the line; counts as unsafe in coverage |
Rule verdicts (used only where no context read exists): script path -> CLI; no interpolation -> SAFE; strong name + interpolation -> UNVERIFIED; HTTP response + exception text -> DISCLOSURE; exception text or weak names -> DISCLOSURE; else SAFE. A context read always replaces the rule verdict and is marked basis = read.
What makes a row LEAK rather than DISCLOSURE: the value is a password, token, API key, private key, presigned URL, session token, or a config/connection object that embeds one. Presigned URLs and OAuth codes are credentials with a TTL; still LEAK, note the TTL. Hostnames, user names, ids, emails (PII), column names and query text are DISCLOSURE.
Verified library facts (reuse, cite the version)
| Fact | Verified |
|---|---|
LiteLLM 1.91.5 masks key= in Gemini URLs: fake key absent from str(e), repr(e), cause chain and 98 DEBUG lines on auth-error, unknown-model and timeout paths; its DEBUG curl dump masks headers |
probe, 2026-09-23 |
LiteLLM sets the httpx logger to WARNING at import (litellm/_logging.py); without it httpx logs HTTP Request: <method> <full URL> at INFO |
read + probe |
httpx.HTTPStatusError, requests.HTTPError, requests.ConnectionError and aiohttp.ClientResponseError texts include the full URL with query string; httpx/aiohttp connect-level errors do not |
probe (aiohttp 3.14.3) |
logger.exception / exc_info=True prints the traceback, whose last line is the exception text: a redacted message does not protect a presigned URL raised by raise_for_status() |
rag extract/endpoints.py:388 |
pymongo 4.8 and 4.18 MongoClient/Database repr and motor 3.7 delegate exclude username/password; no underscore-free attribute path reaches the credential (53 nodes, depth 5) |
probe |
pymongo InvalidURI and ServerSelectionTimeoutError texts carry hosts, not passwords; pymongo redacts auth commands in its DEBUG command logging |
probe + docs |
psycopg 3.2/3.3 connect and bad-conninfo errors, oracledb DPY-6005, pyodbc/cloudrift SQLConnectionError("... at <server>: ...") carry host/user/error code, never the password |
probe + read |
SQLAlchemy make_url: a password containing @ is split at the first @ and the tail becomes the host name, which libpq then echoes (could not translate host name); URL repr masks the password; ArgumentError on an unparsable URL echoes the whole URL |
probe (2.0.31/2.0.38) |
boto3 before-send event handlers (registered through a public path) see Authorization (access key id) and X-Amz-Security-Token (session token verbatim); the secret key never appears |
probe (boto3 1.40) |
Celery 5.6.3 / kombu 5.6.2 mask the broker password in the banner, as_uri() and humanize(); the host stays; celery -q/--quiet suppresses the banner; --loglevel=info puts a handler at INFO on the root logger |
probe + read |
uvicorn's access log records path and query string; x-api-key travels in a header and is not logged |
read |
pydantic v2 embeds input_value in ValidationError text unless hide_input_in_errors=True; a model-level validator error carries the whole input dict |
read |
Python tracebacks never include local variables; traceback.format_exc() carries file paths and the exception chain text only |
fact |
Report structure
- Verdict table (prints, logger calls, exceptions to clients, non-log sinks, sandbox if any).
- Ranked findings F1..Fn with severity, every site as
file:line, the mechanism, the probe or read that proves it, and a fix shape. - Log-pipeline facts that decide severity (table: fact, where, consequence).
- Method and regexes (checkout commit, extractors, regexes verbatim, probes run, counts table, verdict totals).
- Print inventory, then Appendix A logger calls, B exception-text rows, C HTTP passthroughs, D raised errors, E non-log sinks: every row with
#,file:line, level/call, truncated call, verdict, note, basis (read/rule). - Recommendations, ordered.
- Safety coverage (grounded), with the "How to read each measure" block.
- Summary.
Coverage measures (all computed from the same rows)
- Credential-safe rows: verdict not LEAK/LEAK-IF (and not UNVERIFIED in the generic report). The headline.
- Unverified rows: the honest gap.
- Disclosure-free rows: SAFE or CLI, split by logger / sinks / prints.
- Disclosure rows DEBUG-gated vs live at INFO+ or in HTTP responses (response rows come first).
- Rows read in context.
- Safety controls in place: checklist with file:line evidence. Python controls: no print in service code; root logger configured;
LOG_LEVELdefault INFO+; no forced DEBUG;builtins.printguard; central exception handler; nodetail=str(e);hide_input_in_errors; access log off; noformat_exc()outside DEBUG;httpxlogger pinned. Go: nofmt.Print*; structured logger; recovery middleware; noerr.Error()in responses. TS: no server-sideconsole.*. All:.envgit-ignored; none committed;.dockerignoreexcludes.env.
Always include the explanation block: a high credential-safe share with a low controls share means the service is safe because libraries happen not to echo secrets, not because the code strips them.
Severity guidance
HIGH: a secret stored or returned to a principal who does not own it (rag: owner API key echoed in LiveSourceResponse to shared readers), or logged at default level. MEDIUM: presigned URLs or other TTL credentials in logs; raw exception text/tracebacks to clients; error text or handles sent to an LLM provider; exec sandboxes with print and no stdout guard. MEDIUM (DEBUG-gated): config dumps or URIs at DEBUG when LOG_LEVEL is one env var away. LOW: upstream error bodies, PII in logs, access-log query strings with short-lived tokens, missing .dockerignore.
Gotchas
rag_audit.py-style extractors must skip.venv,node_modules,tests; the runner also skipsvendor,.next,dist,build.- Line numbers shift between commits; key overrides by
file:lineof the audited commit and re-map by source text when re-auditing a moved master. - Go extractor runs via
go run -C <skill>/scripts/goaudit . <root> <out.json>(Go 1.20+). - Markdown tables: escape
|in snippets (the generators do);<placeholder>inside reportlabXPreformattedmust be HTML-escaped. - Reading a PDF report back page by page is the only reliable layout check for generated PDFs.