Skip to content

Commit a853db1

Browse files
authored
fix: add threat scanning of uploaded assets using aws guard duty (#4530)
1 parent 32929c5 commit a853db1

File tree

100 files changed

+1840
-591
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

100 files changed

+1840
-591
lines changed

Diff for: apps/api/webiny.application.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createApiApp } from "@webiny/serverless-cms-aws";
1+
import { createApiApp } from "@webiny/serverless-cms-aws/enterprise";
22

33
export default createApiApp({
44
pulumiResourceNamePrefix: "wby-"

Diff for: apps/core/webiny.application.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createCoreApp } from "@webiny/serverless-cms-aws";
1+
import { createCoreApp } from "@webiny/serverless-cms-aws/enterprise";
22

33
export default createCoreApp({
44
pulumiResourceNamePrefix: "wby-"

Diff for: docs/DEPLOY_WEBINY_PROJECT_CF_TEMPLATE.yaml

+20
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,26 @@ Resources:
516516
- logs:Unmask
517517
- logs:GetLogEvents
518518

519+
# Malware Protection Plan (File Manager Threat Detection)
520+
- Effect: Allow
521+
Resource: "arn:aws:guardduty:*:*:malware-protection-plan/*"
522+
Action:
523+
- guardduty:CreateMalwareProtectionPlan
524+
- guardduty:GetMalwareProtectionPlan
525+
- guardduty:DeleteMalwareProtectionPlan
526+
- guardduty:UpdateMalwareProtectionPlan
527+
528+
# IoT
529+
- Effect: Allow
530+
Resource: "arn:aws:iot:*:*:authorizer/*"
531+
Action:
532+
- iot:CreateAuthorizer
533+
- iot:DescribeAuthorizer
534+
- iot:DescribeDefaultAuthorizer
535+
- iot:UpdateAuthorizer
536+
- iot:DeleteAuthorizer
537+
- iot:ListTagsForResource
538+
519539
UserToDeployWebinyProjectGroup1:
520540
Type: AWS::IAM::UserToGroupAddition
521541
Properties:

Diff for: packages/api-audit-logs/__tests__/helpers/handlerCore.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { ImportExportTaskStorageOperations } from "@webiny/api-page-builder-impo
2727
import { AdminUsersStorageOperations } from "@webiny/api-admin-users/types";
2828
import createAdminUsersApp from "@webiny/api-admin-users";
2929
import { createMailerContext } from "@webiny/api-mailer";
30+
import { NullLicense } from "@webiny/wcp";
3031
import { createAcoAuditLogsContext } from "~/app";
3132

3233
export interface CreateHandlerCoreParams {
@@ -65,6 +66,11 @@ export const createHandlerCore = (params?: CreateHandlerCoreParams) => {
6566

6667
const enableContextPlugin = createContextPlugin<AuditLogsContext>(async context => {
6768
context.wcp = {
69+
getProject: () => null,
70+
getRawLicense: () => null,
71+
canUseRecordLocking: () => true,
72+
canUseAuditLogs: () => true,
73+
canUseFileManagerThreatDetection: () => false,
6874
ensureCanUseFeature: () => void 0,
6975
canUseFolderLevelPermissions: () => true,
7076
canUseAacl: () => true,
@@ -75,7 +81,7 @@ export const createHandlerCore = (params?: CreateHandlerCoreParams) => {
7581
decrementTenants: async () => void 0,
7682
incrementTenants: async () => void 0,
7783
getProjectEnvironment: () => null,
78-
getProjectLicense: () => null,
84+
getProjectLicense: () => new NullLicense(),
7985
canUseFeature: () => true
8086
};
8187
});

Diff for: packages/api-audit-logs/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@webiny/api-page-builder-import-export": "0.0.0",
4646
"@webiny/error": "0.0.0",
4747
"@webiny/handler": "0.0.0",
48-
"@webiny/utils": "0.0.0"
48+
"@webiny/utils": "0.0.0",
49+
"@webiny/wcp": "0.0.0"
4950
}
5051
}

Diff for: packages/api-audit-logs/tsconfig.build.json

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
{ "path": "../error/tsconfig.build.json" },
1313
{ "path": "../handler/tsconfig.build.json" },
1414
{ "path": "../utils/tsconfig.build.json" },
15+
{ "path": "../wcp/tsconfig.build.json" },
1516
{ "path": "../api-admin-users/tsconfig.build.json" },
1617
{ "path": "../api-file-manager/tsconfig.build.json" },
1718
{ "path": "../api-headless-cms/tsconfig.build.json" },

Diff for: packages/api-audit-logs/tsconfig.json

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
{ "path": "../error" },
1313
{ "path": "../handler" },
1414
{ "path": "../utils" },
15+
{ "path": "../wcp" },
1516
{ "path": "../api-admin-users" },
1617
{ "path": "../api-file-manager" },
1718
{ "path": "../api-headless-cms" },
@@ -50,6 +51,8 @@
5051
"@webiny/handler": ["../handler/src"],
5152
"@webiny/utils/*": ["../utils/src/*"],
5253
"@webiny/utils": ["../utils/src"],
54+
"@webiny/wcp/*": ["../wcp/src/*"],
55+
"@webiny/wcp": ["../wcp/src"],
5356
"@webiny/api-admin-users/*": ["../api-admin-users/src/*"],
5457
"@webiny/api-admin-users": ["../api-admin-users/src"],
5558
"@webiny/api-file-manager/*": ["../api-file-manager/src/*"],

Diff for: packages/api-file-manager-s3/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@
1313
"@webiny/api": "0.0.0",
1414
"@webiny/api-file-manager": "0.0.0",
1515
"@webiny/api-security": "0.0.0",
16+
"@webiny/api-wcp": "0.0.0",
17+
"@webiny/api-websockets": "0.0.0",
1618
"@webiny/aws-sdk": "0.0.0",
1719
"@webiny/error": "0.0.0",
1820
"@webiny/handler": "0.0.0",
21+
"@webiny/handler-aws": "0.0.0",
1922
"@webiny/handler-graphql": "0.0.0",
2023
"@webiny/plugins": "0.0.0",
2124
"@webiny/tasks": "0.0.0",

Diff for: packages/api-file-manager-s3/src/assetDelivery/assetDeliveryConfig.ts

+1-11
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,7 @@ import { S3 } from "@webiny/aws-sdk/client-s3";
66
import { S3AssetResolver } from "~/assetDelivery/s3/S3AssetResolver";
77
import { S3OutputStrategy } from "~/assetDelivery/s3/S3OutputStrategy";
88
import { SharpTransform } from "~/assetDelivery/s3/SharpTransform";
9-
10-
export type AssetDeliveryParams = Parameters<typeof createBaseAssetDelivery>[0] & {
11-
imageResizeWidths?: number[];
12-
/**
13-
* BE CAREFUL!
14-
* Setting this to more than 1 hour may cause your URLs to still expire before the desired expiration time.
15-
* @see https://repost.aws/knowledge-center/presigned-url-s3-bucket-expiration
16-
*/
17-
presignedUrlTtl?: number;
18-
assetStreamingMaxSize?: number;
19-
};
9+
import type { AssetDeliveryParams } from "~/assetDelivery/types";
2010

2111
export const assetDeliveryConfig = (params: AssetDeliveryParams) => {
2212
const bucket = process.env.S3_BUCKET as string;
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
import { createAssetDeliveryPluginLoader } from "@webiny/api-file-manager";
22
import { PluginFactory } from "@webiny/plugins/types";
3-
import type { AssetDeliveryParams } from "./assetDeliveryConfig";
3+
import { createThreatDetectionPluginLoader } from "~/assetDelivery/threatDetection";
4+
import type { AssetDeliveryParams } from "~/assetDelivery/types";
45

5-
export const createAssetDelivery = (params: AssetDeliveryParams): PluginFactory => {
6-
/**
7-
* We only want to load this plugin in the context of the Asset Delivery Lambda function.
8-
*/
9-
return createAssetDeliveryPluginLoader(() => {
10-
return import(/* webpackChunkName: "s3AssetDelivery" */ "./assetDeliveryConfig").then(
11-
({ assetDeliveryConfig }) => assetDeliveryConfig(params)
12-
);
13-
});
6+
export const createAssetDelivery = (params: AssetDeliveryParams): PluginFactory[] => {
7+
return [
8+
/**
9+
* We only want to load this plugin in the context of the Asset Delivery Lambda function.
10+
*/
11+
createAssetDeliveryPluginLoader(() => {
12+
return import(/* webpackChunkName: "s3AssetDelivery" */ "./assetDeliveryConfig").then(
13+
({ assetDeliveryConfig }) => assetDeliveryConfig(params)
14+
);
15+
}),
16+
/**
17+
* We only want to load this plugin in the context of the Threat Detection Lambda function.
18+
*/
19+
createThreatDetectionPluginLoader(() => {
20+
return import(
21+
/* webpackChunkName: "threatDetectionEventHandler" */ "./threatDetection/createThreatDetectionEventHandler"
22+
).then(({ createThreatDetectionEventHandler }) => createThreatDetectionEventHandler());
23+
})
24+
];
1425
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { S3 } from "@webiny/aws-sdk/client-s3";
2+
import { createEventBridgeEventHandler } from "@webiny/handler-aws";
3+
import { createHandlerOnRequest } from "@webiny/handler";
4+
import { GuardDutyEvent, ThreatDetectionContext } from "./types";
5+
import { processThreatScanResult } from "./processThreatScanResult";
6+
import { S3AssetMetadataReader } from "~/assetDelivery/s3/S3AssetMetadataReader";
7+
import { EventBridgeEvent } from "@webiny/aws-sdk/types";
8+
9+
const detailType = "GuardDuty Malware Protection Object Scan Result";
10+
11+
const bucket = process.env.S3_BUCKET as string;
12+
const region = process.env.AWS_REGION as string;
13+
14+
export const createThreatDetectionEventHandler = () => {
15+
const s3 = new S3({ region });
16+
17+
const handlerOnRequest = createHandlerOnRequest(async request => {
18+
const payload = request.body as EventBridgeEvent<string, GuardDutyEvent>;
19+
20+
if (payload["detail-type"] !== detailType) {
21+
return;
22+
}
23+
24+
const objectKey = payload.detail.s3ObjectDetails.objectKey;
25+
if (objectKey.endsWith(".metadata")) {
26+
return;
27+
}
28+
29+
try {
30+
const s3Metadata = new S3AssetMetadataReader(s3, bucket);
31+
const metadata = await s3Metadata.getMetadata(payload.detail.s3ObjectDetails.objectKey);
32+
33+
request.headers = {
34+
...request.headers,
35+
"x-tenant": metadata.tenant,
36+
"x-i18n-locale": `default:${metadata.locale};content:${metadata.locale};`
37+
};
38+
} catch {
39+
// If metadata can't be loaded, we ignore the file.
40+
// Most likely it's because the file is a rendition of the original file,
41+
// so we don't need to do anything with it.
42+
}
43+
});
44+
// Guard Duty event handler.
45+
const threatScanEventHandler = createEventBridgeEventHandler<typeof detailType, GuardDutyEvent>(
46+
async ({ payload, next, ...rest }) => {
47+
const context = rest.context as ThreatDetectionContext;
48+
49+
const threatDetectionEnabled = context.wcp.canUseFileManagerThreatDetection();
50+
51+
if (!threatDetectionEnabled || payload["detail-type"] !== detailType) {
52+
return next();
53+
}
54+
55+
await processThreatScanResult(context, payload.detail);
56+
}
57+
);
58+
59+
// Assign a human-readable name for easier debugging.
60+
threatScanEventHandler.name = threatScanEventHandler.type + ".threatDetectionEventHandler";
61+
62+
return [handlerOnRequest, threatScanEventHandler];
63+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { PluginFactory } from "@webiny/plugins/types";
2+
import { createConditionalPluginFactory } from "@webiny/api";
3+
4+
export const createThreatDetectionPluginLoader = (cb: PluginFactory) => {
5+
return createConditionalPluginFactory(
6+
() => process.env.WEBINY_FUNCTION_TYPE === "threat-detection-event-handler",
7+
cb
8+
);
9+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./createThreatDetectionEventHandler";
2+
export * from "./createThreatDetectionPluginLoader";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { GuardDutyEvent, ThreatDetectionContext } from "./types";
2+
3+
export const processThreatScanResult = async (
4+
context: ThreatDetectionContext,
5+
eventDetail: GuardDutyEvent
6+
) => {
7+
await context.security.withoutAuthorization(async () => {
8+
try {
9+
const scanStatus = eventDetail.scanResultDetails.scanResultStatus;
10+
const s3Object = eventDetail.s3ObjectDetails;
11+
12+
const [[file]] = await context.fileManager.listFiles({
13+
limit: 1,
14+
where: {
15+
key: s3Object.objectKey
16+
}
17+
});
18+
19+
if (!file) {
20+
return;
21+
}
22+
23+
const allConnections = await context.websockets.listConnections();
24+
25+
if (scanStatus === "NO_THREATS_FOUND") {
26+
const newTags = file.tags.filter(tag => tag !== "threatScanInProgress");
27+
await context.fileManager.updateFile(file.id, {
28+
tags: newTags,
29+
savedBy: file.savedBy
30+
});
31+
32+
await context.websockets.sendToConnections(allConnections, {
33+
action: "fm.threatScan.noThreatFound",
34+
data: {
35+
id: file.id,
36+
tags: newTags
37+
}
38+
});
39+
40+
return;
41+
}
42+
43+
if (scanStatus === "THREATS_FOUND") {
44+
// Delete infected file.
45+
await context.fileManager.deleteFile(file.id);
46+
47+
await context.websockets.sendToConnections(allConnections, {
48+
action: "fm.threatScan.threatDetected",
49+
data: {
50+
id: file.id,
51+
name: file.name
52+
}
53+
});
54+
55+
return;
56+
}
57+
58+
// For all other outcomes, we delete the file, until better logic is implemented.
59+
await context.fileManager.deleteFile(file.id);
60+
61+
await context.websockets.sendToConnections(allConnections, {
62+
action: "fm.threatScan.unsupported",
63+
data: {
64+
id: file.id,
65+
name: file.name
66+
}
67+
});
68+
} catch (e) {
69+
console.log(e.message);
70+
}
71+
});
72+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { Context as IWebsocketsContext } from "@webiny/api-websockets";
2+
import type { WcpContext } from "@webiny/api-wcp/types";
3+
import type { FileManagerContext } from "@webiny/api-file-manager/types";
4+
5+
export type ThreatDetectionContext = FileManagerContext & IWebsocketsContext & WcpContext;
6+
7+
export type GuardDutyEvent = {
8+
scanResultDetails: {
9+
scanResultStatus:
10+
| "UNSUPPORTED"
11+
| "FAILED"
12+
| "ACCESS_DENIED"
13+
| "THREATS_FOUND"
14+
| "NO_THREATS_FOUND";
15+
};
16+
s3ObjectDetails: {
17+
bucketName: string;
18+
objectKey: string;
19+
};
20+
};
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { createAssetDelivery as createBaseAssetDelivery } from "@webiny/api-file-manager";
2+
3+
export type AssetDeliveryParams = Parameters<typeof createBaseAssetDelivery>[0] & {
4+
imageResizeWidths?: number[];
5+
/**
6+
* BE CAREFUL!
7+
* Setting this to more than 1 hour may cause your URLs to still expire before the desired expiration time.
8+
* @see https://repost.aws/knowledge-center/presigned-url-s3-bucket-expiration
9+
*/
10+
presignedUrlTtl?: number;
11+
assetStreamingMaxSize?: number;
12+
};

Diff for: packages/api-file-manager-s3/tsconfig.build.json

+3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
{ "path": "../api/tsconfig.build.json" },
66
{ "path": "../api-file-manager/tsconfig.build.json" },
77
{ "path": "../api-security/tsconfig.build.json" },
8+
{ "path": "../api-wcp/tsconfig.build.json" },
9+
{ "path": "../api-websockets/tsconfig.build.json" },
810
{ "path": "../aws-sdk/tsconfig.build.json" },
911
{ "path": "../error/tsconfig.build.json" },
1012
{ "path": "../handler/tsconfig.build.json" },
13+
{ "path": "../handler-aws/tsconfig.build.json" },
1114
{ "path": "../handler-graphql/tsconfig.build.json" },
1215
{ "path": "../plugins/tsconfig.build.json" },
1316
{ "path": "../tasks/tsconfig.build.json" },

Diff for: packages/api-file-manager-s3/tsconfig.json

+9
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
{ "path": "../api" },
66
{ "path": "../api-file-manager" },
77
{ "path": "../api-security" },
8+
{ "path": "../api-wcp" },
9+
{ "path": "../api-websockets" },
810
{ "path": "../aws-sdk" },
911
{ "path": "../error" },
1012
{ "path": "../handler" },
13+
{ "path": "../handler-aws" },
1114
{ "path": "../handler-graphql" },
1215
{ "path": "../plugins" },
1316
{ "path": "../tasks" },
@@ -27,12 +30,18 @@
2730
"@webiny/api-file-manager": ["../api-file-manager/src"],
2831
"@webiny/api-security/*": ["../api-security/src/*"],
2932
"@webiny/api-security": ["../api-security/src"],
33+
"@webiny/api-wcp/*": ["../api-wcp/src/*"],
34+
"@webiny/api-wcp": ["../api-wcp/src"],
35+
"@webiny/api-websockets/*": ["../api-websockets/src/*"],
36+
"@webiny/api-websockets": ["../api-websockets/src"],
3037
"@webiny/aws-sdk/*": ["../aws-sdk/src/*"],
3138
"@webiny/aws-sdk": ["../aws-sdk/src"],
3239
"@webiny/error/*": ["../error/src/*"],
3340
"@webiny/error": ["../error/src"],
3441
"@webiny/handler/*": ["../handler/src/*"],
3542
"@webiny/handler": ["../handler/src"],
43+
"@webiny/handler-aws/*": ["../handler-aws/src/*"],
44+
"@webiny/handler-aws": ["../handler-aws/src"],
3645
"@webiny/handler-graphql/*": ["../handler-graphql/src/*"],
3746
"@webiny/handler-graphql": ["../handler-graphql/src"],
3847
"@webiny/plugins/*": ["../plugins/src/*"],

0 commit comments

Comments
 (0)