solution/solution-compliance-audit-for-data-plane/source/function/ecs-patch-baseline/index.js (293 lines of code) (raw):
'use strict';
const RPCClient = require('@alicloud/pop-core').RPCClient;
const _ = require("lodash");
const httpModule = require('https');
// 配置审计地域
// 根据账号归属站点的不同,选择不同地域:
// 1. 中国站:cn-shanghai
// 2. International:ap-southeast-1
const CONFIG_SERVICE_REGION = 'cn-shanghai';
//合规
const COMPLIANCE_TYPE_COMPLIANT = 'COMPLIANT';
//不合规
const COMPLIANCE_TYPE_NON_COMPLIANT = 'NON_COMPLIANT';
//不适用
const COMPLIANCE_TYPE_NOT_APPLICABLE = 'NOT_APPLICABLE';
const keepAliveAgent = new httpModule.Agent({
keepAlive: false,
});
const requestOption = {
method: 'POST',
formatParams: false,
timeout: 10000,
agent: keepAliveAgent,
};
exports.handler = (event, context, callback) => {
const params = JSON.parse(event.toString());
main(params, context)
.then(() => {
callback(null);
})
.catch((err) => callback(err));
};
async function main(eventParams, context) {
const { logger } = context;
const {
invokingEvent: {
configurationItem
},
ruleParameters: {
tagScopes
}
} = eventParams;
if (!configurationItem) {
logger.error(`There is no configurationItem in invokingEvent. Params is ${JSON.stringify(eventParams)}`);
return;
}
const { tags, resourceId, accountId, regionId } = configurationItem;
logger.info(`Start evaluating for resource ${resourceId} of account ${accountId} in region ${regionId}`);
//校验资源标签是否在要检测的范围内
if (tagScopes) {
const allowedTags = JSON.parse(tagScopes);
if (!tags) {
logger.info(`Resource ${resourceId} don't need to evaluate`);
return;
}
const resourceTags = JSON.parse(tags);
var needEvaluate = false;
for (let i = 0; i < allowedTags.length; i++) {
if (resourceTags[allowedTags[i].TagKey] != null && resourceTags[allowedTags[i].TagKey].indexOf(allowedTags[i].TagValue) > -1) {
needEvaluate = true;
break;
}
}
//忽略资源不在需要巡检的范围内的资源
if (needEvaluate === false) {
logger.info(`Resource ${resourceId} don't need to evaluate`);
return;
}
}
// 构造 oos 服务的 client
const client = await getOosClient(eventParams, context);
// 根据 oos 补丁基线进行扫描
const {
Execution: {
ExecutionId: executionId,
}
} = await startExecution(configurationItem, client);
let execution;
while (true) {
execution = await getExecution(executionId, client);
if (execution == null) {
throw new Error(`The specified oos execution ${executionId} does not exist.`);
}
const { Status, StatusReason } = execution;
switch (Status) {
case 'Failed':
logger.error(`The specified oos execution ${executionId} failed. Reason is ${StatusReason}.`);
throw new Error(`The specified oos execution ${executionId} failed.`);
case 'Cancelled':
logger.error(`The specified oos execution ${executionId} has been cancelled.`);
return;
case 'Success':
// 提交自定义函数规则的评估结果
const {complianceType, annotation} = await getEvaluationResult(configurationItem, client, context);
await putEvaluationResult(complianceType, annotation, eventParams, context);
return;
}
await sleep(15000);
}
}
async function getOosClient(eventParams, context) {
const { regionId, accountId } = eventParams.invokingEvent.configurationItem;
const { credentials } = context;
// Assume Role 到需要检测的目标账号
const stsClient = new RPCClient({
accessKeyId: credentials.accessKeyId,
accessKeySecret: credentials.accessKeySecret,
securityToken: credentials.securityToken,
endpoint: `https://sts.${regionId}.aliyuncs.com`,
apiVersion: '2015-04-01',
});
const accountCredentials = await stsClient.request(
'AssumeRole',
{
RegionId: regionId,
RoleArn: `acs:ram::${accountId}:role/${eventParams.ruleParameters.configFcExecutionRoleName}`,
RoleSessionName: 'EcsPatchBaselineInspection',
},
requestOption
);
// 构造 oos 服务的 client
const oosClient = new RPCClient({
accessKeyId: accountCredentials.Credentials.AccessKeyId,
accessKeySecret: accountCredentials.Credentials.AccessKeySecret,
securityToken: accountCredentials.Credentials.SecurityToken,
endpoint: `https://oos.${regionId}.aliyuncs.com`,
apiVersion: '2019-06-01',
});
return oosClient;
}
async function startExecution(configurationItem, client) {
const { regionId, resourceId } = configurationItem;
return await client.request(
'StartExecution',
{
TemplateName: 'ACS-ECS-BulkyApplyPatchBaseline',
Mode: 'Automatic',
LoopMode: 'Automatic',
SafetyCheck: 'Skip',
Parameters: JSON.stringify({
rebootIfNeed: false,
OOSAssumeRole: '',
regionId: regionId,
action: 'scan',
rateControl: {
MaxErrors: 0,
Concurrency: 1,
Mode: 'Concurrency',
},
whetherCreateSnapshot: false,
targets: {
Type: 'ResourceIds',
ResourceIds: [resourceId],
RegionId: regionId,
},
resourceType: 'ALIYUN::ECS::Instance',
}),
},
requestOption
);
}
async function getExecution(executionId, client) {
const executions = await client.request(
'ListExecutions',
{
ExecutionId: executionId,
},
requestOption
);
return _.get(executions, 'Executions.0', null);
}
async function getEvaluationResult(configurationItem, client, context) {
const { resourceId } = configurationItem;
const { logger } = context;
// 获取实例补丁状态
const patchStates = await client.request(
'ListInstancePatchStates',
{
InstanceIds: JSON.stringify([resourceId]),
},
requestOption
);
const patchState = _.get(patchStates, 'InstancePatchStates.0', null);
if (patchState == null) {
logger.error(`The patch info of instance ${resourceId} is empty.`);
return;
}
let complianceType = '';
let annotation = {};
const {
MissingCount = 0,
FailedCount = 0,
InstalledPendingRebootCount = 0,
InstalledRejectedCount = 0,
} = patchState;
if (MissingCount == 0 && FailedCount == 0 && InstalledPendingRebootCount == 0 && InstalledRejectedCount == 0) {
complianceType = COMPLIANCE_TYPE_COMPLIANT;
} else {
complianceType = COMPLIANCE_TYPE_NON_COMPLIANT;
// 获取详细补丁信息
const configuration = {
missingCount: MissingCount,
failedCount: FailedCount,
installedPendingRebootCount: InstalledPendingRebootCount,
installedRejectedCount: InstalledRejectedCount,
missingPatches: [],
failedPatches: [],
installedPendingRebootPatches: [],
installedRejectedPatches: [],
};
const patches = await listInstancePatches(resourceId, client);
for (const patch of patches) {
switch (patch.Status) {
case 'Missing':
configuration.missingPatches.push(patch);
break;
case 'InstalledPendingReboot':
configuration.installedPendingRebootPatches.push(patch);
break;
case 'Failed':
configuration.failedPatches.push(patch);
break;
case 'InstalledRejected':
configuration.installedRejectedPatches.push(patch);
break;
}
}
annotation = {
reason: `Not Installed: ${MissingCount}; Pending Restart: ${InstalledPendingRebootCount}; Install failed: ${FailedCount}; Installed Rejected Patch: ${InstalledRejectedCount};`,
configuration: JSON.stringify(configuration),
};
}
return {
complianceType,
annotation,
};
}
async function listInstancePatches(resourceId, client) {
let patches = [];
let nextToken = '';
while (true) {
const res = await client.request(
'ListInstancePatches',
{
InstanceId: resourceId,
PatchStatuses: JSON.stringify([
'Missing',
'InstalledPendingReboot',
'Failed',
'InstalledRejected',
]),
MaxResults: 100,
NextToken: nextToken,
},
requestOption
);
const _patches = _.get(res, 'Patches', []);
if (_.isEmpty(_patches)) {
break;
}
patches = patches.concat(_patches);
nextToken = _.get(res, 'NextToken', '');
if (nextToken == null || nextToken == '') {
break;
}
}
return patches;
}
async function putEvaluationResult(complianceType, annotation, eventParams, context) {
const {
invokingEvent: {
accountId,
configurationItem: { regionId, resourceId, resourceType },
},
resultToken,
orderingTimestamp,
} = eventParams;
const client = getConfigClient(context);
return client.request('PutEvaluations', {
ResultToken: resultToken,
Evaluations: JSON.stringify([
{
accountId,
annotation: JSON.stringify(annotation || {}),
complianceResourceId: resourceId,
complianceResourceType: resourceType,
complianceRegionId: regionId,
complianceType,
orderingTimestamp,
},
]),
//启用删除模式
DeleteMode: true
}, requestOption);
}
function getConfigClient(context) {
const { credentials } = context;
return new RPCClient({
accessKeyId: credentials.accessKeyId,
accessKeySecret: credentials.accessKeySecret,
securityToken: credentials.securityToken,
endpoint: `https://config.${CONFIG_SERVICE_REGION}.aliyuncs.com`,
apiVersion: '2020-09-07',
});
}
function sleep(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
});
}