Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: diagnose #10383

Merged
merged 25 commits into from
Jun 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .circleci/amplify_init.exp
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ send -- "$env(AWS_SECRET_ACCESS_KEY)\r"
expect -exact "region:"
log_user 1;
send -- "j\r"
expect "Help improve Amplify CLI by sharing non sensitive configurations on failures"
send -- "\r"
interact;
4 changes: 4 additions & 0 deletions .eslint-dictionary.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
"codebase",
"codegen",
"codepipeline",
"columnify",
"cognito",
"cors",
"createCipheriv",
"creds",
"datasource",
"decrypt",
Expand Down Expand Up @@ -83,13 +85,15 @@
"nspawn",
"nullability",
"nullable",
"oaepHash",
"oauth",
"oidc",
"openid",
"opensearch",
"orgs",
"parens",
"pathname",
"pbkdf2Sync",
"pipelined",
"positionally",
"posix",
Expand Down
2 changes: 2 additions & 0 deletions packages/amplify-cli-core/src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export class CustomPoliciesFormatError extends Error {}
export class ExportPathValidationError extends Error {}
export class ExportedStackNotFoundError extends Error {}
export class ExportedStackNotInValidStateError extends Error {}
export class DebugConfigValueNotSetError extends Error {}
export class DiagnoseReportUploadError extends Error {}

