Merge branch 'master' into copilot/fix-1042

This commit is contained in:
Göran Sander
2025-07-25 22:21:36 +02:00
committed by GitHub
14 changed files with 1420 additions and 1005 deletions

View File

@@ -0,0 +1,140 @@
# System Information and OS Command Execution
## Overview
Butler SOS collects system information for monitoring and diagnostic purposes. This information helps with troubleshooting, resource monitoring, and identifying system characteristics. However, on Windows systems, this may trigger security alerts in enterprise environments due to OS command execution.
## Root Cause
Butler SOS uses the [systeminformation](https://www.npmjs.com/package/systeminformation) npm package to gather detailed system information. On Windows, this package internally executes several OS commands to collect system details:
- `cmd.exe /d /s /c \chcp` - Gets code page information
- `netstat -r` - Gets routing table information
- `cmd.exe /d /s /c \echo %COMPUTERNAME%.%USERDNSDOMAIN%` - Gets computer name and domain
These commands are **not executed directly by Butler SOS** but by the systeminformation package dependency. The commands are legitimate system information gathering commands, but they may trigger alerts in security monitoring tools.
## Code Location
The system information gathering occurs in the `initHostInfo()` function in `src/globals.js` (lines 1027-1085), which is called during Butler SOS startup from `src/butler-sos.js`.
## Security Configuration
Starting with version 11.1.0, Butler SOS provides a configuration option to disable detailed system information gathering for security-sensitive environments.
### Configuration Option
Add this section to your Butler SOS configuration file:
```yaml
Butler-SOS:
# System information gathering
# Butler SOS collects system information for monitoring and diagnostic purposes.
# On Windows, this may trigger security alerts as it executes OS commands like:
# - cmd.exe /d /s /c \chcp (to get code page info)
# - netstat -r (to get routing table)
# - cmd.exe /d /s /c \echo %COMPUTERNAME%.%USERDNSDOMAIN% (to get computer/domain names)
# These commands are executed by the 'systeminformation' npm package, not directly by Butler SOS.
systemInfo:
enable: true # Set to false in security-sensitive environments
```
### When to Disable System Information
Consider setting `systemInfo.enable: false` if:
- Your security monitoring tools flag the OS command execution as suspicious
- Your organization has strict policies against any automated OS command execution
- You don't need detailed system information in logs and monitoring outputs
- Butler SOS runs in a highly secured environment
### Impact of Disabling System Information
When `systemInfo.enable` is set to `false`:
**✅ Benefits:**
- No OS commands are executed by the systeminformation package
- Eliminates security alerts from monitoring tools
- Butler SOS continues to function normally
- Basic system information is still collected using Node.js built-in APIs
**⚠️ Limitations:**
- Reduced detail in system information logs
- Some monitoring dashboards may show less detailed host information
- Telemetry data will contain minimal system details
**What's Still Collected:**
- Node.js version and platform information
- Basic OS platform, architecture, and version
- Memory and CPU count from Node.js APIs
- Application version and instance ID
**What's Not Collected:**
- Detailed CPU model and specifications
- Detailed OS distribution information
- Network interface details
- Docker information
- Detailed memory specifications
## Example Configuration Files
### High Security Environment
```yaml
Butler-SOS:
systemInfo:
enable: false # Disable to prevent OS command execution
# ... rest of configuration
```
### Standard Environment
```yaml
Butler-SOS:
systemInfo:
enable: true # Default - enables full system information gathering
# ... rest of configuration
```
## Testing the Configuration
To test that the configuration is working correctly:
1. Set `systemInfo.enable: false` in your config
2. Start Butler SOS
3. Check the logs for: `"SYSTEM INFO: Detailed system information gathering is disabled. Using minimal system info."`
4. Verify that your security monitoring tools no longer flag the OS command execution
## Troubleshooting
### Configuration Validation Errors
If you see configuration validation errors related to `systemInfo`:
1. Ensure the `systemInfo` section is properly nested under `Butler-SOS`
2. Verify that `enable` is set to a boolean value (`true` or `false`), not a string
3. Check YAML indentation is correct
### Missing System Information
If you need some system information but want to minimize OS command execution:
1. Consider using `systemInfo.enable: true` but monitor which specific commands trigger alerts
2. Work with your security team to whitelist the specific systeminformation package commands
3. Use Butler SOS logging to capture the minimal system information that's still collected
## Security Best Practices
1. **Principle of Least Privilege**: Only enable detailed system information gathering if you need it for your monitoring use case
2. **Security Monitoring**: Work with your security team to understand which specific commands trigger alerts
3. **Documentation**: Document your `systemInfo.enable` setting choice in your deployment documentation
4. **Testing**: Test configuration changes in a development environment first
5. **Monitoring**: Monitor Butler SOS logs to ensure it's working correctly with your chosen configuration
## Related Issues
- [Issue #1037](https://github.com/ptarmiganlabs/butler-sos/issues/1037) - Original investigation of OS command execution
- [systeminformation package documentation](https://github.com/sebhildebrandt/systeminformation) - For understanding what information is collected
## Version History
- **11.1.0**: Added `systemInfo.enable` configuration option
- **11.0.3**: Initial report of OS command execution alerts

File diff suppressed because it is too large Load Diff

View File

@@ -1022,17 +1022,68 @@ class Settings {
* Gathers and returns information about the host system where Butler SOS is running.
* Includes OS details, network info, hardware details, and a unique ID.
*
* Note: On Windows, this function may execute OS commands via the 'systeminformation' npm package:
* - cmd.exe /d /s /c \chcp (to get code page info)
* - netstat -r (to get routing table info)
* - cmd.exe /d /s /c \echo %COMPUTERNAME%.%USERDNSDOMAIN% (to get computer/domain names)
*
* These commands are not executed directly by Butler SOS, but by the systeminformation package
* to gather system details. If this triggers security alerts, you can disable detailed system
* information gathering by setting Butler-SOS.systemInfo.enable to false in the config file.
*
* @returns {object | null} Object containing host information or null if an error occurs
*/
async initHostInfo() {
try {
const siCPU = await si.cpu();
const siSystem = await si.system();
const siMem = await si.mem();
const siOS = await si.osInfo();
const siDocker = await si.dockerInfo();
const siNetwork = await si.networkInterfaces();
const siNetworkDefault = await si.networkInterfaceDefault();
// Check if detailed system info gathering is enabled
const enableSystemInfo = this.config.get('Butler-SOS.systemInfo.enable');
let siCPU = {};
let siSystem = {};
let siMem = {};
let siOS = {};
let siDocker = {};
let siNetwork = [];
let siNetworkDefault = '';
// Only gather detailed system info if enabled in config
if (enableSystemInfo) {
siCPU = await si.cpu();
siSystem = await si.system();
siMem = await si.mem();
siOS = await si.osInfo();
siDocker = await si.dockerInfo();
siNetwork = await si.networkInterfaces();
siNetworkDefault = await si.networkInterfaceDefault();
} else {
// If detailed system info is disabled, use minimal fallback values
this.logger.info(
'SYSTEM INFO: Detailed system information gathering is disabled. Using minimal system info.'
);
siSystem = { uuid: 'disabled' };
siMem = { total: 0 };
siOS = {
platform: os.platform(),
arch: os.arch(),
release: 'unknown',
distro: 'unknown',
codename: 'unknown',
};
siCPU = {
processors: 1,
physicalCores: 1,
cores: 1,
hypervizor: 'unknown',
};
siNetwork = [
{
iface: 'default',
mac: '00:00:00:00:00:00',
ip4: '127.0.0.1',
},
];
siNetworkDefault = 'default';
}
const defaultNetworkInterface = siNetworkDefault;
@@ -1040,8 +1091,17 @@ class Settings {
(item) => item.iface === defaultNetworkInterface
);
const idSrc = networkInterface[0].mac + networkInterface[0].ip4 + siSystem.uuid;
const salt = networkInterface[0].mac;
// Ensure we have at least one network interface for ID generation
const netIface =
networkInterface.length > 0
? networkInterface[0]
: siNetwork[0] || {
mac: '00:00:00:00:00:00',
ip4: '127.0.0.1',
};
const idSrc = netIface.mac + netIface.ip4 + siSystem.uuid;
const salt = netIface.mac;
const hash = crypto.createHmac('sha256', salt);
hash.update(idSrc);

View File

@@ -1,6 +1,6 @@
/**
* Test suite for conditional configuration validation based on feature enable flags.
*
*
* Tests that Butler SOS only validates configuration sections when the associated feature is enabled.
* When a feature is disabled, its detailed configuration should not be validated.
*/
@@ -55,16 +55,16 @@ describe('Conditional Configuration Validation', () => {
enable: false,
host: 'localhost',
port: 3100,
obfuscate: true
obfuscate: true,
},
heartbeat: {
enable: false,
remoteURL: 'http://example.com',
frequency: 'every 1 hour'
frequency: 'every 1 hour',
},
dockerHealthCheck: {
enable: false,
port: 12398
port: 12398,
},
uptimeMonitor: {
enable: true,
@@ -72,7 +72,7 @@ describe('Conditional Configuration Validation', () => {
logLevel: 'verbose',
storeInInfluxdb: {
butlerSOSMemoryUsage: true,
instanceTag: 'DEV'
instanceTag: 'DEV',
},
storeNewRelic: {
enable: false,
@@ -80,45 +80,45 @@ describe('Conditional Configuration Validation', () => {
metric: {
dynamic: {
butlerMemoryUsage: { enable: true },
butlerUptime: { enable: true }
}
butlerUptime: { enable: true },
},
},
attribute: {
static: [],
dynamic: {
butlerVersion: { enable: true }
}
}
}
butlerVersion: { enable: true },
},
},
},
},
thirdPartyToolsCredentials: {
newRelic: []
newRelic: [],
},
qlikSenseEvents: {
influxdb: {
enable: false,
writeFrequency: 20000
writeFrequency: 20000,
},
eventCount: {
enable: false,
influxdb: {
measurementName: 'event_count',
tags: []
}
tags: [],
},
},
rejectedEventCount: {
enable: false,
influxdb: {
measurementName: 'rejected_event_count'
}
}
measurementName: 'rejected_event_count',
},
},
},
userEvents: {
enable: false,
excludeUser: [],
udpServerConfig: {
serverHost: 'localhost',
portUserActivityEvents: 9997
portUserActivityEvents: 9997,
},
tags: [],
sendToMQTT: {
@@ -128,20 +128,20 @@ describe('Conditional Configuration Validation', () => {
sessionStartTopic: { enable: true, topic: 'test' },
sessionStopTopic: { enable: true, topic: 'test' },
connectionOpenTopic: { enable: true, topic: 'test' },
connectionCloseTopic: { enable: true, topic: 'test' }
}
connectionCloseTopic: { enable: true, topic: 'test' },
},
},
sendToInfluxdb: { enable: true },
sendToNewRelic: {
enable: false,
destinationAccount: [],
scramble: true
}
scramble: true,
},
},
logEvents: {
udpServerConfig: {
serverHost: 'localhost',
portLogEvents: 9996
portLogEvents: 9996,
},
tags: [],
source: {
@@ -149,44 +149,44 @@ describe('Conditional Configuration Validation', () => {
proxy: { enable: false },
repository: { enable: false },
scheduler: { enable: false },
qixPerf: { enable: true }
qixPerf: { enable: true },
},
categorise: {
enable: false,
rules: []
rules: [],
},
appNameLookup: {
enable: true
enable: true,
},
appAccess: {
enable: false,
influxdb: {
enable: false,
measurementName: 'app_access'
measurementName: 'app_access',
},
rejectedEventCount: {
enable: false,
influxdb: {
measurementName: 'rejected_app_access'
}
measurementName: 'rejected_app_access',
},
},
allApps: {
enable: false,
appsInclude: [],
appsExclude: []
appsExclude: [],
},
someApps: {
enable: false,
appsInclude: []
}
appsInclude: [],
},
},
sendToMQTT: {
enable: false,
baseTopic: 'qliksense/logevent',
postTo: {
baseTopic: true,
subsystemTopics: true
}
subsystemTopics: true,
},
},
sendToInfluxdb: { enable: false },
sendToNewRelic: {
@@ -195,34 +195,34 @@ describe('Conditional Configuration Validation', () => {
source: {
engine: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
proxy: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
repository: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
scheduler: {
enable: true,
logLevel: { error: true, warn: true }
}
}
}
logLevel: { error: true, warn: true },
},
},
},
},
cert: {
clientCert: '/path/to/cert',
clientCertKey: '/path/to/key',
clientCertCA: '/path/to/ca',
clientCertPassphrase: ''
clientCertPassphrase: '',
},
mqttConfig: {
enable: false,
brokerHost: 'localhost',
brokerPort: 1883,
baseTopic: 'butler-sos/'
baseTopic: 'butler-sos/',
},
newRelic: {
enable: false,
@@ -232,9 +232,9 @@ describe('Conditional Configuration Validation', () => {
attribute: {
static: [],
dynamic: {
butlerSosVersion: { enable: true }
}
}
butlerSosVersion: { enable: true },
},
},
},
metric: {
destinationAccount: [],
@@ -248,33 +248,33 @@ describe('Conditional Configuration Validation', () => {
selections: { enable: true },
sessions: { enable: true },
users: { enable: true },
saturated: { enable: true }
saturated: { enable: true },
},
apps: {
docCount: { enable: true },
activeDocs: { enable: true },
loadedDocs: { enable: true },
inMemoryDocs: { enable: true }
inMemoryDocs: { enable: true },
},
cache: {
cache: { enable: true }
cache: { enable: true },
},
proxy: {
sessions: { enable: true }
}
sessions: { enable: true },
},
},
attribute: {
static: [],
dynamic: {
butlerSosVersion: { enable: true }
}
}
}
butlerSosVersion: { enable: true },
},
},
},
},
prometheus: {
enable: false,
host: 'localhost',
port: 9842
port: 9842,
},
influxdbConfig: {
enable: true,
@@ -284,7 +284,7 @@ describe('Conditional Configuration Validation', () => {
auth: {
enable: false,
username: '',
password: ''
password: '',
},
dbName: 'butler-sos',
instanceTag: 'DEV',
@@ -292,70 +292,70 @@ describe('Conditional Configuration Validation', () => {
host: 'localhost',
port: 8086,
database: 'butler-sos',
retentionPolicy: 'autogen'
retentionPolicy: 'autogen',
},
v2Config: {
url: 'http://localhost:8086',
token: 'token',
org: 'org',
bucket: 'bucket'
bucket: 'bucket',
},
writeSchedule: {
frequency: 10000,
tags: []
}
tags: [],
},
},
appNames: {
enableAppNameLookup: true,
lookupFrequency: 30000,
influxdb: {
enable: true
enable: true,
},
newRelic: {
enable: true,
destinationAccount: [],
metric: {
dynamic: {
butlerSosVersion: { enable: true }
}
butlerSosVersion: { enable: true },
},
},
attribute: {
static: [],
dynamic: {
butlerSosVersion: { enable: true }
}
}
}
butlerSosVersion: { enable: true },
},
},
},
},
userSessions: {
enableSessionExtract: true,
pollingFrequency: 30000,
excludeUser: [],
influxdb: {
enable: true
enable: true,
},
newRelic: {
enable: false,
destinationAccount: [],
metric: {
dynamic: {
butlerSosVersion: { enable: true }
}
butlerSosVersion: { enable: true },
},
},
attribute: {
static: [],
dynamic: {
butlerSosVersion: { enable: true }
}
}
}
butlerSosVersion: { enable: true },
},
},
},
},
serversToMonitor: {
pollingFrequency: 30000,
serverTagsDefinition: [],
servers: []
}
}
servers: [],
},
},
};
test('should pass validation with minimal config when MQTT is disabled', async () => {
@@ -370,16 +370,16 @@ describe('Conditional Configuration Validation', () => {
enable: false,
host: 'localhost',
port: 3100,
obfuscate: true
obfuscate: true,
},
heartbeat: {
enable: false,
remoteURL: 'http://example.com',
frequency: 'every 1 hour'
frequency: 'every 1 hour',
},
dockerHealthCheck: {
enable: false,
port: 12398
port: 12398,
},
uptimeMonitor: {
enable: true,
@@ -387,7 +387,7 @@ describe('Conditional Configuration Validation', () => {
logLevel: 'verbose',
storeInInfluxdb: {
butlerSOSMemoryUsage: true,
instanceTag: 'DEV'
instanceTag: 'DEV',
},
storeNewRelic: {
enable: false,
@@ -395,45 +395,45 @@ describe('Conditional Configuration Validation', () => {
metric: {
dynamic: {
butlerMemoryUsage: { enable: true },
butlerUptime: { enable: true }
}
butlerUptime: { enable: true },
},
},
attribute: {
static: [],
dynamic: {
butlerVersion: { enable: true }
}
}
}
butlerVersion: { enable: true },
},
},
},
},
thirdPartyToolsCredentials: {
newRelic: []
newRelic: [],
},
qlikSenseEvents: {
influxdb: {
enable: false,
writeFrequency: 20000
writeFrequency: 20000,
},
eventCount: {
enable: false,
influxdb: {
measurementName: 'event_count',
tags: []
}
tags: [],
},
},
rejectedEventCount: {
enable: false,
influxdb: {
measurementName: 'rejected_event_count'
}
}
measurementName: 'rejected_event_count',
},
},
},
userEvents: {
enable: false,
excludeUser: [],
udpServerConfig: {
serverHost: 'localhost',
portUserActivityEvents: 9997
portUserActivityEvents: 9997,
},
tags: [],
sendToMQTT: {
@@ -443,20 +443,20 @@ describe('Conditional Configuration Validation', () => {
sessionStartTopic: { enable: true, topic: 'test' },
sessionStopTopic: { enable: true, topic: 'test' },
connectionOpenTopic: { enable: true, topic: 'test' },
connectionCloseTopic: { enable: true, topic: 'test' }
}
connectionCloseTopic: { enable: true, topic: 'test' },
},
},
sendToInfluxdb: { enable: true },
sendToNewRelic: {
enable: false,
destinationAccount: [],
scramble: true
}
scramble: true,
},
},
logEvents: {
udpServerConfig: {
serverHost: 'localhost',
portLogEvents: 9996
portLogEvents: 9996,
},
tags: [],
source: {
@@ -464,23 +464,23 @@ describe('Conditional Configuration Validation', () => {
proxy: { enable: false },
repository: { enable: false },
scheduler: { enable: false },
qixPerf: { enable: true }
qixPerf: { enable: true },
},
categorise: {
enable: false,
ruleDefault: { enable: false },
rules: []
rules: [],
},
appNameLookup: {
enable: true
enable: true,
},
sendToMQTT: {
enable: false,
baseTopic: 'qliksense/logevent',
postTo: {
baseTopic: true,
subsystemTopics: true
}
subsystemTopics: true,
},
},
sendToInfluxdb: { enable: false },
sendToNewRelic: {
@@ -489,34 +489,34 @@ describe('Conditional Configuration Validation', () => {
source: {
engine: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
proxy: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
repository: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
scheduler: {
enable: true,
logLevel: { error: true, warn: true }
}
}
}
logLevel: { error: true, warn: true },
},
},
},
},
cert: {
clientCert: '/path/to/cert',
clientCertKey: '/path/to/key',
clientCertCA: '/path/to/ca',
clientCertPassphrase: ''
clientCertPassphrase: '',
},
mqttConfig: {
enable: false,
brokerHost: 'INVALID_HOST_FORMAT', // This should be ignored when disabled
brokerPort: 'INVALID_PORT', // This should be ignored when disabled
baseTopic: '' // This should be ignored when disabled
brokerHost: 'INVALID_HOST_FORMAT', // This should be ignored when disabled
brokerPort: 'INVALID_PORT', // This should be ignored when disabled
baseTopic: '', // This should be ignored when disabled
},
newRelic: {
enable: false,
@@ -526,9 +526,9 @@ describe('Conditional Configuration Validation', () => {
attribute: {
static: [],
dynamic: {
butlerSosVersion: { enable: true }
}
}
butlerSosVersion: { enable: true },
},
},
},
metric: {
destinationAccount: [],
@@ -542,33 +542,33 @@ describe('Conditional Configuration Validation', () => {
selections: { enable: true },
sessions: { enable: true },
users: { enable: true },
saturated: { enable: true }
saturated: { enable: true },
},
apps: {
docCount: { enable: true },
activeDocs: { enable: true },
loadedDocs: { enable: true },
inMemoryDocs: { enable: true }
inMemoryDocs: { enable: true },
},
cache: {
cache: { enable: true }
cache: { enable: true },
},
proxy: {
sessions: { enable: true }
}
sessions: { enable: true },
},
},
attribute: {
static: [],
dynamic: {
butlerSosVersion: { enable: true }
}
}
}
butlerSosVersion: { enable: true },
},
},
},
},
prometheus: {
enable: false,
host: 'localhost',
port: 9842
port: 9842,
},
influxdbConfig: {
enable: false,
@@ -580,42 +580,42 @@ describe('Conditional Configuration Validation', () => {
bucket: 'bucket',
description: 'description',
token: 'token',
retentionDuration: '30d'
retentionDuration: '30d',
},
v1Config: {
auth: {
enable: false,
username: 'user',
password: 'pass'
password: 'pass',
},
dbName: 'butler-sos',
retentionPolicy: {
name: 'autogen',
duration: '30d'
}
duration: '30d',
},
},
includeFields: {
activeDocs: true,
loadedDocs: true,
inMemoryDocs: true
}
inMemoryDocs: true,
},
},
appNames: {
enableAppNameLookup: true,
lookupFrequency: 30000
lookupFrequency: 30000,
},
userSessions: {
enableSessionExtract: true,
pollingInterval: 30000,
excludeUser: []
excludeUser: [],
},
serversToMonitor: {
pollingInterval: 30000,
rejectUnauthorized: true,
serverTagsDefinition: [],
servers: []
}
}
servers: [],
},
},
};
const configPath = await createTempConfig(minimalConfig);
@@ -629,9 +629,9 @@ describe('Conditional Configuration Validation', () => {
const config = JSON.parse(JSON.stringify(baseValidConfig));
config['Butler-SOS'].mqttConfig = {
enable: true,
brokerHost: 'INVALID_HOST_FORMAT', // This should cause validation to fail
brokerPort: 'INVALID_PORT', // This should cause validation to fail
baseTopic: '' // This should cause validation to fail
brokerHost: 'INVALID_HOST_FORMAT', // This should cause validation to fail
brokerPort: 'INVALID_PORT', // This should cause validation to fail
baseTopic: '', // This should cause validation to fail
};
const configPath = await createTempConfig(config);
@@ -646,10 +646,10 @@ describe('Conditional Configuration Validation', () => {
config['Butler-SOS'].newRelic = {
enable: false,
event: {
url: 'INVALID_URL', // This should be ignored when disabled
header: 'INVALID_HEADER', // This should be ignored when disabled
attribute: 'INVALID_ATTRIBUTE' // This should be ignored when disabled
}
url: 'INVALID_URL', // This should be ignored when disabled
header: 'INVALID_HEADER', // This should be ignored when disabled
attribute: 'INVALID_ATTRIBUTE', // This should be ignored when disabled
},
// Missing required 'metric' property - should be ignored when disabled
};
@@ -673,4 +673,4 @@ describe('Conditional Configuration Validation', () => {
const result = await verifyConfigFileSchema(configPath);
expect(result).toBe(true);
});
});
});

