Skip to content

Commit

Permalink
Merge pull request #1501 from DataDog/etnbrd/SYNTH-16419/fips-option
Browse files Browse the repository at this point in the history
add a fips option to all existing commands
  • Loading branch information
etnbrd authored Dec 12, 2024
2 parents 12d6709 + 44d2588 commit 9e8c5cf
Show file tree
Hide file tree
Showing 41 changed files with 743 additions and 397 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ jobs:
- run: yarn install --immutable
- run: yarn build
- name: Create standalone binary
run: yarn dist-standalone -t node14-linux-x64 -o datadog-ci_linux-x64
run: yarn dist-standalone -t node18-linux-x64 -o datadog-ci_linux-x64
- name: Remove dist and src folder to check that binary can stand alone
run: |
rm -rf dist
Expand All @@ -165,7 +165,7 @@ jobs:
- run: yarn install --immutable
- run: yarn build
- name: Create standalone binary
run: yarn dist-standalone -t node14-linux-arm64 -o datadog-ci_linux-arm64
run: yarn dist-standalone -t node18-linux-arm64 -o datadog-ci_linux-arm64
- name: Remove dist and src folder to check that binary can stand alone
run: |
rm -rf dist
Expand All @@ -185,7 +185,7 @@ jobs:
- run: yarn install --immutable
- run: yarn build:win
- name: Create standalone binary
run: yarn dist-standalone -t node14-win-x64 -o datadog-ci_win-x64
run: yarn dist-standalone -t node18-win-x64 -o datadog-ci_win-x64
- name: Remove dist and src folder to check that binary can stand alone
run: |
rm dist -r
Expand All @@ -207,7 +207,7 @@ jobs:
- run: yarn install --immutable
- run: yarn build
- name: Create standalone binary
run: yarn dist-standalone -t node14-macos-x64 -o datadog-ci_darwin-x64
run: yarn dist-standalone -t node18-macos-x64 -o datadog-ci_darwin-x64
- name: Remove dist and src folder to check that binary can stand alone
run: |
rm -rf dist
Expand All @@ -227,7 +227,7 @@ jobs:
- run: yarn install --immutable
- run: yarn build
- name: Create standalone binary
run: yarn dist-standalone -t node14-macos-arm64 -o datadog-ci_darwin-arm64
run: yarn dist-standalone -t node18-macos-arm64 -o datadog-ci_darwin-arm64
- name: Remove dist and src folder to check that binary can stand alone
run: |
rm -rf dist
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
- name: Bundle library
run: yarn build
- name: Create standalone binary
run: yarn dist-standalone -t node14-linux-x64 -o datadog-ci_linux-x64
run: yarn dist-standalone -t node18-linux-x64 -o datadog-ci_linux-x64
- name: Remove dist folder to check that binary can stand alone
run: |
rm -rf dist
Expand Down Expand Up @@ -96,7 +96,7 @@ jobs:
- name: Bundle library
run: yarn build
- name: Create standalone binary
run: yarn dist-standalone -t node14-linux-arm64 -o datadog-ci_linux-arm64
run: yarn dist-standalone -t node18-linux-arm64 -o datadog-ci_linux-arm64
- name: Remove dist folder to check that binary can stand alone
run: |
rm -rf dist
Expand Down Expand Up @@ -132,7 +132,7 @@ jobs:
- name: Bundle library
run: yarn build:win
- name: Create standalone binary
run: yarn dist-standalone -t node14-win-x64 -o datadog-ci_win-x64
run: yarn dist-standalone -t node18-win-x64 -o datadog-ci_win-x64
- name: Remove dist folder to check that binary can stand alone
run: |
rm dist -r
Expand Down Expand Up @@ -170,7 +170,7 @@ jobs:
- name: Bundle library
run: yarn build
- name: Create standalone binary
run: yarn dist-standalone -t node14-macos-x64 -o datadog-ci_darwin-x64
run: yarn dist-standalone -t node18-macos-x64 -o datadog-ci_darwin-x64
- name: Remove dist folder to check that binary can stand alone
run: |
rm -rf dist
Expand Down Expand Up @@ -206,7 +206,7 @@ jobs:
- name: Bundle library
run: yarn build
- name: Create standalone binary
run: yarn dist-standalone -t node14-macos-arm64 -o datadog-ci_darwin-arm64
run: yarn dist-standalone -t node18-macos-arm64 -o datadog-ci_darwin-arm64
- name: Remove dist folder to check that binary can stand alone
run: |
rm -rf dist
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,34 @@ The following are **beta** commands, you can enable them with with `DD_BETA_COMM
#### `gate`
- `evaluate`: Evaluate [Quality Gates](src/commands/gate) rules in Datadog. [📚](https://docs.datadoghq.com/quality_gates/)


### FIPS support

The `fips` option allows `datadog-ci` to use a FIPS cryptographic module provider if the OpenSSL library installed on the host system provides it.

**Note**: `datadog-ci` cannot assert if such a provider is available, and doesn't throw any error if the provider is not FIPS validated.

Node.js versions below 17 are incompatible with OpenSSL 3, which provides FIPS support.
If you are using a Node.js version below 17, enabling the `fips` option causes the command to throw an error.
The option `fips-ignore-error` ignores this error.
The released `datadog-ci` binary now uses Node.js version 18 to be compatible with OpenSSL 3.

#### `fips`
Enable `datadog-ci` FIPS support if a FIPS validated provider is installed on the host system.
If you do not have a FIPS provider installed, `datadog-ci` does not raise an error.

ENV variable: `DATADOG_FIPS=true`
CLI param: `--fips`

#### `fips-ignore-error`
Ignore Node.js errors if FIPS cannot be enabled on the host system.

**Note**: the absence of an error doesn't indicate that FIPS is enabled successfully.

ENV variable: `DATADOG_FIPS_IGNORE_ERROR=true`
CLI param: `--fips-ignore-error`


## More ways to install the CLI

### Standalone binary
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@
"eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-prettier": "4.0.0",
"jest": "29.6.2",
"pkg": "5.5.2",
"pkg": "5.8.1",
"prettier": "2.0.5",
"proxy": "^2.1.1",
"ts-jest": "29.1.1",
Expand Down
120 changes: 83 additions & 37 deletions src/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,97 @@
// import crypto from 'crypto'

import {Builtins, CommandClass} from 'clipanion'

// Test all commands, including beta ones.
process.env.DD_BETA_COMMANDS_ENABLED = '1'

import {cli, BETA_COMMANDS} from '../cli'
import {enableFips} from '../helpers/fips'

const builtins: CommandClass[] = [Builtins.HelpCommand, Builtins.VersionCommand]

jest.mock('../helpers/fips')

describe('cli', () => {
const commands = Array.from(cli['registrations'].keys())
const userDefinedCommands = commands.filter((command) => !builtins.includes(command))
const commandPaths: {command: CommandClass; commandPath: string[]}[] = []
for (const command of userDefinedCommands) {
for (const commandPath of command.paths ?? []) {
commandPaths.push({command, commandPath})
}
}

const cases: [string, string, string[], CommandClass][] = commandPaths.map(({command, commandPath}) => {
const [rootPath, subPath] = commandPath
const commandName = BETA_COMMANDS.includes(rootPath) ? `${rootPath} (beta)` : rootPath // e.g. synthetics
const subcommandName = subPath || '<root>' // e.g. run-tests

return [commandName, subcommandName, commandPath, command]
})

describe('all commands have the right metadata', () => {
const commands = Array.from(cli['registrations'].keys())
const userDefinedCommands = commands.filter((command) => !builtins.includes(command))

const cases: [string, [string, CommandClass][]][] = Object.entries(
userDefinedCommands.reduce((acc, command) => {
const commandRootPath = command.paths?.[0][0] || 'unknown' // e.g. synthetics
const commandName = BETA_COMMANDS.includes(commandRootPath) ? `${commandRootPath} (beta)` : commandRootPath
const subcommandName = command.paths?.[0].slice(1).join(' ') || '<root>' // e.g. run-tests
const newCase: [string, CommandClass] = [subcommandName, command]

return {
...acc,
[commandName]: [...(acc[commandName] ?? []), newCase],
}
}, {} as Record<string, [string, CommandClass][]>)
)

describe.each(cases)('%s', (commandName, subcommandCases) => {
test.each(subcommandCases)('%s', (_, command) => {
expect(command).toHaveProperty('paths')
expect(command).toHaveProperty('usage')

if (commandName !== 'version') {
// Please categorize the commands by product. You can refer to the CODEOWNERS file.
// eslint-disable-next-line jest/no-conditional-expect
expect(command.usage).toHaveProperty('category')
}

// You should at least document the command with a description, otherwise it will show as "undocumented" in `--help`.
expect(command.usage).toHaveProperty('description')

// Please end your description with a period.
expect(command.usage?.description).toMatch(/\.$/)

// Please uppercase the first letter of the category and description.
expect(command.usage?.category?.charAt(0)).toStrictEqual(command.usage?.category?.charAt(0).toUpperCase())
expect(command.usage?.description?.charAt(0)).toStrictEqual(command.usage?.description?.charAt(0).toUpperCase())
test.each(cases)('%s %s', (commandName, _subcommandName, _commandPath, command) => {
expect(command).toHaveProperty('paths')
expect(command).toHaveProperty('usage')

if (commandName !== 'version') {
// Please categorize the commands by product. You can refer to the CODEOWNERS file.
// eslint-disable-next-line jest/no-conditional-expect
expect(command.usage).toHaveProperty('category')
}

// You should at least document the command with a description, otherwise it will show as "undocumented" in `--help`.
expect(command.usage).toHaveProperty('description')

// Please end your description with a period.
expect(command.usage?.description).toMatch(/\.$/)

// Please uppercase the first letter of the category and description.
expect(command.usage?.category?.charAt(0)).toStrictEqual(command.usage?.category?.charAt(0).toUpperCase())
expect(command.usage?.description?.charAt(0)).toStrictEqual(command.usage?.description?.charAt(0).toUpperCase())
})
})

describe('fips', () => {
const mockedEnableFips = enableFips as jest.MockedFunction<typeof enableFips>
mockedEnableFips.mockImplementation(() => true)

// Without the required options, the commands are not executed at all
const requiredOptions: Record<string, string[]> = {
'dora deployment': ['--started-at', '0', '--dry-run'],
'dsyms upload': ['.', '--dry-run'],
'elf-symbols upload': ['non-existing-file', '--dry-run'],
'gate evaluate': ['--no-wait', '--dry-run'],
'junit upload': ['.', '--dry-run'],
'sarif upload': ['.', '--dry-run'],
'sbom upload': ['.'],
'sourcemaps upload': ['.', '--dry-run'],
trace: ['id', '--dry-run'],
}

// version doesn't support --fips option
const fipsCases = cases.filter(([commandName]) => !['version'].includes(commandName))

describe.each(fipsCases)('%s %s', (_commandName, _subcommandName, commandPath) => {
const command = [...commandPath, ...(requiredOptions[commandPath.join(' ')] ?? [])]

test('supports the --fips option', async () => {
// When running the command with the --fips option
const exitCode = await cli.run([...command, '--fips'])

// The command calls the enableFips function with the right parameters
expect([0, 1]).toContain(exitCode)
expect(mockedEnableFips).toHaveBeenCalledWith(true, false)
})

test('supports the --fips-ignore-error option', async () => {
// When running the command with the --fips and --fips-ignore-error options
const exitCode = await cli.run([...command, '--fips', '--fips-ignore-error'])

// The command calls the enableFips function with the right parameters
expect([0, 1]).toContain(exitCode)
expect(mockedEnableFips).toHaveBeenCalledWith(true, true)
})
})
})
Expand Down
13 changes: 13 additions & 0 deletions src/commands/cloud-run/flare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ import {
ADDITIONAL_FILES_DIRECTORY,
API_KEY_ENV_VAR,
CI_API_KEY_ENV_VAR,
FIPS_ENV_VAR,
FIPS_IGNORE_ERROR_ENV_VAR,
FLARE_OUTPUT_DIRECTORY,
FLARE_PROJECT_FILES,
INSIGHTS_FILE_NAME,
LOGS_DIRECTORY,
PROJECT_FILES_DIRECTORY,
} from '../../constants'
import {toBoolean} from '../../helpers/env'
import {enableFips} from '../../helpers/fips'
import {getProjectFiles, sendToDatadog, validateFilePath, validateStartEndFlags} from '../../helpers/flare'
import {createDirectories, deleteFolder, writeFile, zipContents} from '../../helpers/fs'
import {requestConfirmation, requestFilePath} from '../../helpers/prompt'
Expand Down Expand Up @@ -79,12 +83,21 @@ export class CloudRunFlareCommand extends Command {

private apiKey?: string

private fips = Option.Boolean('--fips', false)
private fipsIgnoreError = Option.Boolean('--fips-ignore-error', false)
private config = {
fips: toBoolean(process.env[FIPS_ENV_VAR]) ?? false,
fipsIgnoreError: toBoolean(process.env[FIPS_IGNORE_ERROR_ENV_VAR]) ?? false,
}

/**
* Entry point for the `cloud-run flare` command.
* Gathers Cloud Run service configuration and sends it to Datadog.
* @returns 0 if the command ran successfully, 1 otherwise.
*/
public async execute() {
enableFips(this.fips || this.config.fips, this.fipsIgnoreError || this.config.fipsIgnoreError)

this.context.stdout.write(helpersRenderer.renderFlareHeader('Cloud Run', this.isDryRun))

const errorMessages: string[] = []
Expand Down
10 changes: 10 additions & 0 deletions src/commands/deployment/correlate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import chalk from 'chalk'
import {Command, Option} from 'clipanion'
import simpleGit from 'simple-git'

import {FIPS_ENV_VAR, FIPS_IGNORE_ERROR_ENV_VAR} from '../../constants'
import {getCISpanTags} from '../../helpers/ci'
import {toBoolean} from '../../helpers/env'
import {enableFips} from '../../helpers/fips'
import {gitRepositoryURL, gitLocalCommitShas, gitCurrentBranch} from '../../helpers/git/get-git-data'
import {Logger, LogLevel} from '../../helpers/logger'
import {retryRequest} from '../../helpers/retry'
Expand Down Expand Up @@ -43,13 +46,20 @@ export class DeploymentCorrelateCommand extends Command {
private configurationShas = Option.Array('--config-shas')
private dryRun = Option.Boolean('--dry-run', false)

private fips = Option.Boolean('--fips', false)
private fipsIgnoreError = Option.Boolean('--fips-ignore-error', false)

private config = {
apiKey: process.env.DATADOG_API_KEY || process.env.DD_API_KEY,
fips: toBoolean(process.env[FIPS_ENV_VAR]) ?? false,
fipsIgnoreError: toBoolean(process.env[FIPS_IGNORE_ERROR_ENV_VAR]) ?? false,
}

private logger: Logger = new Logger((s: string) => this.context.stdout.write(s), LogLevel.INFO)

public async execute() {
enableFips(this.fips || this.config.fips, this.fipsIgnoreError || this.config.fipsIgnoreError)

if (!this.config.apiKey) {
this.logger.error(
`Neither ${chalk.red.bold('DATADOG_API_KEY')} nor ${chalk.red.bold('DD_API_KEY')} is in your environment.`
Expand Down
13 changes: 13 additions & 0 deletions src/commands/deployment/mark.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import {Command, Option} from 'clipanion'

import {FIPS_ENV_VAR, FIPS_IGNORE_ERROR_ENV_VAR} from '../../constants'
import {toBoolean} from '../../helpers/env'
import {enableFips} from '../../helpers/fips'

import {TagCommand} from '../tag/tag'

import {
Expand Down Expand Up @@ -51,7 +55,16 @@ export class DeploymentMarkCommand extends Command {
})
private tags = Option.Array('--tags')

private fips = Option.Boolean('--fips', false)
private fipsIgnoreError = Option.Boolean('--fips-ignore-error', false)
private config = {
fips: toBoolean(process.env[FIPS_ENV_VAR]) ?? false,
fipsIgnoreError: toBoolean(process.env[FIPS_IGNORE_ERROR_ENV_VAR]) ?? false,
}

public async execute() {
enableFips(this.fips || this.config.fips, this.fipsIgnoreError || this.config.fipsIgnoreError)

const tagJobCommand = new TagCommand()
tagJobCommand.setLevel('job')
tagJobCommand.setTags(this.createJobDeploymentTags())
Expand Down
Loading

0 comments on commit 9e8c5cf

Please sign in to comment.