Skip to content

Commit b589b59

Browse files
committed
feat: support scanning images from ECR
- try to identify the source registry of the images we're pulling - for ECR, attempt to generate an authentication token using aws's sdk. pass the resulting credentials to Skopeo's copy command. - for any other registry, don't change existing behaviour (keep searching the credentials in the default dir of ~/.docker/config.json)
1 parent 6e19cc7 commit b589b59

File tree

8 files changed

+222
-5
lines changed

8 files changed

+222
-5
lines changed

package-lock.json

Lines changed: 89 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@types/node": "^10.12.5",
3434
"@types/sinon": "^7.5.1",
3535
"async": "^2.6.2",
36+
"aws-sdk": "^2.596.0",
3637
"bunyan": "^1.8.12",
3738
"child-process-promise": "^2.2.1",
3839
"lru-cache": "^5.1.1",

src/common/process.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export function exec(bin: string, ...args: string[]):
1616
return spawn(bin, args, { env, capture: [ 'stdout', 'stderr' ] })
1717
.catch((error) => {
1818
const message = (error && error.stderr) || 'Unknown reason';
19+
// TODO: sanitise args for secrets
1920
logger.warn({message, bin, args}, 'could not spawn the process');
2021
throw error;
2122
});

src/images/credentials.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as aws from 'aws-sdk';
2+
3+
export async function getSourceCredentials(imageSource: string): Promise<string | undefined> {
4+
// TODO is this the best way we can determine the image's source?
5+
if (imageSource.indexOf('.ecr.') !== -1) {
6+
return getEcrCredentials();
7+
}
8+
return undefined;
9+
}
10+
11+
function getEcrCredentials(): Promise<string> {
12+
return new Promise(async (resolve, reject) => {
13+
// TODO grab region... from...? ask users to provide it?
14+
const ecr = new aws.ECR({region: 'us-east-2'});
15+
return ecr.getAuthorizationToken({}, (err, data) => {
16+
if (err) {
17+
return reject(err);
18+
}
19+
20+
if (!(
21+
data &&
22+
data.authorizationData &&
23+
Array.isArray(data.authorizationData) &&
24+
data.authorizationData.length > 0
25+
)) {
26+
return reject('unexpected data format from ecr.getAuthorizationToken');
27+
}
28+
29+
const authorizationTokenBase64 = data.authorizationData[0].authorizationToken;
30+
31+
if (!authorizationTokenBase64) {
32+
return reject('empty authorization token from ecr.getAuthorizationToken');
33+
}
34+
35+
const buff = new Buffer(authorizationTokenBase64, 'base64');
36+
const userColonPassword = buff.toString('utf-8');
37+
return resolve(userColonPassword);
38+
});
39+
});
40+
}

src/images/skopeo.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { SkopeoRepositoryType } from '../kube-scanner/types';
22
import { SpawnPromiseResult } from 'child-process-promise';
33
import { exec } from '../common/process';
44
import config = require('../common/config');
5+
import * as credentials from './credentials';
56

67
function getUniqueIdentifier(): string {
78
const [seconds, nanoseconds] = process.hrtime();
@@ -28,12 +29,24 @@ function prefixRespository(target: string, type: SkopeoRepositoryType): string {
2829
}
2930
}
3031

