From 9af04f39650e0c7fbbab8491d15709c0ed781859 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 15 May 2026 02:06:46 -0400 Subject: [PATCH] fix: harden dashboard canary and IOC coverage --- ecc_dashboard.py | 12 +- scripts/ci/scan-supply-chain-iocs.js | 153 +++++++++++++++++++++++- skills/canary-watch/SKILL.md | 8 ++ tests/ci/scan-supply-chain-iocs.test.js | 52 ++++++++ tests/docs/canary-watch.test.js | 45 +++++++ 5 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 tests/docs/canary-watch.test.js diff --git a/ecc_dashboard.py b/ecc_dashboard.py index 089eb9fd..efbab3bc 100644 --- a/ecc_dashboard.py +++ b/ecc_dashboard.py @@ -902,8 +902,16 @@ Project: github.com/affaan-m/everything-claude-code""" widget.configure(background=bg_color) except Exception: logger.debug("Cannot set background on %s", widget.__class__.__name__, exc_info=True) - for child in widget.winfo_children(): - update_widget_colors(child) + try: + children = widget.winfo_children() + except Exception: + logger.debug("Cannot list child widgets on %s", widget.__class__.__name__, exc_info=True) + return + for child in children: + try: + update_widget_colors(child) + except Exception: + logger.debug("Cannot update child widget colors on %s", child.__class__.__name__, exc_info=True) update_widget_colors(self) diff --git a/scripts/ci/scan-supply-chain-iocs.js b/scripts/ci/scan-supply-chain-iocs.js index 69d0ab64..9f012644 100755 --- a/scripts/ci/scan-supply-chain-iocs.js +++ b/scripts/ci/scan-supply-chain-iocs.js @@ -11,10 +11,78 @@ const path = require('path'); const DEFAULT_ROOT = path.resolve(__dirname, '../..'); const MALICIOUS_PACKAGE_VERSIONS = { - '@mistralai/mistralai': ['2.2.3', '2.2.4'], - '@mistralai/mistralai-azure': ['1.7.2', '1.7.3'], - '@mistralai/mistralai-gcp': ['1.7.2', '1.7.3'], - '@opensearch-project/opensearch': ['3.6.2', '3.8.0'], + '@beproduct/nestjs-auth': [ + '0.1.2', + '0.1.3', + '0.1.4', + '0.1.5', + '0.1.6', + '0.1.7', + '0.1.8', + '0.1.9', + '0.1.10', + '0.1.11', + '0.1.12', + '0.1.13', + '0.1.14', + '0.1.15', + '0.1.16', + '0.1.17', + '0.1.18', + '0.1.19', + ], + '@cap-js/db-service': ['2.10.1'], + '@cap-js/postgres': ['2.2.2'], + '@cap-js/sqlite': ['2.2.2'], + '@dirigible-ai/sdk': ['0.6.2', '0.6.3'], + '@draftauth/client': ['0.2.1', '0.2.2'], + '@draftauth/core': ['0.13.1', '0.13.2'], + '@draftlab/auth': ['0.24.1', '0.24.2'], + '@draftlab/auth-router': ['0.5.1', '0.5.2'], + '@draftlab/db': ['0.16.1', '0.16.2'], + '@mesadev/rest': ['0.28.3'], + '@mesadev/saguaro': ['0.4.22'], + '@mesadev/sdk': ['0.28.3'], + '@ml-toolkit-ts/preprocessing': ['1.0.2', '1.0.3'], + '@ml-toolkit-ts/xgboost': ['1.0.3', '1.0.4'], + '@mistralai/mistralai': ['2.2.2', '2.2.3', '2.2.4'], + '@mistralai/mistralai-azure': ['1.7.1', '1.7.2', '1.7.3'], + '@mistralai/mistralai-gcp': ['1.7.1', '1.7.2', '1.7.3'], + '@opensearch-project/opensearch': ['3.5.3', '3.6.2', '3.7.0', '3.8.0'], + '@squawk/airport-data': ['0.7.4', '0.7.5', '0.7.6', '0.7.7', '0.7.8'], + '@squawk/airports': ['0.6.2', '0.6.3', '0.6.4', '0.6.5', '0.6.6'], + '@squawk/airspace': ['0.8.1', '0.8.2', '0.8.3', '0.8.4', '0.8.5'], + '@squawk/airspace-data': ['0.5.3', '0.5.4', '0.5.5', '0.5.6', '0.5.7'], + '@squawk/airway-data': ['0.5.4', '0.5.5', '0.5.6', '0.5.7', '0.5.8'], + '@squawk/airways': ['0.4.2', '0.4.3', '0.4.4', '0.4.5', '0.4.6'], + '@squawk/fix-data': ['0.6.4', '0.6.5', '0.6.6', '0.6.7', '0.6.8'], + '@squawk/fixes': ['0.3.2', '0.3.3', '0.3.4', '0.3.5', '0.3.6'], + '@squawk/flight-math': ['0.5.4', '0.5.5', '0.5.6', '0.5.7', '0.5.8'], + '@squawk/flightplan': ['0.5.2', '0.5.3', '0.5.4', '0.5.5', '0.5.6'], + '@squawk/geo': ['0.4.4', '0.4.5', '0.4.6', '0.4.7', '0.4.8'], + '@squawk/icao-registry': ['0.5.2', '0.5.3', '0.5.4', '0.5.5', '0.5.6'], + '@squawk/icao-registry-data': ['0.8.4', '0.8.5', '0.8.6', '0.8.7', '0.8.8'], + '@squawk/mcp': ['0.9.1', '0.9.2', '0.9.3', '0.9.4', '0.9.5'], + '@squawk/navaid-data': ['0.6.4', '0.6.5', '0.6.6', '0.6.7', '0.6.8'], + '@squawk/navaids': ['0.4.2', '0.4.3', '0.4.4', '0.4.5', '0.4.6'], + '@squawk/notams': ['0.3.6', '0.3.7', '0.3.8', '0.3.9', '0.3.10'], + '@squawk/procedure-data': ['0.7.3', '0.7.4', '0.7.5', '0.7.6', '0.7.7'], + '@squawk/procedures': ['0.5.2', '0.5.3', '0.5.4', '0.5.5', '0.5.6'], + '@squawk/types': ['0.8.1', '0.8.2', '0.8.3', '0.8.4', '0.8.5'], + '@squawk/units': ['0.4.3', '0.4.4', '0.4.5', '0.4.6', '0.4.7'], + '@squawk/weather': ['0.5.6', '0.5.7', '0.5.8', '0.5.9', '0.5.10'], + '@supersurkhet/cli': ['0.0.2', '0.0.3', '0.0.4', '0.0.5', '0.0.6', '0.0.7'], + '@supersurkhet/sdk': ['0.0.2', '0.0.3', '0.0.4', '0.0.5', '0.0.6', '0.0.7'], + '@tallyui/components': ['1.0.1', '1.0.2', '1.0.3'], + '@tallyui/connector-medusa': ['1.0.1', '1.0.2', '1.0.3'], + '@tallyui/connector-shopify': ['1.0.1', '1.0.2', '1.0.3'], + '@tallyui/connector-vendure': ['1.0.1', '1.0.2', '1.0.3'], + '@tallyui/connector-woocommerce': ['1.0.1', '1.0.2', '1.0.3'], + '@tallyui/core': ['0.2.1', '0.2.2', '0.2.3'], + '@tallyui/database': ['1.0.1', '1.0.2', '1.0.3'], + '@tallyui/pos': ['0.1.1', '0.1.2', '0.1.3'], + '@tallyui/storage-sqlite': ['0.2.1', '0.2.2', '0.2.3'], + '@tallyui/theme': ['0.2.1', '0.2.2', '0.2.3'], '@tanstack/arktype-adapter': ['1.166.12', '1.166.15'], '@tanstack/eslint-plugin-router': ['1.161.9', '1.161.12'], '@tanstack/eslint-plugin-start': ['0.0.4', '0.0.7'], @@ -57,16 +125,89 @@ const MALICIOUS_PACKAGE_VERSIONS = { '@tanstack/vue-start-client': ['1.166.46', '1.166.49'], '@tanstack/vue-start-server': ['1.166.50', '1.166.53'], '@tanstack/zod-adapter': ['1.166.12', '1.166.15'], + '@taskflow-corp/cli': ['0.1.24', '0.1.25', '0.1.26', '0.1.27', '0.1.28', '0.1.29'], + '@tolka/cli': ['1.0.2', '1.0.3', '1.0.4', '1.0.5', '1.0.6'], + '@uipath/access-policy-sdk': ['0.3.1'], + '@uipath/access-policy-tool': ['0.3.1'], '@uipath/agent.sdk': ['0.0.18'], '@uipath/agent-sdk': ['1.0.2'], + '@uipath/agent-tool': ['1.0.1'], + '@uipath/admin-tool': ['0.1.1'], + '@uipath/aops-policy-tool': ['0.3.1'], + '@uipath/ap-chat': ['1.5.7'], + '@uipath/api-workflow-tool': ['1.0.1'], '@uipath/apollo-core': ['5.9.2'], + '@uipath/apollo-react': ['4.24.5'], + '@uipath/apollo-wind': ['2.16.2'], + '@uipath/auth': ['1.0.1'], + '@uipath/case-tool': ['1.0.1'], '@uipath/cli': ['1.0.1'], + '@uipath/codedagent-tool': ['1.0.1'], + '@uipath/codedagents-tool': ['0.1.12'], + '@uipath/codedapp-tool': ['1.0.1'], + '@uipath/common': ['1.0.1'], + '@uipath/context-grounding-tool': ['0.1.1'], + '@uipath/data-fabric-tool': ['1.0.2'], + '@uipath/docsai-tool': ['1.0.1'], + '@uipath/filesystem': ['1.0.1'], + '@uipath/flow-tool': ['1.0.2'], + '@uipath/functions-tool': ['1.0.1'], + '@uipath/gov-tool': ['0.3.1'], + '@uipath/identity-tool': ['0.1.1'], + '@uipath/insights-sdk': ['1.0.1'], + '@uipath/insights-tool': ['1.0.1'], + '@uipath/integrationservice-sdk': ['1.0.2'], + '@uipath/integrationservice-tool': ['1.0.2'], + '@uipath/llmgw-tool': ['1.0.1'], + '@uipath/maestro-sdk': ['1.0.1'], + '@uipath/maestro-tool': ['1.0.1'], + '@uipath/orchestrator-tool': ['1.0.1'], + '@uipath/packager-tool-apiworkflow': ['0.0.19'], + '@uipath/packager-tool-bpmn': ['0.0.9'], + '@uipath/packager-tool-case': ['0.0.9'], + '@uipath/packager-tool-connector': ['0.0.19'], + '@uipath/packager-tool-flow': ['0.0.19'], + '@uipath/packager-tool-functions': ['0.1.1'], + '@uipath/packager-tool-webapp': ['1.0.6'], + '@uipath/packager-tool-workflowcompiler': ['0.0.16'], + '@uipath/packager-tool-workflowcompiler-browser': ['0.0.34'], + '@uipath/platform-tool': ['1.0.1'], + '@uipath/project-packager': ['1.1.16'], + '@uipath/resource-tool': ['1.0.1'], + '@uipath/resourcecatalog-tool': ['0.1.1'], + '@uipath/resources-tool': ['0.1.11'], '@uipath/robot': ['1.3.4'], + '@uipath/rpa-legacy-tool': ['1.0.1'], + '@uipath/rpa-tool': ['0.9.5'], + '@uipath/solution-packager': ['0.0.35'], + '@uipath/solution-tool': ['1.0.1'], + '@uipath/solutionpackager-sdk': ['1.0.11'], + '@uipath/solutionpackager-tool-core': ['0.0.34'], + '@uipath/tasks-tool': ['1.0.1'], + '@uipath/telemetry': ['0.0.7'], + '@uipath/test-manager-tool': ['1.0.2'], + '@uipath/tool-workflowcompiler': ['0.0.12'], + '@uipath/traces-tool': ['1.0.1'], + '@uipath/ui-widgets-multi-file-upload': ['1.0.1'], + '@uipath/uipath-python-bridge': ['1.0.1'], + '@uipath/vertical-solutions-tool': ['1.0.1'], + '@uipath/vss': ['0.1.6'], + '@uipath/widget.sdk': ['1.2.3'], + 'agentwork-cli': ['0.1.4', '0.1.5'], 'cmux-agent-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.6', '0.1.7', '0.1.8'], + 'cross-stitch': ['1.1.3', '1.1.4', '1.1.5', '1.1.6', '1.1.7'], + 'git-branch-selector': ['1.3.3', '1.3.4', '1.3.5', '1.3.6', '1.3.7'], + 'git-git-git': ['1.0.8', '1.0.9', '1.0.10', '1.0.11', '1.0.12'], 'guardrails-ai': ['0.10.1'], + 'intercom-client': ['7.0.4'], + 'lightning': ['2.6.2', '2.6.3'], + 'mbt': ['1.2.48'], 'mistralai': ['2.4.6'], + 'ml-toolkit-ts': ['1.0.4', '1.0.5'], 'nextmove-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.7'], 'safe-action': ['0.8.3', '0.8.4'], + 'ts-dna': ['3.0.1', '3.0.2', '3.0.3', '3.0.4', '3.0.5'], + 'wot-api': ['0.8.1', '0.8.2', '0.8.3', '0.8.4'], }; const CRITICAL_TEXT_INDICATORS = [ @@ -75,6 +216,8 @@ const CRITICAL_TEXT_INDICATORS = [ 'router_init.js', 'router_runtime.js', 'tanstack_runner.js', + 'execution.js', + 'transformers.pyz', 'gh-token-monitor', 'com.user.gh-token-monitor', 'filev2.getsession.org', @@ -85,6 +228,7 @@ const CRITICAL_TEXT_INDICATORS = [ '83.142.209.194', 'api.masscan.cloud', 'A Mini Shai-Hulud has Appeared', + 'Shai-Hulud: Here We Go Again', 'PUSH UR T3MPRR', ]; @@ -113,6 +257,7 @@ const PAYLOAD_FILENAMES = new Set([ 'router_init.js', 'router_runtime.js', 'tanstack_runner.js', + 'execution.js', 'gh-token-monitor.sh', ]); diff --git a/skills/canary-watch/SKILL.md b/skills/canary-watch/SKILL.md index 27a592d8..f52fb6ba 100644 --- a/skills/canary-watch/SKILL.md +++ b/skills/canary-watch/SKILL.md @@ -27,6 +27,8 @@ Monitors a deployed URL for regressions. Runs in a loop until stopped or until t 4. Performance — LCP/CLS/INP regression vs baseline? 5. Content — did key elements disappear? (h1, nav, footer, CTA) 6. API Health — are critical endpoints responding within SLA? +7. Static Assets — are JS, CSS, image, and font requests returning 2xx/3xx with expected content types? +8. SSE Streams — do event-stream endpoints connect and receive an initial event or heartbeat? ``` ### Watch Modes @@ -54,12 +56,16 @@ critical: # immediate alert - Console error count > 5 (new errors only) - LCP > 4s - API endpoint returns 5xx + - Static asset returns 4xx/5xx + - SSE endpoint cannot connect or drops before first heartbeat warning: # flag in report - LCP increased > 500ms from baseline - CLS > 0.1 - New console warnings - Response time > 2x baseline + - Static asset content type changed unexpectedly + - SSE heartbeat latency > 2x baseline info: # log only - Minor performance variance @@ -87,6 +93,8 @@ When a critical threshold is crossed: | LCP | 1.8s ✓ | 1.6s | +200ms | | CLS | 0.01 ✓ | 0.01 | — | | API /health | 145ms ✓ | 120ms | +25ms | +| Static assets | 42/42 ✓ | 42/42 | — | +| SSE /events | connected ✓ | connected | +80ms heartbeat | ### No regressions detected. Deploy is clean. ``` diff --git a/tests/ci/scan-supply-chain-iocs.test.js b/tests/ci/scan-supply-chain-iocs.test.js index 939c9bb1..cbd7804c 100755 --- a/tests/ci/scan-supply-chain-iocs.test.js +++ b/tests/ci/scan-supply-chain-iocs.test.js @@ -68,6 +68,38 @@ function run() { }); })) passed++; else failed++; + if (test('rejects expanded Mini Shai-Hulud campaign package versions', () => { + withFixture({ + 'package-lock.json': JSON.stringify({ + packages: { + 'node_modules/@opensearch-project/opensearch': { + version: '3.5.3', + }, + 'node_modules/@squawk/mcp': { + version: '0.9.5', + }, + 'node_modules/@mistralai/mistralai': { + version: '2.2.2', + }, + }, + }, null, 2), + 'requirements.txt': [ + 'mistralai==2.4.6', + 'guardrails-ai==0.10.1', + 'lightning==2.6.3', + ].join('\n'), + }, rootDir => { + const result = scanSupplyChainIocs({ rootDir }); + const indicators = result.findings.map(finding => finding.indicator); + assert.ok(indicators.includes('@opensearch-project/opensearch@3.5.3')); + assert.ok(indicators.includes('@squawk/mcp@0.9.5')); + assert.ok(indicators.includes('@mistralai/mistralai@2.2.2')); + assert.ok(indicators.includes('mistralai@2.4.6')); + assert.ok(indicators.includes('guardrails-ai@0.10.1')); + assert.ok(indicators.includes('lightning@2.6.3')); + }); + })) passed++; else failed++; + if (test('passes clean versions of watched packages', () => { withFixture({ 'package-lock.json': JSON.stringify({ @@ -116,6 +148,26 @@ function run() { }); })) passed++; else failed++; + if (test('rejects current dead-drop and import-time payload markers', () => { + withFixture({ + '.vscode/tasks.json': JSON.stringify({ + tasks: [{ + label: 'watch', + command: 'python3 /tmp/transformers.pyz && node execution.js', + runOptions: { runOn: 'folderOpen' }, + }], + }, null, 2), + 'package.json': JSON.stringify({ + description: 'Shai-Hulud: Here We Go Again', + }, null, 2), + }, rootDir => { + const result = scanSupplyChainIocs({ rootDir }); + assert.ok(result.findings.some(finding => finding.indicator === 'transformers.pyz')); + assert.ok(result.findings.some(finding => finding.indicator === 'execution.js')); + assert.ok(result.findings.some(finding => finding.indicator === 'Shai-Hulud: Here We Go Again')); + }); + })) passed++; else failed++; + if (test('rejects installed payload filenames in node_modules', () => { withFixture({ 'node_modules/@tanstack/react-router/router_init.js': '/* payload */', diff --git a/tests/docs/canary-watch.test.js b/tests/docs/canary-watch.test.js new file mode 100644 index 00000000..fc777c83 --- /dev/null +++ b/tests/docs/canary-watch.test.js @@ -0,0 +1,45 @@ +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const SKILL_PATH = path.join(__dirname, '..', '..', 'skills', 'canary-watch', 'SKILL.md'); + +function test(name, fn) { + try { + fn(); + console.log(` \u2713 ${name}`); + return true; + } catch (error) { + console.log(` \u2717 ${name}`); + console.log(` Error: ${error.message}`); + return false; + } +} + +function runTests() { + console.log('\n=== Testing canary-watch skill docs ===\n'); + + let passed = 0; + let failed = 0; + const body = fs.readFileSync(SKILL_PATH, 'utf8'); + + if (test('description monitoring claims are backed by watch sections', () => { + for (const phrase of [ + 'HTTP endpoints', + 'SSE streams', + 'static assets', + 'console errors', + 'performance regressions', + ]) { + assert.ok(body.toLowerCase().includes(phrase.toLowerCase()), `missing phrase: ${phrase}`); + } + assert.ok(body.includes('Static Assets'), 'watch list should include static assets'); + assert.ok(body.includes('SSE Streams'), 'watch list should include SSE streams'); + assert.ok(body.includes('SSE endpoint cannot connect'), 'critical thresholds should cover SSE failures'); + })) passed++; else failed++; + + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); + process.exit(failed > 0 ? 1 : 0); +} + +runTests();