Skip to content

Commit

Permalink
wip: static report
Browse files Browse the repository at this point in the history
  • Loading branch information
tonylyjones committed Jun 17, 2024
1 parent c493a1c commit c2b1597
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 31 deletions.
6 changes: 3 additions & 3 deletions cli/src/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ import {
import {getConnector, track} from "@azimutt/gateway";
import {version} from "./version.js";
import {loggerNoOp} from "./utils/logger.js";
import {fileExists, fileList, fileReadJson, fileWrite, fileWriteJson, mkParentDirs} from "./utils/file.js";
import { rootHtml } from "./utils/html.js";
import {fileExists, fileList, fileRead, fileReadJson, fileWrite, fileWriteJson, mkParentDirs, __dirname} from "./utils/file.js";

export type Opts = {
folder?: string
Expand Down Expand Up @@ -266,7 +265,8 @@ async function writeHtmlReport(folder: string, rulesByLevel: Record<string, Rule
})}
}, {})

const html = rootHtml(rules)
let html = await fileRead('./src/report.html')
html = html.replace("const report = []", `const report = ${JSON.stringify(rules)}`)
await fileWrite(path, html)
logger.log(`Analysis report written to ${path}`)
logger.log('')
Expand Down
99 changes: 99 additions & 0 deletions cli/src/report.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Azimutt</title>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/default.min.css" />
<style>
aside {
width: 300px;
height: 100svh;
overflow-y: scroll;
overflow-x: hidden;

.empty {
opacity: 0.35;
}

}

main {
height: 100svh;
overflow-y: scroll;
overflow-x: hidden;
}
</style>
</head>

<body>
<div class="uk-flex uk-width-1-1">
<aside class="uk-padding-small">
<h4 class="uk-h4">Severity</h4>
<ul id="levels">
</ul>
</aside>

<main class="uk-width-1-1 uk-padding-small">
<div class="uk-container">
<div id="list"></div>
</div>
</main>
</div>


