| /*
* Sai web - ./src/web/w-findings.c
*
* Copyright (C) 2019 - 2026 Andy Green <andy@warmcat.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation:
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*
* Findings, for admins only: see READMEs/README-findings.md
*
* Like cloneinfo, we answer these from the databases sai-server keeps, which we
* read directly: the events db lists the repos' pools, and each pool's db has
* its findings grouped into bugs. Changes to a group go to sai-server.
*
* Findings can be unfixed security bugs, so the caller must only call these
* for admins.
*/
#include <libwebsockets.h>
#include <string.h>
#include "w-private.h"
/* how many groups of each pool we list */
#define SAIW_FINDINGS_GROUPS_MAX 200
/* how big the list reply can get */
#define SAIW_FINDINGS_LIST_MAX (256 * 1024)
/* the most of a report we send */
#define SAIW_FINDINGS_REPORT_MAX (64 * 1024)
/* the biggest reproducer we send to the browser */
#define SAIW_FINDINGS_REPRO_MAX (512 * 1024)
static int
saiw_findings_open(struct vhd *vhd, const char *repo, const char *pool,
sqlite3 **ppdb)
{
char fn[256];
if (!sai_pool_name_ok(pool) || sai_str_has_shell_metachars(repo))
return 1;
sai_pool_db_path(fn, sizeof(fn), vhd->sqlite3_path_lhs, repo, pool);
/* sai-server makes it; if it isn't there, there's nothing to show */
if (sqlite3_open_v2(fn, ppdb, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK) {
sqlite3_close(*ppdb);
*ppdb = NULL;
return 1;
}
sqlite3_busy_timeout(*ppdb, SAI_SQLITE3_BUSY_TIMEOUT_MS);
return 0;
}
static const char *
saiw_col(sqlite3_stmt *sm, int col, char *esc, size_t len)
{
const char *c = (const char *)sqlite3_column_text(sm, col);
return lws_json_purify(esc, c ? c : "", (int)len - 1, NULL);
}
/*
* com.warmcat.sai.findings: every pool's groups, unacknowledged and open
* ones first
*/
int
saiw_browser_send_findings(struct vhd *vhd, struct pss *pss)
{
char *buf, *start, *p, *end, e1[256], e2[256], q[384];
sqlite3_stmt *sm = NULL, *gsm;
int first_pool = 1, ret = 1;
buf = malloc(LWS_PRE + SAIW_FINDINGS_LIST_MAX);
if (!buf)
return 1;
start = p = buf + LWS_PRE;
end = start + SAIW_FINDINGS_LIST_MAX - 1024;
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
"{\"schema\":\"com.warmcat.sai.findings\","
"\"pools\":[");
/* sai-server creates the table; without it, there are no pools yet */
if (sqlite3_prepare_v2(vhd->pdb, "SELECT repo, pool FROM pools ORDER "
"BY repo, pool", -1, &sm, NULL) != SQLITE_OK)
goto done;
lws_snprintf(q, sizeof(q), "SELECT id, sub, kind, frames, status, "
"acked, regressed, hits, first_seen, last_seen, "
"first_hash, last_hash, last_ok_hash, last_ok_time, "
"platforms, repro_len FROM groups ORDER BY acked, status, "
"last_seen DESC LIMIT %d", SAIW_FINDINGS_GROUPS_MAX);
while (sqlite3_step(sm) == SQLITE_ROW && p < end) {
const char *repo = (const char *)sqlite3_column_text(sm, 0),
*pool = (const char *)sqlite3_column_text(sm, 1);
int first = 1;
sqlite3 *pdb;
if (!repo || !pool || !saiw_project_visible(vhd, repo) ||
saiw_findings_open(vhd, repo, pool, &pdb))
continue;
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
"%s{\"repo\":\"%s\",\"pool\":\"%s\","
"\"groups\":[", first_pool ? "" : ",",
lws_json_purify(e1, repo, sizeof(e1) - 1, NULL),
lws_json_purify(e2, pool, sizeof(e2) - 1, NULL));
first_pool = 0;
if (sqlite3_prepare_v2(pdb, q, -1, &gsm, NULL) == SQLITE_OK) {
while (sqlite3_step(gsm) == SQLITE_ROW &&
lws_ptr_diff_size_t(end, p) > 2048) {
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
"%s{\"id\":\"%s\",", first ? "" : ",",
saiw_col(gsm, 0, e1, sizeof(e1)));
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
"\"sub\":\"%s\",",
saiw_col(gsm, 1, e1, sizeof(e1)));
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
"\"kind\":\"%s\",",
saiw_col(gsm, 2, e1, sizeof(e1)));
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
"\"frames\":\"%s\",",
saiw_col(gsm, 3, e1, sizeof(e1)));
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
"\"status\":%d,\"acked\":%d,"
"\"regressed\":%d,\"hits\":%d,"
"\"first_seen\":%lld,\"last_seen\":%lld,",
sqlite3_column_int(gsm, 4),
sqlite3_column_int(gsm, 5),
sqlite3_column_int(gsm, 6),
sqlite3_column_int(gsm, 7),
(long long)sqlite3_column_int64(gsm, 8),
(long long)sqlite3_column_int64(gsm, 9));
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
"\"first_hash\":\"%s\",",
saiw_col(gsm, 10, e1, sizeof(e1)));
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
"\"last_hash\":\"%s\",",
saiw_col(gsm, 11, e1, sizeof(e1)));
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
"\"last_ok_hash\":\"%s\","
"\"last_ok_time\":%lld,",
saiw_col(gsm, 12, e1, sizeof(e1)),
(long long)sqlite3_column_int64(gsm, 13));
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
"\"platforms\":\"%s\",\"repro_len\":%lld}",
saiw_col(gsm, 14, e1, sizeof(e1)),
(long long)sqlite3_column_int64(gsm, 15));
first = 0;
}
sqlite3_finalize(gsm);
}
sqlite3_close(pdb);
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "]}");
}
sqlite3_finalize(sm);
done:
p += lws_snprintf(p, lws_ptr_diff_size_t(end + 1024, p), "]}");
ret = saiw_ws_browser_queue_REQUIRES_LWS_PRE(pss, start,
lws_ptr_diff_size_t(p, start), LWS_WRITE_TEXT);
free(buf);
return ret;
}
/*
* com.warmcat.sai.findingget: one group's report and reproducer
*/
int
saiw_browser_send_finding(struct vhd *vhd, struct pss *pss,
const sai_findingset_t *fs)
{
char *buf = NULL, *start, *p, *end, e1[256], sub[33] = "",
rname[65] = "", *esc = NULL;
sqlite3_stmt *sm;
sqlite3 *pdb;
size_t blen;
int ret = 1;
if (saiw_findings_open(vhd, fs->repo, fs->pool, &pdb))
return 1;
if (sqlite3_prepare_v2(pdb, "SELECT sub, repro_name FROM groups WHERE "
"id = ?", -1, &sm, NULL) != SQLITE_OK)
goto bail;
sqlite3_bind_text(sm, 1, fs->group, -1, SQLITE_TRANSIENT);
if (sqlite3_step(sm) == SQLITE_ROW) {
const char *c;
if ((c = (const char *)sqlite3_column_text(sm, 0)))
lws_strncpy(sub, c, sizeof(sub));
if ((c = (const char *)sqlite3_column_text(sm, 1)))
lws_strncpy(rname, c, sizeof(rname));
}
sqlite3_finalize(sm);
if (!sub[0] || !rname[0])
goto bail;
/* the report can JSON-escape to 6x, the reproducer b64s to 4/3 */
blen = LWS_PRE + 1024 + (SAIW_FINDINGS_REPORT_MAX * 6) +
((SAIW_FINDINGS_REPRO_MAX * 4) / 3) + 16;
buf = malloc(blen);
esc = malloc((SAIW_FINDINGS_REPORT_MAX * 6) + 8);
if (!buf || !esc)
goto bail;
start = p = buf + LWS_PRE;
end = buf + blen - 8;
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
"{\"schema\":\"com.warmcat.sai.finding\","
"\"repo\":\"%s\",",
lws_json_purify(e1, fs->repo, sizeof(e1) - 1, NULL));
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "\"pool\":\"%s\",",
lws_json_purify(e1, fs->pool, sizeof(e1) - 1, NULL));
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "\"group\":\"%s\",",
lws_json_purify(e1, fs->group, sizeof(e1) - 1, NULL));
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "\"name\":\"%s\",",
lws_json_purify(e1, rname, sizeof(e1) - 1, NULL));
/* the report on the group's reproducer */
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "\"report\":\"");
if (sqlite3_prepare_v2(pdb, "SELECT blob FROM findings WHERE sub = ? "
"AND name = ? || '.log'", -1, &sm,
NULL) == SQLITE_OK) {
sqlite3_bind_text(sm, 1, sub, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(sm, 2, rname, -1, SQLITE_TRANSIENT);
if (sqlite3_step(sm) == SQLITE_ROW) {
int l = sqlite3_column_bytes(sm, 0);
char *rep;
if (l > SAIW_FINDINGS_REPORT_MAX)
l = SAIW_FINDINGS_REPORT_MAX;
rep = malloc((size_t)l + 1);
if (rep) {
if (l)
memcpy(rep, sqlite3_column_blob(sm, 0),
(size_t)l);
rep[l] = '\0';
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
"%s", lws_json_purify(esc, rep,
(SAIW_FINDINGS_REPORT_MAX * 6) + 7,
NULL));
free(rep);
}
}
sqlite3_finalize(sm);
}
/* the reproducer itself, if it's not too big for this */
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "\",\"repro\":\"");
if (sqlite3_prepare_v2(pdb, "SELECT blob FROM findings WHERE sub = ? "
"AND name = ?", -1, &sm, NULL) == SQLITE_OK) {
sqlite3_bind_text(sm, 1, sub, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(sm, 2, rname, -1, SQLITE_TRANSIENT);
if (sqlite3_step(sm) == SQLITE_ROW) {
int l = sqlite3_column_bytes(sm, 0);
if (l <= SAIW_FINDINGS_REPRO_MAX && l > 0) {
int n = lws_b64_encode_string(
(const char *)sqlite3_column_blob(sm, 0),
l, p, (int)lws_ptr_diff_size_t(end, p));
if (n > 0)
p += n;
}
}
sqlite3_finalize(sm);
}
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "\"}");
ret = saiw_ws_browser_queue_REQUIRES_LWS_PRE(pss, start,
lws_ptr_diff_size_t(p, start), LWS_WRITE_TEXT);
bail:
free(esc);
free(buf);
sqlite3_close(pdb);
return ret;
}
|