View File

@@ -32,6 +32,9 @@ describe('config-file-schema', () => {
fileLogging: true,
logDirectory: './log',
anonTelemetry: false,
systemInfo: {
enable: true,
},
configVisualisation: {
enable: false,
host: 'localhost',

View File

@@ -48,28 +48,28 @@ describe('Issue #1036: Conditional Config Validation', () => {
fileLogging: true,
logDirectory: 'log',
anonTelemetry: true,
// Features with enable flags - all disabled with placeholder/invalid values
configVisualisation: {
enable: false,
host: 'localhost',
port: 3100,
obfuscate: true
obfuscate: true,
},
heartbeat: {
enable: false,
remoteURL: 'http://my.monitoring.server/some/path/', // Placeholder from template
frequency: 'every 1 hour'
frequency: 'every 1 hour',
},
dockerHealthCheck: {
enable: false,
port: 12398
port: 12398,
},
mqttConfig: {
enable: false,
brokerHost: '<IP of MQTT broker/server>', // Placeholder - invalid hostname
brokerHost: '<IP of MQTT broker/server>', // Placeholder - invalid hostname
brokerPort: 1883,
baseTopic: 'butler-sos/'
baseTopic: 'butler-sos/',
},
newRelic: {
enable: false,
@@ -79,9 +79,9 @@ describe('Issue #1036: Conditional Config Validation', () => {
attribute: {
static: [],
dynamic: {
butlerSosVersion: { enable: true }
}
}
butlerSosVersion: { enable: true },
},
},
},
metric: {
destinationAccount: [],
@@ -95,33 +95,33 @@ describe('Issue #1036: Conditional Config Validation', () => {
selections: { enable: true },
sessions: { enable: true },
users: { enable: true },
saturated: { enable: true }
saturated: { enable: true },
},
apps: {
docCount: { enable: true },
activeDocs: { enable: true },
loadedDocs: { enable: true },
inMemoryDocs: { enable: true }
inMemoryDocs: { enable: true },
},
cache: {
cache: { enable: true }
cache: { enable: true },
},
proxy: {
sessions: { enable: true }
}
sessions: { enable: true },
},
},
attribute: {
static: [],
dynamic: {
butlerSosVersion: { enable: true }
}
}
}
butlerSosVersion: { enable: true },
},
},
},
},
prometheus: {
enable: false,
host: 'localhost',
port: 9842
port: 9842,
},
influxdbConfig: {
enable: false,
@@ -133,27 +133,27 @@ describe('Issue #1036: Conditional Config Validation', () => {
bucket: 'bucket',
description: 'description',
token: 'token',
retentionDuration: '30d'
retentionDuration: '30d',
},
v1Config: {
auth: {
enable: false,
username: 'user',
password: 'pass'
password: 'pass',
},
dbName: 'butler-sos',
retentionPolicy: {
name: 'autogen',
duration: '30d'
}
duration: '30d',
},
},
includeFields: {
activeDocs: true,
loadedDocs: true,
inMemoryDocs: true
}
inMemoryDocs: true,
},
},
// Required sections with minimal valid config
uptimeMonitor: {
enable: true,
@@ -161,7 +161,7 @@ describe('Issue #1036: Conditional Config Validation', () => {
logLevel: 'verbose',
storeInInfluxdb: {
butlerSOSMemoryUsage: true,
instanceTag: 'DEV'
instanceTag: 'DEV',
},
storeNewRelic: {
enable: false,
@@ -169,45 +169,45 @@ describe('Issue #1036: Conditional Config Validation', () => {
metric: {
dynamic: {
butlerMemoryUsage: { enable: true },
butlerUptime: { enable: true }
}
butlerUptime: { enable: true },
},
},
attribute: {
static: [],
dynamic: {
butlerVersion: { enable: true }
}
}
}
butlerVersion: { enable: true },
},
},
},
},
thirdPartyToolsCredentials: {
newRelic: []
newRelic: [],
},
qlikSenseEvents: {
influxdb: {
enable: false,
writeFrequency: 20000
writeFrequency: 20000,
},
eventCount: {
enable: false,
influxdb: {
measurementName: 'event_count',
tags: []
}
tags: [],
},
},
rejectedEventCount: {
enable: false,
influxdb: {
measurementName: 'rejected_event_count'
}
}
measurementName: 'rejected_event_count',
},
},
},
userEvents: {
enable: false,
excludeUser: [],
udpServerConfig: {
serverHost: 'localhost',
portUserActivityEvents: 9997
portUserActivityEvents: 9997,
},
tags: [],
sendToMQTT: {
@@ -217,20 +217,20 @@ describe('Issue #1036: Conditional Config Validation', () => {
sessionStartTopic: { enable: true, topic: 'test' },
sessionStopTopic: { enable: true, topic: 'test' },
connectionOpenTopic: { enable: true, topic: 'test' },
connectionCloseTopic: { enable: true, topic: 'test' }
}
connectionCloseTopic: { enable: true, topic: 'test' },
},
},
sendToInfluxdb: { enable: true },
sendToNewRelic: {
enable: false,
destinationAccount: [],
scramble: true
}
scramble: true,
},
},
logEvents: {
udpServerConfig: {
serverHost: 'localhost',
portLogEvents: 9996
portLogEvents: 9996,
},
tags: [],
source: {
@@ -238,26 +238,26 @@ describe('Issue #1036: Conditional Config Validation', () => {
proxy: { enable: false },
repository: { enable: false },
scheduler: { enable: false },
qixPerf: { enable: true }
qixPerf: { enable: true },
},
categorise: {
enable: false,
ruleDefault: {
ruleDefault: {
enable: false,
category: []
category: [],
},
rules: []
rules: [],
},
appNameLookup: {
enable: true
enable: true,
},
sendToMQTT: {
enable: false,
baseTopic: 'qliksense/logevent',
postTo: {
baseTopic: true,
subsystemTopics: true
}
subsystemTopics: true,
},
},
sendToInfluxdb: { enable: false },
sendToNewRelic: {
@@ -266,53 +266,53 @@ describe('Issue #1036: Conditional Config Validation', () => {
source: {
engine: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
proxy: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
repository: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
scheduler: {
enable: true,
logLevel: { error: true, warn: true }
}
}
}
logLevel: { error: true, warn: true },
},
},
},
},
cert: {
clientCert: '/path/to/cert',
clientCertKey: '/path/to/key',
clientCertCA: '/path/to/ca',
clientCertPassphrase: ''
clientCertPassphrase: '',
},
appNames: {
enableAppNameExtract: true,
extractInterval: 30000,
hostIP: 'localhost',
enableAppNameLookup: true,
lookupFrequency: 30000
lookupFrequency: 30000,
},
userSessions: {
enableSessionExtract: true,
pollingInterval: 30000,
excludeUser: []
excludeUser: [],
},
serversToMonitor: {
pollingInterval: 30000,
rejectUnauthorized: true,
serverTagsDefinition: [],
servers: []
}
}
servers: [],
},
},
};
const configPath = await createTempConfig(config);
const result = await verifyConfigFileSchema(configPath);
// Before the fix, this would fail because MQTT validation would reject '<IP of MQTT broker/server>'
// After the fix, this should pass because MQTT is disabled
expect(result).toBe(true);
@@ -329,22 +329,22 @@ describe('Issue #1036: Conditional Config Validation', () => {
enable: false,
host: 'localhost',
port: 3100,
obfuscate: true
obfuscate: true,
},
heartbeat: {
enable: false,
remoteURL: 'http://example.com',
frequency: 'every 1 hour'
frequency: 'every 1 hour',
},
dockerHealthCheck: {
enable: false,
port: 12398
port: 12398,
},
mqttConfig: {
enable: true, // Enabled with invalid config
brokerHost: '<INVALID>', // Invalid hostname format
enable: true, // Enabled with invalid config
brokerHost: '<INVALID>', // Invalid hostname format
brokerPort: 1883,
baseTopic: 'butler-sos/'
baseTopic: 'butler-sos/',
},
newRelic: {
enable: false,
@@ -354,9 +354,9 @@ describe('Issue #1036: Conditional Config Validation', () => {
attribute: {
static: [],
dynamic: {
butlerSosVersion: { enable: true }
}
}
butlerSosVersion: { enable: true },
},
},
},
metric: {
destinationAccount: [],
@@ -370,33 +370,33 @@ describe('Issue #1036: Conditional Config Validation', () => {
selections: { enable: true },
sessions: { enable: true },
users: { enable: true },
saturated: { enable: true }
saturated: { enable: true },
},
apps: {
docCount: { enable: true },
activeDocs: { enable: true },
loadedDocs: { enable: true },
inMemoryDocs: { enable: true }
inMemoryDocs: { enable: true },
},
cache: {
cache: { enable: true }
cache: { enable: true },
},
proxy: {
sessions: { enable: true }
}
sessions: { enable: true },
},
},
attribute: {
static: [],
dynamic: {
butlerSosVersion: { enable: true }
}
}
}
butlerSosVersion: { enable: true },
},
},
},
},
prometheus: {
enable: false,
host: 'localhost',
port: 9842
port: 9842,
},
influxdbConfig: {
enable: false,
@@ -408,25 +408,25 @@ describe('Issue #1036: Conditional Config Validation', () => {
bucket: 'bucket',
description: 'description',
token: 'token',
retentionDuration: '30d'
retentionDuration: '30d',
},
v1Config: {
auth: {
enable: false,
username: 'user',
password: 'pass'
password: 'pass',
},
dbName: 'butler-sos',
retentionPolicy: {
name: 'autogen',
duration: '30d'
}
duration: '30d',
},
},
includeFields: {
activeDocs: true,
loadedDocs: true,
inMemoryDocs: true
}
inMemoryDocs: true,
},
},
uptimeMonitor: {
enable: true,
@@ -434,7 +434,7 @@ describe('Issue #1036: Conditional Config Validation', () => {
logLevel: 'verbose',
storeInInfluxdb: {
butlerSOSMemoryUsage: true,
instanceTag: 'DEV'
instanceTag: 'DEV',
},
storeNewRelic: {
enable: false,
@@ -442,45 +442,45 @@ describe('Issue #1036: Conditional Config Validation', () => {
metric: {
dynamic: {
butlerMemoryUsage: { enable: true },
butlerUptime: { enable: true }
}
butlerUptime: { enable: true },
},
},
attribute: {
static: [],
dynamic: {
butlerVersion: { enable: true }
}
}
}
butlerVersion: { enable: true },
},
},
},
},
thirdPartyToolsCredentials: {
newRelic: []
newRelic: [],
},
qlikSenseEvents: {
influxdb: {
enable: false,
writeFrequency: 20000
writeFrequency: 20000,
},
eventCount: {
enable: false,
influxdb: {
measurementName: 'event_count',
tags: []
}
tags: [],
},
},
rejectedEventCount: {
enable: false,
influxdb: {
measurementName: 'rejected_event_count'
}
}
measurementName: 'rejected_event_count',
},
},
},
userEvents: {
enable: false,
excludeUser: [],
udpServerConfig: {
serverHost: 'localhost',
portUserActivityEvents: 9997
portUserActivityEvents: 9997,
},
tags: [],
sendToMQTT: {
@@ -490,20 +490,20 @@ describe('Issue #1036: Conditional Config Validation', () => {
sessionStartTopic: { enable: true, topic: 'test' },
sessionStopTopic: { enable: true, topic: 'test' },
connectionOpenTopic: { enable: true, topic: 'test' },
connectionCloseTopic: { enable: true, topic: 'test' }
}
connectionCloseTopic: { enable: true, topic: 'test' },
},
},
sendToInfluxdb: { enable: true },
sendToNewRelic: {
enable: false,
destinationAccount: [],
scramble: true
}
scramble: true,
},
},
logEvents: {
udpServerConfig: {
serverHost: 'localhost',
portLogEvents: 9996
portLogEvents: 9996,
},
tags: [],
source: {
@@ -511,26 +511,26 @@ describe('Issue #1036: Conditional Config Validation', () => {
proxy: { enable: false },
repository: { enable: false },
scheduler: { enable: false },
qixPerf: { enable: true }
qixPerf: { enable: true },
},
categorise: {
enable: false,
ruleDefault: {
ruleDefault: {
enable: false,
category: []
category: [],
},
rules: []
rules: [],
},
appNameLookup: {
enable: true
enable: true,
},
sendToMQTT: {
enable: false,
baseTopic: 'qliksense/logevent',
postTo: {
baseTopic: true,
subsystemTopics: true
}
subsystemTopics: true,
},
},
sendToInfluxdb: { enable: false },
sendToNewRelic: {
@@ -539,54 +539,54 @@ describe('Issue #1036: Conditional Config Validation', () => {
source: {
engine: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
proxy: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
repository: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
scheduler: {
enable: true,
logLevel: { error: true, warn: true }
}
}
}
logLevel: { error: true, warn: true },
},
},
},
},
cert: {
clientCert: '/path/to/cert',
clientCertKey: '/path/to/key',
clientCertCA: '/path/to/ca',
clientCertPassphrase: ''
clientCertPassphrase: '',
},
appNames: {
enableAppNameExtract: true,
extractInterval: 30000,
hostIP: 'localhost',
enableAppNameLookup: true,
lookupFrequency: 30000
lookupFrequency: 30000,
},
userSessions: {
enableSessionExtract: true,
pollingInterval: 30000,
excludeUser: []
excludeUser: [],
},
serversToMonitor: {
pollingInterval: 30000,
rejectUnauthorized: true,
serverTagsDefinition: [],
servers: []
}
}
servers: [],
},
},
};
const configPath = await createTempConfig(config);
const result = await verifyConfigFileSchema(configPath);
// This should fail because MQTT is enabled with invalid hostname
expect(result).toBe(false);
});
});
});