<script type="text/javascript">
//const report = [{ "level": "high", "levelViolationsCount": 5, "rules": [{ "rule": "duplicated index", "ruleViolationsCount": 5, "violations": ["Index mfa_factors_user_id_idx on auth.mfa_factors(user_id) can be deleted, it's covered by: factor_id_created_at_idx(user_id, created_at).", "Index refresh_tokens_instance_id_idx on auth.refresh_tokens(instance_id) can be deleted, it's covered by: refresh_tokens_instance_id_user_id_idx(instance_id, user_id).", "Index sessions_user_id_idx on auth.sessions(user_id) can be deleted, it's covered by: user_id_created_at_idx(user_id, created_at)."] }, { "rule": "too slow query", "ruleViolationsCount": 0, "violations": [] }, { "rule": "degrading query", "ruleViolationsCount": 0, "violations": [] }, { "rule": "entity not clean", "ruleViolationsCount": 0, "violations": [] }, { "rule": "missing primary key", "ruleViolationsCount": 0, "violations": [] }, { "rule": "entity without index", "ruleViolationsCount": 0, "violations": [] }, { "rule": "misaligned relation", "ruleViolationsCount": 0, "violations": [] }, { "rule": "attribute not found in relation", "ruleViolationsCount": 0, "violations": [] }, { "rule": "entity not found in relation", "ruleViolationsCount": 0, "violations": [] }] }, { "level": "medium", "levelViolationsCount": 88, "rules": [{ "rule": "unused entity", "ruleViolationsCount": 0, "violations": [] }, { "rule": "unused index", "ruleViolationsCount": 0, "violations": [] }, { "rule": "bad attribute type", "ruleViolationsCount": 0, "violations": [] }, { "rule": "fast growing entity", "ruleViolationsCount": 0, "violations": [] }, { "rule": "fast growing index", "ruleViolationsCount": 0, "violations": [] }, { "rule": "entity too large", "ruleViolationsCount": 2, "violations": ["Entity auth.users has too many attributes (35).", "Entity extensions.pg_stat_statements has too many attributes (43)."] }, { "rule": "entity with too many indexes", "ruleViolationsCount": 0, "violations": [] }, { "rule": "entity with too heavy indexes", "ruleViolationsCount": 15, "violations": ["Entity auth.users has too heavy indexes (10x data size, 11 indexes).", "Entity public.gallery has too heavy indexes (6x data size, 3 indexes).", "Entity public.organizations has too heavy indexes (6x data size, 3 indexes)."] }, { "rule": "business primary key forbidden", "ruleViolationsCount": 3, "violations": ["Entity auth.schema_migrations should have a technical primary key, current one is: (version).", "Entity public.schema_migrations should have a technical primary key, current one is: (version).", "Entity realtime.schema_migrations should have a technical primary key, current one is: (version)."] }, { "rule": "index on relation", "ruleViolationsCount": 26, "violations": ["Create an index on auth.mfa_challenges(factor_id) to improve auth.mfa_challenges(factor_id)->auth.mfa_factors(id) relation.", "Create an index on auth.saml_relay_states(flow_state_id) to improve auth.saml_relay_states(flow_state_id)->auth.flow_state(id) relation.", "Create an index on pgsodium.key(parent_key) to improve pgsodium.key(parent_key)->pgsodium.key(id) relation."] }, { "rule": "missing relation", "ruleViolationsCount": 42, "violations": ["Create a relation from auth.audit_log_entries(instance_id) to auth.instances(id).", "Create a relation from auth.flow_state(user_id) to auth.users(id).", "Create a relation from auth.flow_state(user_id) to public.users(id)."] }] }, { "level": "low", "levelViolationsCount": 0, "rules": [{ "rule": "empty entity", "ruleViolationsCount": 0, "violations": [] }, { "rule": "empty attribute", "ruleViolationsCount": 0, "violations": [] }, { "rule": "inconsistent entity name", "ruleViolationsCount": 0, "violations": [] }, { "rule": "inconsistent attribute name", "ruleViolationsCount": 0, "violations": [] }] }, { "level": "hint", "levelViolationsCount": 57, "rules": [{ "rule": "inconsistent attribute type", "ruleViolationsCount": 17, "violations": ["Attribute id has several types: integer in storage.migrations(id), text in storage.buckets(id) and 1 other, bigint in auth.refresh_tokens(id) and 2 others, uuid in auth.audit_log_entries(id) and 32 others.", "Attribute created_at has several types: timestamp without time zone in auth.one_time_tokens(created_at) and 14 others, timestamp with time zone in auth.audit_log_entries(created_at) and 19 others.", "Attribute ip_address has several types: character varying(64) in auth.audit_log_entries(ip_address), inet in auth.mfa_challenges(ip_address)."] }, { "rule": "expensive query", "ruleViolationsCount": 20, "violations": ["Query 1374137181295181600 is one of the most expensive, cumulated 5085 ms exec time in 46 executions (SELECT name FROM pg_timezone_names)", "Query -156763288877666600 on pg_type is one of the most expensive, cumulated 1146 ms exec time in 46 executions ( WITH base_types AS ( WITH RECURSIVE recurse AS ( SELECT oid, typbasetype, COALESCE(NULLIF(typbasetype, $3), oid) AS base FROM pg_type UNION SELECT t.oid, b.typbasetype, COALESCE(NULLIF(b.typbasety...)", "Query 8014584162185131000 on pg_attribute is one of the most expensive, cumulated 393 ms exec time in 46 executions (WITH columns AS ( SELECT nc.nspname::name AS table_schema, c.relname::name AS table_name, a.attname::name AS column_name, d.description AS description, CASE WHEN t.typbasetype != $2 THEN pg_get_ex...)"] }, { "rule": "query with high variation", "ruleViolationsCount": 20, "violations": ["Query 1374137181295181600 has high variation, with 194 ms standard deviation and exec time ranging from 59 ms to 999 ms (SELECT name FROM pg_timezone_names)", "Query 7339462770142107000 on pg_namespace has high variation, with 13 ms standard deviation and exec time ranging from 33 ms to 67 ms (with tables as (SELECT c.oid :: int8 AS id, nc.nspname AS schema, c.relname AS name, c.relrowsecurity AS rls_enabled, c.relforcerowsecurity AS rls_forced, CASE WHEN c.relreplident = $1 THEN $2 WHEN...)", "Query -4726471486296252000 on pg_attribute has high variation, with 8 ms standard deviation and exec time ranging from 5 ms to 22 ms (SELECT t.oid, t.typname, t.typsend, t.typrec... FROM pg_catalog.pg_type s WHERE s.typrelid != $7 AND s.oid = t.typelem)))"] }] }]
const report = []

