Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions docs/logging-quality-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Logging Quality Audit

## Scope

This audit covers backend/API logging quality only:

- `thinx-core.js`
- `thinx.js`
- `lib/**/*.js`
- selected service entrypoints:
- `services/worker/worker.js`
- `services/transformer/index.js`
- `services/transformer/app.js`
- `services/transformer/transformer.js`

The audit intentionally does not mass-convert every remaining `console.*` call,
and does not scan bundled console frontend vendor assets.

## Current Audit Counts

Latest command:

```sh
npm run --silent logging-audit -- --json
```

Current report summary:

- Files scanned: 80
- `console.*` calls: 897
- `logger.*` calls: 20
- Tracked event occurrences: 26
- Sensitive findings: 21
- High-risk sensitive findings: 0
- Severity-string mismatches: 607
- Tracked event quality gaps: 0

## Fixed High-Risk Findings

- Raw websocket cookie headers in `thinx-core.js` are redacted with
`Util.redactCookieHeader()`, preserving cookie names only.
- OAuth handoff/access token logs in `lib/router.github.js`,
`lib/router.google.js`, `lib/router.gdpr.js`, and `lib/router.auth.js` now
redact tokens or avoid logging them.
- Full OAuth `userWrapper` and GitHub `hdata` payload logging was removed.
- Invalid local-login failures no longer log the submitted username.
- `LOGIN_INVALID`, `BUILD_FAILED`, `BUILD_STARTED`, `BUILD_SUCCESS`,
`DEVICE_CHECKIN`, and `DEVICE_NEW` now have warn-level logger coverage while
preserving existing `InfluxConnector.statsLog` calls.

## Build Event Decision

`BUILD_COMPLETED` remains an operational line. It is not part of
`statistics.js` `owner_template`, which tracks `BUILD_SUCCESS`. Successful local
build exits now emit `BUILD_SUCCESS` via the logger/metrics helper so local
build success can be counted consistently with the existing statistics model.

## Bounded Auth-Module Fixes (audit.js, router.js, apikey.js)

A second, deliberately narrow remediation slice targets the auth-sensitive
modules that the first slice left untouched. These are log-only changes (no
control-flow changes), verified by `npm run lint` and the isolated
`UtilSpec` / `LoggingQualityAuditSpec` suites:

- **Severity mismatches → correct level.** Genuine error paths that were logged
via `console.log` now use `console.error`; `[warning]`-tagged lines now use
`console.warn`:
- `lib/thinx/audit.js`: `_buildRecord` missing-message notice → `console.warn`;
`log()` insertion failure and `fetch()` failure → `console.error`.
- `lib/router.js`: host-header-mismatch and blacklist-check-failed warnings →
`console.warn`; failed API-key authentication → `console.warn`.
- `lib/thinx/apikey.js`: `save_apikeys` set failure, circuit-breaker OPEN,
key-generator errors, Redis-unavailable/`get` errors, and the
`revoke`/`list` error paths → `console.error`.
- **Secret/token leakage → redacted or removed.**
- `lib/thinx/apikey.js` no longer logs the full `json_keys` blob (cleartext
keys + hashes) in `create()` and `revoke()`; the "saving first API key"
debug line logs only the alias.
- Attempted invalid API-key values in `log_invalid_key()` (both the audit-log
entry and the console warning) and `key_in_keys()` are now passed through
`Util.redactToken()`, printing only a short deterministic prefix.

The stats-parser contract is untouched by this slice: none of these files emit
`[OID:...] [EVENT]` lines consumed by `statistics.js`, so no `logger.warn()`
level was changed or downgraded.

## Remaining Backlog

The remaining findings are lower-risk and intentionally left for follow-up:

- Convert broad backend `console.*` usage to the shared logger.
- Resolve severity-string mismatches where messages tagged `[error]`,
`[warning]`, `[info]`, or `[debug]` still go through `console.log`.
- Review remaining medium-risk payload logs (device response payloads in
particular) and replace them with redacted structured context. API-key
payload logging was addressed in the bounded auth-module slice above.
- Consider standardizing non-template event markers such as `NEW_SESSION`,
`DEVICE_ATTACH`, `MESH_ATTACH`, and transfer events.