View File

@@ -46,16 +46,16 @@ describe('MQTT Conditional Validation', () => {
enable: false,
host: 'localhost',
port: 3100,
obfuscate: true
obfuscate: true,
},
heartbeat: {
enable: false,
remoteURL: 'http://example.com',
frequency: 'every 1 hour'
frequency: 'every 1 hour',
},
dockerHealthCheck: {
enable: false,
port: 12398
port: 12398,
},
uptimeMonitor: {
enable: true,
@@ -63,7 +63,7 @@ describe('MQTT Conditional Validation', () => {
logLevel: 'verbose',
storeInInfluxdb: {
butlerSOSMemoryUsage: true,
instanceTag: 'DEV'
instanceTag: 'DEV',
},
storeNewRelic: {
enable: false,
@@ -71,45 +71,45 @@ describe('MQTT Conditional Validation', () => {
metric: {
dynamic: {
butlerMemoryUsage: { enable: true },
butlerUptime: { enable: true }
}
butlerUptime: { enable: true },
},
},
attribute: {
static: [],
dynamic: {
butlerVersion: { enable: true }
}
}
}
butlerVersion: { enable: true },
},
},
},
},
thirdPartyToolsCredentials: {
newRelic: []
newRelic: [],
},
qlikSenseEvents: {
influxdb: {
enable: false,
writeFrequency: 20000
writeFrequency: 20000,
},
eventCount: {
enable: false,
influxdb: {
measurementName: 'event_count',
tags: []
}
tags: [],
},
},
rejectedEventCount: {
enable: false,
influxdb: {
measurementName: 'rejected_event_count'
}
}
measurementName: 'rejected_event_count',
},
},
},
userEvents: {
enable: false,
excludeUser: [],
udpServerConfig: {
serverHost: 'localhost',
portUserActivityEvents: 9997
portUserActivityEvents: 9997,
},
tags: [],
sendToMQTT: {
@@ -119,20 +119,20 @@ describe('MQTT Conditional Validation', () => {
sessionStartTopic: { enable: true, topic: 'test' },
sessionStopTopic: { enable: true, topic: 'test' },
connectionOpenTopic: { enable: true, topic: 'test' },
connectionCloseTopic: { enable: true, topic: 'test' }
}
connectionCloseTopic: { enable: true, topic: 'test' },
},
},
sendToInfluxdb: { enable: true },
sendToNewRelic: {
enable: false,
destinationAccount: [],
scramble: true
}
scramble: true,
},
},
logEvents: {
udpServerConfig: {
serverHost: 'localhost',
portLogEvents: 9996
portLogEvents: 9996,
},
tags: [],
source: {
@@ -140,26 +140,26 @@ describe('MQTT Conditional Validation', () => {
proxy: { enable: false },
repository: { enable: false },
scheduler: { enable: false },
qixPerf: { enable: true }
qixPerf: { enable: true },
},
categorise: {
enable: false,
ruleDefault: {
ruleDefault: {
enable: false,
category: []
category: [],
},
rules: []
rules: [],
},
appNameLookup: {
enable: true
enable: true,
},
sendToMQTT: {
enable: false,
baseTopic: 'qliksense/logevent',
postTo: {
baseTopic: true,
subsystemTopics: true
}
subsystemTopics: true,
},
},
sendToInfluxdb: { enable: false },
sendToNewRelic: {
@@ -168,34 +168,34 @@ describe('MQTT Conditional Validation', () => {
source: {
engine: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
proxy: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
repository: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
scheduler: {
enable: true,
logLevel: { error: true, warn: true }
}
}
}
logLevel: { error: true, warn: true },
},
},
},
},
cert: {
clientCert: '/path/to/cert',
clientCertKey: '/path/to/key',
clientCertCA: '/path/to/ca',
clientCertPassphrase: ''
clientCertPassphrase: '',
},
mqttConfig: {
enable: false,
brokerHost: 'INVALID_HOST_FORMAT', // This should be allowed when disabled
brokerPort: 'INVALID_PORT', // This should be allowed when disabled
baseTopic: '' // This should be allowed when disabled
brokerHost: 'INVALID_HOST_FORMAT', // This should be allowed when disabled
brokerPort: 'INVALID_PORT', // This should be allowed when disabled
baseTopic: '', // This should be allowed when disabled
},
newRelic: {
enable: false,
@@ -205,9 +205,9 @@ describe('MQTT Conditional Validation', () => {
attribute: {
static: [],
dynamic: {
butlerSosVersion: { enable: true }
}
}
butlerSosVersion: { enable: true },
},
},
},
metric: {
destinationAccount: [],
@@ -221,33 +221,33 @@ describe('MQTT Conditional Validation', () => {
selections: { enable: true },
sessions: { enable: true },
users: { enable: true },
saturated: { enable: true }
saturated: { enable: true },
},
apps: {
docCount: { enable: true },
activeDocs: { enable: true },
loadedDocs: { enable: true },
inMemoryDocs: { enable: true }
inMemoryDocs: { enable: true },
},
cache: {
cache: { enable: true }
cache: { enable: true },
},
proxy: {
sessions: { enable: true }
}
sessions: { enable: true },
},
},
attribute: {
static: [],
dynamic: {
butlerSosVersion: { enable: true }
}
}
}
butlerSosVersion: { enable: true },
},
},
},
},
prometheus: {
enable: false,
host: 'localhost',
port: 9842
port: 9842,
},
influxdbConfig: {
enable: false,
@@ -259,50 +259,50 @@ describe('MQTT Conditional Validation', () => {
bucket: 'bucket',
description: 'description',
token: 'token',
retentionDuration: '30d'
retentionDuration: '30d',
},
v1Config: {
auth: {
enable: false,
username: 'user',
password: 'pass'
password: 'pass',
},
dbName: 'butler-sos',
retentionPolicy: {
name: 'autogen',
duration: '30d'
}
duration: '30d',
},
},
includeFields: {
activeDocs: true,
loadedDocs: true,
inMemoryDocs: true
}
inMemoryDocs: true,
},
},
appNames: {
enableAppNameExtract: true,
extractInterval: 30000,
hostIP: 'localhost',
enableAppNameLookup: true,
lookupFrequency: 30000
lookupFrequency: 30000,
},
userSessions: {
enableSessionExtract: true,
pollingInterval: 30000,
excludeUser: []
excludeUser: [],
},
serversToMonitor: {
pollingInterval: 30000,
rejectUnauthorized: true,
serverTagsDefinition: [],
servers: []
}
}
servers: [],
},
},
};
const configPath = await createTempConfig(config);
const result = await verifyConfigFileSchema(configPath);
// This should pass because MQTT is disabled, so invalid values should be ignored
expect(result).toBe(true);
});
@@ -318,16 +318,16 @@ describe('MQTT Conditional Validation', () => {
enable: false,
host: 'localhost',
port: 3100,
obfuscate: true
obfuscate: true,
},
heartbeat: {
enable: false,
remoteURL: 'http://example.com',
frequency: 'every 1 hour'
frequency: 'every 1 hour',
},
dockerHealthCheck: {
enable: false,
port: 12398
port: 12398,
},
uptimeMonitor: {
enable: true,
@@ -335,7 +335,7 @@ describe('MQTT Conditional Validation', () => {
logLevel: 'verbose',
storeInInfluxdb: {
butlerSOSMemoryUsage: true,
instanceTag: 'DEV'
instanceTag: 'DEV',
},
storeNewRelic: {
enable: false,
@@ -343,45 +343,45 @@ describe('MQTT Conditional Validation', () => {
metric: {
dynamic: {
butlerMemoryUsage: { enable: true },
butlerUptime: { enable: true }
}
butlerUptime: { enable: true },
},
},
attribute: {
static: [],
dynamic: {
butlerVersion: { enable: true }
}
}
}
butlerVersion: { enable: true },
},
},
},
},
thirdPartyToolsCredentials: {
newRelic: []
newRelic: [],
},
qlikSenseEvents: {
influxdb: {
enable: false,
writeFrequency: 20000
writeFrequency: 20000,
},
eventCount: {
enable: false,
influxdb: {
measurementName: 'event_count',
tags: []
}
tags: [],
},
},
rejectedEventCount: {
enable: false,
influxdb: {
measurementName: 'rejected_event_count'
}
}
measurementName: 'rejected_event_count',
},
},
},
userEvents: {
enable: false,
excludeUser: [],
udpServerConfig: {
serverHost: 'localhost',
portUserActivityEvents: 9997
portUserActivityEvents: 9997,
},
tags: [],
sendToMQTT: {
@@ -391,20 +391,20 @@ describe('MQTT Conditional Validation', () => {
sessionStartTopic: { enable: true, topic: 'test' },
sessionStopTopic: { enable: true, topic: 'test' },
connectionOpenTopic: { enable: true, topic: 'test' },
connectionCloseTopic: { enable: true, topic: 'test' }
}
connectionCloseTopic: { enable: true, topic: 'test' },
},
},
sendToInfluxdb: { enable: true },
sendToNewRelic: {
enable: false,
destinationAccount: [],
scramble: true
}
scramble: true,
},
},
logEvents: {
udpServerConfig: {
serverHost: 'localhost',
portLogEvents: 9996
portLogEvents: 9996,
},
tags: [],
source: {
@@ -412,26 +412,26 @@ describe('MQTT Conditional Validation', () => {
proxy: { enable: false },
repository: { enable: false },
scheduler: { enable: false },
qixPerf: { enable: true }
qixPerf: { enable: true },
},
categorise: {
enable: false,
ruleDefault: {
ruleDefault: {
enable: false,
category: []
category: [],
},
rules: []
rules: [],
},
appNameLookup: {
enable: true
enable: true,
},
sendToMQTT: {
enable: false,
baseTopic: 'qliksense/logevent',
postTo: {
baseTopic: true,
subsystemTopics: true
}
subsystemTopics: true,
},
},
sendToInfluxdb: { enable: false },
sendToNewRelic: {
@@ -440,34 +440,34 @@ describe('MQTT Conditional Validation', () => {
source: {
engine: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
proxy: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
repository: {
enable: true,
logLevel: { error: true, warn: true }
logLevel: { error: true, warn: true },
},
scheduler: {
enable: true,
logLevel: { error: true, warn: true }
}
}
}
logLevel: { error: true, warn: true },
},
},
},
},
cert: {
clientCert: '/path/to/cert',
clientCertKey: '/path/to/key',
clientCertCA: '/path/to/ca',
clientCertPassphrase: ''
clientCertPassphrase: '',
},
mqttConfig: {
enable: true, // MQTT is enabled
brokerHost: 'INVALID_HOST_FORMAT', // This should cause validation to fail
brokerPort: 'INVALID_PORT', // This should cause validation to fail
baseTopic: '' // This should cause validation to fail
enable: true, // MQTT is enabled
brokerHost: 'INVALID_HOST_FORMAT', // This should cause validation to fail
brokerPort: 'INVALID_PORT', // This should cause validation to fail
baseTopic: '', // This should cause validation to fail
},
newRelic: {
enable: false,
@@ -477,9 +477,9 @@ describe('MQTT Conditional Validation', () => {
attribute: {
static: [],
dynamic: {
butlerSosVersion: { enable: true }
}
}
butlerSosVersion: { enable: true },
},
},
},
metric: {
destinationAccount: [],
@@ -493,33 +493,33 @@ describe('MQTT Conditional Validation', () => {
selections: { enable: true },
sessions: { enable: true },
users: { enable: true },
saturated: { enable: true }
saturated: { enable: true },
},
apps: {
docCount: { enable: true },
activeDocs: { enable: true },
loadedDocs: { enable: true },
inMemoryDocs: { enable: true }
inMemoryDocs: { enable: true },
},
cache: {
cache: { enable: true }
cache: { enable: true },
},
proxy: {
sessions: { enable: true }
}
sessions: { enable: true },
},
},
attribute: {
static: [],
dynamic: {
butlerSosVersion: { enable: true }
}
}
}
butlerSosVersion: { enable: true },
},
},
},
},
prometheus: {
enable: false,
host: 'localhost',
port: 9842
port: 9842,
},
influxdbConfig: {
enable: false,
@@ -531,51 +531,51 @@ describe('MQTT Conditional Validation', () => {
bucket: 'bucket',
description: 'description',
token: 'token',
retentionDuration: '30d'
retentionDuration: '30d',
},
v1Config: {
auth: {
enable: false,
username: 'user',
password: 'pass'
password: 'pass',
},
dbName: 'butler-sos',
retentionPolicy: {
name: 'autogen',
duration: '30d'
}
duration: '30d',
},
},
includeFields: {
activeDocs: true,
loadedDocs: true,
inMemoryDocs: true
}
inMemoryDocs: true,
},
},
appNames: {
enableAppNameExtract: true,
extractInterval: 30000,
hostIP: 'localhost',
enableAppNameLookup: true,
lookupFrequency: 30000
lookupFrequency: 30000,
},
userSessions: {
enableSessionExtract: true,
pollingInterval: 30000,
excludeUser: []
excludeUser: [],
},
serversToMonitor: {
pollingInterval: 30000,
rejectUnauthorized: true,
serverTagsDefinition: [],
servers: []
}
}
servers: [],
},
},
};
const configPath = await createTempConfig(config);
const result = await verifyConfigFileSchema(configPath);
// This should fail because MQTT is enabled with invalid configuration
expect(result).toBe(false);
});
});
});

