From 5828eed35b2483a7fed03832743a157796489bef Mon Sep 17 00:00:00 2001 From: Paulo Nonato Date: Mon, 20 Apr 2026 17:53:30 -0300 Subject: [PATCH] Add resource inspector to Quantum dashboard --- README.md | 24 ++++++ deploy/quanto/api/app.py | 144 ++++++++++++++++++++++++++++++++++ deploy/quanto/html/index.html | 35 +++++++++ 3 files changed, 203 insertions(+) diff --git a/README.md b/README.md index c0ccbcb..cd6b786 100644 --- a/README.md +++ b/README.md @@ -302,6 +302,30 @@ Expected evidence: 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: ```bash diff --git a/deploy/quanto/api/app.py b/deploy/quanto/api/app.py index 7a7c004..92ffa72 100644 --- a/deploy/quanto/api/app.py +++ b/deploy/quanto/api/app.py @@ -158,6 +158,150 @@ def send_sqs_message(): 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") def invoke_lambda(): response = client("lambda").invoke( diff --git a/deploy/quanto/html/index.html b/deploy/quanto/html/index.html index b55ae7c..41b223f 100644 --- a/deploy/quanto/html/index.html +++ b/deploy/quanto/html/index.html @@ -129,6 +129,10 @@ .result { margin-top: 18px; } + + .inspect { + margin-top: 18px; + } @@ -146,16 +150,30 @@ +
+ + + + + + +
+

Last Action

Ready.
+
+

Resource Inspector

+
Select a resource to inspect.
+