export class NotInitializedError extends Error {
public constructor() {
Expand Down
60 changes: 33 additions & 27 deletions packages/amplify-cli-logger/src/Redactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,68 @@ const containsToRedact = ['key', 'id', 'password', 'name', 'arn', 'address', 'ap
const quotes = '\\\\?"';
const keyMatcher = `\\w*?(${containsToRedact.join('|')})\\w*?`;
const completeMatch = `${quotes}(${keyMatcher})${quotes}:\\s?${quotes}([^!\\\\?"]+)${quotes}`;

export function Redactor(arg: string | undefined): string {
/**
* Redacts json string
* @param arg JSON string to redact
* @returns redacted json string
*/
export const Redactor = (arg: string | undefined): string => {
if (!arg) return '';

// matches any json and gives values in json
const jsonregex: RegExp = new RegExp(completeMatch, 'gmi');
const jsonRegex = new RegExp(completeMatch, 'gmi');
// test for value in containsToRedact
if (jsonregex.test(arg)) {
jsonregex.lastIndex = 0;
if (jsonRegex.test(arg)) {
jsonRegex.lastIndex = 0;
let m: RegExpExecArray | null;
const valuestToRedact: Array<string> = [];
const valuesToRedact: Array<string> = [];
do {
m = jsonregex.exec(arg);
m = jsonRegex.exec(arg);
if (m !== null) {
valuestToRedact.push(m[3]);
valuesToRedact.push(m[3]);
}
} while (m !== null);
valuestToRedact.forEach(val => {
//replace value using string Masker
valuesToRedact.forEach(val => {
// replace value using string Masker
arg = arg?.replace(val, stringMasker);
});
}

return arg;
}
function stringMasker(s: string): string {
};

/**
* Mask string with redaction
* @param s string to mask
* @returns replaced string
*/
export const stringMasker = (s: string): string => {
if (!s.includes('-') && !s.includes('/')) return redactPart(s);

// if string only includes '/' char
if (s.includes('/') && !s.includes('-')) return redactBySlashSplit(s);
const newString = s
.split('-') // split string by '-'
.map(part => {
// and then redact the smaller pieces selarated by '/'
// and then redact the smaller pieces separated by '/'
if (part.includes('/')) {
// start redacting only when it contains '/'
return redactBySlashSplit(part);
} else {
return redactPart(part);
}
return redactPart(part);
})
.join('-');

return newString;
}
//redacts all the pieces joined by '/' individually
function redactBySlashSplit(s: string): string {
return s
.split('/')
.map(redactPart)
.join('/');
}
};

// redacts all the pieces joined by '/' individually
const redactBySlashSplit = (s: string): string => s.split('/').map(redactPart).join('/');

// replaces 60% of string by [***]
function redactPart(s: string): string {
const length = s.length;
const redactPart = (s: string): string => {
const { length } = s;
const maskPercentage = 60 / 100;
const replaceLength = Math.floor(length * maskPercentage);
return '[***]' + s.substring(replaceLength, length);
}
return `[***]${s.substring(replaceLength, length)}`;
};
2 changes: 1 addition & 1 deletion packages/amplify-cli-logger/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AmplifyLogger } from './AmplifyLogger';
import { IAmplifyLogger } from './IAmplifyLogger';
import { constants } from './constants';
export { Redactor } from './Redactor';
export { Redactor, stringMasker } from './Redactor';
export const logger: IAmplifyLogger = new AmplifyLogger();
export const LocalLogDirectory = constants.LOG_DIRECTORY;
4 changes: 3 additions & 1 deletion packages/amplify-cli/amplify-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"configure",
"console",
"delete",
"diagnose",
"env",
"export",
"help",
Expand All @@ -21,7 +22,8 @@
"uninstall",
"upgrade",
"version",
"build"
"build",
"report"
],
"commandAliases": {
"h": "help",
Expand Down
4 changes: 4 additions & 0 deletions packages/amplify-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,14 @@
"promise-sequential": "^1.1.1",
"semver": "^7.3.5",
"tar-fs": "^2.1.1",
"treeify": "^1.1.0",
"update-notifier": "^5.1.0",
"uuid": "^8.3.2",
"which": "^2.0.2"
},
"devDependencies": {
"@types/archiver": "^5.3.1",
"@types/columnify": "^1.5.1",
"@types/ci-info": "^2.0.0",
"@types/folder-hash": "^4.0.1",
"@types/fs-extra": "^8.0.1",
Expand All @@ -114,6 +117,7 @@
"@types/progress": "^2.0.3",
"@types/promise-sequential": "^1.1.0",
"@types/tar-fs": "^2.0.0",
"@types/treeify": "^1.0.0",
"@types/update-notifier": "^5.1.0",
"amplify-function-plugin-interface": "1.9.5",
"cloudform-types": "^4.2.0",
Expand Down
170 changes: 170 additions & 0 deletions packages/amplify-cli/src/__tests__/commands/diagnose.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import * as fs from 'fs-extra';
import archiver from 'archiver';
import { pathManager, stateManager } from 'amplify-cli-core';
import { Redactor } from 'amplify-cli-logger';
import { WriteStream } from 'fs-extra';
import fetch from 'node-fetch';
import * as uuid from 'uuid';
import { collectFiles } from '../../commands/helpers/collect-files';
import { run } from '../../commands/diagnose';
import { Context } from '../../domain/context';

jest.mock('uuid');
jest.mock('amplify-cli-core');
jest.mock('../../commands/helpers/collect-files');
jest.mock('../../commands/helpers/encryption-helpers');
jest.mock('archiver');
jest.mock('fs-extra');
jest.mock('amplify-cli-logger', () => ({
Redactor: jest.fn(),
stringMasker: jest.fn(),
}));

jest.mock('path');
jest.mock('crypto', () => ({
publicEncrypt: jest.fn().mockReturnValue(Buffer.from([])),
createHash: jest.fn().mockReturnValue({
update: jest.fn().mockReturnValue({
digest: jest.fn().mockReturnValue('projectId'),
}),
}),
randomBytes: jest.fn().mockReturnValue(Buffer.from('RandomBytes')),
/* eslint-disable spellcheck/spell-checker*/
pbkdf2Sync: jest.fn(),
createCipheriv: jest.fn(),
/* eslint-enable spellcheck/spell-checker*/
}));
jest.mock('node-fetch', () => jest.fn().mockReturnValue({ status: 200 }));

const mockMeta = {
providers: {
awscloudformation: {
// eslint-disable-next-line spellcheck/spell-checker
AmplifyAppId: 'd2ew5jdgc57sa7',
},
},
hosting: {},
auth: {
testAuth: {
service: 'Cognito',
},
},
storage: {
testBucket: {
service: 'S3',
},
},
api: {
myApi: {
service: 'AppSync',
},
},
};
const collectedFiles : { filePath: string, redact: boolean }[] = [
{
filePath: 'file.ts',
redact: false,
},
{
filePath: 'file.json',
redact: true,
},
];

describe('run report command', () => {
it('runs report command for only a resource', async () => {
const contextMock = {
usageData: {
getUsageDataPayload: jest.fn().mockReturnValue({
sessionUuid: 'sessionId',
installationUuid: '',

}),
},
exeInfo: {
/* eslint-disable spellcheck/spell-checker */
cloudFormationEvents: [
{
StackId: 'arn:aws:cloudformation:us-east-1:1234567891009:stack/amplify-pushfail-dev-230444/d7470930-8ac5-11ec-a30c-0a84db46e9eb',
EventId: 'd006c2e0-c0f4-11ec-841d-0e43d8dbed1f',
StackName: 'amplify-pushfail-dev-230444',
LogicalResourceId: 'amplify-pushfail-dev-230444',
PhysicalResourceId: 'arn:aws:cloudformation:us-east-1:1234567891009:stack/amplify-pushfail-dev-230444/d7470930-8ac5-11ec-a30c-0a84db46e9eb',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: '2022-04-20T21:57:03.599Z',
ResourceStatus: 'UPDATE_IN_PROGRESS',
ResourceStatusReason: 'User Initiated',
},
{
StackId: 'arn:aws:cloudformation:us-east-1:1234567891009:stack/amplify-pushfail-dev-230444/d7470930-8ac5-11ec-a30c-0a84db46e9eb',
EventId: 'apipushfail-CREATE_IN_PROGRESS-2022-04-20T21:57:09.528Z',
StackName: 'amplify-pushfail-dev-230444',
LogicalResourceId: 'apipushfail',
PhysicalResourceId: '',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: '2022-04-20T21:57:09.528Z',
ResourceStatus: 'CREATE_IN_PROGRESS',
},
{
StackId: 'arn:aws:cloudformation:us-east-1:1234567891009:stack/amplify-pushfail-dev-230444/d7470930-8ac5-11ec-a30c-0a84db46e9eb',
EventId: 'UpdateRolesWithIDPFunctionRole-CREATE_IN_PROGRESS-2022-04-20T21:57:09.540Z',
StackName: 'amplify-pushfail-dev-230444',
LogicalResourceId: 'UpdateRolesWithIDPFunctionRole',
PhysicalResourceId: '',
ResourceType: 'AWS::IAM::Role',
Timestamp: '2022-04-20T21:57:09.540Z',
ResourceStatus: 'CREATE_IN_PROGRESS',
},
],
/* eslint-enable spellcheck/spell-checker */

},
input: {
options: {
'send-report': true,
},
},
};
const mockRootPath = 'user/source/myProject';
const pathManagerMock = pathManager as jest.Mocked<typeof pathManager>;
pathManagerMock.findProjectRoot = jest.fn().mockReturnValue(mockRootPath);

const stateManagerMock = stateManager as jest.Mocked<typeof stateManager>;
stateManagerMock.getBackendConfig = jest.fn().mockReturnValue(mockMeta);
stateManagerMock.getProjectConfig = jest.fn().mockReturnValue({ projectName: 'myProject' });

const collectFilesMock = collectFiles as jest.MockedFunction<typeof collectFiles>;

collectFilesMock.mockReturnValue(collectedFiles);

const mockArchiver = archiver as jest.Mocked<typeof archiver>;
const zipperMock = {
append: jest.fn(),
pipe: jest.fn(),
finalize: jest.fn(),
};
mockArchiver.create = jest.fn().mockReturnValue(zipperMock);

const fsMock = fs as jest.Mocked<typeof fs>;
fsMock.createWriteStream.mockReturnValue({
on: jest.fn().mockImplementation((event, resolveFunction) => {
if (event === 'close') {
resolveFunction();
}
}),
error: jest.fn(),
} as unknown as WriteStream);

const uuidMock = uuid as jest.Mocked<typeof uuid>;
uuidMock.v4.mockReturnValue('randomPassPhrase');

const contextMockTyped = contextMock as unknown as Context;
await run(contextMockTyped, new Error('mock error'));
expect(fsMock.readFileSync).toBeCalled();
expect(Redactor).toBeCalledTimes(1);
expect(zipperMock.pipe).toBeCalled();
expect(zipperMock.finalize).toBeCalled();
expect(fetch).toBeCalled();
expect(zipperMock.append).toBeCalledTimes(3);
});
});
Loading