View File

@@ -44,9 +44,9 @@ describe('Realistic Config Conditional Validation', () => {
// Modify the MQTT config to have disabled feature with invalid placeholders
templateConfig['Butler-SOS'].mqttConfig = {
enable: false,
brokerHost: '<IP of MQTT broker/server>', // Placeholder from template - should be allowed when disabled
brokerHost: '<IP of MQTT broker/server>', // Placeholder from template - should be allowed when disabled
brokerPort: 1883,
baseTopic: 'butler-sos/'
baseTopic: 'butler-sos/',
};
// Also disable other features that might have validation issues in the template
@@ -58,7 +58,7 @@ describe('Realistic Config Conditional Validation', () => {
templateConfig['Butler-SOS'].dockerHealthCheck.enable = false;
const configPath = await createTempConfig(templateConfig);
// This should pass validation because features are disabled
const result = await verifyConfigFileSchema(configPath);
expect(result).toBe(true);
@@ -72,16 +72,16 @@ describe('Realistic Config Conditional Validation', () => {
// Enable MQTT but leave placeholder values that should be invalid
templateConfig['Butler-SOS'].mqttConfig = {
enable: true, // Enable MQTT
brokerHost: '<IP of MQTT broker/server>', // Invalid placeholder - should cause validation to fail
enable: true, // Enable MQTT
brokerHost: '<IP of MQTT broker/server>', // Invalid placeholder - should cause validation to fail
brokerPort: 1883,
baseTopic: 'butler-sos/'
baseTopic: 'butler-sos/',
};
const configPath = await createTempConfig(templateConfig);
// This should fail validation because MQTT is enabled with invalid config
const result = await verifyConfigFileSchema(configPath);
expect(result).toBe(false);
});
});
});

