securenow 7.8.1 → 8.0.1
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/NPM_README.md +14 -19
- package/README.md +11 -11
- package/SKILL-API.md +15 -15
- package/SKILL-CLI.md +13 -12
- package/app-config.js +12 -145
- package/cli/apiKey.js +22 -6
- package/cli/auth.js +4 -4
- package/cli/config.js +3 -10
- package/cli/credentials.js +2 -2
- package/cli/diagnostics.js +2 -2
- package/cli/human.js +13 -8
- package/cli/run.js +1 -5
- package/cli/utils.js +2 -3
- package/cli.js +13 -15
- package/console-instrumentation.js +1 -1
- package/firewall-only.js +4 -7
- package/mcp/catalog.js +540 -16
- package/mcp/server.js +1 -1
- package/nextjs-auto-capture.js +3 -6
- package/nextjs-middleware.js +2 -4
- package/nextjs-wrapper.js +3 -6
- package/nextjs.js +0 -10
- package/package.json +2 -3
- package/rate-limits.js +0 -2
- package/register-vite.js +5 -12
- package/register.js +5 -13
- package/resolve-ip.js +1 -1
- package/tracing.d.ts +1 -1
- package/tracing.js +2 -2
- package/web-vite.mjs +58 -62
package/app-config.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* ./.securenow/credentials.production.json are also accepted when the
|
|
10
10
|
* canonical file is not present. Filename selection is deterministic and does
|
|
11
11
|
* not read environment variables.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Every SDK setting has a file-backed equivalent so customers do not need
|
|
13
|
+
* .env files.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const fs = require('fs');
|
|
@@ -21,7 +21,6 @@ const FREE_TRIAL_INSTANCE = 'https://ingest.securenow.ai';
|
|
|
21
21
|
const DEFAULT_API_URL = 'https://api.securenow.ai';
|
|
22
22
|
const DEFAULT_FIREWALL_API_URL = FREE_TRIAL_INSTANCE;
|
|
23
23
|
const LEGACY_SECURENOW_GATEWAY = 'https://api.securenow.ai/api/otlp';
|
|
24
|
-
const LEGACY_ENV_FALLBACK_FLAG = 'SECURENOW_ENABLE_LEGACY_ENV';
|
|
25
24
|
const CONFIG_SCHEMA_VERSION = 2;
|
|
26
25
|
const CREDENTIAL_FILE_ENVIRONMENTS = Object.freeze([
|
|
27
26
|
'staging',
|
|
@@ -94,7 +93,7 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
94
93
|
|
|
95
94
|
const CONFIG_EXPLANATIONS = Object.freeze({
|
|
96
95
|
'token': 'Legacy CLI session token. New admin/control-plane auth is written to admin.json. Secret: do not commit.',
|
|
97
|
-
'apiKey': 'Scoped
|
|
96
|
+
'apiKey': 'Scoped runtime API key (`snk_live_...`) minted by `securenow app connect` or `securenow api-key set`. It authenticates telemetry ingestion and firewall sync. Secret: do not commit.',
|
|
98
97
|
'app.key': 'SecureNow application routing UUID. The SDK uses this as OTel service.name so dashboard queries match exactly.',
|
|
99
98
|
'app.name': 'Human-readable app label shown in CLI output.',
|
|
100
99
|
'app.routing': 'Telemetry uses the default SecureNow ingestion gateway and routes by app.key; runtime credentials do not expose per-instance collector URLs.',
|
|
@@ -106,7 +105,7 @@ const CONFIG_EXPLANATIONS = Object.freeze({
|
|
|
106
105
|
'config.otel.endpoint': 'Optional OTLP base endpoint override for advanced/self-hosted collectors. Leave null for the SecureNow ingestion gateway.',
|
|
107
106
|
'config.otel.tracesEndpoint': 'Optional full traces endpoint override, for split collectors.',
|
|
108
107
|
'config.otel.logsEndpoint': 'Optional full logs endpoint override, for split collectors.',
|
|
109
|
-
'config.otel.headers': 'Optional OTLP headers. The SDK auto-adds x-
|
|
108
|
+
'config.otel.headers': 'Optional OTLP headers. The SDK auto-adds x-securenow-app-key from app.key and Authorization from apiKey when missing.',
|
|
110
109
|
'config.otel.logLevel': 'OpenTelemetry diagnostic log level: error, warn, info, debug, or none.',
|
|
111
110
|
'config.otel.disableInstrumentations': 'Optional OTel instrumentation package names to disable.',
|
|
112
111
|
'config.runtime.deploymentEnvironment': 'deployment.environment resource attribute. Set this in the credentials file for production.',
|
|
@@ -131,45 +130,6 @@ const CONFIG_EXPLANATIONS = Object.freeze({
|
|
|
131
130
|
'config.networking.trustedProxies': 'Additional proxy IPs whose X-Forwarded-For headers should be trusted.',
|
|
132
131
|
});
|
|
133
132
|
|
|
134
|
-
const ENV_TO_CONFIG_PATH = Object.freeze({
|
|
135
|
-
SECURENOW_LOGGING_ENABLED: 'logging.enabled',
|
|
136
|
-
SECURENOW_CAPTURE_BODY: 'capture.body',
|
|
137
|
-
SECURENOW_CAPTURE_MULTIPART: 'capture.multipart',
|
|
138
|
-
SECURENOW_MAX_BODY_SIZE: 'capture.maxBodySize',
|
|
139
|
-
SECURENOW_SENSITIVE_FIELDS: 'capture.sensitiveFields',
|
|
140
|
-
SECURENOW_DISABLE_INSTRUMENTATIONS: 'otel.disableInstrumentations',
|
|
141
|
-
OTEL_EXPORTER_OTLP_ENDPOINT: 'otel.endpoint',
|
|
142
|
-
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: 'otel.tracesEndpoint',
|
|
143
|
-
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: 'otel.logsEndpoint',
|
|
144
|
-
OTEL_EXPORTER_OTLP_HEADERS: 'otel.headers',
|
|
145
|
-
OTEL_LOG_LEVEL: 'otel.logLevel',
|
|
146
|
-
SECURENOW_ENVIRONMENT: 'runtime.deploymentEnvironment',
|
|
147
|
-
SECURENOW_DEPLOYMENT_ENVIRONMENT: 'runtime.deploymentEnvironment',
|
|
148
|
-
NODE_ENV: 'runtime.deploymentEnvironment',
|
|
149
|
-
SECURENOW_NO_UUID: 'runtime.noUuid',
|
|
150
|
-
SECURENOW_STRICT: 'runtime.strict',
|
|
151
|
-
SECURENOW_TEST_SPAN: 'runtime.testSpan',
|
|
152
|
-
SECURENOW_HIDE_BANNER: 'runtime.hideBanner',
|
|
153
|
-
SECURENOW_API_URL: 'firewall.apiUrl',
|
|
154
|
-
SECURENOW_FIREWALL_VERSION_INTERVAL: 'firewall.versionCheckInterval',
|
|
155
|
-
SECURENOW_FIREWALL_SYNC_INTERVAL: 'firewall.syncInterval',
|
|
156
|
-
SECURENOW_FIREWALL_FAIL_MODE: 'firewall.failMode',
|
|
157
|
-
SECURENOW_FIREWALL_STATUS_CODE: 'firewall.statusCode',
|
|
158
|
-
SECURENOW_FIREWALL_LOG: 'firewall.log',
|
|
159
|
-
SECURENOW_FIREWALL_TCP: 'firewall.tcp',
|
|
160
|
-
SECURENOW_FIREWALL_IPTABLES: 'firewall.iptables',
|
|
161
|
-
SECURENOW_FIREWALL_CLOUD: 'firewall.cloud',
|
|
162
|
-
SECURENOW_FIREWALL_CLOUD_DRY_RUN: 'firewall.cloudDryRun',
|
|
163
|
-
CLOUDFLARE_API_TOKEN: 'firewall.cloudflare.apiToken',
|
|
164
|
-
CLOUDFLARE_ACCOUNT_ID: 'firewall.cloudflare.accountId',
|
|
165
|
-
AWS_WAF_IP_SET_ID: 'firewall.aws.wafIpSetId',
|
|
166
|
-
AWS_WAF_IP_SET_NAME: 'firewall.aws.wafIpSetName',
|
|
167
|
-
AWS_WAF_SCOPE: 'firewall.aws.wafScope',
|
|
168
|
-
GCP_PROJECT_ID: 'firewall.gcp.projectId',
|
|
169
|
-
GCP_SECURITY_POLICY: 'firewall.gcp.securityPolicy',
|
|
170
|
-
SECURENOW_TRUSTED_PROXIES: 'networking.trustedProxies',
|
|
171
|
-
});
|
|
172
|
-
|
|
173
133
|
function clone(value) {
|
|
174
134
|
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
175
135
|
}
|
|
@@ -460,7 +420,7 @@ function withCredentialDefaults(credentials) {
|
|
|
460
420
|
out._securenow = mergeMissing(out._securenow, {
|
|
461
421
|
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
462
422
|
note: 'Local SecureNow credentials and secure SDK defaults. This file may contain secrets; keep .securenow/ in .gitignore.',
|
|
463
|
-
precedence:
|
|
423
|
+
precedence: 'The same credentials file works in local development and production. SDK runtime config is read from credentials JSON only.',
|
|
464
424
|
explanations: CONFIG_EXPLANATIONS,
|
|
465
425
|
});
|
|
466
426
|
return out;
|
|
@@ -476,19 +436,6 @@ function getPath(obj, dotted) {
|
|
|
476
436
|
return cur;
|
|
477
437
|
}
|
|
478
438
|
|
|
479
|
-
function legacyEnvFallbackEnabled() {
|
|
480
|
-
const raw =
|
|
481
|
-
process.env[LEGACY_ENV_FALLBACK_FLAG] ??
|
|
482
|
-
process.env.SECURENOW_ALLOW_ENV_CONFIG ??
|
|
483
|
-
process.env.SECURENOW_LEGACY_ENV;
|
|
484
|
-
return /^(1|true|yes)$/i.test(String(raw || '').trim());
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function rawEnv(key) {
|
|
488
|
-
if (!legacyEnvFallbackEnabled()) return undefined;
|
|
489
|
-
return process.env[key] ?? process.env[key.toUpperCase()] ?? process.env[key.toLowerCase()];
|
|
490
|
-
}
|
|
491
|
-
|
|
492
439
|
function normalizeInstanceEndpoint(value) {
|
|
493
440
|
if (value === undefined || value === null || value === '') return value;
|
|
494
441
|
const endpoint = String(value).trim().replace(/\/$/, '');
|
|
@@ -626,24 +573,11 @@ function headersToString(headers) {
|
|
|
626
573
|
.join(',');
|
|
627
574
|
}
|
|
628
575
|
|
|
629
|
-
function
|
|
630
|
-
if (value === undefined || value === null || value === '') return undefined;
|
|
631
|
-
if (typeof value === 'boolean') return value ? '1' : '0';
|
|
632
|
-
if (Array.isArray(value)) return value.join(',');
|
|
633
|
-
if (typeof value === 'object') return headersToString(value);
|
|
634
|
-
return String(value);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function resolveConfigPath(configPath, envKeys = [], fallback) {
|
|
576
|
+
function resolveConfigPath(configPath, _envKeys = [], fallback) {
|
|
638
577
|
const creds = loadCredentials();
|
|
639
578
|
const fromCreds = pick(getPath(creds && creds.config, configPath));
|
|
640
579
|
if (fromCreds != null) return fromCreds;
|
|
641
580
|
|
|
642
|
-
for (const key of envKeys) {
|
|
643
|
-
const fromEnv = pick(rawEnv(key));
|
|
644
|
-
if (fromEnv != null) return fromEnv;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
581
|
const fromDefault = pick(getPath(DEFAULT_CONFIG, configPath));
|
|
648
582
|
if (fromDefault != null) return fromDefault;
|
|
649
583
|
|
|
@@ -669,28 +603,12 @@ function listConfig(configPath) {
|
|
|
669
603
|
function resolveAppKey() {
|
|
670
604
|
const creds = loadCredentials();
|
|
671
605
|
if (creds && creds.app && pick(creds.app.key)) return String(pick(creds.app.key));
|
|
672
|
-
|
|
673
|
-
const fromEnv = pick(rawEnv('SECURENOW_APPID')) || pick(rawEnv('securenow'));
|
|
674
|
-
if (fromEnv) return String(fromEnv);
|
|
675
|
-
|
|
676
|
-
// Legacy compatibility: older docs sometimes used SECURENOW_API_KEY as the
|
|
677
|
-
// app routing UUID. Real firewall keys start with snk_live_ and are handled
|
|
678
|
-
// by resolveApiKey(), not as service.name.
|
|
679
|
-
const legacyApiKey = pick(rawEnv('SECURENOW_API_KEY'));
|
|
680
|
-
if (legacyApiKey && !String(legacyApiKey).startsWith('snk_live_')) {
|
|
681
|
-
return String(legacyApiKey);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
606
|
return null;
|
|
685
607
|
}
|
|
686
608
|
|
|
687
609
|
function resolveAppName() {
|
|
688
610
|
const creds = loadCredentials();
|
|
689
611
|
if (creds && creds.app && pick(creds.app.name)) return String(pick(creds.app.name));
|
|
690
|
-
|
|
691
|
-
const fromEnv = pick(rawEnv('OTEL_SERVICE_NAME'));
|
|
692
|
-
if (fromEnv) return String(fromEnv);
|
|
693
|
-
|
|
694
612
|
return loadPackageJsonName();
|
|
695
613
|
}
|
|
696
614
|
|
|
@@ -702,23 +620,12 @@ function resolveApiKey() {
|
|
|
702
620
|
const creds = loadCredentials();
|
|
703
621
|
const fromCreds = creds && pick(creds.apiKey);
|
|
704
622
|
if (fromCreds && String(fromCreds).startsWith('snk_live_')) return String(fromCreds);
|
|
705
|
-
|
|
706
|
-
const fromEnv = pick(rawEnv('SECURENOW_API_KEY'));
|
|
707
|
-
if (fromEnv && String(fromEnv).startsWith('snk_live_')) return String(fromEnv);
|
|
708
|
-
|
|
709
623
|
return null;
|
|
710
624
|
}
|
|
711
625
|
|
|
712
626
|
function resolveInstance() {
|
|
713
627
|
const fromConfig = pick(resolveConfigPath('otel.endpoint'));
|
|
714
628
|
if (fromConfig) return normalizeInstanceEndpoint(fromConfig);
|
|
715
|
-
|
|
716
|
-
const fromEnv =
|
|
717
|
-
pick(rawEnv('SECURENOW_INSTANCE')) ||
|
|
718
|
-
pick(rawEnv('securenow_instance')) ||
|
|
719
|
-
pick(rawEnv('OTEL_EXPORTER_OTLP_ENDPOINT'));
|
|
720
|
-
if (fromEnv) return normalizeInstanceEndpoint(fromEnv);
|
|
721
|
-
|
|
722
629
|
return FREE_TRIAL_INSTANCE;
|
|
723
630
|
}
|
|
724
631
|
|
|
@@ -743,51 +650,18 @@ function resolveNoUuid(opts = {}) {
|
|
|
743
650
|
const fromConfig = pick(getPath(loadCredentials()?.config, 'runtime.noUuid'));
|
|
744
651
|
if (fromConfig !== null) return parseBool(fromConfig, false);
|
|
745
652
|
|
|
746
|
-
const raw = pick(rawEnv('SECURENOW_NO_UUID'));
|
|
747
|
-
if (raw !== null) return parseBool(raw, false);
|
|
748
|
-
|
|
749
653
|
return !!resolveAppKey();
|
|
750
654
|
}
|
|
751
655
|
|
|
752
|
-
function resolveEnvKey(key) {
|
|
753
|
-
const upper = String(key).toUpperCase();
|
|
754
|
-
|
|
755
|
-
if (upper === 'SECURENOW_APPID') return resolveAppKey();
|
|
756
|
-
if (upper === 'OTEL_SERVICE_NAME') return resolveAppName();
|
|
757
|
-
if (upper === 'SECURENOW_API_KEY') return resolveApiKey();
|
|
758
|
-
if (upper === 'SECURENOW_INSTANCE' || upper === 'OTEL_EXPORTER_OTLP_ENDPOINT') return resolveInstance();
|
|
759
|
-
if (upper === 'SECURENOW_FIREWALL_ENABLED') return resolveFirewallEnabled();
|
|
760
|
-
|
|
761
|
-
const configPath = ENV_TO_CONFIG_PATH[upper];
|
|
762
|
-
if (configPath) return resolveConfigPath(configPath);
|
|
763
|
-
|
|
764
|
-
const fromEnv = pick(rawEnv(upper));
|
|
765
|
-
if (fromEnv != null) return fromEnv;
|
|
766
|
-
|
|
767
|
-
return undefined;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function env(key) {
|
|
771
|
-
return toEnvString(resolveEnvKey(key));
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function boolEnv(key, fallback = false) {
|
|
775
|
-
return parseBool(resolveEnvKey(key), fallback);
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function numberEnv(key, fallback, min) {
|
|
779
|
-
return parseNumber(resolveEnvKey(key), fallback, min);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function listEnv(key) {
|
|
783
|
-
return parseList(resolveEnvKey(key));
|
|
784
|
-
}
|
|
785
|
-
|
|
786
656
|
function resolveOtlpHeaders() {
|
|
787
657
|
const headers = parseHeaders(configValue('otel.headers', {}));
|
|
788
658
|
const appKey = resolveAppKey();
|
|
789
|
-
|
|
790
|
-
|
|
659
|
+
const apiKey = resolveApiKey();
|
|
660
|
+
if (appKey && !headers['x-securenow-app-key']) {
|
|
661
|
+
headers['x-securenow-app-key'] = appKey;
|
|
662
|
+
}
|
|
663
|
+
if (apiKey && !headers.authorization) {
|
|
664
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
791
665
|
}
|
|
792
666
|
const deploymentEnvironment = resolveDeploymentEnvironment();
|
|
793
667
|
if (deploymentEnvironment && !headers['x-securenow-environment']) {
|
|
@@ -855,14 +729,12 @@ module.exports = {
|
|
|
855
729
|
DEFAULT_API_URL,
|
|
856
730
|
DEFAULT_FIREWALL_API_URL,
|
|
857
731
|
LEGACY_SECURENOW_GATEWAY,
|
|
858
|
-
LEGACY_ENV_FALLBACK_FLAG,
|
|
859
732
|
CONFIG_SCHEMA_VERSION,
|
|
860
733
|
CREDENTIAL_FILE_ENVIRONMENTS,
|
|
861
734
|
RUNTIME_CREDENTIALS_FILENAME,
|
|
862
735
|
ADMIN_CREDENTIALS_FILENAME,
|
|
863
736
|
DEFAULT_CONFIG,
|
|
864
737
|
CONFIG_EXPLANATIONS,
|
|
865
|
-
ENV_TO_CONFIG_PATH,
|
|
866
738
|
withCredentialDefaults,
|
|
867
739
|
mergeCredentials,
|
|
868
740
|
resolveConfigPath,
|
|
@@ -876,7 +748,6 @@ module.exports = {
|
|
|
876
748
|
resolveApiKey,
|
|
877
749
|
resolveInstance,
|
|
878
750
|
normalizeDeploymentEnvironment,
|
|
879
|
-
legacyEnvFallbackEnabled,
|
|
880
751
|
resolveDeploymentEnvironment,
|
|
881
752
|
resolveAll,
|
|
882
753
|
resolveNoUuid,
|
|
@@ -888,10 +759,6 @@ module.exports = {
|
|
|
888
759
|
resolveFirewallApiUrl,
|
|
889
760
|
resolveFirewallApiFallbacks,
|
|
890
761
|
resolveFirewallOptions,
|
|
891
|
-
env,
|
|
892
|
-
boolEnv,
|
|
893
|
-
numberEnv,
|
|
894
|
-
listEnv,
|
|
895
762
|
parseHeaders,
|
|
896
763
|
normalizeInstanceEndpoint,
|
|
897
764
|
normalizeSignalEndpoint,
|
package/cli/apiKey.js
CHANGED
|
@@ -4,6 +4,10 @@ const { api, requireAuth } = require('./client');
|
|
|
4
4
|
const config = require('./config');
|
|
5
5
|
const ui = require('./ui');
|
|
6
6
|
|
|
7
|
+
const PRESET_SCOPES = {
|
|
8
|
+
runtime_app: ['traces:write', 'logs:write', 'firewall:read', 'blocklist:read', 'allowlist:read'],
|
|
9
|
+
};
|
|
10
|
+
|
|
7
11
|
function maskKey(key) {
|
|
8
12
|
if (!key || key.length < 16) return key || '';
|
|
9
13
|
return `${key.slice(0, 12)}••••••${key.slice(-4)}`;
|
|
@@ -12,16 +16,27 @@ function maskKey(key) {
|
|
|
12
16
|
async function create(args, flags) {
|
|
13
17
|
requireAuth();
|
|
14
18
|
|
|
15
|
-
const name = flags.name || args.join(' ').trim() || 'CLI
|
|
16
|
-
const preset = flags.preset || '
|
|
19
|
+
const name = flags.name || args.join(' ').trim() || 'CLI runtime';
|
|
20
|
+
const preset = flags.preset || 'runtime_app';
|
|
17
21
|
const local = flags.global ? false : true;
|
|
22
|
+
const app = config.getApp();
|
|
18
23
|
|
|
19
24
|
const s = ui.spinner(`Creating ${preset} API key`);
|
|
20
25
|
try {
|
|
21
|
-
const
|
|
26
|
+
const body = { name, preset };
|
|
27
|
+
if (PRESET_SCOPES[preset]) {
|
|
28
|
+
body.scopes = PRESET_SCOPES[preset];
|
|
29
|
+
}
|
|
30
|
+
if (flags.app) {
|
|
31
|
+
body.apps = [flags.app];
|
|
32
|
+
} else if (preset === 'runtime_app' && app?.key) {
|
|
33
|
+
body.apps = [app.key];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result = await api.post('/api-keys', body);
|
|
22
37
|
const key = result && result.key;
|
|
23
38
|
if (!key || !key.startsWith('snk_live_')) {
|
|
24
|
-
throw new Error('API did not return a valid
|
|
39
|
+
throw new Error('API did not return a valid runtime key');
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
config.setApiKey(key, { local });
|
|
@@ -34,6 +49,7 @@ async function create(args, flags) {
|
|
|
34
49
|
name: result.apiKey?.name || name,
|
|
35
50
|
preset,
|
|
36
51
|
key: maskKey(key),
|
|
52
|
+
appScope: body.apps || [],
|
|
37
53
|
savedTo: local ? 'project .securenow/runtime.json' : '~/.securenow/runtime.json',
|
|
38
54
|
});
|
|
39
55
|
return;
|
|
@@ -43,7 +59,7 @@ async function create(args, flags) {
|
|
|
43
59
|
ui.info(local
|
|
44
60
|
? 'Stored in project .securenow/runtime.json (local)'
|
|
45
61
|
: 'Stored in ~/.securenow/runtime.json (global)');
|
|
46
|
-
ui.info('The plaintext key is not printed. The SDK will read it from the credentials file.');
|
|
62
|
+
ui.info('The plaintext key is not printed. The SDK will read it from the runtime credentials file.');
|
|
47
63
|
} catch (err) {
|
|
48
64
|
s.fail('Failed to create API key');
|
|
49
65
|
throw err;
|
|
@@ -87,7 +103,7 @@ async function clear(args, flags) {
|
|
|
87
103
|
async function show() {
|
|
88
104
|
const key = config.getApiKey();
|
|
89
105
|
if (!key) {
|
|
90
|
-
ui.info('Runtime
|
|
106
|
+
ui.info('Runtime API key is missing. Run `npx securenow app connect` or `npx securenow api-key create` to refresh .securenow/runtime.json.');
|
|
91
107
|
return;
|
|
92
108
|
}
|
|
93
109
|
console.log(maskKey(key));
|
package/cli/auth.js
CHANGED
|
@@ -275,7 +275,7 @@ async function login(args, flags) {
|
|
|
275
275
|
ui.info('Runtime app config was not changed.');
|
|
276
276
|
}
|
|
277
277
|
if (apiKey) {
|
|
278
|
-
ui.info(`
|
|
278
|
+
ui.info(`Runtime API key saved — telemetry ingestion and firewall sync will authenticate automatically on next start`);
|
|
279
279
|
}
|
|
280
280
|
if (exp) {
|
|
281
281
|
const days = Math.ceil((exp - Date.now()) / (1000 * 60 * 60 * 24));
|
|
@@ -343,9 +343,9 @@ async function appConnect(args, flags) {
|
|
|
343
343
|
ui.success(`Runtime app connected to ${ui.c.bold(app.name || app.key)}`);
|
|
344
344
|
ui.info(local ? 'Saved to project .securenow/runtime.json' : 'Saved to ~/.securenow/runtime.json');
|
|
345
345
|
if (apiKey) {
|
|
346
|
-
ui.info('
|
|
346
|
+
ui.info('Runtime API key saved; SDK telemetry and firewall sync can run without an admin token.');
|
|
347
347
|
} else {
|
|
348
|
-
ui.warn('No
|
|
348
|
+
ui.warn('No runtime API key was returned. Run `securenow api-key create` before sending telemetry.');
|
|
349
349
|
}
|
|
350
350
|
ui.info('Admin auth was not changed.');
|
|
351
351
|
}
|
|
@@ -393,7 +393,7 @@ async function whoami() {
|
|
|
393
393
|
['Status', runtime.app?.key ? ui.c.green('connected') : ui.c.red('no app selected')],
|
|
394
394
|
['App', runtime.app?.name || '—'],
|
|
395
395
|
['App Key', runtime.app?.key || '—'],
|
|
396
|
-
['
|
|
396
|
+
['Runtime API Key', runtime.apiKey ? ui.c.green('present') : ui.c.yellow('missing')],
|
|
397
397
|
['Environment', runtime.config?.runtime?.deploymentEnvironment || 'production'],
|
|
398
398
|
['Runtime Source', config.getRuntimeSource()],
|
|
399
399
|
['API', config.getApiUrl()],
|
package/cli/config.js
CHANGED
|
@@ -127,7 +127,6 @@ function resolveRuntimeCredentialsFile() {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
function getAuthSource() {
|
|
130
|
-
if (process.env.SECURENOW_TOKEN) return 'env (SECURENOW_TOKEN)';
|
|
131
130
|
const file = appConfig.resolveLocalAdminCredentialsFile() || appConfig.resolveGlobalAdminCredentialsFile();
|
|
132
131
|
if (!file) return 'not configured';
|
|
133
132
|
if (file.includes(`${path.sep}.securenow${path.sep}`) && file.startsWith(process.cwd())) {
|
|
@@ -188,9 +187,6 @@ function saveCredentials(creds, { local = false } = {}) {
|
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
function loadAdminCredentials() {
|
|
191
|
-
if (process.env.SECURENOW_TOKEN) {
|
|
192
|
-
return { token: process.env.SECURENOW_TOKEN, source: 'env' };
|
|
193
|
-
}
|
|
194
190
|
const adminFile = pickExistingFile(
|
|
195
191
|
appConfig.resolveLocalAdminCredentialsFile(),
|
|
196
192
|
appConfig.resolveGlobalAdminCredentialsFile()
|
|
@@ -305,8 +301,6 @@ function clearCredentials({ local } = {}) {
|
|
|
305
301
|
}
|
|
306
302
|
|
|
307
303
|
function getToken() {
|
|
308
|
-
if (process.env.SECURENOW_TOKEN) return process.env.SECURENOW_TOKEN;
|
|
309
|
-
|
|
310
304
|
const creds = loadAdminCredentials();
|
|
311
305
|
if (!creds.token) return null;
|
|
312
306
|
|
|
@@ -412,18 +406,17 @@ function ensureLocalGitignore() {
|
|
|
412
406
|
}
|
|
413
407
|
|
|
414
408
|
function getApiUrl() {
|
|
415
|
-
if (process.env.SECURENOW_API_URL) return process.env.SECURENOW_API_URL;
|
|
416
409
|
const cfg = loadConfig();
|
|
417
410
|
if (cfg.apiUrl && cfg.apiUrl !== DEFAULTS.apiUrl) return cfg.apiUrl;
|
|
418
|
-
return
|
|
411
|
+
return cfg.apiUrl;
|
|
419
412
|
}
|
|
420
413
|
|
|
421
414
|
function getAppUrl() {
|
|
422
|
-
return
|
|
415
|
+
return loadConfig().appUrl;
|
|
423
416
|
}
|
|
424
417
|
|
|
425
418
|
function getDefaultApp() {
|
|
426
|
-
return
|
|
419
|
+
return loadConfig().defaultApp;
|
|
427
420
|
}
|
|
428
421
|
|
|
429
422
|
module.exports = {
|
package/cli/credentials.js
CHANGED
|
@@ -70,7 +70,7 @@ async function runtime(_args, flags) {
|
|
|
70
70
|
warn('No app key found. Run `npx securenow app connect` first so telemetry routes to the selected app.');
|
|
71
71
|
}
|
|
72
72
|
if (!creds.apiKey) {
|
|
73
|
-
warn('Runtime
|
|
73
|
+
warn('Runtime API key is missing. Run `npx securenow app connect` or `npx securenow api-key create` before generating production runtime credentials.');
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
if (flags.stdout) {
|
|
@@ -87,7 +87,7 @@ async function runtime(_args, flags) {
|
|
|
87
87
|
ui.info('Credential filename lookup is fixed-order and does not depend on NODE_ENV.');
|
|
88
88
|
ui.info(`Environment: ${envName}`);
|
|
89
89
|
ui.info(`App: ${creds.app?.name || '(unnamed)'} ${creds.app?.key ? `(${creds.app.key})` : ''}`);
|
|
90
|
-
ui.info(`
|
|
90
|
+
ui.info(`Runtime API key: ${creds.apiKey ? maskSecret(creds.apiKey) : '(missing)'}`);
|
|
91
91
|
ui.info('OAuth token/email were not included.');
|
|
92
92
|
}
|
|
93
93
|
|
package/cli/diagnostics.js
CHANGED
|
@@ -500,9 +500,9 @@ async function doctor(_args, flags) {
|
|
|
500
500
|
warnings.push('Using the legacy free-trial collector directly. Regenerate credentials so telemetry flows through https://ingest.securenow.ai.');
|
|
501
501
|
}
|
|
502
502
|
if (!cfg.apiKey && token) {
|
|
503
|
-
warnings.push('Admin CLI/MCP auth is connected, but runtime
|
|
503
|
+
warnings.push('Admin CLI/MCP auth is connected, but the runtime API key is missing. Run `npx securenow app connect` or `npx securenow api-key create` to refresh .securenow/runtime.json.');
|
|
504
504
|
} else if (!cfg.apiKey) {
|
|
505
|
-
warnings.push('Runtime
|
|
505
|
+
warnings.push('Runtime API key is missing. Run `npx securenow app connect` or `npx securenow api-key create` to refresh .securenow/runtime.json.');
|
|
506
506
|
}
|
|
507
507
|
|
|
508
508
|
const ok = checks.every((c) => c.ok);
|
package/cli/human.js
CHANGED
|
@@ -533,18 +533,23 @@ function mcpPromptText(ref, flags = {}) {
|
|
|
533
533
|
: '2. For each task, call securenow_notifications_get and securenow_human_action_report for the notificationId and IP.',
|
|
534
534
|
'3. Read the AI report, finalDecision, investigation steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
|
|
535
535
|
'4. Open/inspect trace evidence with securenow_traces_show and securenow_logs_for_trace when trace IDs are available.',
|
|
536
|
-
'5. Decide one outcome: block the IP, mark a scoped false positive,
|
|
537
|
-
'6.
|
|
538
|
-
'7. For
|
|
539
|
-
'8.
|
|
540
|
-
'9.
|
|
541
|
-
'10.
|
|
542
|
-
'11.
|
|
536
|
+
'5. Decide one outcome: block the IP, rate limit, mark a scoped false positive, apply/prepare alert-rule tuning, create/prepare a new rule, approve/reject a case action, or mark ambiguous/skipped with exact missing proof.',
|
|
537
|
+
'6. Finished does not always mean enforcement changed. If no write is safe, record or prepare a structured no-write decision report with the exact proof gap, missing permission, or vague guard.',
|
|
538
|
+
'7. For mixed evidence, split the decision by IP/cluster. Block only proven malicious IPs, false-positive only exact benign shapes, rate-limit only route-specific volume abuse, and leave ambiguous members with missing proof.',
|
|
539
|
+
'8. If the right fix is global/system rule tuning and MCP returns access denied, do not create customer-specific false positives. Prepare the exact attack-preserving guard/SQL, dry-run it if possible, and record outcome=rule_tuning with missingProof naming the missing permission.',
|
|
540
|
+
'9. Reject vague AI proposed actions. If "shared benign shape" or similar lacks concrete rule id, path, method, status, user-agent/body/header guard, benign samples, and attack examples that still match, do not execute it.',
|
|
541
|
+
'10. For block decisions, call securenow_human_action_block with confirm:true, a precise reason, and a decisionReport with summary/evidence/reviewedHistory/traceIds.',
|
|
542
|
+
'11. For rate-limit decisions, call securenow_rate_limit_create_from_text with confirm:true, then add securenow_human_action_decision_report_add with outcome=rate_limited.',
|
|
543
|
+
'12. For false positives, call securenow_human_action_false_positive with confirm:true, the narrowest available conditions, a precise reason, and a decisionReport.',
|
|
544
|
+
'13. For case-level tune_rule/create_exclusion rows, inspect the notification case and use securenow_human_case_action_update only when the proposed action is safe.',
|
|
545
|
+
'14. For skipped/ambiguous/rule-tuning-needed/new-rule-needed rows that should be auditable without changing enforcement status, call securenow_human_action_decision_report_add or securenow_human_case_decision_report_add with the missing proof.',
|
|
546
|
+
'15. If many IPs share the same benign path/status/user-agent pattern, recommend tightening the alert rule with a precise guard instead of reviewing each IP as malicious.',
|
|
547
|
+
'16. Summarize handled count, writes executed, verification result, rule tuning/admin permission blockers, partial resolutions, skipped proof gaps, and still waiting. Do not globally trust an IP by default.',
|
|
543
548
|
'',
|
|
544
549
|
'Safety:',
|
|
545
550
|
'- Do not call write tools without confirm:true and a reason.',
|
|
546
551
|
'- Do not use broad false-positive scopes if the evidence only supports one alert rule/path/method/status/user-agent/body pattern.',
|
|
547
|
-
'- Prefer no
|
|
552
|
+
'- Prefer an auditable no-write decision report over a weak substitute action.',
|
|
548
553
|
].join('\n');
|
|
549
554
|
}
|
|
550
555
|
|
package/cli/run.js
CHANGED
|
@@ -152,11 +152,7 @@ function run(rawArgs) {
|
|
|
152
152
|
|
|
153
153
|
const nodeExe = process.execPath;
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
console.log(`[securenow] ${ui.c.dim('exec:')} ${nodeExe} ${finalArgs.join(' ')}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const child = spawn(nodeExe, finalArgs, {
|
|
155
|
+
const child = spawn(nodeExe, finalArgs, {
|
|
160
156
|
stdio: 'inherit',
|
|
161
157
|
env: process.env,
|
|
162
158
|
cwd: process.cwd(),
|
package/cli/utils.js
CHANGED
|
@@ -39,9 +39,8 @@ function redact(args, flags) {
|
|
|
39
39
|
const extra = flags.fields
|
|
40
40
|
? String(flags.fields).split(',').map((s) => s.trim()).filter(Boolean)
|
|
41
41
|
: [];
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const fields = [...DEFAULT_SENSITIVE_FIELDS, ...envExtra, ...extra];
|
|
42
|
+
const configExtra = appConfig.listConfig('capture.sensitiveFields');
|
|
43
|
+
const fields = [...DEFAULT_SENSITIVE_FIELDS, ...configExtra, ...extra];
|
|
45
44
|
|
|
46
45
|
const result = redactSensitiveData(parsed, fields);
|
|
47
46
|
|
package/cli.js
CHANGED
|
@@ -91,7 +91,7 @@ const COMMANDS = {
|
|
|
91
91
|
usage: 'securenow app <subcommand> [options]',
|
|
92
92
|
sub: {
|
|
93
93
|
connect: {
|
|
94
|
-
desc: 'Select/create app, mint
|
|
94
|
+
desc: 'Select/create app, mint runtime API key, and write SDK runtime config',
|
|
95
95
|
usage: 'securenow app connect [--global]',
|
|
96
96
|
flags: { global: 'Save runtime config to ~/.securenow/runtime.json' },
|
|
97
97
|
run: (a, f) => require('./cli/auth').appConnect(a, f),
|
|
@@ -109,17 +109,18 @@ const COMMANDS = {
|
|
|
109
109
|
desc: 'Show admin auth and SDK runtime status',
|
|
110
110
|
usage: 'securenow whoami',
|
|
111
111
|
run: () => require('./cli/auth').whoami(),
|
|
112
|
-
},
|
|
112
|
+
},
|
|
113
113
|
'api-key': {
|
|
114
|
-
desc: 'Manage the
|
|
114
|
+
desc: 'Manage the runtime API key stored in runtime credentials',
|
|
115
115
|
usage: 'securenow api-key <subcommand> [options]',
|
|
116
116
|
sub: {
|
|
117
117
|
create: {
|
|
118
|
-
desc: 'Create a
|
|
119
|
-
usage: 'securenow api-key create [name] [--name <name>] [--preset
|
|
118
|
+
desc: 'Create a runtime API key with your session token and save it',
|
|
119
|
+
usage: 'securenow api-key create [name] [--name <name>] [--preset runtime_app] [--app <appKey>] [--global]',
|
|
120
120
|
flags: {
|
|
121
121
|
name: 'Human-readable key name',
|
|
122
|
-
preset: 'API key preset to create (default:
|
|
122
|
+
preset: 'API key preset to create (default: runtime_app)',
|
|
123
|
+
app: 'Application key/id to scope this key to',
|
|
123
124
|
global: 'Save to ~/.securenow/ instead of project-local',
|
|
124
125
|
},
|
|
125
126
|
run: (a, f) => require('./cli/apiKey').create(a, f),
|
|
@@ -771,12 +772,9 @@ async function main() {
|
|
|
771
772
|
}
|
|
772
773
|
}
|
|
773
774
|
|
|
774
|
-
main().catch((err) => {
|
|
775
|
-
if (err.name !== 'CLIError') {
|
|
776
|
-
ui.error(err.message || 'An unexpected error occurred');
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
781
|
-
process.exit(1);
|
|
782
|
-
});
|
|
775
|
+
main().catch((err) => {
|
|
776
|
+
if (err.name !== 'CLIError') {
|
|
777
|
+
ui.error(err.message || 'An unexpected error occurred');
|
|
778
|
+
}
|
|
779
|
+
process.exit(1);
|
|
780
|
+
});
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
const tracing = require('./tracing');
|
|
25
25
|
|
|
26
26
|
if (!tracing.isLoggingEnabled()) {
|
|
27
|
-
console.warn('[securenow] Console instrumentation loaded but logging is disabled (
|
|
27
|
+
console.warn('[securenow] Console instrumentation loaded but logging is disabled (config.logging.enabled=false).');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// Get a logger instance
|
package/firewall-only.js
CHANGED
|
@@ -7,13 +7,10 @@
|
|
|
7
7
|
* node -r securenow/firewall-only app.js
|
|
8
8
|
* NODE_OPTIONS='-r securenow/firewall-only' next start
|
|
9
9
|
*
|
|
10
|
-
* Reads
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
try { require('dotenv').config({ quiet: true }); } catch (_) {}
|
|
16
|
-
|
|
10
|
+
* Reads SecureNow runtime credentials from .securenow/*.json. Initialises the
|
|
11
|
+
* HTTP-level firewall when a snk_live_ API key is resolvable.
|
|
12
|
+
*/
|
|
13
|
+
|
|
17
14
|
const appConfig = require('./app-config');
|
|
18
15
|
const firewallOptions = appConfig.resolveFirewallOptions();
|
|
19
16
|
|