Add resource inspector to Quantum dashboard

This commit is contained in:
2026-04-20 17:53:30 -03:00
parent fd4597a309
commit 5828eed35b
3 changed files with 203 additions and 0 deletions
+24
View File
@@ -302,6 +302,30 @@ Expected evidence:
Hello Quantum Hello Quantum
``` ```
The Quantum screen includes action buttons and inspection buttons:
```text
Refresh Evidence
Write S3 Evidence
Send SQS Message
Check Lambda
Inspect S3
Inspect SQS
Inspect Lambda
Inspect IAM
Inspect Secret
Inspect Logs
```
The inspection buttons call the `quanto-api` backend and return live LocalStack data:
- `Inspect S3` lists objects in the Quantum bucket.
- `Inspect SQS` shows queue counters, DLQ counters, and a sample of visible messages.
- `Inspect Lambda` shows runtime, handler, state, environment variables, and event source mappings.
- `Inspect IAM` shows the Lambda role and attached policies.
- `Inspect Secret` shows the application secret metadata and current value.
- `Inspect Logs` shows the CloudWatch Log Group and recent log streams.
On the Docker Swarm host: On the Docker Swarm host:
```bash ```bash
+144
View File
@@ -158,6 +158,150 @@ def send_sqs_message():
return jsonify(ok("sqs-message", {"queue": QUEUE_NAME, "messageId": response.get("MessageId")})) return jsonify(ok("sqs-message", {"queue": QUEUE_NAME, "messageId": response.get("MessageId")}))
@app.get("/api/s3/objects")
def list_s3_objects():
response = client("s3").list_objects_v2(Bucket=BUCKET_NAME)
objects = [
{
"key": item.get("Key"),
"size": item.get("Size"),
"lastModified": item.get("LastModified").isoformat() if item.get("LastModified") else None,
"etag": item.get("ETag"),
}
for item in response.get("Contents", [])
]
return jsonify(ok("s3-objects", {"bucket": BUCKET_NAME, "objectCount": len(objects), "objects": objects}))
@app.get("/api/sqs/details")
def sqs_details():
sqs = client("sqs")
main_url = queue_url(QUEUE_NAME)
dlq_url = queue_url(DLQ_NAME)
attributes = [
"ApproximateNumberOfMessages",
"ApproximateNumberOfMessagesNotVisible",
"ApproximateNumberOfMessagesDelayed",
"CreatedTimestamp",
"LastModifiedTimestamp",
"VisibilityTimeout",
]
main_attrs = sqs.get_queue_attributes(QueueUrl=main_url, AttributeNames=attributes)["Attributes"]
dlq_attrs = sqs.get_queue_attributes(QueueUrl=dlq_url, AttributeNames=attributes)["Attributes"]
messages = sqs.receive_message(
QueueUrl=main_url,
MaxNumberOfMessages=5,
VisibilityTimeout=0,
WaitTimeSeconds=0,
AttributeNames=["All"],
).get("Messages", [])
visible_messages = [
{
"messageId": message.get("MessageId"),
"body": message.get("Body"),
"attributes": message.get("Attributes", {}),
}
for message in messages
]
return jsonify(
ok(
"sqs-details",
{
"queue": {"name": QUEUE_NAME, "url": main_url, "attributes": main_attrs},
"dlq": {"name": DLQ_NAME, "url": dlq_url, "attributes": dlq_attrs},
"visibleMessagesSample": visible_messages,
},
)
)
@app.get("/api/lambda/details")
def lambda_details():
lambda_client = client("lambda")
function = lambda_client.get_function(FunctionName=LAMBDA_NAME)
configuration = function.get("Configuration", {})
mappings = lambda_client.list_event_source_mappings(FunctionName=LAMBDA_NAME).get("EventSourceMappings", [])
return jsonify(
ok(
"lambda-details",
{
"functionName": configuration.get("FunctionName"),
"runtime": configuration.get("Runtime"),
"handler": configuration.get("Handler"),
"state": configuration.get("State"),
"lastModified": configuration.get("LastModified"),
"role": configuration.get("Role"),
"environment": configuration.get("Environment", {}).get("Variables", {}),
"eventSourceMappings": [
{
"uuid": mapping.get("UUID"),
"state": mapping.get("State"),
"batchSize": mapping.get("BatchSize"),
"eventSourceArn": mapping.get("EventSourceArn"),
}
for mapping in mappings
],
},
)
)
@app.get("/api/iam/details")
def iam_details():
iam = client("iam")
role = iam.get_role(RoleName=ROLE_NAME)["Role"]
policies = iam.list_attached_role_policies(RoleName=ROLE_NAME).get("AttachedPolicies", [])
return jsonify(ok("iam-details", {"role": role, "attachedPolicies": policies}))
@app.get("/api/secrets/details")
def secrets_details():
secrets = client("secretsmanager")
description = secrets.describe_secret(SecretId=SECRET_NAME)
value = secrets.get_secret_value(SecretId=SECRET_NAME)
return jsonify(
ok(
"secrets-details",
{
"name": description.get("Name"),
"arn": description.get("ARN"),
"versionIds": list(description.get("VersionIdsToStages", {}).keys()),
"secretString": value.get("SecretString"),
},
)
)
@app.get("/api/logs/details")
def log_details():
logs_client = client("logs")
groups = logs_client.describe_log_groups(logGroupNamePrefix=LOG_GROUP_NAME).get("logGroups", [])
streams = logs_client.describe_log_streams(
logGroupName=LOG_GROUP_NAME,
orderBy="LastEventTime",
descending=True,
limit=5,
).get("logStreams", []) if groups else []
return jsonify(
ok(
"cloudwatch-logs-details",
{
"logGroup": LOG_GROUP_NAME,
"groups": groups,
"streams": streams,
},
)
)
@app.post("/api/lambda/invoke") @app.post("/api/lambda/invoke")
def invoke_lambda(): def invoke_lambda():
response = client("lambda").invoke( response = client("lambda").invoke(
+35
View File
@@ -129,6 +129,10 @@
.result { .result {
margin-top: 18px; margin-top: 18px;
} }
.inspect {
margin-top: 18px;
}
</style> </style>
</head> </head>
<body> <body>
@@ -146,16 +150,30 @@
<button id="lambda" class="secondary" type="button">Check Lambda</button> <button id="lambda" class="secondary" type="button">Check Lambda</button>
</section> </section>
<section class="actions" aria-label="Inspect resources">
<button data-inspect="/api/s3/objects" class="secondary" type="button">Inspect S3</button>
<button data-inspect="/api/sqs/details" class="secondary" type="button">Inspect SQS</button>
<button data-inspect="/api/lambda/details" class="secondary" type="button">Inspect Lambda</button>
<button data-inspect="/api/iam/details" class="secondary" type="button">Inspect IAM</button>
<button data-inspect="/api/secrets/details" class="secondary" type="button">Inspect Secret</button>
<button data-inspect="/api/logs/details" class="secondary" type="button">Inspect Logs</button>
</section>
<section id="cards" class="grid" aria-live="polite"></section> <section id="cards" class="grid" aria-live="polite"></section>
<section class="card result"> <section class="card result">
<h2>Last Action</h2> <h2>Last Action</h2>
<pre id="result">Ready.</pre> <pre id="result">Ready.</pre>
</section> </section>
<section class="card inspect">
<h2>Resource Inspector</h2>
<pre id="inspect">Select a resource to inspect.</pre>
</section>
</main> </main>
<script> <script>
const cards = document.querySelector("#cards"); const cards = document.querySelector("#cards");
const result = document.querySelector("#result"); const result = document.querySelector("#result");
const inspect = document.querySelector("#inspect");
function renderJson(value) { function renderJson(value) {
return JSON.stringify(value, null, 2); return JSON.stringify(value, null, 2);
@@ -165,6 +183,10 @@
result.textContent = typeof value === "string" ? value : renderJson(value); result.textContent = typeof value === "string" ? value : renderJson(value);
} }
function setInspect(value) {
inspect.textContent = typeof value === "string" ? value : renderJson(value);
}
function renderChecks(data) { function renderChecks(data) {
cards.innerHTML = ""; cards.innerHTML = "";
@@ -214,12 +236,25 @@
} }
} }
async function inspectResource(path) {
try {
setInspect("Loading resource details...");
const data = await request(path);
setInspect(data);
} catch (error) {
setInspect({ error: error.message });
}
}
document.querySelector("#refresh").addEventListener("click", () => { document.querySelector("#refresh").addEventListener("click", () => {
refresh().catch((error) => setResult({ error: error.message })); refresh().catch((error) => setResult({ error: error.message }));
}); });
document.querySelector("#s3").addEventListener("click", () => runAction("/api/s3/marker")); document.querySelector("#s3").addEventListener("click", () => runAction("/api/s3/marker"));
document.querySelector("#sqs").addEventListener("click", () => runAction("/api/sqs/message")); document.querySelector("#sqs").addEventListener("click", () => runAction("/api/sqs/message"));
document.querySelector("#lambda").addEventListener("click", () => runAction("/api/lambda/invoke")); document.querySelector("#lambda").addEventListener("click", () => runAction("/api/lambda/invoke"));
document.querySelectorAll("[data-inspect]").forEach((button) => {
button.addEventListener("click", () => inspectResource(button.dataset.inspect));
});
refresh().catch((error) => setResult({ error: error.message })); refresh().catch((error) => setResult({ error: error.message }));
</script> </script>