View File

@@ -0,0 +1,117 @@
import { jest } from '@jest/globals';
import { basicSettingsSchema } from '../config-schemas/basic-settings.js';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import addKeywords from 'ajv-keywords';
describe('System Information Configuration Schema', () => {
let ajv;
beforeAll(() => {
ajv = new Ajv({ strict: false });
addFormats(ajv);
addKeywords(ajv);
});
test('should validate configuration with systemInfo.enable set to false', () => {
// Test just the systemInfo part of the schema for simplicity
const systemInfoSchema = {
type: 'object',
properties: {
systemInfo: basicSettingsSchema.systemInfo,
},
required: ['systemInfo'],
};
const config = {
systemInfo: {
enable: false,
},
};
const validate = ajv.compile(systemInfoSchema);
const isValid = validate(config);
if (!isValid) {
console.log('Validation errors:', validate.errors);
}
expect(isValid).toBe(true);
});
test('should validate configuration with systemInfo.enable set to true', () => {
// Test just the systemInfo part of the schema for simplicity
const systemInfoSchema = {
type: 'object',
properties: {
systemInfo: basicSettingsSchema.systemInfo,
},
required: ['systemInfo'],
};
const config = {
systemInfo: {
enable: true,
},
};
const validate = ajv.compile(systemInfoSchema);
const isValid = validate(config);
expect(isValid).toBe(true);
});
test('should fail validation when systemInfo.enable is not a boolean', () => {
// Test just the systemInfo part of the schema for simplicity
const systemInfoSchema = {
type: 'object',
properties: {
systemInfo: basicSettingsSchema.systemInfo,
},
required: ['systemInfo'],
};
const config = {
systemInfo: {
enable: 'not-a-boolean',
},
};
const validate = ajv.compile(systemInfoSchema);
const isValid = validate(config);
expect(isValid).toBe(false);
expect(validate.errors).toContainEqual(
expect.objectContaining({
instancePath: '/systemInfo/enable',
keyword: 'type',
})
);
});
test('should fail validation when systemInfo is missing enable property', () => {
// Test just the systemInfo part of the schema for simplicity
const systemInfoSchema = {
type: 'object',
properties: {
systemInfo: basicSettingsSchema.systemInfo,
},
required: ['systemInfo'],
};
const config = {
systemInfo: {},
};
const validate = ajv.compile(systemInfoSchema);
const isValid = validate(config);
expect(isValid).toBe(false);
expect(validate.errors).toContainEqual(
expect.objectContaining({
instancePath: '/systemInfo',
keyword: 'required',
})
);
});
});

