averecion-lite 1.4.7 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dash.css +233 -0
- package/dashboard/dash.js +67 -0
- package/dashboard/demo-script.md +1 -1
- package/dashboard/index.html +42 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +10 -0
- package/dist/src/threat-assessment.d.ts +26 -0
- package/dist/src/threat-assessment.d.ts.map +1 -0
- package/dist/src/threat-assessment.js +551 -0
- package/package.json +1 -1
package/dashboard/dash.css
CHANGED
|
@@ -1132,3 +1132,236 @@ body {
|
|
|
1132
1132
|
color: var(--success);
|
|
1133
1133
|
font-style: italic;
|
|
1134
1134
|
}
|
|
1135
|
+
|
|
1136
|
+
.security-arch-section {
|
|
1137
|
+
margin-top: 2rem;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
.security-arch-grid {
|
|
1141
|
+
display: grid;
|
|
1142
|
+
grid-template-columns: repeat(2, 1fr);
|
|
1143
|
+
gap: 1rem;
|
|
1144
|
+
margin-top: 1rem;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
.arch-item {
|
|
1148
|
+
background: var(--bg-card);
|
|
1149
|
+
border: 1px solid var(--border);
|
|
1150
|
+
border-radius: 12px;
|
|
1151
|
+
padding: 1.25rem;
|
|
1152
|
+
transition: border-color 0.2s;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
.arch-item:hover {
|
|
1156
|
+
border-color: var(--primary);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
.arch-icon {
|
|
1160
|
+
font-size: 1.5rem;
|
|
1161
|
+
margin-bottom: 0.5rem;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
.arch-title {
|
|
1165
|
+
font-weight: 600;
|
|
1166
|
+
color: var(--text-primary);
|
|
1167
|
+
margin-bottom: 0.4rem;
|
|
1168
|
+
font-size: 0.95rem;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
.arch-desc {
|
|
1172
|
+
font-size: 0.8rem;
|
|
1173
|
+
color: var(--text-secondary);
|
|
1174
|
+
line-height: 1.5;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
.threat-section {
|
|
1178
|
+
margin-top: 2rem;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
.threat-header {
|
|
1182
|
+
margin-top: 1rem;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
.threat-scan-btn {
|
|
1186
|
+
background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%);
|
|
1187
|
+
border: none;
|
|
1188
|
+
color: #fff;
|
|
1189
|
+
padding: 0.75rem 1.5rem;
|
|
1190
|
+
border-radius: 10px;
|
|
1191
|
+
font-size: 0.95rem;
|
|
1192
|
+
font-weight: 600;
|
|
1193
|
+
cursor: pointer;
|
|
1194
|
+
transition: all 0.2s;
|
|
1195
|
+
box-shadow: 0 4px 15px rgba(124, 58, 237, 0.3);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
.threat-scan-btn:hover {
|
|
1199
|
+
transform: translateY(-1px);
|
|
1200
|
+
box-shadow: 0 6px 20px rgba(124, 58, 237, 0.4);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
.threat-scan-btn:disabled {
|
|
1204
|
+
opacity: 0.7;
|
|
1205
|
+
cursor: not-allowed;
|
|
1206
|
+
transform: none;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
.threat-score-display {
|
|
1210
|
+
display: flex;
|
|
1211
|
+
align-items: center;
|
|
1212
|
+
gap: 1.5rem;
|
|
1213
|
+
margin-top: 1rem;
|
|
1214
|
+
padding: 1.25rem;
|
|
1215
|
+
background: var(--bg-card);
|
|
1216
|
+
border: 1px solid var(--border);
|
|
1217
|
+
border-radius: 12px;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
.threat-grade {
|
|
1221
|
+
font-size: 2.5rem;
|
|
1222
|
+
font-weight: 800;
|
|
1223
|
+
width: 60px;
|
|
1224
|
+
height: 60px;
|
|
1225
|
+
display: flex;
|
|
1226
|
+
align-items: center;
|
|
1227
|
+
justify-content: center;
|
|
1228
|
+
border-radius: 12px;
|
|
1229
|
+
background: var(--bg-card-hover);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
.threat-grade.grade-a { color: var(--success); border: 2px solid var(--success); }
|
|
1233
|
+
.threat-grade.grade-b { color: #60a5fa; border: 2px solid #60a5fa; }
|
|
1234
|
+
.threat-grade.grade-c { color: var(--warning); border: 2px solid var(--warning); }
|
|
1235
|
+
.threat-grade.grade-d { color: #f97316; border: 2px solid #f97316; }
|
|
1236
|
+
.threat-grade.grade-f { color: var(--danger); border: 2px solid var(--danger); }
|
|
1237
|
+
|
|
1238
|
+
.threat-score-info {
|
|
1239
|
+
display: flex;
|
|
1240
|
+
flex-direction: column;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
.threat-score-value {
|
|
1244
|
+
font-size: 1.3rem;
|
|
1245
|
+
font-weight: 700;
|
|
1246
|
+
color: var(--text-primary);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
.threat-score-label {
|
|
1250
|
+
font-size: 0.8rem;
|
|
1251
|
+
color: var(--text-secondary);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
.threat-summary {
|
|
1255
|
+
display: flex;
|
|
1256
|
+
gap: 0.75rem;
|
|
1257
|
+
margin-left: auto;
|
|
1258
|
+
flex-wrap: wrap;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.threat-summary-badge {
|
|
1262
|
+
padding: 0.3rem 0.6rem;
|
|
1263
|
+
border-radius: 6px;
|
|
1264
|
+
font-size: 0.75rem;
|
|
1265
|
+
font-weight: 600;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
.threat-summary-badge.critical { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
|
|
1269
|
+
.threat-summary-badge.high { background: rgba(249, 115, 22, 0.2); color: #f97316; }
|
|
1270
|
+
.threat-summary-badge.medium { background: rgba(234, 179, 8, 0.2); color: var(--warning); }
|
|
1271
|
+
.threat-summary-badge.low { background: rgba(96, 165, 250, 0.2); color: #60a5fa; }
|
|
1272
|
+
.threat-summary-badge.passed { background: rgba(34, 197, 94, 0.2); color: var(--success); }
|
|
1273
|
+
|
|
1274
|
+
.threat-findings {
|
|
1275
|
+
margin-top: 1rem;
|
|
1276
|
+
display: flex;
|
|
1277
|
+
flex-direction: column;
|
|
1278
|
+
gap: 0.5rem;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
.threat-finding {
|
|
1282
|
+
background: var(--bg-card);
|
|
1283
|
+
border: 1px solid var(--border);
|
|
1284
|
+
border-radius: 10px;
|
|
1285
|
+
padding: 1rem 1.25rem;
|
|
1286
|
+
display: flex;
|
|
1287
|
+
align-items: flex-start;
|
|
1288
|
+
gap: 0.75rem;
|
|
1289
|
+
animation: slideInLeft 0.3s ease-out;
|
|
1290
|
+
transition: border-color 0.2s;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
.threat-finding:hover {
|
|
1294
|
+
border-color: var(--border-hover, #333);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
.threat-finding.finding-passed {
|
|
1298
|
+
opacity: 0.7;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
.finding-severity {
|
|
1302
|
+
min-width: 28px;
|
|
1303
|
+
height: 28px;
|
|
1304
|
+
border-radius: 6px;
|
|
1305
|
+
display: flex;
|
|
1306
|
+
align-items: center;
|
|
1307
|
+
justify-content: center;
|
|
1308
|
+
font-size: 0.85rem;
|
|
1309
|
+
font-weight: 700;
|
|
1310
|
+
flex-shrink: 0;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
.finding-severity.critical { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
|
|
1314
|
+
.finding-severity.high { background: rgba(249, 115, 22, 0.2); color: #f97316; }
|
|
1315
|
+
.finding-severity.medium { background: rgba(234, 179, 8, 0.2); color: var(--warning); }
|
|
1316
|
+
.finding-severity.low { background: rgba(96, 165, 250, 0.2); color: #60a5fa; }
|
|
1317
|
+
.finding-severity.info { background: rgba(148, 163, 184, 0.2); color: var(--text-secondary); }
|
|
1318
|
+
.finding-severity.pass { background: rgba(34, 197, 94, 0.2); color: var(--success); }
|
|
1319
|
+
|
|
1320
|
+
.finding-content {
|
|
1321
|
+
flex: 1;
|
|
1322
|
+
min-width: 0;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
.finding-title {
|
|
1326
|
+
font-weight: 600;
|
|
1327
|
+
color: var(--text-primary);
|
|
1328
|
+
font-size: 0.9rem;
|
|
1329
|
+
margin-bottom: 0.25rem;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
.finding-desc {
|
|
1333
|
+
font-size: 0.8rem;
|
|
1334
|
+
color: var(--text-secondary);
|
|
1335
|
+
line-height: 1.4;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
.finding-recommendation {
|
|
1339
|
+
font-size: 0.75rem;
|
|
1340
|
+
color: var(--primary);
|
|
1341
|
+
margin-top: 0.4rem;
|
|
1342
|
+
font-style: italic;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
.finding-category {
|
|
1346
|
+
font-size: 0.65rem;
|
|
1347
|
+
text-transform: uppercase;
|
|
1348
|
+
letter-spacing: 0.05em;
|
|
1349
|
+
color: var(--text-muted);
|
|
1350
|
+
padding: 0.2rem 0.5rem;
|
|
1351
|
+
background: var(--bg-card-hover);
|
|
1352
|
+
border-radius: 4px;
|
|
1353
|
+
flex-shrink: 0;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
@media (max-width: 768px) {
|
|
1357
|
+
.security-arch-grid {
|
|
1358
|
+
grid-template-columns: 1fr;
|
|
1359
|
+
}
|
|
1360
|
+
.threat-score-display {
|
|
1361
|
+
flex-wrap: wrap;
|
|
1362
|
+
}
|
|
1363
|
+
.threat-summary {
|
|
1364
|
+
margin-left: 0;
|
|
1365
|
+
margin-top: 0.5rem;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
package/dashboard/dash.js
CHANGED
|
@@ -911,6 +911,7 @@
|
|
|
911
911
|
initProtectionToggle();
|
|
912
912
|
initNotificationToggle();
|
|
913
913
|
initResetButton();
|
|
914
|
+
initThreatScan();
|
|
914
915
|
|
|
915
916
|
function initNotificationToggle() {
|
|
916
917
|
const btn = document.getElementById("btn-notifications");
|
|
@@ -948,6 +949,72 @@
|
|
|
948
949
|
});
|
|
949
950
|
}
|
|
950
951
|
|
|
952
|
+
function initThreatScan() {
|
|
953
|
+
const btn = document.getElementById("btn-scan");
|
|
954
|
+
const scoreDisplay = document.getElementById("threat-score-display");
|
|
955
|
+
const findingsEl = document.getElementById("threat-findings");
|
|
956
|
+
if (!btn || !scoreDisplay || !findingsEl) return;
|
|
957
|
+
|
|
958
|
+
btn.addEventListener("click", async () => {
|
|
959
|
+
btn.disabled = true;
|
|
960
|
+
btn.textContent = "🔍 Scanning...";
|
|
961
|
+
|
|
962
|
+
try {
|
|
963
|
+
const headers = {};
|
|
964
|
+
if (SECRET) headers["X-Lite-Secret"] = SECRET;
|
|
965
|
+
const res = await fetch("/api/threat-assessment", { headers });
|
|
966
|
+
if (!res.ok) throw new Error("Scan failed");
|
|
967
|
+
const data = await res.json();
|
|
968
|
+
renderThreatResults(data, scoreDisplay, findingsEl);
|
|
969
|
+
btn.textContent = "🔍 Rescan";
|
|
970
|
+
} catch (err) {
|
|
971
|
+
btn.textContent = "⚠️ Scan Failed — Retry";
|
|
972
|
+
console.error("Threat scan error:", err);
|
|
973
|
+
}
|
|
974
|
+
btn.disabled = false;
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function renderThreatResults(data, scoreDisplay, findingsEl) {
|
|
979
|
+
scoreDisplay.classList.remove("hidden");
|
|
980
|
+
findingsEl.classList.remove("hidden");
|
|
981
|
+
|
|
982
|
+
const gradeEl = document.getElementById("threat-grade");
|
|
983
|
+
const scoreVal = document.getElementById("threat-score-value");
|
|
984
|
+
const summaryEl = document.getElementById("threat-summary");
|
|
985
|
+
|
|
986
|
+
gradeEl.textContent = data.grade;
|
|
987
|
+
gradeEl.className = "threat-grade grade-" + data.grade.toLowerCase();
|
|
988
|
+
scoreVal.textContent = data.score + "/100";
|
|
989
|
+
|
|
990
|
+
let summaryHtml = "";
|
|
991
|
+
if (data.summary.critical > 0) summaryHtml += `<span class="threat-summary-badge critical">${data.summary.critical} Critical</span>`;
|
|
992
|
+
if (data.summary.high > 0) summaryHtml += `<span class="threat-summary-badge high">${data.summary.high} High</span>`;
|
|
993
|
+
if (data.summary.medium > 0) summaryHtml += `<span class="threat-summary-badge medium">${data.summary.medium} Medium</span>`;
|
|
994
|
+
if (data.summary.low > 0) summaryHtml += `<span class="threat-summary-badge low">${data.summary.low} Low</span>`;
|
|
995
|
+
summaryHtml += `<span class="threat-summary-badge passed">${data.summary.passed} Passed</span>`;
|
|
996
|
+
summaryEl.innerHTML = summaryHtml;
|
|
997
|
+
|
|
998
|
+
const failed = data.findings.filter(f => !f.passed);
|
|
999
|
+
const passed = data.findings.filter(f => f.passed);
|
|
1000
|
+
const sorted = [...failed, ...passed];
|
|
1001
|
+
|
|
1002
|
+
findingsEl.innerHTML = sorted.map(f => {
|
|
1003
|
+
const sevIcon = f.passed ? "✓" : (f.severity === "critical" ? "!!" : f.severity === "high" ? "!" : f.severity === "medium" ? "~" : "·");
|
|
1004
|
+
const sevClass = f.passed ? "pass" : f.severity;
|
|
1005
|
+
const passedClass = f.passed ? "finding-passed" : "";
|
|
1006
|
+
return `<div class="threat-finding ${passedClass}">
|
|
1007
|
+
<div class="finding-severity ${sevClass}">${sevIcon}</div>
|
|
1008
|
+
<div class="finding-content">
|
|
1009
|
+
<div class="finding-title">${f.title}</div>
|
|
1010
|
+
<div class="finding-desc">${f.description}</div>
|
|
1011
|
+
${!f.passed ? `<div class="finding-recommendation">💡 ${f.recommendation}</div>` : ""}
|
|
1012
|
+
</div>
|
|
1013
|
+
<div class="finding-category">${f.category}</div>
|
|
1014
|
+
</div>`;
|
|
1015
|
+
}).join("");
|
|
1016
|
+
}
|
|
1017
|
+
|
|
951
1018
|
function initResetButton() {
|
|
952
1019
|
const btn = document.getElementById("btn-reset");
|
|
953
1020
|
if (!btn) return;
|
package/dashboard/demo-script.md
CHANGED
package/dashboard/index.html
CHANGED
|
@@ -196,6 +196,48 @@
|
|
|
196
196
|
</div>
|
|
197
197
|
</section>
|
|
198
198
|
|
|
199
|
+
<section class="security-arch-section">
|
|
200
|
+
<h2>Dashboard Security <span class="legend-help" data-tooltip="Why your bot can never access or tamper with this dashboard">?</span></h2>
|
|
201
|
+
<div class="security-arch-grid">
|
|
202
|
+
<div class="arch-item">
|
|
203
|
+
<div class="arch-icon">🔒</div>
|
|
204
|
+
<div class="arch-title">Local-Only Binding</div>
|
|
205
|
+
<div class="arch-desc">Dashboard listens on localhost only. No external network can reach it — even if your server is public.</div>
|
|
206
|
+
</div>
|
|
207
|
+
<div class="arch-item">
|
|
208
|
+
<div class="arch-icon">🔑</div>
|
|
209
|
+
<div class="arch-title">Secret Key Auth</div>
|
|
210
|
+
<div class="arch-desc">Every request requires a secret key your bot never sees. Even local processes can't access data without it.</div>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="arch-item">
|
|
213
|
+
<div class="arch-icon">👁️</div>
|
|
214
|
+
<div class="arch-title">Passive Monitoring</div>
|
|
215
|
+
<div class="arch-desc">Clawguard only reads log files — it never writes to the bot, sends commands, or modifies any configuration.</div>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="arch-item">
|
|
218
|
+
<div class="arch-icon">🚫</div>
|
|
219
|
+
<div class="arch-title">No Write-Back Path</div>
|
|
220
|
+
<div class="arch-desc">There is no API, socket, or channel from the dashboard back to your bot. The connection is strictly one-way.</div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</section>
|
|
224
|
+
|
|
225
|
+
<section class="threat-section" id="threat-section">
|
|
226
|
+
<h2>Threat Assessment <span class="legend-help" data-tooltip="Holistic security scan of your server">?</span></h2>
|
|
227
|
+
<div class="threat-header" id="threat-header">
|
|
228
|
+
<button class="threat-scan-btn" id="btn-scan" data-testid="btn-scan">🔍 Run Security Scan</button>
|
|
229
|
+
<div class="threat-score-display hidden" id="threat-score-display">
|
|
230
|
+
<div class="threat-grade" id="threat-grade">-</div>
|
|
231
|
+
<div class="threat-score-info">
|
|
232
|
+
<span class="threat-score-value" id="threat-score-value">-/100</span>
|
|
233
|
+
<span class="threat-score-label">Security Score</span>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="threat-summary" id="threat-summary"></div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="threat-findings hidden" id="threat-findings" data-testid="threat-findings"></div>
|
|
239
|
+
</section>
|
|
240
|
+
|
|
199
241
|
<section class="extensions-section">
|
|
200
242
|
<h2>ClawdBot Extensions <span class="extension-count" id="extension-count">0</span></h2>
|
|
201
243
|
<div class="extensions-grid" id="extensions-grid" data-testid="grid-extensions">
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"AAuCA,wBAAsB,WAAW,CAAC,IAAI,SAAO,EAAE,IAAI,SAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAmK9E;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAiBhD"}
|
package/dist/server.js
CHANGED
|
@@ -35,6 +35,7 @@ const metrics_1 = require("./metrics");
|
|
|
35
35
|
const storage_1 = require("./storage");
|
|
36
36
|
const risk_engine_1 = require("./src/risk-engine");
|
|
37
37
|
const capability_manifest_1 = require("./src/capability-manifest");
|
|
38
|
+
const threat_assessment_1 = require("./src/threat-assessment");
|
|
38
39
|
const log_watcher_1 = require("./log-watcher");
|
|
39
40
|
let server = null;
|
|
40
41
|
let wss = null;
|
|
@@ -152,6 +153,15 @@ async function startServer(port = 4321, host = "0.0.0.0") {
|
|
|
152
153
|
const assessment = (0, risk_engine_1.assessActionRisk)({ type, tool, command, url, path: filePath }, agentId);
|
|
153
154
|
res.json(assessment);
|
|
154
155
|
});
|
|
156
|
+
app.get("/api/threat-assessment", localOnly, validateSecret, async (_req, res) => {
|
|
157
|
+
try {
|
|
158
|
+
const assessment = await (0, threat_assessment_1.runThreatAssessment)();
|
|
159
|
+
res.json(assessment);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
res.status(500).json({ error: "Threat assessment failed" });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
155
165
|
app.get("/api/manifest-template", localOnly, (req, res) => {
|
|
156
166
|
const framework = typeof req.query?.framework === "string" ? req.query.framework : undefined;
|
|
157
167
|
res.type("application/json").send((0, capability_manifest_1.generateManifestTemplate)(framework));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ThreatFinding {
|
|
2
|
+
id: string;
|
|
3
|
+
category: "process" | "files" | "secrets" | "network" | "system" | "bot";
|
|
4
|
+
severity: "critical" | "high" | "medium" | "low" | "info";
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
recommendation: string;
|
|
8
|
+
passed: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ThreatAssessment {
|
|
11
|
+
timestamp: string;
|
|
12
|
+
score: number;
|
|
13
|
+
grade: string;
|
|
14
|
+
findings: ThreatFinding[];
|
|
15
|
+
summary: {
|
|
16
|
+
critical: number;
|
|
17
|
+
high: number;
|
|
18
|
+
medium: number;
|
|
19
|
+
low: number;
|
|
20
|
+
info: number;
|
|
21
|
+
passed: number;
|
|
22
|
+
total: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare function runThreatAssessment(): ThreatAssessment;
|
|
26
|
+
//# sourceMappingURL=threat-assessment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"threat-assessment.d.ts","sourceRoot":"","sources":["../../src/threat-assessment.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,KAAK,CAAC;IACzE,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,OAAO,EAAE;QACP,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAieD,wBAAgB,mBAAmB,IAAI,gBAAgB,CAgDtD"}
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.runThreatAssessment = runThreatAssessment;
|
|
27
|
+
const fs = __importStar(require("fs"));
|
|
28
|
+
const path = __importStar(require("path"));
|
|
29
|
+
const os = __importStar(require("os"));
|
|
30
|
+
const child_process_1 = require("child_process");
|
|
31
|
+
const CLAWGUARD_DIR = path.join(os.homedir(), ".clawguard");
|
|
32
|
+
const CLAWDBOT_LOG_DIR = "/tmp/clawdbot";
|
|
33
|
+
const SEVERITY_PENALTIES = {
|
|
34
|
+
critical: 25,
|
|
35
|
+
high: 15,
|
|
36
|
+
medium: 8,
|
|
37
|
+
low: 3,
|
|
38
|
+
info: 0,
|
|
39
|
+
};
|
|
40
|
+
function calculateGrade(score) {
|
|
41
|
+
if (score >= 90)
|
|
42
|
+
return "A";
|
|
43
|
+
if (score >= 80)
|
|
44
|
+
return "B";
|
|
45
|
+
if (score >= 70)
|
|
46
|
+
return "C";
|
|
47
|
+
if (score >= 60)
|
|
48
|
+
return "D";
|
|
49
|
+
return "F";
|
|
50
|
+
}
|
|
51
|
+
function checkProcessSecurity() {
|
|
52
|
+
const findings = [];
|
|
53
|
+
try {
|
|
54
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : -1;
|
|
55
|
+
const isRoot = uid === 0;
|
|
56
|
+
findings.push({
|
|
57
|
+
id: "proc-root",
|
|
58
|
+
category: "process",
|
|
59
|
+
severity: isRoot ? "critical" : "info",
|
|
60
|
+
title: "Running as root",
|
|
61
|
+
description: isRoot
|
|
62
|
+
? "The process is running as root (UID 0). This grants unrestricted access to the entire system."
|
|
63
|
+
: "The process is running as a non-root user.",
|
|
64
|
+
recommendation: isRoot
|
|
65
|
+
? "Run the service under a dedicated unprivileged user account."
|
|
66
|
+
: "No action needed.",
|
|
67
|
+
passed: !isRoot,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
try {
|
|
72
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
73
|
+
findings.push({
|
|
74
|
+
id: "proc-node-env",
|
|
75
|
+
category: "process",
|
|
76
|
+
severity: isProduction ? "info" : "low",
|
|
77
|
+
title: "NODE_ENV production check",
|
|
78
|
+
description: isProduction
|
|
79
|
+
? "NODE_ENV is set to production."
|
|
80
|
+
: `NODE_ENV is "${process.env.NODE_ENV || "(not set)"}". Production hardening may be disabled.`,
|
|
81
|
+
recommendation: isProduction
|
|
82
|
+
? "No action needed."
|
|
83
|
+
: "Set NODE_ENV=production in production deployments.",
|
|
84
|
+
passed: isProduction,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
return findings;
|
|
89
|
+
}
|
|
90
|
+
function checkFilePermissions() {
|
|
91
|
+
const findings = [];
|
|
92
|
+
try {
|
|
93
|
+
const exists = fs.existsSync(CLAWGUARD_DIR);
|
|
94
|
+
if (exists) {
|
|
95
|
+
const stat = fs.statSync(CLAWGUARD_DIR);
|
|
96
|
+
const mode = stat.mode;
|
|
97
|
+
const otherWrite = (mode & 0o002) !== 0;
|
|
98
|
+
findings.push({
|
|
99
|
+
id: "files-clawguard-dir",
|
|
100
|
+
category: "files",
|
|
101
|
+
severity: otherWrite ? "high" : "info",
|
|
102
|
+
title: "Clawguard directory permissions",
|
|
103
|
+
description: otherWrite
|
|
104
|
+
? `~/.clawguard/ is world-writable (mode ${(mode & 0o777).toString(8)}). Any user can modify governance data.`
|
|
105
|
+
: `~/.clawguard/ exists with mode ${(mode & 0o777).toString(8)}.`,
|
|
106
|
+
recommendation: otherWrite
|
|
107
|
+
? "Run: chmod 700 ~/.clawguard"
|
|
108
|
+
: "No action needed.",
|
|
109
|
+
passed: !otherWrite,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
findings.push({
|
|
114
|
+
id: "files-clawguard-dir",
|
|
115
|
+
category: "files",
|
|
116
|
+
severity: "info",
|
|
117
|
+
title: "Clawguard directory permissions",
|
|
118
|
+
description: "~/.clawguard/ directory does not exist yet. It will be created on first use.",
|
|
119
|
+
recommendation: "No action needed.",
|
|
120
|
+
passed: true,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
125
|
+
try {
|
|
126
|
+
const logsDir = path.join(CLAWGUARD_DIR, "logs");
|
|
127
|
+
if (fs.existsSync(logsDir)) {
|
|
128
|
+
const logFiles = fs.readdirSync(logsDir).filter(f => f.endsWith(".jsonl"));
|
|
129
|
+
let worldReadable = false;
|
|
130
|
+
for (const file of logFiles) {
|
|
131
|
+
try {
|
|
132
|
+
const stat = fs.statSync(path.join(logsDir, file));
|
|
133
|
+
if ((stat.mode & 0o004) !== 0) {
|
|
134
|
+
worldReadable = true;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch { }
|
|
139
|
+
}
|
|
140
|
+
findings.push({
|
|
141
|
+
id: "files-logs-readable",
|
|
142
|
+
category: "files",
|
|
143
|
+
severity: worldReadable ? "medium" : "info",
|
|
144
|
+
title: "Log files world-readable",
|
|
145
|
+
description: worldReadable
|
|
146
|
+
? "One or more log files in ~/.clawguard/logs/ are world-readable. Logs may contain sensitive action data."
|
|
147
|
+
: "Log files are not world-readable.",
|
|
148
|
+
recommendation: worldReadable
|
|
149
|
+
? "Run: chmod 600 ~/.clawguard/logs/*.jsonl"
|
|
150
|
+
: "No action needed.",
|
|
151
|
+
passed: !worldReadable,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch { }
|
|
156
|
+
try {
|
|
157
|
+
const configFile = path.join(CLAWGUARD_DIR, "config.json");
|
|
158
|
+
if (fs.existsSync(configFile)) {
|
|
159
|
+
const stat = fs.statSync(configFile);
|
|
160
|
+
const worldWritable = (stat.mode & 0o002) !== 0;
|
|
161
|
+
findings.push({
|
|
162
|
+
id: "files-config-writable",
|
|
163
|
+
category: "files",
|
|
164
|
+
severity: worldWritable ? "high" : "info",
|
|
165
|
+
title: "Config file world-writable",
|
|
166
|
+
description: worldWritable
|
|
167
|
+
? "~/.clawguard/config.json is world-writable. Any user can alter governance settings."
|
|
168
|
+
: "Config file permissions are acceptable.",
|
|
169
|
+
recommendation: worldWritable
|
|
170
|
+
? "Run: chmod 600 ~/.clawguard/config.json"
|
|
171
|
+
: "No action needed.",
|
|
172
|
+
passed: !worldWritable,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch { }
|
|
177
|
+
return findings;
|
|
178
|
+
}
|
|
179
|
+
function checkSecrets() {
|
|
180
|
+
const findings = [];
|
|
181
|
+
try {
|
|
182
|
+
const secret = process.env.LITE_ADAPTER_SECRET;
|
|
183
|
+
const isSet = !!secret;
|
|
184
|
+
const isStrong = isSet && secret.length >= 16;
|
|
185
|
+
findings.push({
|
|
186
|
+
id: "secrets-adapter-secret",
|
|
187
|
+
category: "secrets",
|
|
188
|
+
severity: !isSet ? "high" : !isStrong ? "high" : "info",
|
|
189
|
+
title: "LITE_ADAPTER_SECRET strength",
|
|
190
|
+
description: !isSet
|
|
191
|
+
? "LITE_ADAPTER_SECRET is not set. The adapter API is unprotected."
|
|
192
|
+
: !isStrong
|
|
193
|
+
? `LITE_ADAPTER_SECRET is only ${secret.length} characters. A minimum of 16 is recommended.`
|
|
194
|
+
: "LITE_ADAPTER_SECRET is set and meets minimum length.",
|
|
195
|
+
recommendation: !isStrong
|
|
196
|
+
? "Generate a strong random secret: openssl rand -hex 24"
|
|
197
|
+
: "No action needed.",
|
|
198
|
+
passed: isStrong,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
catch { }
|
|
202
|
+
try {
|
|
203
|
+
const leakyVars = [
|
|
204
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
205
|
+
"AWS_SESSION_TOKEN",
|
|
206
|
+
"GITHUB_TOKEN",
|
|
207
|
+
"SLACK_TOKEN",
|
|
208
|
+
"STRIPE_SECRET_KEY",
|
|
209
|
+
"SENDGRID_API_KEY",
|
|
210
|
+
"TWILIO_AUTH_TOKEN",
|
|
211
|
+
"OPENAI_API_KEY",
|
|
212
|
+
];
|
|
213
|
+
const found = leakyVars.filter(v => !!process.env[v]);
|
|
214
|
+
const dbUrlHasPassword = process.env.DATABASE_URL
|
|
215
|
+
? /\/\/[^:]+:[^@]+@/.test(process.env.DATABASE_URL)
|
|
216
|
+
: false;
|
|
217
|
+
if (dbUrlHasPassword)
|
|
218
|
+
found.push("DATABASE_URL (contains password)");
|
|
219
|
+
findings.push({
|
|
220
|
+
id: "secrets-leaked-env",
|
|
221
|
+
category: "secrets",
|
|
222
|
+
severity: found.length > 0 ? "high" : "info",
|
|
223
|
+
title: "Sensitive environment variables exposed",
|
|
224
|
+
description: found.length > 0
|
|
225
|
+
? `Found ${found.length} sensitive env var(s) in the process environment: ${found.join(", ")}. If this process is compromised, these secrets are accessible.`
|
|
226
|
+
: "No common sensitive environment variables detected in the process.",
|
|
227
|
+
recommendation: found.length > 0
|
|
228
|
+
? "Use a secrets manager or vault instead of plain environment variables. Rotate any exposed credentials."
|
|
229
|
+
: "No action needed.",
|
|
230
|
+
passed: found.length === 0,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch { }
|
|
234
|
+
try {
|
|
235
|
+
const envFile = path.join(process.cwd(), ".env");
|
|
236
|
+
if (fs.existsSync(envFile)) {
|
|
237
|
+
const stat = fs.statSync(envFile);
|
|
238
|
+
const worldReadable = (stat.mode & 0o004) !== 0;
|
|
239
|
+
findings.push({
|
|
240
|
+
id: "secrets-dotenv-readable",
|
|
241
|
+
category: "secrets",
|
|
242
|
+
severity: worldReadable ? "medium" : "info",
|
|
243
|
+
title: ".env file world-readable",
|
|
244
|
+
description: worldReadable
|
|
245
|
+
? ".env file in the working directory is world-readable. Any local user can read secrets."
|
|
246
|
+
: ".env file exists but is not world-readable.",
|
|
247
|
+
recommendation: worldReadable
|
|
248
|
+
? "Run: chmod 600 .env"
|
|
249
|
+
: "No action needed.",
|
|
250
|
+
passed: !worldReadable,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch { }
|
|
255
|
+
return findings;
|
|
256
|
+
}
|
|
257
|
+
function checkNetworkSecurity() {
|
|
258
|
+
const findings = [];
|
|
259
|
+
try {
|
|
260
|
+
const allowRemote = process.env.LITE_ALLOW_REMOTE === "true";
|
|
261
|
+
findings.push({
|
|
262
|
+
id: "net-remote-access",
|
|
263
|
+
category: "network",
|
|
264
|
+
severity: allowRemote ? "medium" : "info",
|
|
265
|
+
title: "Remote access enabled",
|
|
266
|
+
description: allowRemote
|
|
267
|
+
? "LITE_ALLOW_REMOTE is true. The adapter accepts connections from any IP, not just localhost."
|
|
268
|
+
: "Remote access is disabled. Only localhost connections are accepted.",
|
|
269
|
+
recommendation: allowRemote
|
|
270
|
+
? "Disable remote access unless required. Use a reverse proxy with TLS for remote deployments."
|
|
271
|
+
: "No action needed.",
|
|
272
|
+
passed: !allowRemote,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
catch { }
|
|
276
|
+
try {
|
|
277
|
+
const riskyPorts = [21, 23, 25, 3306, 5432, 6379, 27017, 9200];
|
|
278
|
+
const listeningRisky = [];
|
|
279
|
+
try {
|
|
280
|
+
const tcpData = fs.readFileSync("/proc/net/tcp", "utf-8");
|
|
281
|
+
const lines = tcpData.split("\n").slice(1);
|
|
282
|
+
for (const line of lines) {
|
|
283
|
+
const parts = line.trim().split(/\s+/);
|
|
284
|
+
if (parts.length < 4)
|
|
285
|
+
continue;
|
|
286
|
+
const localAddr = parts[1];
|
|
287
|
+
const state = parts[3];
|
|
288
|
+
if (state !== "0A")
|
|
289
|
+
continue;
|
|
290
|
+
const portHex = localAddr.split(":")[1];
|
|
291
|
+
if (!portHex)
|
|
292
|
+
continue;
|
|
293
|
+
const port = parseInt(portHex, 16);
|
|
294
|
+
if (riskyPorts.includes(port)) {
|
|
295
|
+
listeningRisky.push(port);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch { }
|
|
300
|
+
findings.push({
|
|
301
|
+
id: "net-risky-ports",
|
|
302
|
+
category: "network",
|
|
303
|
+
severity: "info",
|
|
304
|
+
title: "Risky ports listening",
|
|
305
|
+
description: listeningRisky.length > 0
|
|
306
|
+
? `Detected listening on potentially risky ports: ${listeningRisky.join(", ")}. Verify these services are intended.`
|
|
307
|
+
: "No common risky ports detected listening.",
|
|
308
|
+
recommendation: listeningRisky.length > 0
|
|
309
|
+
? "Ensure these services are firewalled and only accessible to intended clients."
|
|
310
|
+
: "No action needed.",
|
|
311
|
+
passed: listeningRisky.length === 0,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
catch { }
|
|
315
|
+
try {
|
|
316
|
+
let sshPasswordAuth = false;
|
|
317
|
+
try {
|
|
318
|
+
const sshdConfig = fs.readFileSync("/etc/ssh/sshd_config", "utf-8");
|
|
319
|
+
const passwordLine = sshdConfig
|
|
320
|
+
.split("\n")
|
|
321
|
+
.find(l => /^\s*PasswordAuthentication\s+yes/i.test(l));
|
|
322
|
+
if (passwordLine)
|
|
323
|
+
sshPasswordAuth = true;
|
|
324
|
+
}
|
|
325
|
+
catch { }
|
|
326
|
+
findings.push({
|
|
327
|
+
id: "net-ssh-password",
|
|
328
|
+
category: "network",
|
|
329
|
+
severity: sshPasswordAuth ? "medium" : "info",
|
|
330
|
+
title: "SSH password authentication",
|
|
331
|
+
description: sshPasswordAuth
|
|
332
|
+
? "SSH password authentication appears to be enabled. This is vulnerable to brute-force attacks."
|
|
333
|
+
: "SSH password authentication is disabled or sshd_config is not accessible.",
|
|
334
|
+
recommendation: sshPasswordAuth
|
|
335
|
+
? "Disable PasswordAuthentication in /etc/ssh/sshd_config and use key-based auth."
|
|
336
|
+
: "No action needed.",
|
|
337
|
+
passed: !sshPasswordAuth,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
catch { }
|
|
341
|
+
return findings;
|
|
342
|
+
}
|
|
343
|
+
function checkSystemHardening() {
|
|
344
|
+
const findings = [];
|
|
345
|
+
try {
|
|
346
|
+
let firewallActive = false;
|
|
347
|
+
try {
|
|
348
|
+
const ufwOutput = (0, child_process_1.execSync)("ufw status 2>/dev/null", { encoding: "utf-8", timeout: 5000 });
|
|
349
|
+
if (/Status:\s*active/i.test(ufwOutput))
|
|
350
|
+
firewallActive = true;
|
|
351
|
+
}
|
|
352
|
+
catch { }
|
|
353
|
+
if (!firewallActive) {
|
|
354
|
+
try {
|
|
355
|
+
const iptablesOutput = (0, child_process_1.execSync)("iptables -L -n 2>/dev/null | head -20", { encoding: "utf-8", timeout: 5000 });
|
|
356
|
+
const ruleLines = iptablesOutput.split("\n").filter(l => l.trim() && !l.startsWith("Chain") && !l.startsWith("target"));
|
|
357
|
+
if (ruleLines.length > 0)
|
|
358
|
+
firewallActive = true;
|
|
359
|
+
}
|
|
360
|
+
catch { }
|
|
361
|
+
}
|
|
362
|
+
findings.push({
|
|
363
|
+
id: "sys-firewall",
|
|
364
|
+
category: "system",
|
|
365
|
+
severity: firewallActive ? "info" : "medium",
|
|
366
|
+
title: "Firewall status",
|
|
367
|
+
description: firewallActive
|
|
368
|
+
? "A firewall appears to be active."
|
|
369
|
+
: "No active firewall detected (ufw/iptables). The server may be exposed to unwanted traffic.",
|
|
370
|
+
recommendation: firewallActive
|
|
371
|
+
? "No action needed."
|
|
372
|
+
: "Enable a firewall: ufw enable, or configure iptables rules.",
|
|
373
|
+
passed: firewallActive,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
catch { }
|
|
377
|
+
try {
|
|
378
|
+
let autoUpdate = false;
|
|
379
|
+
try {
|
|
380
|
+
(0, child_process_1.execSync)("dpkg -l unattended-upgrades 2>/dev/null", { encoding: "utf-8", timeout: 5000 });
|
|
381
|
+
autoUpdate = true;
|
|
382
|
+
}
|
|
383
|
+
catch { }
|
|
384
|
+
if (!autoUpdate) {
|
|
385
|
+
try {
|
|
386
|
+
(0, child_process_1.execSync)("systemctl is-active dnf-automatic 2>/dev/null", { encoding: "utf-8", timeout: 5000 });
|
|
387
|
+
autoUpdate = true;
|
|
388
|
+
}
|
|
389
|
+
catch { }
|
|
390
|
+
}
|
|
391
|
+
findings.push({
|
|
392
|
+
id: "sys-auto-update",
|
|
393
|
+
category: "system",
|
|
394
|
+
severity: autoUpdate ? "info" : "low",
|
|
395
|
+
title: "Automatic security updates",
|
|
396
|
+
description: autoUpdate
|
|
397
|
+
? "Automatic security updates appear to be configured."
|
|
398
|
+
: "No automatic update mechanism detected (unattended-upgrades / dnf-automatic).",
|
|
399
|
+
recommendation: autoUpdate
|
|
400
|
+
? "No action needed."
|
|
401
|
+
: "Install and enable unattended-upgrades or equivalent for automatic security patches.",
|
|
402
|
+
passed: autoUpdate,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
catch { }
|
|
406
|
+
try {
|
|
407
|
+
let fdLimit = 0;
|
|
408
|
+
let procLimit = 0;
|
|
409
|
+
try {
|
|
410
|
+
const fdOut = (0, child_process_1.execSync)("ulimit -n 2>/dev/null", { encoding: "utf-8", timeout: 5000 }).trim();
|
|
411
|
+
fdLimit = parseInt(fdOut, 10) || 0;
|
|
412
|
+
}
|
|
413
|
+
catch { }
|
|
414
|
+
try {
|
|
415
|
+
const procOut = (0, child_process_1.execSync)("ulimit -u 2>/dev/null", { encoding: "utf-8", timeout: 5000 }).trim();
|
|
416
|
+
procLimit = parseInt(procOut, 10) || 0;
|
|
417
|
+
}
|
|
418
|
+
catch { }
|
|
419
|
+
findings.push({
|
|
420
|
+
id: "sys-ulimits",
|
|
421
|
+
category: "system",
|
|
422
|
+
severity: "info",
|
|
423
|
+
title: "Resource limits (ulimits)",
|
|
424
|
+
description: `File descriptor limit: ${fdLimit || "unknown"}, Process limit: ${procLimit || "unknown"}.`,
|
|
425
|
+
recommendation: fdLimit > 0 && fdLimit < 1024
|
|
426
|
+
? "Consider increasing file descriptor limit for production workloads (ulimit -n 65536)."
|
|
427
|
+
: "No action needed.",
|
|
428
|
+
passed: true,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
catch { }
|
|
432
|
+
return findings;
|
|
433
|
+
}
|
|
434
|
+
function checkBotSecurity() {
|
|
435
|
+
const findings = [];
|
|
436
|
+
try {
|
|
437
|
+
const credentialPatterns = [
|
|
438
|
+
/Bearer\s+[A-Za-z0-9\-._~+/]+=*/,
|
|
439
|
+
/token=[A-Za-z0-9\-._~+/]{10,}/i,
|
|
440
|
+
/password\s*[=:]\s*\S{4,}/i,
|
|
441
|
+
/api[_-]?key\s*[=:]\s*\S{10,}/i,
|
|
442
|
+
/secret\s*[=:]\s*\S{10,}/i,
|
|
443
|
+
/-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/,
|
|
444
|
+
];
|
|
445
|
+
let credentialLeak = false;
|
|
446
|
+
let leakDetail = "";
|
|
447
|
+
try {
|
|
448
|
+
if (fs.existsSync(CLAWDBOT_LOG_DIR)) {
|
|
449
|
+
const logFiles = fs.readdirSync(CLAWDBOT_LOG_DIR)
|
|
450
|
+
.filter(f => f.endsWith(".log"))
|
|
451
|
+
.sort()
|
|
452
|
+
.slice(-3);
|
|
453
|
+
for (const file of logFiles) {
|
|
454
|
+
try {
|
|
455
|
+
const filePath = path.join(CLAWDBOT_LOG_DIR, file);
|
|
456
|
+
const stat = fs.statSync(filePath);
|
|
457
|
+
const readSize = Math.min(stat.size, 50000);
|
|
458
|
+
const fd = fs.openSync(filePath, "r");
|
|
459
|
+
const buffer = Buffer.alloc(readSize);
|
|
460
|
+
const readFrom = Math.max(0, stat.size - readSize);
|
|
461
|
+
fs.readSync(fd, buffer, 0, readSize, readFrom);
|
|
462
|
+
fs.closeSync(fd);
|
|
463
|
+
const content = buffer.toString("utf-8");
|
|
464
|
+
for (const pattern of credentialPatterns) {
|
|
465
|
+
if (pattern.test(content)) {
|
|
466
|
+
credentialLeak = true;
|
|
467
|
+
leakDetail = `Pattern "${pattern.source}" matched in ${file}`;
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch { }
|
|
473
|
+
if (credentialLeak)
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
catch { }
|
|
479
|
+
findings.push({
|
|
480
|
+
id: "bot-log-credentials",
|
|
481
|
+
category: "bot",
|
|
482
|
+
severity: credentialLeak ? "critical" : "info",
|
|
483
|
+
title: "Credentials in bot logs",
|
|
484
|
+
description: credentialLeak
|
|
485
|
+
? `Potential credentials or tokens found in bot logs. ${leakDetail}`
|
|
486
|
+
: "No credential patterns detected in recent bot log files.",
|
|
487
|
+
recommendation: credentialLeak
|
|
488
|
+
? "Immediately rotate any exposed credentials. Add log scrubbing to prevent future leaks."
|
|
489
|
+
: "No action needed.",
|
|
490
|
+
passed: !credentialLeak,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
catch { }
|
|
494
|
+
try {
|
|
495
|
+
findings.push({
|
|
496
|
+
id: "bot-clawguard-writeback",
|
|
497
|
+
category: "bot",
|
|
498
|
+
severity: "info",
|
|
499
|
+
title: "Clawguard write-back disabled",
|
|
500
|
+
description: "Clawguard operates in read-only observation mode. It does not write back to the bot or modify its behavior directly.",
|
|
501
|
+
recommendation: "No action needed. This is the intended design.",
|
|
502
|
+
passed: true,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
catch { }
|
|
506
|
+
return findings;
|
|
507
|
+
}
|
|
508
|
+
function runThreatAssessment() {
|
|
509
|
+
const findings = [];
|
|
510
|
+
const checks = [
|
|
511
|
+
checkProcessSecurity,
|
|
512
|
+
checkFilePermissions,
|
|
513
|
+
checkSecrets,
|
|
514
|
+
checkNetworkSecurity,
|
|
515
|
+
checkSystemHardening,
|
|
516
|
+
checkBotSecurity,
|
|
517
|
+
];
|
|
518
|
+
for (const check of checks) {
|
|
519
|
+
try {
|
|
520
|
+
findings.push(...check());
|
|
521
|
+
}
|
|
522
|
+
catch { }
|
|
523
|
+
}
|
|
524
|
+
const summary = {
|
|
525
|
+
critical: 0,
|
|
526
|
+
high: 0,
|
|
527
|
+
medium: 0,
|
|
528
|
+
low: 0,
|
|
529
|
+
info: 0,
|
|
530
|
+
passed: 0,
|
|
531
|
+
total: findings.length,
|
|
532
|
+
};
|
|
533
|
+
let score = 100;
|
|
534
|
+
for (const finding of findings) {
|
|
535
|
+
summary[finding.severity]++;
|
|
536
|
+
if (finding.passed) {
|
|
537
|
+
summary.passed++;
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
score -= SEVERITY_PENALTIES[finding.severity];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
score = Math.max(0, Math.min(100, score));
|
|
544
|
+
return {
|
|
545
|
+
timestamp: new Date().toISOString(),
|
|
546
|
+
score,
|
|
547
|
+
grade: calculateGrade(score),
|
|
548
|
+
findings,
|
|
549
|
+
summary,
|
|
550
|
+
};
|
|
551
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "averecion-lite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Real-time AI agent monitoring - watches logs, detects dangerous commands and prompt injection attempts",
|
|
5
5
|
"author": "Averecion <hello@averecion.com>",
|
|
6
6
|
"homepage": "https://github.com/averecion/clawguard#readme",
|