Skip to content

Commit 5f816f1

Browse files
test(ec2): add security related unit tests (#5778)
## Problem We want unit tests to ensure certain security measures are maintained in the code. Properties tested in this PR include: 1. create inline policy with minimal actions necessary to perform desired action 2. prompt user to confirm attached role and allow cancellation. 3. test run the ssh agent before using it for remote connections. ## Solution For each test we do the following: 1. assert that the actions we are adding to the policy remains as the core 6. 2. use `getTestWindow()` to ensure user's are able to see policies added, and cancel the process. 3. spy on `tryRun` to ensure it is used before returning the ssh path. As part of (3), we refactor the pathfinding code to be contained in its own class. This makes the code easier to test since we can now use `sinon.spy` and `sinon.stub`. It also allows us to move some of the shared logic into one place. --- <!--- REMINDER: Ensure that your PR meets the guidelines in CONTRIBUTING.md --> License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Justin M. Keyes <[email protected]>
1 parent 0f30f19 commit 5f816f1

File tree

8 files changed

+171
-10
lines changed

8 files changed

+171
-10
lines changed

packages/core/src/awsService/ec2/sshKeyPair.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ export class SshKeyPair {
4545

4646
public static async generateSshKeyPair(keyPath: string): Promise<void> {
4747
const keyGenerated = await SshKeyPair.tryKeyTypes(keyPath, ['ed25519', 'rsa'])
48-
await SshKeyPair.assertGenerated(keyPath, keyGenerated)
4948
// Should already be the case, but just in case we assert permissions.
5049
// skip on Windows since it only allows write permission to be changed.
5150
if (!globals.isWeb && os.platform() !== 'win32') {
5251
await fs.chmod(keyPath, 0o600)
52+
await SshKeyPair.assertGenerated(keyPath, keyGenerated)
5353
}
5454
}
5555
/**

packages/core/src/codewhisperer/commands/basicCommands.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ import { ToolkitError, getTelemetryReason, getTelemetryReasonDesc } from '../../
4141
import { isRemoteWorkspace } from '../../shared/vscode/env'
4242
import { isBuilderIdConnection } from '../../auth/connection'
4343
import globals from '../../shared/extensionGlobals'
44-
import { getVscodeCliPath, tryRun } from '../../shared/utilities/pathFind'
44+
import { getVscodeCliPath } from '../../shared/utilities/pathFind'
4545
import { setContext } from '../../shared/vscode/setContext'
46+
import { tryRun } from '../../shared/utilities/pathFind'
4647

4748
const MessageTimeOut = 5_000
4849

packages/core/src/shared/remoteSession.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export interface MissingTool {
3030
readonly reason?: string
3131
}
3232

33-
const minimumSsmActions = [
33+
export const minimumSsmActions = [
3434
'ssmmessages:CreateControlChannel',
3535
'ssmmessages:CreateDataChannel',
3636
'ssmmessages:OpenControlChannel',

packages/core/src/shared/utilities/pathFind.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ export async function findTypescriptCompiler(): Promise<string | undefined> {
115115
* Gets the configured `ssh` path, or falls back to "ssh" (not absolute),
116116
* or tries common locations, or returns undefined.
117117
*/
118-
export async function findSshPath(): Promise<string | undefined> {
119-
if (sshPath !== undefined) {
118+
export async function findSshPath(useCache: boolean = true): Promise<string | undefined> {
119+
if (useCache && sshPath !== undefined) {
120120
return sshPath
121121
}
122122

@@ -133,7 +133,7 @@ export async function findSshPath(): Promise<string | undefined> {
133133
continue
134134
}
135135
if (await tryRun(p, ['-G', 'x'], 'noresult' /* "ssh -G" prints quasi-sensitive info. */)) {
136-
sshPath = p
136+
sshPath = useCache ? p : sshPath
137137
return p
138138
}
139139
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import assert from 'assert'
6+
import { minimumSsmActions, promptToAddInlinePolicy } from '../../shared/remoteSession'
7+
import { IamClient } from '../../shared/clients/iamClient'
8+
import { getTestWindow } from './vscode/window'
9+
import { cancel } from '../../shared'
10+
11+
describe('minimumSsmActions', function () {
12+
it('should contain minimal actions needed for ssm connection', function () {
13+
assert.deepStrictEqual(minimumSsmActions, [
14+
'ssmmessages:CreateControlChannel',
15+
'ssmmessages:CreateDataChannel',
16+
'ssmmessages:OpenControlChannel',
17+
'ssmmessages:OpenDataChannel',
18+
'ssm:DescribeAssociation',
19+
'ssm:ListAssociations',
20+
'ssm:UpdateInstanceInformation',
21+
])
22+
})
23+
24+
it('prompts the user for confirmation before adding policies and allow cancels', async function () {
25+
getTestWindow().onDidShowMessage((message) => {
26+
assert.ok(message.message.includes('add'), 'should prompt to add policies')
27+
getTestWindow().getFirstMessage().selectItem(cancel)
28+
})
29+
const added = await promptToAddInlinePolicy({} as IamClient, 'roleArnTest')
30+
assert.ok(!added, 'should not add policies by default')
31+
})
32+
})

packages/core/src/test/shared/utilities/pathFind.test.ts

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,15 @@ import assert from 'assert'
77
import * as vscode from 'vscode'
88
import * as os from 'os'
99
import * as path from 'path'
10-
1110
import * as testutil from '../../testUtil'
12-
import { findTypescriptCompiler, getVscodeCliPath } from '../../../shared/utilities/pathFind'
1311
import { fs } from '../../../shared'
12+
import { findSshPath, findTypescriptCompiler, getVscodeCliPath, tryRun } from '../../../shared/utilities/pathFind'
13+
import { isCI, isWin } from '../../../shared/vscode/env'
1414

1515
describe('pathFind', function () {
1616
it('findTypescriptCompiler()', async function () {
17-
const iswin = process.platform === 'win32'
1817
const workspace = vscode.workspace.workspaceFolders![0]
19-
const tscNodemodules = path.join(workspace.uri.fsPath, `foo/bar/node_modules/.bin/tsc${iswin ? '.cmd' : ''}`)
18+
const tscNodemodules = path.join(workspace.uri.fsPath, `foo/bar/node_modules/.bin/tsc${isWin() ? '.cmd' : ''}`)
2019
await fs.delete(tscNodemodules, { force: true })
2120

2221
// The test workspace normally doesn't have node_modules so this will
@@ -42,4 +41,90 @@ describe('pathFind', function () {
4241
const regex = /bin[\\\/](code|code-insiders)$/
4342
assert.ok(regex.test(vscPath), `expected regex ${regex} to match: "${vscPath}"`)
4443
})
44+
45+
describe('findSshPath', function () {
46+
let previousPath: string | undefined
47+
48+
beforeEach(function () {
49+
previousPath = process.env.PATH
50+
})
51+
52+
afterEach(function () {
53+
process.env.PATH = previousPath
54+
})
55+
56+
it('first tries ssh in $PATH (Non-Windows)', async function () {
57+
// skip on windows because ssh in path will never work with .exe extension.
58+
if (isWin()) {
59+
return
60+
}
61+
const workspace = await testutil.createTestWorkspaceFolder()
62+
const fakeSshPath = path.join(workspace.uri.fsPath, `ssh`)
63+
64+
process.env.PATH = workspace.uri.fsPath
65+
const firstResult = await findSshPath(false)
66+
67+
await testutil.createExecutableFile(fakeSshPath, '')
68+
69+
const secondResult = await findSshPath(false)
70+
71+
assert.notStrictEqual(firstResult, secondResult)
72+
assert.strictEqual(secondResult, 'ssh')
73+
})
74+
75+
it('only returns valid executable ssh path (Non-Windows)', async function () {
76+
if (isWin()) {
77+
return
78+
}
79+
// On non-windows, we can overwrite path and create our own executable to find.
80+
const workspace = await testutil.createTestWorkspaceFolder()
81+
const fakeSshPath = path.join(workspace.uri.fsPath, `ssh`)
82+
83+
process.env.PATH = workspace.uri.fsPath
84+
85+
await testutil.createExecutableFile(fakeSshPath, '')
86+
87+
const ssh = await findSshPath(false)
88+
assert.ok(ssh)
89+
const result = await tryRun(ssh, [], 'yes')
90+
assert.ok(result)
91+
})
92+
93+
it('caches result from previous runs (Non-Windows)', async function () {
94+
if (isWin()) {
95+
return
96+
}
97+
// On non-windows, we can overwrite path and create our own executable to find.
98+
const workspace = await testutil.createTestWorkspaceFolder()
99+
// We move the ssh to a temp directory temporarily to test if cache works.
100+
const fakeSshPath = path.join(workspace.uri.fsPath, `ssh`)
101+
102+
process.env.PATH = workspace.uri.fsPath
103+
104+
await testutil.createExecutableFile(fakeSshPath, '')
105+
106+
const ssh1 = (await findSshPath(true))!
107+
108+
await fs.delete(fakeSshPath)
109+
110+
const ssh2 = await findSshPath(true)
111+
112+
assert.strictEqual(ssh1, ssh2)
113+
})
114+
115+
it('finds valid executable path (Windows CI)', async function () {
116+
// Don't want to be messing with System32 on peoples local machines.
117+
if (!isWin() || !isCI()) {
118+
return
119+
}
120+
const expectedPathInCI = 'C:/Windows/System32/OpenSSH/ssh.exe'
121+
122+
if (!(await fs.exists(expectedPathInCI))) {
123+
await testutil.createExecutableFile(expectedPathInCI, '')
124+
}
125+
const ssh = (await findSshPath(true))!
126+
const result = await tryRun(ssh, ['-G', 'x'], 'noresult')
127+
assert.ok(result)
128+
})
129+
})
45130
})
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import assert from 'assert'
6+
import { createExecutableFile, createTestWorkspaceFolder, copyEnv } from '../../testUtil'
7+
import path from 'path'
8+
import { fs } from '../../../shared'
9+
import { isWin } from '../../../shared/vscode/env'
10+
import { tryRun } from '../../../shared/utilities/pathFind'
11+
12+
describe('copyEnv', function () {
13+
it('modifies the node environment variables (Non-Windows)', function () {
14+
// PATH returns undefined on Windows.
15+
if (isWin()) {
16+
this.skip()
17+
}
18+
19+
const originalPath = copyEnv().PATH
20+
const fakePath = 'fakePath'
21+
process.env.PATH = fakePath
22+
assert.strictEqual(copyEnv().PATH, fakePath)
23+
24+
process.env.PATH = originalPath
25+
assert.strictEqual(copyEnv().PATH, originalPath)
26+
})
27+
})
28+
29+
describe('createExecutableFile', function () {
30+
it('creates a file that can be executed', async function () {
31+
const tempDir = await createTestWorkspaceFolder()
32+
const filePath = path.join(tempDir.uri.fsPath, `exec${isWin() ? '.cmd' : ''}`)
33+
await createExecutableFile(filePath, '')
34+
35+
const result = await tryRun(filePath, [], 'yes')
36+
assert.ok(result)
37+
await fs.delete(tempDir.uri, { force: true, recursive: true })
38+
})
39+
})

packages/core/src/test/testUtil.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,3 +630,7 @@ export function tryRegister(command: DeclaredCommand<() => Promise<any>>) {
630630
export function getFetchStubWithResponse(response: Partial<Response>) {
631631
return stub(request, 'fetch').returns({ response: new Promise((res, _) => res(response)) } as any)
632632
}
633+
634+
export function copyEnv(): NodeJS.ProcessEnv {
635+
return { ...process.env }
636+
}

0 commit comments

Comments
 (0)