31-
export function pull(
32+
export async function pull(
3233
image: string,
3334
destination: string,
3435
): Promise<SpawnPromiseResult> {
35-
return exec('skopeo', 'copy',
36+
const creds = await credentials.getSourceCredentials(image);
37+
const credentialsParameters = getCredentialParameters(creds);
38+
39+
return exec('skopeo', 'copy', ...credentialsParameters,
3640
prefixRespository(image, SkopeoRepositoryType.ImageRegistry),
3741
prefixRespository(destination, SkopeoRepositoryType.DockerArchive),
3842
);
3943
}
44+
45+
export function getCredentialParameters(credentials: string | undefined): Array<string> {
46+
const credentialsParameters: Array<string> = [];
47+
if (credentials) {
48+
credentialsParameters.push('--src-creds');
49+
credentialsParameters.push(credentials);
50+
}
51+
return credentialsParameters;
52+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: debian-ecr
5+
namespace: services
6+
spec:
7+
selector:
8+
matchLabels:
9+
app: debian-ecr
10+
template:
11+
metadata:
12+
labels:
13+
app: debian-ecr
14+
spec:
15+
containers:
16+
- name: debian-ecr
17+
image: 291964488713.dkr.ecr.us-east-2.amazonaws.com/snyk/debian:10
18+
imagePullPolicy: Always
19+
securityContext: {}
20+
command: ['sh', '-c', 'echo Hello from ECR alpine pod! && sleep 360000']

test/integration/kubernetes.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,41 @@ tap.test('snyk-monitor pulls images from a private gcr.io registry and sends dat
164164
'snyk-monitor sent expected data to upstream in the expected timeframe');
165165
});
166166

167+
tap.test('snyk-monitor pulls images from a private ECR and sends data to homebase', async (t) => {
168+
if (process.env['TEST_PLATFORM'] !== 'eks') {
169+
t.pass('Not testing private ECR images because we\'re not running in EKS');
170+
return;
171+
}
172+
173+
t.plan(3);
174+
175+
const deploymentName = 'debian-ecr';
176+
const namespace = 'services';
177+
const clusterName = 'Default cluster';
178+
const deploymentType = WorkloadKind.Deployment;
179+
const imageName = '291964488713.dkr.ecr.us-east-2.amazonaws.com/snyk/debian';
180+
181+
await kubectl.applyK8sYaml('./test/fixtures/private-registries/debian-deployment-ecr.yaml');
182+
console.log(`Begin polling upstream for the expected private ECR image with integration ID ${integrationId}...`);
183+
184+
const validatorFn: WorkloadLocatorValidator = (workloads) => {
185+
return workloads !== undefined &&
186+
workloads.find((workload) => workload.name === deploymentName &&
187+
workload.type === WorkloadKind.Deployment) !== undefined;
188+
};
189+
190+
const homebaseTestResult = await validateHomebaseStoredData(
191+
validatorFn, `api/v2/workloads/${integrationId}/${clusterName}/${namespace}`);
192+
t.ok(homebaseTestResult, 'snyk-monitor sent expected data to upstream in the expected timeframe');
193+
194+
const depGraphResult = await getHomebaseResponseBody(
195+
`api/v1/dependency-graphs/${integrationId}/${clusterName}/${namespace}/${deploymentType}/${deploymentName}`);
196+
t.ok('dependencyGraphResults' in depGraphResult,
197+
'expected dependencyGraphResults field to exist in /dependency-graphs response');
198+
t.ok('imageMetadata' in JSON.parse(depGraphResult.dependencyGraphResults[imageName]),
199+
'snyk-monitor sent expected data to upstream in the expected timeframe');
200+
});
201+
167202
tap.test('snyk-monitor sends deleted workload to homebase', async (t) => {
168203
// First ensure the deployment exists from the previous test
169204
const deploymentValidatorFn: WorkloadLocatorValidator = (workloads) => {

test/unit/skopeo.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as tap from 'tap';
2+
3+
import * as skopeo from '../../src/images/skopeo';
4+
5+
tap.test('getCredentialParameters()', async (t) => {
6+
const noCredentials = undefined;
7+
const credentialParametersForNoCredentials = skopeo.getCredentialParameters(noCredentials);
8+
t.same(credentialParametersForNoCredentials, [], 'returns an empty array for no credentials');
9+
10+
const emptyCredentials = '';
11+
const credentialParametersForEmptyCredentials = skopeo.getCredentialParameters(emptyCredentials);
12+
t.same(credentialParametersForEmptyCredentials, [], 'returns an empty array for empty credentials');
13+
14+
const someCredentials = 'secret-things-happening';
15+
const credentialParametersForSomeCredentials = skopeo.getCredentialParameters(someCredentials);
16+
t.same(
17+
credentialParametersForSomeCredentials,
18+
['--src-creds', someCredentials],
19+
'returns Skopeo\'s args for source credentials',
20+
);
21+
});

0 commit comments

Comments
 (0)