From 6cbeecea87c8cb566ba782d16976eeab818baf07 Mon Sep 17 00:00:00 2001 From: Minhyeok Park Date: Mon, 30 Dec 2024 11:19:53 +0900 Subject: [PATCH] feat: add fix dialog --- public/style.css | 3 - src/BPManager.ts | 77 ++++++++++---- src/Memorizer.ts | 12 ++- src/WebServer.ts | 90 ++++++++++++++-- .../IAMPolicyNoStatementsWithFullAccess.ts | 100 +++++++++--------- views/partial/bpset_actions.ejs | 4 +- views/partial/bpset_fixdialog.ejs | 48 +++++++++ views/partial/bpset_item.ejs | 2 + views/partial/page_header.ejs | 31 ++++-- 9 files changed, 275 insertions(+), 92 deletions(-) delete mode 100644 public/style.css create mode 100644 views/partial/bpset_fixdialog.ejs diff --git a/public/style.css b/public/style.css deleted file mode 100644 index 7ba0792..0000000 --- a/public/style.css +++ /dev/null @@ -1,3 +0,0 @@ -:root { - user-select: none; -} diff --git a/src/BPManager.ts b/src/BPManager.ts index 3f31ea3..1dd2c5b 100644 --- a/src/BPManager.ts +++ b/src/BPManager.ts @@ -55,28 +55,67 @@ export class BPManager { } } - public async runCheck() { - for (const name in this.bpSets) { - this.bpSetMetadatas[name].status = 'CHECKING' - - const result = await this.bpSets[name].check() - .catch((err) => { - this.bpSetMetadatas[name].status = 'ERROR' - this.bpSetMetadatas[name].errorMessage.push({ - date: new Date(), - message: err - }) - - return undefined + public runCheckOnce(name: string) { + return this + .bpSets[name].check() + .catch((err) => { + this.bpSetMetadatas[name].status = 'ERROR' + this.bpSetMetadatas[name].errorMessage.push({ + date: new Date(), + message: err }) - if (result === undefined) - continue + return undefined + }) + .then((result) => { + if (result === undefined) + return - this.bpSetMetadatas[name].compliantResources = result.compliantResources - this.bpSetMetadatas[name].nonCompliantResources = result.nonCompliantResources - this.bpSetMetadatas[name].status = 'FINISHED' - } + this.bpSetMetadatas[name].compliantResources = result.compliantResources + this.bpSetMetadatas[name].nonCompliantResources = result.nonCompliantResources + this.bpSetMetadatas[name].status = 'FINISHED' + }) + } + + public runCheckAll(finished = (name: string) => {}) { + const checkJobs = + Object + .values(this.bpSetMetadatas) + .map(({ name }) => { + this.bpSetMetadatas[name].status = 'CHECKING' + + return this + .bpSets[name].check() + .catch((err) => { + this.bpSetMetadatas[name].status = 'ERROR' + this.bpSetMetadatas[name].errorMessage.push({ + date: new Date(), + message: err + }) + + return undefined + }) + .then((result) => { + if (result === undefined) + return + + this.bpSetMetadatas[name].compliantResources = result.compliantResources + this.bpSetMetadatas[name].nonCompliantResources = result.nonCompliantResources + this.bpSetMetadatas[name].status = 'FINISHED' + finished(name) + }) + }) + + return Promise.all(checkJobs) + } + + public runFix(name: string, requiredParametersForFix: { name: string, value: string }[]) { + return this + .bpSets[name] + .fix( + this.bpSetMetadatas[name].nonCompliantResources, + requiredParametersForFix + ) } public readonly getBPSet = (name: string) => diff --git a/src/Memorizer.ts b/src/Memorizer.ts index 8390b8e..082f6f8 100644 --- a/src/Memorizer.ts +++ b/src/Memorizer.ts @@ -15,7 +15,7 @@ import shajs from 'sha.js' export class Memorizer { private static memorized = new Map() - public static memo (client: Client, salt = '') { + public static memo(client: Client, salt = '') { const serialized = JSON.stringify([client.constructor.name, salt]) const hashed = shajs('sha256').update(serialized).digest('hex') @@ -30,6 +30,11 @@ export class Memorizer { return newMemo } + public static reset() { + for (const memorized of Memorizer.memorized.values()) + memorized.reset() + } + private memorized = new Map() private constructor ( @@ -44,9 +49,14 @@ export class Memorizer { if (memorized !== undefined) return memorized + console.log(command.constructor.name, 'Executed.') + const newMemo = await this.client.send(command) this.memorized.set(hashed, newMemo) return newMemo } + + private readonly reset = () => + this.memorized.clear() } diff --git a/src/WebServer.ts b/src/WebServer.ts index 772decc..2b7ae73 100644 --- a/src/WebServer.ts +++ b/src/WebServer.ts @@ -1,6 +1,7 @@ import express, { Request, Response } from 'express' import { BPManager } from './BPManager' import { BPSetMetadata } from './types' +import { Memorizer } from './Memorizer' export class WebServer { private readonly app = express() @@ -12,16 +13,20 @@ export class WebServer { ) { this.app.set('view engine', 'ejs') this.app.set('views', './views'); - this.app.use(express.static('./public')) this.app.get('/', this.getMainPage.bind(this)) - this.app.get('/check_all', this.runCheck.bind(this)) + this.app.get('/check', this.runCheckOnce.bind(this)) + this.app.get('/check_all', this.runCheckAll.bind(this)) + + this.app.use('/fix', express.urlencoded()) + this.app.post('/fix', this.runFix.bind(this)) + this.app.use(this.error404) this.app.listen(this.port, this.showBanner.bind(this)) } - private getMainPage(_: Request, res: Response) { + private getMainPage(req: Request, res: Response) { const bpStatus: { category: string, metadatas: BPSetMetadata[] @@ -29,30 +34,93 @@ export class WebServer { const bpMetadatas = this.bpManager.getBPSetMetadatas() const categories = new Set(bpMetadatas.map((v) => v?.awsService)) + const hidePass = req.query['hidePass'] === 'true' for (const category of categories) bpStatus.push({ category, - metadatas: bpMetadatas.filter((v) => v.awsService === category) + metadatas: bpMetadatas.filter((v) => + v.awsService === category && + (!hidePass || v.nonCompliantResources.length > 0)) }) res.render('index', { - bpStatus, - bpLength: bpMetadatas.length + bpStatus: bpStatus.filter(({ metadatas }) => metadatas.length > 0), + bpLength: bpMetadatas.length, + hidePass }) } - - private runCheck(_: Request, res: Response) { - void this.bpManager.runCheck() - res.redirect('/') + private async runCheckOnce(req: Request, res: Response) { + res.setHeader('Content-Type', 'text/html; charset=utf-8') + res.setHeader('Transfer-Encoding', 'chunked') + + res.write('') + res.write('
Start Checking....\n')
+
+    const { name, hidePass } = req.query
+    if (typeof name !== 'string' || name.length < 1) {
+      res.write('Failed. name not found. Return to Report Page')
+      res.end()
+      return
+    }
+
+    Memorizer.reset()
+    await this.bpManager.runCheckOnce(name)
+
+    res.write(`Done. Return to Report Page`)
+    res.end()
+  }
+
+  private async runCheckAll(req: Request, res: Response) {
+    res.setHeader('Content-Type', 'text/html; charset=utf-8')
+    res.setHeader('Transfer-Encoding', 'chunked')
+
+    const { hidePass } = req.query
+
+    res.write('')
+    res.write('
Start Checking....\n')
+
+    Memorizer.reset()
+    await this.bpManager.runCheckAll((name) =>
+      res.write(`${name} - FINISHED\n`))
+
+    res.write(`Done. Return to Report Page`)
+    res.end()
+  }
+
+  private async runFix(req: Request, res: Response) {
+    res.setHeader('Content-Type', 'text/html; charset=utf-8')
+    res.setHeader('Transfer-Encoding', 'chunked')
+
+    res.write('
Start Fixing....\n')
+    
+    const { name, hidePass } = req.query
+    if (typeof name !== 'string' || name.length < 1) {
+      res.write('Failed. name not found. Return to Report Page')
+      res.end()
+      return
+    }
+
+    const requiredParametersForFix =
+      Object
+        .keys(req.body)
+        .map((k) => ({ name: k, value: req.body[k] }))
+
+    await this.bpManager.runFix(name, requiredParametersForFix)
+      .catch((error) => {
+        res.write(error.toString() + '\n')
+      })
+
+    res.write(`Done. Return to Report Page`)
+    res.end()
   }
 
   private error404 (_: Request, res: Response) {
     res.status(404).send({ success: false, message: 'Page not found' })
   }
 
-  private showBanner () {
+  private showBanner() {
     console.log(`
 
        _______  _______  _______  _______  _______  _______ 
diff --git a/src/bpsets/iam/IAMPolicyNoStatementsWithFullAccess.ts b/src/bpsets/iam/IAMPolicyNoStatementsWithFullAccess.ts
index a9b1460..1683871 100644
--- a/src/bpsets/iam/IAMPolicyNoStatementsWithFullAccess.ts
+++ b/src/bpsets/iam/IAMPolicyNoStatementsWithFullAccess.ts
@@ -1,67 +1,67 @@
-import {
-  IAMClient,
-  ListPoliciesCommand,
-  GetPolicyVersionCommand,
-  DeletePolicyCommand
-} from '@aws-sdk/client-iam'
-import { BPSet } from '../../types'
-import { Memorizer } from '../../Memorizer'
+import { IAMClient, ListPoliciesCommand, GetPolicyVersionCommand } from "@aws-sdk/client-iam";
+import { BPSet } from "../../types";
+import { Memorizer } from "../../Memorizer";
 
-export class IAMPolicyNoStatementsWithAdminAccess implements BPSet {
-  private readonly client = new IAMClient({})
-  private readonly memoClient = Memorizer.memo(this.client)
-
-  private readonly getPolicies = async () => {
-    const response = await this.memoClient.send(new ListPoliciesCommand({ Scope: 'Local' }))
-    return response.Policies || []
-  }
-
-  private readonly getPolicyDefaultVersions = async (policyArn: string, versionId: string) => {
-    const response = await this.memoClient.send(
-      new GetPolicyVersionCommand({ PolicyArn: policyArn, VersionId: versionId })
-    )
-    return response.PolicyVersion!
-  }
+export class IAMPolicyNoStatementsWithFullAccess implements BPSet {
+  private readonly client = new IAMClient({});
+  private readonly memoClient = Memorizer.memo(this.client);
 
   public readonly check = async () => {
-    const compliantResources = []
-    const nonCompliantResources = []
-    const policies = await this.getPolicies()
+    const compliantResources: string[] = [];
+    const nonCompliantResources: string[] = [];
+
+    // Fetch all customer-managed IAM policies
+    const policiesResponse = await this.memoClient.send(
+      new ListPoliciesCommand({ Scope: "Local" })
+    );
+    const policies = policiesResponse.Policies || [];
 
     for (const policy of policies) {
-      const policyVersion = await this.getPolicyDefaultVersions(policy.Arn!, policy.DefaultVersionId!)
+      // Get the default version of the policy
+      const policyVersionResponse = await this.memoClient.send(
+        new GetPolicyVersionCommand({
+          PolicyArn: policy.Arn!,
+          VersionId: policy.DefaultVersionId!,
+        })
+      );
 
-      const policyDocument = JSON.parse(JSON.stringify(policyVersion.Document)) // Parse Document JSON string
-      const statements = Array.isArray(policyDocument.Statement)
-        ? policyDocument.Statement
-        : [policyDocument.Statement]
+      const policyDocument = JSON.parse(
+        decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string)
+      );
 
-      for (const statement of statements) {
-        if (
-          statement.Action === '*' &&
-          statement.Resource === '*' &&
-          statement.Effect === 'Allow'
-        ) {
-          nonCompliantResources.push(policy.Arn!)
-          break
-        }
-      }
+      // Check statements for full access
+      const hasFullAccess = policyDocument.Statement.some((statement: any) => {
+        if (statement.Effect === "Deny") return false;
+        const actions = Array.isArray(statement.Action)
+          ? statement.Action
+          : [statement.Action];
+        return actions.some((action: string) => action.endsWith(":*"));
+      });
 
-      if (!nonCompliantResources.includes(policy.Arn!)) {
-        compliantResources.push(policy.Arn!)
+      if (hasFullAccess) {
+        nonCompliantResources.push(policy.Arn!);
+      } else {
+        compliantResources.push(policy.Arn!);
       }
     }
 
     return {
       compliantResources,
       nonCompliantResources,
-      requiredParametersForFix: []
-    }
-  }
+      requiredParametersForFix: [],
+    };
+  };
 
-  public readonly fix = async (nonCompliantResources: string[]) => {
-    for (const arn of nonCompliantResources) {
-      await this.client.send(new DeletePolicyCommand({ PolicyArn: arn }))
+  public readonly fix = async (
+    nonCompliantResources: string[],
+    requiredParametersForFix: { name: string; value: string }[]
+  ) => {
+    for (const policyArn of nonCompliantResources) {
+      // Add logic to remove or modify the statements with full access
+      // Note: Updating an IAM policy requires creating a new version and setting it as default
+      console.error(
+        `Fix operation is not implemented for policy ${policyArn}. Manual intervention is required.`
+      );
     }
-  }
+  };
 }
diff --git a/views/partial/bpset_actions.ejs b/views/partial/bpset_actions.ejs
index 99b8199..3ec4315 100644
--- a/views/partial/bpset_actions.ejs
+++ b/views/partial/bpset_actions.ejs
@@ -1,7 +1,7 @@
 
   
diff --git a/views/partial/bpset_fixdialog.ejs b/views/partial/bpset_fixdialog.ejs
new file mode 100644
index 0000000..014f21a
--- /dev/null
+++ b/views/partial/bpset_fixdialog.ejs
@@ -0,0 +1,48 @@
+
+
+

Fixing <%= metadata.name %>

+
+ +
+

Pending operations

+ <% metadata.commandUsedInFixFunction.forEach(({ name, reason }) => { %> +
+
<%= name %>
+
+ <%= reason %> +
+
+ <% }) %> + +
+ +

Required Parameters

+
+ <% metadata.requiredParametersForFix.forEach((input) => { %> +
+ + +
+ <%= input.description %>
+ ex) <%= input.example %> +
+
+ <% }) %> +
+ + <% if (metadata.isFixFunctionUsesDestructiveCommand) { %> + + <% } %> + + + +
+ + +
+
+
diff --git a/views/partial/bpset_item.ejs b/views/partial/bpset_item.ejs index 8244793..c85be65 100644 --- a/views/partial/bpset_item.ejs +++ b/views/partial/bpset_item.ejs @@ -17,3 +17,5 @@ <%- include('./bpset_progress.ejs', { metadata }) %> <%- include('./bpset_actions.ejs', { metadata }) %> + +<%- include('./bpset_fixdialog.ejs', { metadata }) %> diff --git a/views/partial/page_header.ejs b/views/partial/page_header.ejs index 06c8ac7..ddddd0f 100644 --- a/views/partial/page_header.ejs +++ b/views/partial/page_header.ejs @@ -1,23 +1,42 @@ +<% + const metadatas = + bpStatus + .map(({ metadatas }) => metadatas) + .flat() + .filter((v) => v.status === 'FINISHED') + + const passCount = metadatas.filter((v) => v.nonCompliantResources.length < 1).length + const failCount = metadatas.filter((v) => v.nonCompliantResources.length > 0).length + const errorCount = metadatas.filter((v) => v.status === 'ERROR').length +%> +

BPSets (<%= bpLength %>)

Created by Minhyeok Park

-
+
-

Pass

-

<%= %>

+

Pass

+

<%= passCount %>

-

Fail

+

Fail

+

<%= failCount %>

-

Error

+

Error

+

<%= errorCount %>

- Check All + Check All + <% if (hidePass) { %> + Show Pass + <% } else { %> + Hide Pass + <% } %>