## Verification

Focused verification commands:

```sh
npm run --silent logging-audit -- --json
npx jasmine spec/jasmine/LoggingQualityAuditSpec.js spec/jasmine/UtilSpec.js spec/jasmine/LoggerSpec.js spec/jasmine/MetricsCoverageSpec.js
```
16 changes: 8 additions & 8 deletions lib/router.auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const Globals = require("./thinx/globals");

const Util = require("./thinx/util");
const logger = require("./thinx/logger");

const Sanitka = require("./thinx/sanitka"); let sanitka = new Sanitka();
const AuditLog = require("./thinx/audit"); let alog = new AuditLog();
Expand Down Expand Up @@ -36,6 +37,7 @@ module.exports = function (app) {

function auditLogError(owner, data) {
if (!Util.isDefined(owner)) owner = "0";
logger.warn(`[OID:${owner}] [LOGIN_INVALID] ${data}`);
InfluxConnector.statsLog(owner, "LOGIN_INVALID", data);
}

Expand All @@ -45,8 +47,8 @@ module.exports = function (app) {
redis.get(oauth, (error, userWrapper) => {

if ((typeof(userWrapper) === "undefined") || (userWrapper === null)) {
console.log("Login failed, wrapper not found for token", oauth);
auditLogError(oauth, "wrapper_error_1");
console.log("Login failed, wrapper not found for token", Util.redactToken(oauth));
auditLogError(null, "wrapper_error_1");
return Util.failureResponse(res, 403, "wrapper error");
}

Expand Down Expand Up @@ -213,12 +215,10 @@ module.exports = function (app) {
if (password.indexOf(user_data.password) === -1) {
let p = user_data.password;
if (typeof (p) === "undefined" || p === null) {
console.log(`[OID:${user_data.owner}] [LOGIN_INVALID] not activated/no password.`);
auditLogError(user_data.owner, "not_activated");
alog.log(req.session.owner, "Password missing");
Util.responder(stored_response, false, "password_missing");
} else {
console.log(`[OID:${user_data.owner}] [LOGIN_INVALID] Password mismatch.`);
auditLogError(user_data.owner, "password_mismatch");
alog.log(req.session.owner, "Password mismatch.");
stored_response.status(401);
Expand Down Expand Up @@ -290,7 +290,7 @@ module.exports = function (app) {

let username = sanitka.username(req.body.username);
let password = sha256(prefix + req.body.password);
console.log(`🔨 [debug] [auth] Username/password login attempt for: ${username}`);
console.log("🔨 [debug] [auth] Username/password login attempt");

// Search the user in DB, should search by key and return one only
user.validate(username, (db_body) => {
Expand All @@ -300,12 +300,12 @@ module.exports = function (app) {
// `db_body.rows` throws a TypeError -> unhandled -> HTTP 500 with
// an HTML error page, which the JSON-expecting console cannot parse.
if (db_body === false) {
console.log(`[OID:0] [LOGIN_ERROR] user directory unavailable for ${username}`);
console.log("[OID:0] [LOGIN_ERROR] user directory unavailable");
return Util.failureResponse(res, 503, "service_unavailable");
}

if ((typeof (db_body.rows) === "undefined") || (db_body.rows.length == 0)) {
console.log(`[OID:0] [LOGIN_INVALID] with username ${username}`);
auditLogError(null, "unknown_username");
return Util.failureResponse(res, 403, "invalid_credentials");
}

Expand Down Expand Up @@ -371,4 +371,4 @@ module.exports = function (app) {
logoutAction(req, res);
});

};
};
4 changes: 2 additions & 2 deletions lib/router.gdpr.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ module.exports = function (app) {

var wrapper = JSON.parse(userWrapper);
if (typeof (wrapper) === "undefined" || wrapper === null) {
console.log("Not found wrapper", userWrapper, "for token", token);
console.log("Not found GDPR wrapper for token", Util.redactToken(token));
return Util.responder(res, false, "handover_failed");
}

Expand Down Expand Up @@ -166,4 +166,4 @@ module.exports = function (app) {
revokeGDPR(req, res);
});

};
};
11 changes: 7 additions & 4 deletions lib/router.github.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ module.exports = function (app) {
// shows the GDPR consent gate before logging in.
const courl = oauthReturn.returnURLFor(response.thx_return_origin, token, false);

console.log("Redirecting to login (2)", courl);
console.log("Redirecting to login (2) with token", Util.redactToken(token));
response.redirect(courl); // for successful login, this must be a response to /oauth/<idp>/callback
});
return;
Expand All @@ -109,7 +109,10 @@ module.exports = function (app) {
console.log(`ℹ️ [info] Calling trackUserLogin on GtHub Auth Callback...`);
user.trackUserLogin(owner_id);

console.log("validateGithubUser", { token }, { userWrapper });
console.log("validateGithubUser", {
token: Util.redactToken(token),
owner: userWrapper.owner
});
app.redis_client.set(token, JSON.stringify(userWrapper));
app.redis_client.expire(token, 3600);

Expand Down Expand Up @@ -139,14 +142,14 @@ module.exports = function (app) {
} else {
family_name = hdata.login;
given_name = hdata.login;
console.log("🔨 [debug] [github] [token] Warning: no name in GitHub access token response, using login: ", { hdata }); // logs personal data in case the user has no name!
console.log("🔨 [debug] [github] [token] Warning: no name in GitHub access token response, using login fallback.");
}
email = hdata.email || hdata.login;

try {
owner_id = sha256(prefix + email);
} catch (e) {
console.log("☣️ [error] [github] [token] error parsing e-mail: " + e + " email: " + email);
console.log("☣️ [error] [github] [token] error parsing e-mail: " + e + " email: " + Util.redactEmail(email));
return res.redirect(app_config.public_url + '/error.html?success=failed&title=Sorry&reason=Missing%20e-mail.');
}
validateGithubUser(original_response, token, {
Expand Down
4 changes: 2 additions & 2 deletions lib/router.google.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var alog = new AuditLog();
const https = require('https');
const sha256 = require("sha256");
const oauthReturn = require("./thinx/oauth_return");
const Util = require("./thinx/util");

const app_config = Globals.app_config(); // public_url and public_url but it is the same now

Expand Down Expand Up @@ -71,8 +72,7 @@ module.exports = function (app) {
// New account: consent not yet given -> g=false so the console shows the
// GDPR consent gate before logging in.
const ourl = oauthReturn.returnURLFor(oauthReturn.takeReturnOrigin(req, ores), token, false);
console.log("OURL", ourl);
console.log("Redirecting to:", ourl);
console.log("Redirecting Google OAuth user with token", Util.redactToken(token));
ores.redirect(ourl);
});
}
Expand Down
6 changes: 3 additions & 3 deletions lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ module.exports = function (app) {


if (req.header.host && (req.header.host !== app_config.public_url)) {
console.log("[warning] host header mismatch, possible hacking attempt: ", req.header.host, " != ", app_config.public_url);
console.warn("⚠️ [warning] host header mismatch, possible hacking attempt: ", req.header.host, " != ", app_config.public_url);
}


Expand Down Expand Up @@ -129,7 +129,7 @@ module.exports = function (app) {
// Session blacklist check (Phase 10 ADMIN-02). Fails OPEN on Redis errors (R1).
app.redis_client.get("revoked:owner:" + payload.username, (rerr, ts) => {
if (rerr) {
console.log("[warning] blacklist check failed", rerr);
console.warn("⚠️ [warning] blacklist check failed", rerr);
return next();
}
if (ts) {
Expand Down Expand Up @@ -202,7 +202,7 @@ module.exports = function (app) {
} else {
res.status(401);
Util.responder(res, false, "Authentication Faled");
console.log("APIKey ", vmessage);
console.warn("⚠️ [warning] APIKey authentication failed:", vmessage);
}
});
return;
Expand Down
Loading