feat: add fix dialog
This commit is contained in:
@ -1,3 +0,0 @@
|
||||
:root {
|
||||
user-select: none;
|
||||
}
|
@ -55,11 +55,9 @@ export class BPManager {
|
||||
}
|
||||
}
|
||||
|
||||
public async runCheck() {
|
||||
for (const name in this.bpSets) {
|
||||
this.bpSetMetadatas[name].status = 'CHECKING'
|
||||
|
||||
const result = await this.bpSets[name].check()
|
||||
public runCheckOnce(name: string) {
|
||||
return this
|
||||
.bpSets[name].check()
|
||||
.catch((err) => {
|
||||
this.bpSetMetadatas[name].status = 'ERROR'
|
||||
this.bpSetMetadatas[name].errorMessage.push({
|
||||
@ -69,14 +67,55 @@ export class BPManager {
|
||||
|
||||
return undefined
|
||||
})
|
||||
|
||||
.then((result) => {
|
||||
if (result === undefined)
|
||||
continue
|
||||
return
|
||||
|
||||
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) =>
|
||||
|
@ -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<string, any>()
|
||||
|
||||
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()
|
||||
}
|
||||
|
@ -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,23 +34,86 @@ 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 async runCheckOnce(req: Request, res: Response) {
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
res.setHeader('Transfer-Encoding', 'chunked')
|
||||
|
||||
private runCheck(_: Request, res: Response) {
|
||||
void this.bpManager.runCheck()
|
||||
res.redirect('/')
|
||||
res.write('<script>setInterval(() => window.scrollTo(0, document.body.scrollHeight), 100)</script>')
|
||||
res.write('<pre>Start Checking....\n')
|
||||
|
||||
const { name, hidePass } = req.query
|
||||
if (typeof name !== 'string' || name.length < 1) {
|
||||
res.write('<a href="/">Failed. name not found. Return to Report Page')
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
Memorizer.reset()
|
||||
await this.bpManager.runCheckOnce(name)
|
||||
|
||||
res.write(`<a href="/?hidePass=${hidePass}">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('<script>setInterval(() => window.scrollTo(0, document.body.scrollHeight), 100)</script>')
|
||||
res.write('<pre>Start Checking....\n')
|
||||
|
||||
Memorizer.reset()
|
||||
await this.bpManager.runCheckAll((name) =>
|
||||
res.write(`${name} - FINISHED\n`))
|
||||
|
||||
res.write(`<a href="/?hidePass=${hidePass}">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('<pre>Start Fixing....\n')
|
||||
|
||||
const { name, hidePass } = req.query
|
||||
if (typeof name !== 'string' || name.length < 1) {
|
||||
res.write('<a href="/">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(`<a href="/?hidePass=${hidePass}">Done. Return to Report Page`)
|
||||
res.end()
|
||||
}
|
||||
|
||||
private error404 (_: Request, res: Response) {
|
||||
|
@ -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.`
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary">Fix</button>
|
||||
<button type="button" class="btn btn-secondary">Recheck</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="offcanvas" data-bs-target="#fixdialog-<%= metadata.idx %>">Fix</button>
|
||||
<a href="/check?name=<%= metadata.name %>&hidePass=<% hidePass %>" type="button" class="btn btn-secondary">Recheck</a>
|
||||
<button type="button" class="btn btn-secondary" data-bs-toggle="collapse" data-bs-target="#detail-<%= metadata.idx %>">Details</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-toggle="collapse" data-bs-target="#logs-<%= metadata.idx %>">Logs</button>
|
||||
</div>
|
||||
|
48
views/partial/bpset_fixdialog.ejs
Normal file
48
views/partial/bpset_fixdialog.ejs
Normal file
@ -0,0 +1,48 @@
|
||||
<div class="offcanvas offcanvas-end" data-bs-scroll="true" tabindex="-1" id="fixdialog-<%= metadata.idx %>">
|
||||
<div class="offcanvas-header d-flex gap-3">
|
||||
<h3 class="offcanvas-title" style="word-break: break-all;">Fixing <%= metadata.name %></h3>
|
||||
</div>
|
||||
|
||||
<div class="offcanvas-body">
|
||||
<h3 class="mb-3">Pending operations</h3>
|
||||
<% metadata.commandUsedInFixFunction.forEach(({ name, reason }) => { %>
|
||||
<div class="card mb-3">
|
||||
<h5 class="card-header"><%= name %></h5>
|
||||
<div class="card-body">
|
||||
<%= reason %>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
|
||||
<br>
|
||||
|
||||
<h3 class="mb-3">Required Parameters</h3>
|
||||
<form method="POST" action="/fix?name=<%= metadata.name %>&hidePass=<%= hidePass %>">
|
||||
<% metadata.requiredParametersForFix.forEach((input) => { %>
|
||||
<div class="mb-3">
|
||||
<label for="<%= `${metadata.name}-${input.name}` %>" class="form-label"><%= input.name %></label>
|
||||
<input type="text" class="form-control" name="<%= input.name %>" id="<%= `${metadata.name}-${input.name}` %>" value="<%= input.default %>" required>
|
||||
<div class="form-text">
|
||||
<%= input.description %><br />
|
||||
ex) <code><%= input.example %></code>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<br>
|
||||
|
||||
<% if (metadata.isFixFunctionUsesDestructiveCommand) { %>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
This Fix Function Has DESTRUCTIVE Command! please review pending operations carefully!
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<%= metadata.adviseBeforeFixFunction %>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<button class="btn btn-primary" type="submit">Fix!</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -17,3 +17,5 @@
|
||||
<%- include('./bpset_progress.ejs', { metadata }) %>
|
||||
<%- include('./bpset_actions.ejs', { metadata }) %>
|
||||
</tr>
|
||||
|
||||
<%- include('./bpset_fixdialog.ejs', { metadata }) %>
|
||||
|
@ -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
|
||||
%>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-end">
|
||||
<div>
|
||||
<h1 class="fw-bold">BPSets (<%= bpLength %>)</h1>
|
||||
<p>Created by Minhyeok Park</p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex">
|
||||
<div class="d-flex status gap-3 text-center">
|
||||
<div>
|
||||
<p>Pass</p>
|
||||
<p><%= %></p>
|
||||
<p class="m-0">Pass</p>
|
||||
<p class="fs-3"><%= passCount %></p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Fail</p>
|
||||
<p class="m-0">Fail</p>
|
||||
<p class="fs-3"><%= failCount %></p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Error</p>
|
||||
<p class="m-0">Error</p>
|
||||
<p class="fs-3"><%= errorCount %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<a href="/check_all" type="button" class="btn btn-primary">Check All</a>
|
||||
<a href="/check_all?hidePass=<% hidePass %>" type="button" class="btn btn-primary">Check All</a>
|
||||
<% if (hidePass) { %>
|
||||
<a href="/?hidePass=false" type="button" class="btn btn-secondary">Show Pass</a>
|
||||
<% } else { %>
|
||||
<a href="/?hidePass=true" type="button" class="btn btn-secondary">Hide Pass</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user