const levels = document.getElementById("levels")

report.forEach(({ level, levelViolationsCount }) => {
const li = document.createElement('li')
li.classList.add("uk-flex")
li.innerHTML = `<p class="uk-flex-1 uk-text-small">${level}</p><span class="uk-badge">${levelViolationsCount}</span>`
if (levelViolationsCount === 0) {
li.classList.add("empty")
}
levels.append(li)
})

const list = document.getElementById("list")

report.forEach(({ level, rules }) => {
rules.forEach(({ rule, ruleViolationsCount, violations }) => {

const card = document.createElement('div')
card.className = 'uk-card uk-card-body uk-margin'

const cardTitle = document.createElement("h3")
cardTitle.className = 'uk-card-title'
cardTitle.innerHTML = `<span>${rule}</span><span class="uk-badge uk-margin-left">${level}</span>`

const cardBody = document.createElement("ul")
cardBody.className = 'uk-margin'
violations.forEach((violation) => {
const li = document.createElement('li')
li.className = 'uk-text-small'
li.innerText = violation
cardBody.append(li)
})
if (violations.length < ruleViolationsCount) {
const more = document.createElement('li')
more.className = 'uk-text-small'
more.innerText = `${ruleViolationsCount - violations.length} more ...`
cardBody.append(more)
}

card.append(cardTitle)
card.append(cardBody)
list.append(card)

})

})
</script>
</body>

</html>
6 changes: 6 additions & 0 deletions cli/src/utils/file.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as fs from "node:fs";
import os from "os";
import {pathParent} from "@azimutt/utils";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

export type FilePath = string
export type FileFormat = 'json' | 'sql'
Expand All @@ -9,8 +11,12 @@ export const mkParentDirs = (path: string): void => { fs.mkdirSync(pathResolve(p
export const fileExists = (path: string): boolean => fs.existsSync(pathResolve(path))
export const fileList = (path: string): Promise<string[]> => fs.promises.readdir(pathResolve(path))
export const fileReadJson = <T extends object>(path: string): Promise<T> => fs.promises.readFile(pathResolve(path)).then(str => JSON.parse(str.toString()))
export const fileRead = (path: string): Promise<string> => fs.promises.readFile(pathResolve(path)).then(str => str.toString())
export const fileWriteJson = <T extends object>(path: string, json: T): Promise<void> => fs.promises.writeFile(pathResolve(path), JSON.stringify(json, null, 2) + '\n')
export const fileWrite = (path: string, content: string): Promise<void> => fs.promises.writeFile(pathResolve(path), content)

export const userHome = (): string => os.homedir()
export const pathResolve = (path: string): string => path.startsWith('~/') ? path.replace(/^~/, userHome()) : path

export const __filename = fileURLToPath(import.meta.url);
export const __dirname = dirname(__filename);
28 changes: 0 additions & 28 deletions cli/src/utils/html.ts

This file was deleted.

0 comments on commit c2b1597

Please sign in to comment.