Skip to content

Commit

Permalink
Audit log support (#806)
Browse files Browse the repository at this point in the history
* Send audit log event to audit service when deployment is success

* remove test endpoint

* Minor code fixes and unit tests

* Update service endpoints
Added unit tests for 100% coverage

* adding deploy and undeploy activity event

* adding deploy and undeploy activity event

* resetting accidental changes

* added deploy undeploy for actions and web assets

* final changes

* added more tests

* added lint

* added lint

* fixed failing test cases

* 100% code coverage added

---------

Co-authored-by: Amulya Kashyap <[email protected]>
Co-authored-by: Shazron Abdullah <[email protected]>
  • Loading branch information
3 people authored Oct 3, 2024
1 parent 26e8172 commit 32fdd10
Show file tree
Hide file tree
Showing 6 changed files with 715 additions and 10 deletions.
31 changes: 26 additions & 5 deletions src/commands/app/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ const BaseCommand = require('../../BaseCommand')
const BuildCommand = require('./build')
const webLib = require('@adobe/aio-lib-web')
const { Flags } = require('@oclif/core')
const { createWebExportFilter, runInProcess, buildExtensionPointPayloadWoMetadata, buildExcShellViewExtensionMetadata } = require('../../lib/app-helper')
const { createWebExportFilter, runInProcess, buildExtensionPointPayloadWoMetadata, buildExcShellViewExtensionMetadata, getCliInfo } = require('../../lib/app-helper')
const rtLib = require('@adobe/aio-lib-runtime')
const LogForwarding = require('../../lib/log-forwarding')
const { sendAuditLogs, getAuditLogEvent, getFilesCountWithExtension } = require('../../lib/audit-logger')

const PRE_DEPLOY_EVENT_REG = 'pre-deploy-event-reg'
const POST_DEPLOY_EVENT_REG = 'post-deploy-event-reg'
Expand Down Expand Up @@ -51,12 +52,11 @@ class Deploy extends BuildCommand {

try {
const aioConfig = (await this.getFullConfig()).aio
const cliDetails = await getCliInfo()

// 1. update log forwarding configuration
// note: it is possible that .aio file does not exist, which means there is no local lg config
if (aioConfig?.project?.workspace &&
flags['log-forwarding-update'] &&
flags.actions) {
if (aioConfig?.project?.workspace && flags['log-forwarding-update'] && flags.actions) {
spinner.start('Updating log forwarding configuration')
try {
const lf = await LogForwarding.init(aioConfig)
Expand Down Expand Up @@ -92,14 +92,30 @@ class Deploy extends BuildCommand {
}
}

// 3. deploy actions and web assets for each extension
// 3. send deploy log event
const logEvent = getAuditLogEvent(flags, aioConfig.project, 'AB_APP_DEPLOY')
if (logEvent) {
await sendAuditLogs(cliDetails.accessToken, logEvent, cliDetails.env)
} else {
this.log(chalk.red(chalk.bold('Warning: No valid config data found to send audit log event for deployment.')))
}

// 4. deploy actions and web assets for each extension
// Possible improvements:
// - parallelize
// - break into smaller pieces deploy, allowing to first deploy all actions then all web assets
for (let i = 0; i < keys.length; ++i) {
const k = keys[i]
const v = values[i]
await this.deploySingleConfig(k, v, flags, spinner)
if (v.app.hasFrontend && flags['web-assets']) {
const opItems = getFilesCountWithExtension(v.web.distProd)
const assetDeployedLogEvent = getAuditLogEvent(flags, aioConfig.project, 'AB_APP_ASSETS_DEPLOYED')
if (assetDeployedLogEvent) {
assetDeployedLogEvent.data.opItems = opItems
await sendAuditLogs(cliDetails.accessToken, assetDeployedLogEvent, cliDetails.env)
}
}
}

// 4. deploy extension manifest
Expand Down Expand Up @@ -204,6 +220,11 @@ class Deploy extends BuildCommand {
} else {
deployedFrontendUrl = await webLib.deployWeb(config, onProgress)
spinner.succeed(chalk.green(message))
const filesLogCount = getFilesCountWithExtension(config.web.distProd)
const filesDeployedMessage = `All static assets for the App Builder application in workspace: ${name} were successfully deployed to the CDN. Files deployed :`
const filesLogFormatted = filesLogCount?.map(file => ` • ${file}`).join('')
const finalMessage = chalk.green(`${filesDeployedMessage}\n${filesLogFormatted}`)
spinner.succeed(chalk.green(finalMessage))
}
} catch (err) {
spinner.fail(chalk.green(message))
Expand Down
23 changes: 20 additions & 3 deletions src/commands/app/undeploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ const { Flags } = require('@oclif/core')

const BaseCommand = require('../../BaseCommand')
const webLib = require('@adobe/aio-lib-web')
const { runInProcess, buildExtensionPointPayloadWoMetadata } = require('../../lib/app-helper')
const { runInProcess, buildExtensionPointPayloadWoMetadata, getCliInfo } = require('../../lib/app-helper')
const rtLib = require('@adobe/aio-lib-runtime')
const { sendAuditLogs, getAuditLogEvent } = require('../../lib/audit-logger')

class Undeploy extends BaseCommand {
async run () {
Expand All @@ -44,14 +45,29 @@ class Undeploy extends BaseCommand {

const spinner = ora()
try {
const aioConfig = (await this.getFullConfig()).aio
const cliDetails = await getCliInfo()
const logEvent = getAuditLogEvent(flags, aioConfig.project, 'AB_APP_UNDEPLOY')

// 1.1. send audit log event for successful undeploy
if (logEvent) {
await sendAuditLogs(cliDetails.accessToken, logEvent, cliDetails.env)
} else {
this.log(chalk.red(chalk.bold('Warning: No valid config data found to send audit log event for deployment.')))
}

for (let i = 0; i < keys.length; ++i) {
const k = keys[i]
const v = values[i]
await this.undeployOneExt(k, v, flags, spinner)
const assetUndeployLogEvent = getAuditLogEvent(flags, aioConfig.project, 'AB_APP_ASSETS_UNDEPLOYED')
if (assetUndeployLogEvent) {
await sendAuditLogs(cliDetails.accessToken, assetUndeployLogEvent, cliDetails.env)
}
}
// 2. unpublish extension manifest

// 1.2. unpublish extension manifest
if (flags.unpublish && !(keys.length === 1 && keys[0] === 'application')) {
const aioConfig = (await this.getFullConfig()).aio
const payload = await this.unpublishExtensionPoints(libConsoleCLI, undeployConfigs, aioConfig, flags['force-unpublish'])
this.log(chalk.blue(chalk.bold(`New Extension Point(s) in Workspace '${aioConfig.project.workspace.name}': '${Object.keys(payload.endpoints)}'`)))
} else {
Expand Down Expand Up @@ -110,6 +126,7 @@ class Undeploy extends BaseCommand {
if (!script) {
await webLib.undeployWeb(config, onProgress)
}

spinner.succeed(chalk.green(`Un-Deploying web assets for ${extName}`))
} catch (err) {
spinner.fail(chalk.green(`Un-Deploying web assets for ${extName}`))
Expand Down
144 changes: 144 additions & 0 deletions src/lib/audit-logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
Copyright 2024 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
const fetch = require('node-fetch')
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')

const OPERATIONS = {
AB_APP_DEPLOY: 'ab_app_deploy',
AB_APP_UNDEPLOY: 'ab_app_undeploy',
AB_APP_TEST: 'ab_app_test', // todo : remove after testing
AB_APP_ASSETS_DEPLOYED: 'ab_app_assets_deployed',
AB_APP_ASSETS_UNDEPLOYED: 'ab_app_assets_undeployed'
}

const AUDIT_SERVICE_ENPOINTS = {
stage: 'https://adp-auditlog-service-stage.adobeioruntime.net/api/v1/web/audit-log-api/event-post',
prod: 'https://adp-auditlog-service-prod.adobeioruntime.net/api/v1/web/audit-log-api/event-post'
}

/**
* Send audit log events to audit service
* @param {string} accessToken valid access token
* @param {object} logEvent logEvent details
* @param {string} env valid env stage|prod
*/
async function sendAuditLogs (accessToken, logEvent, env = 'prod') {
const url = AUDIT_SERVICE_ENPOINTS[env]
const payload = {
event: logEvent
}
const options = {
method: 'POST',
headers: {
Authorization: 'Bearer ' + accessToken,
'Content-type': 'application/json'
},
body: JSON.stringify(payload)
}
const response = await fetch(url, options)
if (response.status !== 200) {
const err = await response.text()
throw new Error('Failed to send audit log - ' + response.status + ' ' + err)
}
}

/**
*
* @param {object} flags cli flags
* @param {object} project details
* @param {string} event log name
* @returns {object} logEvent
*/
function getAuditLogEvent (flags, project, event) {
let logEvent, logStrMsg
if (project && project.org && project.workspace) {
if (event === 'AB_APP_DEPLOY') {
logStrMsg = `Starting deployment for the App Builder application in workspace ${project.workspace.name}`
} else if (event === 'AB_APP_UNDEPLOY') {
logStrMsg = `Starting undeployment for the App Builder application in workspace ${project.workspace.name}`
} else if (event === 'AB_APP_ASSETS_UNDEPLOYED') {
logStrMsg = `All static assets for the App Builder application in workspace: ${project.workspace.name} were successfully undeployed from the CDN`
} else if (event === 'AB_APP_ASSETS_DEPLOYED') {
logStrMsg = `All static assets for the App Builder application in workspace: ${project.workspace.name} were successfully deployed to the CDN.\n Files deployed - `
}

logEvent = {
orgId: project.org.id,
projectId: project.id,
workspaceId: project.workspace.id,
workspaceName: project.workspace.name,
operation: event in OPERATIONS ? OPERATIONS[event] : OPERATIONS.AB_APP_TEST,
timestamp: new Date().valueOf(),
data: {
cliCommandFlags: flags,
opDetailsStr: logStrMsg
}
}
}
return logEvent
}

/**
*
* @param {string} directory | path to assets directory
* @returns {Array} log | array of log messages
*/
function getFilesCountWithExtension (directory) {
const log = []

if (!fs.existsSync(directory)) {
this.log(chalk.red(chalk.bold(`Error: Directory ${directory} does not exist.`)))
return log
}

const files = fs.readdirSync(directory)

if (files.length === 0) {
this.log(chalk.red(chalk.bold(`Error: No files found in directory ${directory}.`)))
return log
}

const fileTypeCounts = {}

files.forEach(file => {
const ext = path.extname(file).toLowerCase() || 'no extension'
if (fileTypeCounts[ext]) {
fileTypeCounts[ext]++
} else {
fileTypeCounts[ext] = 1
}
})

Object.keys(fileTypeCounts).forEach(ext => {
const count = fileTypeCounts[ext]
let description

if (ext === '.js') description = 'Javascript file(s)'
else if (ext === '.css') description = 'CSS file(s)'
else if (ext === '.html') description = 'HTML page(s)'
else if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'].includes(ext)) description = 'image(s)'
else if (ext === 'no extension') description = 'file(s) without extension'
else description = `${ext} file(s)`

log.push(`${count} ${description}\n`)
})

return log
}

module.exports = {
sendAuditLogs,
getAuditLogEvent,
AUDIT_SERVICE_ENPOINTS,
getFilesCountWithExtension
}
Loading

0 comments on commit 32fdd10

Please sign in to comment.