View File

@@ -6,7 +6,7 @@ import configFileSchema from './config-file-schema.js';
/**
* Creates a modified schema that only validates sections when their associated features are enabled.
*
*
* @param {object} parsedConfig - The parsed configuration object
* @param {object} baseSchema - The base schema to modify
* @returns {object} Modified schema with conditional validation
@@ -14,45 +14,45 @@ import configFileSchema from './config-file-schema.js';
function createConditionalSchema(parsedConfig, baseSchema) {
// Deep clone the base schema to avoid modifying the original
const schema = JSON.parse(JSON.stringify(baseSchema));
// Get the Butler-SOS configuration section
const butlerConfig = parsedConfig['Butler-SOS'];
if (!butlerConfig) {
return schema; // Return original schema if no Butler-SOS section
}
const butlerSchema = schema.properties['Butler-SOS'];
// Helper function to create conditional validation for a feature
const makeFeatureConditional = (featureName) => {
const featureSchema = butlerSchema.properties[featureName];
if (!featureSchema) return;
// Store the original schema
const originalSchema = JSON.parse(JSON.stringify(featureSchema));
// Create conditional schema using if/then/else
butlerSchema.properties[featureName] = {
type: 'object',
properties: {
enable: { type: 'boolean' }
enable: { type: 'boolean' },
},
required: ['enable'],
if: {
properties: { enable: { const: true } }
properties: { enable: { const: true } },
},
then: originalSchema,
else: {
type: 'object',
properties: {
enable: { type: 'boolean' }
enable: { type: 'boolean' },
},
required: ['enable'],
additionalProperties: true // Allow any additional properties when disabled
}
additionalProperties: true, // Allow any additional properties when disabled
},
};
};
// Apply conditional validation to features with enable flags
makeFeatureConditional('mqttConfig');
makeFeatureConditional('newRelic');
@@ -62,7 +62,7 @@ function createConditionalSchema(parsedConfig, baseSchema) {
makeFeatureConditional('configVisualisation');
makeFeatureConditional('heartbeat');
makeFeatureConditional('dockerHealthCheck');
return schema;
}
@@ -107,10 +107,12 @@ export async function verifyConfigFileSchema(configFile) {
// Create a conditional schema based on enabled features
const conditionalSchema = createConditionalSchema(parsedFileContent, configFileSchema);
// Log the schema modification for debugging (in development)
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_CONFIG_VALIDATION) {
console.debug('VERIFY CONFIG FILE: Created conditional schema based on enabled features');
console.debug(
'VERIFY CONFIG FILE: Created conditional schema based on enabled features'
);
}
// Validate the parsed YAML file against the conditional schema
@@ -173,6 +175,19 @@ export async function verifyAppConfig(cfg) {
}
}
// Verify that telemetry and system info settings are compatible
// If telemetry is enabled but system info gathering is disabled, this creates an incompatibility
// because telemetry relies on detailed system information for proper functionality
const anonTelemetryEnabled = cfg.get('Butler-SOS.anonTelemetry');
const systemInfoEnabled = cfg.get('Butler-SOS.systemInfo.enable');
if (anonTelemetryEnabled === true && systemInfoEnabled === false) {
console.error(
'VERIFY CONFIG FILE ERROR: Anonymous telemetry is enabled (Butler-SOS.anonTelemetry=true) but system information gathering is disabled (Butler-SOS.systemInfo.enable=false). Telemetry requires system information to function properly. Either disable telemetry by setting Butler-SOS.anonTelemetry=false or enable system info gathering by setting Butler-SOS.systemInfo.enable=true. Exiting.'
);
return false;
}
// Verify that server tags are correctly defined
// In the config file section `Butler-SOS.serversToMonitor.serverTagsDefinition` it's possible to define zero or more tags that can be set for each server that is to be monitored.
// When Butler SOS is started, do the following checks:

View File

@@ -22,6 +22,7 @@ describe('basic-settings schema', () => {
expect(basicSettingsSchema.fileLogging).toBeDefined();
expect(basicSettingsSchema.logDirectory).toBeDefined();
expect(basicSettingsSchema.anonTelemetry).toBeDefined();
expect(basicSettingsSchema.systemInfo).toBeDefined();
});
describe('logLevel property', () => {
@@ -148,11 +149,59 @@ describe('basic-settings schema', () => {
});
});
describe('systemInfo property', () => {
test('should accept valid systemInfo configuration', () => {
const schema = {
type: 'object',
properties: { systemInfo: basicSettingsSchema.systemInfo },
};
const validate = ajv.compile(schema);
expect(validate({ systemInfo: { enable: true } })).toBe(true);
expect(validate({ systemInfo: { enable: false } })).toBe(true);
});
test('should reject invalid systemInfo configuration', () => {
const schema = {
type: 'object',
properties: { systemInfo: basicSettingsSchema.systemInfo },
};
const validate = ajv.compile(schema);
// Missing enable property
expect(validate({ systemInfo: {} })).toBe(false);
// Invalid enable type
expect(validate({ systemInfo: { enable: 'true' } })).toBe(false);
expect(validate({ systemInfo: { enable: 1 } })).toBe(false);
expect(validate({ systemInfo: { enable: null } })).toBe(false);
// Additional properties not allowed
expect(validate({ systemInfo: { enable: true, extra: 'value' } })).toBe(false);
});
test('should require enable property', () => {
const schema = {
type: 'object',
properties: { systemInfo: basicSettingsSchema.systemInfo },
required: ['systemInfo'],
};
const validate = ajv.compile(schema);
expect(validate({ systemInfo: { enable: true } })).toBe(true);
expect(validate({ systemInfo: {} })).toBe(false);
expect(validate({})).toBe(false);
});
});
test('should validate complete basic settings object', () => {
const schema = {
type: 'object',
properties: basicSettingsSchema,
required: ['logLevel', 'fileLogging', 'logDirectory', 'anonTelemetry'],
required: ['logLevel', 'fileLogging', 'logDirectory', 'anonTelemetry', 'systemInfo'],
};
const validate = ajv.compile(schema);
@@ -162,6 +211,7 @@ describe('basic-settings schema', () => {
fileLogging: true,
logDirectory: './log',
anonTelemetry: false,
systemInfo: { enable: true },
};
expect(validate(validConfig)).toBe(true);

View File

@@ -4,6 +4,7 @@
* This schema covers the fundamental application configuration options:
* - Log level and file logging settings
* - Anonymous telemetry settings
* - System information gathering settings
*
* @type {object} JSON Schema object for basic settings validation
*/
@@ -16,4 +17,25 @@ export const basicSettingsSchema = {
fileLogging: { type: 'boolean' },
logDirectory: { type: 'string' },
anonTelemetry: { type: 'boolean' },
/**
* System information gathering configuration.
*
* When enabled (default), Butler SOS uses the systeminformation npm package
* to collect detailed system information. This package executes certain OS commands
* on Windows that may trigger security alerts in enterprise environments:
* - `cmd.exe /d /s /c \chcp` (code page information)
* - `netstat -r` (routing table)
* - `cmd.exe /d /s /c \echo %COMPUTERNAME%.%USERDNSDOMAIN%` (computer/domain names)
*
* Set to false in security-sensitive environments to disable detailed system
* information gathering and prevent these OS command executions.
*/
systemInfo: {
type: 'object',
properties: {
enable: { type: 'boolean' },
},
required: ['enable'],
additionalProperties: false,
},
};

View File

@@ -46,6 +46,7 @@ const configFileSchema = {
'fileLogging',
'logDirectory',
'anonTelemetry',
'systemInfo',
'configVisualisation',
'heartbeat',
'dockerHealthCheck',

View File

@@ -37,7 +37,7 @@ function isBinaryFile(fileExtension) {
*
* @param {string} filePath - The path to the file to prepare
* @param {string} [encoding] - Optional encoding for text files, defaults to 'utf8' for text files
* @returns {Promise<Object>} - An object containing the file extension, a stream of the file contents, and a found flag
* @returns {Promise<object>} - An object containing the file extension, a stream of the file contents, and a found flag
*/
export async function prepareFile(filePath, encoding) {
globals.logger.verbose(`FILE PREP: Preparing file ${filePath}`);