Skip to content

Commit

Permalink
feat: bounds checking for config options (#126)
Browse files Browse the repository at this point in the history
  • Loading branch information
cd-rite authored Jun 13, 2024
1 parent 1009021 commit 24efe12
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 57 deletions.
11 changes: 6 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#!/usr/bin/env node
import { logger, getSymbol } from './lib/logger.js'
import { options, configValid } from './lib/args.js'
import * as CONSTANTS from './lib/consts.js'
const minApiVersion = CONSTANTS.MIN_API_VERSION
const component = 'index'
if (!configValid) {
logger.error({ component, message: 'invalid configuration... Exiting'})
logger.end()
Expand All @@ -13,10 +16,8 @@ import { serializeError } from 'serialize-error'
import { initScanner } from './lib/scan.js'
import semverGte from 'semver/functions/gte.js'
import Alarm from './lib/alarm.js'
import * as CONSTANTS from './lib/consts.js'

const minApiVersion = '1.2.7'
const component = 'index'


process.on('SIGINT', () => {
logger.info({
Expand Down Expand Up @@ -126,7 +127,7 @@ async function preflightServices () {
await hasMinApiVersion()
await auth.getOpenIDConfiguration()
await auth.getToken()
logger.info({ component, message: `preflight token request suceeded`})
logger.info({ component, message: `preflight token request succeeded`})
const promises = [
api.getCollection(options.collectionId),
api.getInstalledStigs(),
Expand All @@ -145,7 +146,7 @@ async function preflightServices () {
logger.warn({ component, message: `preflight user request failed; token may be missing scope 'stig-manager:user:read'? Watcher will not set {"status": "accepted"}`})
Alarm.noGrant(false)
}
logger.info({ component, message: `prefilght api requests suceeded`})
logger.info({ component, message: `preflight api requests succeeded`})
}

function getObfuscatedConfig (options) {
Expand Down
122 changes: 71 additions & 51 deletions lib/args.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { config } from 'dotenv'
import { dirname, resolve, sep, posix } from 'node:path'
import promptSync from 'prompt-sync'
import { createPrivateKey } from 'crypto'
import * as CONSTANTS from './consts.js'

const prompt = promptSync({ sigint:true })
const component = 'args'
Expand Down Expand Up @@ -103,7 +104,7 @@ program
.option('--ignore-glob [glob...]', 'File or directory glob(s) to ignore. Can be invoked multiple times.(`WATCHER_IGNORE_GLOBS=<csv>`)', pe.WATCHER_IGNORE_GLOBS?.split(','))
.option('--event-polling', 'Use polling with `--mode events`, necessary for watching network files (`WATCHER_EVENT_POLLING=1`). Ignored if `--mode scan`, negate with `--no-event-polling`.', getBoolean('WATCHER_EVENT_POLLING', true))
.option('--no-event-polling', 'Don\'t use polling with `--mode events`, reduces CPU usage (`WATCHER_EVENT_POLLING=0`).')
.option('--stability-threshold <ms>', 'If `--mode events`, milliseconds to wait for file size to stabilize. May be helpful when watching network shares. (`WATCHER_STABILITY_THRESHOLD`). Igonred with `--mode scan`', parseIntegerArg, parseIntegerEnv(pe.WATCHER_STABILITY_THRESHOLD) ?? 0)
.option('--stability-threshold <ms>', 'If `--mode events`, milliseconds to wait for file size to stabilize. May be helpful when watching network shares. (`WATCHER_STABILITY_THRESHOLD`). Ignored with `--mode scan`', parseIntegerArg, parseIntegerEnv(pe.WATCHER_STABILITY_THRESHOLD) ?? 0)
.option('--one-shot', 'Process existing files in the path and exit. Sets `--add-existing`.', getBoolean('WATCHER_ONE_SHOT', false))
.option('--log-color', 'Colorize the console log output. Might confound downstream piped processes.', false)
.option('-d, --debug', 'Shortcut for `--log-level debug --log-file-level debug`', false)
Expand All @@ -118,30 +119,9 @@ program
// Options properties are created as camelCase versions of the long option name
program.parse(process.argv)
const options = program.opts()

// deprecate ignoreDir
if (options.ignoreDir) {
const ignoreDirGlobs = options.ignoreDir.map( (dir) => `**/${dir}`)
if (options.ignoreGlob) {
options.ignoreGlob.push(...ignoreDirGlobs)
}
else {
options.ignoreGlob = ignoreDirGlobs
}
}

// add semver info
options.version = version

// Set path variations
options._originalPath = options.path
options._resolvedPath = resolve(options.path)
options.path = options.path.split(sep).join(posix.sep)

// Set dependent options
if (options.oneShot) {
options.addExisting = true
}
if (options.debug) {
options.logLevel = 'debug'
options.logFileLevel = 'debug'
Expand All @@ -153,46 +133,87 @@ if (options.logFile) {
logger.addFileTransport( options.logFileLevel, options.logFile )
}

// Validate we can perform the requested client authentication
if (options.clientKey) {
try {
// Transform the path into a crypto private key object
options.clientKey = getPrivateKey ( options.clientKey, process.env.WATCHER_CLIENT_KEY_PASSPHRASE, options.prompt)
logger.logger.log({
level: 'debug',
component: component,
message: 'parsed options',
options: options
})

for (const key in CONSTANTS.configBounds) {
//skip bounds checks for scan and event options if not in that mode
if ((options.mode !== 'scan' && CONSTANTS.scanBoundsKeys.has(key))
|| (options.mode !== 'event' && CONSTANTS.eventBoundsKeys.has(key))) {
continue
}
catch (e) {
// Could not make a private key
const bounds = CONSTANTS.configBounds[key]
if (options[key] < bounds.min || options[key] > bounds.max) {
logger.logger.log({
level: 'error',
component: component,
message: 'private key error',
file: options.clientKey,
error: e
message: `config value out of bounds: ${key} = ${options[key]}, must be between ${bounds.min} and ${bounds.max}`
})
configValid = false
}
}
else {
// Test if we were provided, or can obtain, a client secret
options.clientSecret = process.env.WATCHER_CLIENT_SECRET
if (options.prompt && !options.clientSecret) {
options.clientSecret = prompt(`Provide the client secret for ${options.clientId}: `, { echo: '*' })

if (configValid) {
// deprecate ignoreDir
if (options.ignoreDir) {
const ignoreDirGlobs = options.ignoreDir.map( (dir) => `**/${dir}`)
if (options.ignoreGlob) {
options.ignoreGlob.push(...ignoreDirGlobs)
}
else {
options.ignoreGlob = ignoreDirGlobs
}
}
if (!options.clientSecret) {
// Don't know the client secret
logger.logger.error({
component: component,
message: 'Missing client secret'
})
configValid = false

// Set path variations
options._originalPath = options.path
options._resolvedPath = resolve(options.path)
options.path = options.path.split(sep).join(posix.sep)

// Set dependent options
if (options.oneShot) {
options.addExisting = true
}

// Validate we can perform the requested client authentication
if (options.clientKey) {
try {
// Transform the path into a crypto private key object
options.clientKey = getPrivateKey ( options.clientKey, process.env.WATCHER_CLIENT_KEY_PASSPHRASE, options.prompt)
}
catch (e) {
// Could not make a private key
logger.logger.log({
level: 'error',
component: component,
message: 'private key error',
file: options.clientKey,
error: e
})
configValid = false
}
}
else {
// Test if we were provided, or can obtain, a client secret
options.clientSecret = process.env.WATCHER_CLIENT_SECRET
if (options.prompt && !options.clientSecret) {
options.clientSecret = prompt(`Provide the client secret for ${options.clientId}: `, { echo: '*' })
}
if (!options.clientSecret) {
// Don't know the client secret
logger.logger.error({
component: component,
message: 'Missing client secret'
})
configValid = false
}
}
}

logger.logger.log({
level: 'debug',
component: component,
message: 'parsed options',
options: options
})

function getPrivateKey( pemFile, passphrase, canPrompt) {
let pemKey
Expand All @@ -218,6 +239,5 @@ function getPrivateKey( pemFile, passphrase, canPrompt) {
}
}


export { options, configValid }

36 changes: 35 additions & 1 deletion lib/consts.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,38 @@ export const ERR_AUTHOFFLINE = 2
export const ERR_NOTOKEN = 3
export const ERR_NOGRANT = 4
export const ERR_UNKNOWN = 5
export const ERR_FAILINIT = 6
export const ERR_FAILINIT = 6

export const MIN_API_VERSION = '1.2.7'

// Minimum and maximum values for Watcher configuration
export const configBounds = {
scanInterval: {
min: 60000, // 60 seconds - Should be greater than WATCHER_CARGO_DELAY
max: 24 * 60 * 60000 // 24 hours
},
cargoDelay: {
min: 2000, // 2 seconds
max: 30000 // 30 seconds
},
cargoSize: {
min: 1,
max: 100
},
historyWriteInterval: {
min: 10000, // 10 seconds
max: 60000 // 60 seconds
},
responseTimeout: {
min: 5000, // 5 seconds
max: 60000 // 60 seconds
},
stabilityThreshold: {
min: 0,
max: 10000 // 10 seconds
}
}

export const scanBoundsKeys = new Set(["scanInterval", "historyWriteInterval"])
export const eventBoundsKeys = new Set(["stabilityThreshold"])

8 changes: 8 additions & 0 deletions lib/help.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Help } from 'commander';
import {configBounds} from './consts.js'

export default function (style = 'cli') {
if (style === 'md') {
Expand Down Expand Up @@ -90,6 +91,13 @@ export default function (style = 'cli') {
// use stringify to match the display of the default value
`choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`);
}

if (configBounds[option.attributeName()]) {
extraInfo.push(
// if min/max values are enforced, show them
`Range: ${configBounds[option.attributeName()].min} to ${configBounds[option.attributeName()].max}`);
}

if (option.defaultValue !== undefined) {
// watcher: changed 'default' to 'currently'
extraInfo.push(`currently: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`);
Expand Down

0 comments on commit 24efe12

Please sign in to comment.