The Security Hub Findings API provides endpoints for retrieving AWS Security Hub findings and managing analyst session tracking. The API is built on AWS Lambda and API Gateway with Cognito JWT authentication.
https://rshuboi4oh.execute-api.us-east-1.amazonaws.com/prod
All API endpoints require JWT ID token authentication via the Authorization header:
Authorization: Bearer {jwt-id-token}- Token Type: JWT ID Token (not access token)
- Issuer: AWS Cognito User Pool (
us-east-1_4QCl4RqHO) - Audience: Cognito Client ID (
4a2vb2k79v0niig5o1vj6vev00) - Required Claims:
email,sub,token_use: "id"
| Method | Path | Description |
|---|---|---|
| GET | /findings |
Retrieve all Security Hub findings |
| PUT | /findings |
Update workflow status for a finding |
| OPTIONS | /findings |
CORS preflight |
| GET | /findings/{findingId}/details |
Get specific finding by ID |
| GET | /findings/{findingId}/session/status |
Check session status for a finding |
| POST | /findings/{findingId}/session/start |
Start analyst session |
| POST | /findings/{findingId}/session/end |
End analyst session |
| GET | /open-cases |
Get all workflow tracker records |
| OPTIONS | /open-cases |
CORS preflight |
| GET | /{proxy+} |
Proxy route (handled by Lambda) |
| POST | /{proxy+} |
Proxy route (handled by Lambda) |
| OPTIONS | /{proxy+} |
CORS preflight for proxy routes |
Retrieve all Security Hub findings with optional session information.
GET /findingsAuthorization: Bearer {jwt-id-token}
Content-Type: application/jsonSuccess (200 OK):
{
"findings": [
{
"id": "f64d5ee8-6f7a-48e8-9b59-12a750361",
"title": "S3.13 S3 buckets should have lifecycle configuration",
"severity": "HIGH",
"status": "NEW",
"description": "This control checks whether Amazon S3 buckets have lifecycle configuration.",
"complianceStatus": "FAILED",
"recordState": "ACTIVE",
"workflowState": "NEW",
"createdAt": "2024-12-29T08:15:30Z",
"updatedAt": "2024-12-29T08:15:30Z",
"resources": [
{
"id": "arn:aws:s3:::example-bucket",
"type": "AwsS3Bucket",
"region": "us-east-1",
"partition": "aws",
"details": {
"AwsS3Bucket": {
"name": "example-bucket",
"ownerId": "641484007123"
}
}
}
],
"remediation": {
"recommendation": {
"text": "Configure lifecycle rules for S3 bucket",
"url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html"
}
},
"activeSession": {
"analystEmail": "analyst@company.com",
"openedAt": "2024-12-29T10:30:00Z"
}
},
{
"id": "another-finding-id",
"title": "EC2.1 Amazon EBS snapshots should not be public",
"severity": "CRITICAL",
"status": "NEW",
"description": "This control checks whether Amazon EBS snapshots are restorable by everyone.",
"complianceStatus": "FAILED",
"recordState": "ACTIVE",
"workflowState": "NEW",
"createdAt": "2024-12-29T09:20:15Z",
"updatedAt": "2024-12-29T09:20:15Z",
"resources": [...],
"remediation": {...}
// No activeSession field - no analyst currently reviewing
}
],
"totalCount": 367,
"timestamp": "2024-12-29T12:45:30Z",
"requestId": "req-20241229124530"
}Error Responses:
// Unauthorized (401)
{
"message": "Unauthorized"
}
// Internal Server Error (500)
{
"statusCode": 500,
"error": "Internal server error",
"details": "Error retrieving findings from Security Hub"
}- activeSession: Present only if an analyst is currently reviewing the finding
- analystEmail: Email of the analyst who opened the session
- openedAt: ISO 8601 timestamp when the session was started
- Missing activeSession: Indicates no analyst is currently reviewing the finding
Retrieve all workflow tracker records (active and resolved sessions).
GET /open-casesAuthorization: Bearer {jwt-id-token}
Content-Type: application/jsonSuccess (200 OK):
{
"cases": [
{
"finding_id": "f64d5ee8-6f7a-48e8-9b59-12a750361",
"opener_email": "analyst@company.com",
"open_timestamp": "2024-12-29T10:30:00Z",
"resolver_email": "",
"resolution_timestamp": "",
"ttl": 1735689000
},
{
"finding_id": "another-finding-id",
"opener_email": "analyst2@company.com",
"open_timestamp": "2024-12-29T09:15:00Z",
"resolver_email": "analyst2@company.com",
"resolution_timestamp": "2024-12-29T11:00:00Z",
"ttl": 1735689000
}
],
"count": 2,
"timestamp": "2024-12-29T12:45:30Z",
"requestId": "req-20241229124530"
}Error Responses:
// Unauthorized (401)
{
"message": "Unauthorized"
}
// Internal Server Error (500)
{
"error": "Internal Server Error",
"message": "Failed to fetch open cases: {error details}"
}- All Records: Returns all DynamoDB workflow tracker records
- Session Status: Records without
resolver_emailare active sessions - Audit Trail: Resolved sessions include resolver and resolution timestamp
- TTL: Records have automatic cleanup via DynamoDB TTL
Start tracking an analyst session for a specific finding.
POST /findings/{findingId}/session/start- findingId: The Security Hub finding ID (extracted from ARN)
Authorization: Bearer {jwt-id-token}
Content-Type: application/json{
"findingId": "f64d5ee8-6f7a-48e8-9b59-12a750361",
"analystEmail": "analyst@company.com",
"timestamp": "2024-12-29T10:30:00Z"
}- findingId: Must match the path parameter
- analystEmail: Email address of the analyst (extracted from JWT token)
- timestamp: ISO 8601 timestamp when session started
Success (200 OK):
{
"statusCode": 200,
"message": "Session started successfully",
"sessionData": {
"findingId": "f64d5ee8-6f7a-48e8-9b59-12a750361",
"analystEmail": "analyst@company.com",
"openTimestamp": "2024-12-29T10:30:00Z"
}
}Error Responses:
// Bad Request (400)
{
"statusCode": 400,
"error": "Invalid finding ID format"
}
// Conflict (409)
{
"statusCode": 409,
"error": "Session already exists for this finding",
"existingSession": {
"analystEmail": "other-analyst@company.com",
"openedAt": "2024-12-29T09:15:00Z"
}
}
// Internal Server Error (500)
{
"statusCode": 500,
"error": "Failed to create session record"
}End an analyst session and record resolution information.
POST /findings/{findingId}/session/end- findingId: The Security Hub finding ID
Authorization: Bearer {jwt-id-token}
Content-Type: application/json{
"findingId": "f64d5ee8-6f7a-48e8-9b59-12a750361",
"status": "RESOLVED",
"resolverEmail": "analyst@company.com",
"timestamp": "2024-12-29T11:45:00Z"
}- findingId: Must match the path parameter
- status: Resolution status (
"RESOLVED"or"SUPPRESSED") - resolverEmail: Email of analyst resolving the finding
- timestamp: ISO 8601 timestamp when session ended
Success (200 OK):
{
"statusCode": 200,
"message": "Session ended successfully",
"sessionData": {
"findingId": "f64d5ee8-6f7a-48e8-9b59-12a750361",
"openerEmail": "analyst@company.com",
"openTimestamp": "2024-12-29T10:30:00Z",
"resolverEmail": "analyst@company.com",
"resolutionTimestamp": "2024-12-29T11:45:00Z",
"status": "RESOLVED"
}
}Error Responses:
// Bad Request (400)
{
"statusCode": 400,
"error": "Invalid status. Must be RESOLVED or SUPPRESSED"
}
// Not Found (404)
{
"statusCode": 404,
"error": "No active session found for this finding"
}
// Internal Server Error (500)
{
"statusCode": 500,
"error": "Failed to update session record"
}Update the workflow status for a specific finding.
PUT /findingsAuthorization: Bearer {jwt-id-token}
Content-Type: application/json{
"findingId": "arn:aws:securityhub:us-east-1:123456789012:subscription/aws-foundational-security-best-practices/v/1.0.0/S3.13/finding/f64d5ee8-6f7a-48e8-9b59-12a750361",
"workflowStatus": "RESOLVED",
"resolverEmail": "analyst@company.com"
}- findingId: The full Security Hub finding ARN or extracted ID
- workflowStatus: New workflow status (
"NEW","NOTIFIED","RESOLVED","SUPPRESSED") - resolverEmail: (Optional) Email of analyst resolving the finding
Success (200 OK):
{
"success": true,
"message": "Workflow status updated successfully",
"findingId": "f64d5ee8-6f7a-48e8-9b59-12a750361",
"workflowStatus": "RESOLVED"
}Error Responses:
// Bad Request (400)
{
"statusCode": 400,
"error": "Bad Request",
"message": "findingId is required in request body"
}
// Bad Request (400)
{
"statusCode": 400,
"error": "Bad Request",
"message": "workflowStatus is required in request body"
}Retrieve details for a specific finding by ID.
GET /findings/{findingId}/details- findingId: The Security Hub finding ID (URL-encoded if contains special characters)
Authorization: Bearer {jwt-id-token}
Content-Type: application/jsonSuccess (200 OK):
{
"finding": {
"id": "f64d5ee8-6f7a-48e8-9b59-12a750361",
"title": "S3.13 S3 buckets should have lifecycle configuration",
"severity": "HIGH",
"status": "NEW",
"description": "This control checks whether Amazon S3 buckets have lifecycle configuration.",
"complianceStatus": "FAILED",
"recordState": "ACTIVE",
"workflowState": "NEW",
"createdAt": "2024-12-29T08:15:30Z",
"updatedAt": "2024-12-29T08:15:30Z",
"resources": [...],
"remediation": {...},
"activeSession": {
"analystEmail": "analyst@company.com",
"openedAt": "2024-12-29T10:30:00Z"
}
},
"found": true,
"timestamp": "2024-12-29T12:45:30Z",
"requestId": "req-20241229124530"
}Error Responses:
// Not Found (404)
{
"finding": null,
"found": false,
"message": "Finding not found",
"timestamp": "2024-12-29T12:45:30Z",
"requestId": "req-20241229124530"
}
// Bad Request (400)
{
"error": "Bad Request",
"message": "Invalid path format"
}Check the session status for a specific finding.
GET /findings/{findingId}/session/status- findingId: The Security Hub finding ID
Authorization: Bearer {jwt-id-token}
Content-Type: application/jsonSuccess - Active Session (200 OK):
{
"hasActiveSession": true,
"sessionExists": true,
"openerEmail": "analyst@company.com",
"openTimestamp": "2024-12-29T10:30:00Z",
"resolverEmail": "",
"resolutionTimestamp": "",
"findingId": "f64d5ee8-6f7a-48e8-9b59-12a750361"
}Success - Resolved Session (200 OK):
{
"hasActiveSession": false,
"sessionExists": true,
"openerEmail": "analyst@company.com",
"openTimestamp": "2024-12-29T10:30:00Z",
"resolverEmail": "analyst@company.com",
"resolutionTimestamp": "2024-12-29T11:45:00Z",
"findingId": "f64d5ee8-6f7a-48e8-9b59-12a750361"
}Success - No Session (200 OK):
{
"hasActiveSession": false,
"sessionExists": false,
"findingId": "f64d5ee8-6f7a-48e8-9b59-12a750361"
}Error Responses:
// Bad Request (400)
{
"error": "Bad Request",
"message": "Invalid path format"
}
// Internal Server Error (500)
{
"error": "Internal Server Error",
"message": "Failed to check session status"
}Handle CORS preflight requests for all endpoints.
OPTIONS /findings
OPTIONS /open-cases
OPTIONS /{proxy+}Success (200 OK):
Headers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Accept,Origin,Referer
Access-Control-Allow-Methods: GET,PUT,POST,OPTIONS,DELETE
Access-Control-Allow-Credentials: false
Catch-all proxy routes that forward requests to the Lambda function.
GET /{proxy+}
POST /{proxy+}
OPTIONS /{proxy+}These routes handle any paths not explicitly defined, allowing the Lambda function to process custom routes dynamically.
interface Finding {
id: string; // Extracted from Security Hub ARN
title: string; // Finding title
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
status: 'NEW' | 'NOTIFIED' | 'RESOLVED' | 'SUPPRESSED';
description: string; // Finding description
complianceStatus: 'PASSED' | 'FAILED' | 'WARNING' | 'NOT_AVAILABLE';
recordState: 'ACTIVE' | 'ARCHIVED';
workflowState: 'NEW' | 'ASSIGNED' | 'IN_PROGRESS' | 'DEFERRED' | 'RESOLVED';
createdAt: string; // ISO 8601 timestamp
updatedAt: string; // ISO 8601 timestamp
resources: Resource[]; // Associated AWS resources
remediation?: Remediation; // Remediation recommendations
activeSession?: ActiveSession; // Current analyst session (if any)
}interface ActiveSession {
analystEmail: string; // Email of reviewing analyst
openedAt: string; // ISO 8601 timestamp when session started
}interface SessionRecord {
finding_id: string; // Primary key
opener_email: string; // Analyst who opened the session
open_timestamp: string; // ISO 8601 timestamp
resolver_email?: string; // Analyst who resolved (if resolved)
resolution_timestamp?: string; // ISO 8601 timestamp (if resolved)
ttl: number; // Unix timestamp for automatic cleanup
}- 200 OK: Successful request
- 400 Bad Request: Invalid request parameters or body
- 401 Unauthorized: Missing or invalid JWT token
- 404 Not Found: Resource not found
- 409 Conflict: Resource conflict (e.g., session already exists)
- 500 Internal Server Error: Server-side error
{
"statusCode": number,
"error": string,
"details": string, // Optional additional details
"timestamp": string // ISO 8601 timestamp
}- If DynamoDB is unavailable,
/findingsendpoint returns findings without session data - Session tracking failures don't affect core finding retrieval functionality
- All errors are logged to CloudWatch for monitoring and debugging
- Findings Endpoint: No explicit rate limit (protected by API Gateway throttling)
- Session Endpoints: Designed for human interaction patterns (low frequency)
- API Gateway Throttling: Default AWS API Gateway limits apply
- API Gateway Access Logs: Request/response logging
- Lambda Function Logs: Detailed execution logging with correlation IDs
- Error Tracking: All errors logged with context for debugging
- Distributed Tracing: Full request flow from API Gateway → Lambda → DynamoDB/Security Hub
- Performance Monitoring: Latency and error analysis
- Service Map: Visual representation of service dependencies
- Request Count: Number of API requests per endpoint
- Error Rate: Percentage of failed requests
- Latency: Response time percentiles (p50, p95, p99)
- Session Activity: Number of active sessions and session duration
- JWT Validation: All tokens validated against Cognito User Pool
- Token Expiration: ID tokens expire after 1 hour
- Scope Validation: Tokens must include required claims (
email,sub)
- Encryption in Transit: All API communication over HTTPS
- Encryption at Rest: DynamoDB encryption enabled
- Access Control: IAM roles with minimal required permissions
- Audit Trail: Complete session tracking for compliance
- Finding ID Format: Validated against expected ARN format
- Email Format: Validated against standard email format
- Timestamp Format: Validated as ISO 8601 format
- Status Values: Restricted to allowed values (
RESOLVED,SUPPRESSED)
// Session Tracking Service
@Injectable()
export class SessionTrackingService {
constructor(private http: HttpClient) {}
startSession(findingId: string): Observable<any> {
const payload = {
findingId,
analystEmail: this.getAnalystEmail(),
timestamp: new Date().toISOString()
};
return this.http.post(
`/findings/${findingId}/session/start`,
payload
);
}
endSession(findingId: string, status: string): Observable<any> {
const payload = {
findingId,
status,
resolverEmail: this.getAnalystEmail(),
timestamp: new Date().toISOString()
};
return this.http.post(
`/findings/${findingId}/session/end`,
payload
);
}
private getAnalystEmail(): string {
// Extract email from JWT token
const token = localStorage.getItem('id_token');
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.email;
}
}import json
import boto3
from datetime import datetime
class SessionTracker:
def __init__(self):
self.dynamodb = boto3.resource('dynamodb')
self.table = self.dynamodb.Table('workflow_tracker')
def start_session(self, finding_id: str, analyst_email: str) -> dict:
"""Start a new analyst session"""
try:
response = self.table.put_item(
Item={
'finding_id': finding_id,
'opener_email': analyst_email,
'open_timestamp': datetime.utcnow().isoformat(),
'ttl': int(datetime.utcnow().timestamp()) + (30 * 24 * 3600) # 30 days
},
ConditionExpression='attribute_not_exists(finding_id)'
)
return {'statusCode': 200, 'message': 'Session started successfully'}
except ClientError as e:
if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
return {'statusCode': 409, 'error': 'Session already exists'}
raise
def end_session(self, finding_id: str, resolver_email: str, status: str) -> dict:
"""End an existing session"""
try:
response = self.table.update_item(
Key={'finding_id': finding_id},
UpdateExpression='SET resolver_email = :resolver, resolution_timestamp = :timestamp',
ExpressionAttributeValues={
':resolver': resolver_email,
':timestamp': datetime.utcnow().isoformat()
},
ConditionExpression='attribute_exists(finding_id) AND attribute_not_exists(resolution_timestamp)'
)
return {'statusCode': 200, 'message': 'Session ended successfully'}
except ClientError as e:
if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
return {'statusCode': 404, 'error': 'No active session found'}
raise# Test session creation
def test_session_creation():
tracker = SessionTracker()
result = tracker.start_session('test-finding-id', 'test@example.com')
assert result['statusCode'] == 200
# Test session conflict
def test_session_conflict():
tracker = SessionTracker()
tracker.start_session('test-finding-id', 'analyst1@example.com')
result = tracker.start_session('test-finding-id', 'analyst2@example.com')
assert result['statusCode'] == 409# Test API endpoints
curl -X POST \
https://rshuboi4oh.execute-api.us-east-1.amazonaws.com/prod/findings/test-id/session/start \
-H "Authorization: Bearer ${JWT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"findingId":"test-id","analystEmail":"test@example.com","timestamp":"2024-12-29T10:30:00Z"}'- Added Open Cases endpoint (
/open-cases) for active session management - Enhanced session tracking with duration calculations
- Improved navigation and user experience with dedicated Open Cases dashboard
- Added comprehensive session analytics and workload distribution features
- Fixed finding ID resolution for complete Security Hub ARNs
- Added URL decoding for proper finding ID handling
- Eliminated dependency on "last 100 findings" window for finding details
- Improved API performance with direct Security Hub queries
- Added session tracking endpoints (
/session/start,/session/end) - Enhanced
/findingsendpoint with active session information - Implemented DynamoDB-based session storage with TTL
- Added comprehensive error handling and graceful degradation
- Integrated with existing JWT authentication system
- Initial API implementation with Security Hub findings retrieval
- JWT authentication with Cognito User Pool integration
- Professional Angular frontend with AWS Console-style UI
- Comprehensive monitoring and logging with CloudWatch and X-Ray