Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/security-and-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
uv pip install -r requirements-dev.txt

- name: Run Bandit
run: bandit -r . -x ./tests,./venv --skip B113,B108,B404
run: bandit -r . -x ./tests,./venv,./scripts --skip B113,B108,B404

- name: Run detect-secrets
run: detect-secrets scan --baseline .secrets.baseline
Expand Down Expand Up @@ -119,8 +119,8 @@ jobs:

- name: Run pytest with coverage
run: |
# Run all tests with coverage (tests that need to be skipped use @pytest.mark.skip)
pytest tests/ -v --cov=. --cov-report=xml --cov-report=term
# Run unit tests with coverage (excluding acceptance tests which require external services)
pytest tests/ -v --ignore=tests/acceptance --cov=. --cov-report=xml --cov-report=term

- name: Code Coverage Report
uses: irongut/CodeCoverageSummary@v1.3.0
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ venv.bak/
# Following file is added in gitignore till we start adding service_now_client event processing implementation
tests/assets/service_now_client/test_service_now_client.py
cdk.out/*
cdk.out*/*
.kiro/specs
171 changes: 169 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,173 @@
"path": "detect_secrets.filters.heuristic.is_templated_secret"
}
],
"results": {},
"generated_at": "2025-05-26T16:01:01Z"
"results": {
"aws_security_incident_response_sample_integrations/aws_security_incident_response_service_now_integration_stack.py": [
{
"type": "Base64 High Entropy String",
"filename": "aws_security_incident_response_sample_integrations/aws_security_incident_response_service_now_integration_stack.py",
"hashed_secret": "0008997b84a1752108378cda03255efe2ad3bc2d",
"is_verified": false,
"line_number": 346
}
],
"aws_security_incident_response_sample_integrations/aws_security_incident_response_slack_integration_stack.py": [
{
"type": "Base64 High Entropy String",
"filename": "aws_security_incident_response_sample_integrations/aws_security_incident_response_slack_integration_stack.py",
"hashed_secret": "0008997b84a1752108378cda03255efe2ad3bc2d",
"is_verified": false,
"line_number": 239
}
],
"scripts/test_incident_creation.py": [
{
"type": "Secret Keyword",
"filename": "scripts/test_incident_creation.py",
"hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6",
"is_verified": false,
"line_number": 56
}
],
"tests/assets/domain/test_enhanced_dynamodb_schema.py": [
{
"type": "Hex High Entropy String",
"filename": "tests/assets/domain/test_enhanced_dynamodb_schema.py",
"hashed_secret": "9eef110c706799e7b6f5fd2bfe93ee23e48489d6",
"is_verified": false,
"line_number": 51
}
],
"tests/assets/domain/test_slack_domain.py": [
{
"type": "Hex High Entropy String",
"filename": "tests/assets/domain/test_slack_domain.py",
"hashed_secret": "9eef110c706799e7b6f5fd2bfe93ee23e48489d6",
"is_verified": false,
"line_number": 159
},
{
"type": "Hex High Entropy String",
"filename": "tests/assets/domain/test_slack_domain.py",
"hashed_secret": "1a59f99a7e5ec6448a301c996b1d04175149456d",
"is_verified": false,
"line_number": 294
}
],
"tests/assets/slack_api_gateway_authorizer/test_slack_api_gateway_authorizer.py": [
{
"type": "Secret Keyword",
"filename": "tests/assets/slack_api_gateway_authorizer/test_slack_api_gateway_authorizer.py",
"hashed_secret": "0e25175d4ace960ef0bc13053bee1db10e2ba647",
"is_verified": false,
"line_number": 40
},
{
"type": "Secret Keyword",
"filename": "tests/assets/slack_api_gateway_authorizer/test_slack_api_gateway_authorizer.py",
"hashed_secret": "7eef88603465ff23d8636134cfde5adecf822f70",
"is_verified": false,
"line_number": 104
},
{
"type": "Secret Keyword",
"filename": "tests/assets/slack_api_gateway_authorizer/test_slack_api_gateway_authorizer.py",
"hashed_secret": "72cb70dbbafe97e5ea13ad88acd65d08389439b0",
"is_verified": false,
"line_number": 140
}
],
"tests/assets/slack_client/test_comment_sync.py": [
{
"type": "Hex High Entropy String",
"filename": "tests/assets/slack_client/test_comment_sync.py",
"hashed_secret": "9eef110c706799e7b6f5fd2bfe93ee23e48489d6",
"is_verified": false,
"line_number": 54
}
],
"tests/assets/slack_client/test_message_sync.py": [
{
"type": "Hex High Entropy String",
"filename": "tests/assets/slack_client/test_message_sync.py",
"hashed_secret": "9eef110c706799e7b6f5fd2bfe93ee23e48489d6",
"is_verified": false,
"line_number": 66
},
{
"type": "Hex High Entropy String",
"filename": "tests/assets/slack_client/test_message_sync.py",
"hashed_secret": "1a59f99a7e5ec6448a301c996b1d04175149456d",
"is_verified": false,
"line_number": 141
}
],
"tests/assets/slack_client/test_slack_client.py": [
{
"type": "Hex High Entropy String",
"filename": "tests/assets/slack_client/test_slack_client.py",
"hashed_secret": "9eef110c706799e7b6f5fd2bfe93ee23e48489d6",
"is_verified": false,
"line_number": 61
}
],
"tests/assets/slack_command_handler/test_slack_command_handler.py": [
{
"type": "Hex High Entropy String",
"filename": "tests/assets/slack_command_handler/test_slack_command_handler.py",
"hashed_secret": "9eef110c706799e7b6f5fd2bfe93ee23e48489d6",
"is_verified": false,
"line_number": 72
}
],
"tests/assets/slack_events_bolt_handler/test_file_upload_handler.py": [
{
"type": "Hex High Entropy String",
"filename": "tests/assets/slack_events_bolt_handler/test_file_upload_handler.py",
"hashed_secret": "1a59f99a7e5ec6448a301c996b1d04175149456d",
"is_verified": false,
"line_number": 269
},
{
"type": "Hex High Entropy String",
"filename": "tests/assets/slack_events_bolt_handler/test_file_upload_handler.py",
"hashed_secret": "9eef110c706799e7b6f5fd2bfe93ee23e48489d6",
"is_verified": false,
"line_number": 280
}
],
"tests/assets/slack_events_bolt_handler/test_slack_events_bolt_handler.py": [
{
"type": "Hex High Entropy String",
"filename": "tests/assets/slack_events_bolt_handler/test_slack_events_bolt_handler.py",
"hashed_secret": "9eef110c706799e7b6f5fd2bfe93ee23e48489d6",
"is_verified": false,
"line_number": 102
},
{
"type": "Hex High Entropy String",
"filename": "tests/assets/slack_events_bolt_handler/test_slack_events_bolt_handler.py",
"hashed_secret": "1a59f99a7e5ec6448a301c996b1d04175149456d",
"is_verified": false,
"line_number": 291
}
],
"tests/assets/wrappers/test_slack_bolt_wrapper.py": [
{
"type": "Secret Keyword",
"filename": "tests/assets/wrappers/test_slack_bolt_wrapper.py",
"hashed_secret": "556a4a2ebba26d57ba3ba51f4ca1bb4e323d2409",
"is_verified": false,
"line_number": 47
},
{
"type": "Hex High Entropy String",
"filename": "tests/assets/wrappers/test_slack_bolt_wrapper.py",
"hashed_secret": "9eef110c706799e7b6f5fd2bfe93ee23e48489d6",
"is_verified": false,
"line_number": 87
}
]
},
"generated_at": "2026-02-05T22:13:20Z"
}
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"kiroAgent.configureMCP": "Disabled",
"kiroAgent.configureMCP": "Enabled",
"workbench.colorCustomizations": {
"terminal.integrated.shellIntegration.decorationsEnabled": "true",
"terminal.selectionForeground": "#ff0000",
Expand Down
92 changes: 72 additions & 20 deletions assets/service_now_notifications_handler/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,20 +347,23 @@ def __should_retry(self, attempt: int, max_retries: int, wait_time: int) -> bool
return False

def __get_incident_by_id(
self, service_now_incident_id: str
self, service_now_incident_id: str, skip_retry: bool = False
) -> List[Dict[str, Any]]:
"""
Scan DynamoDB table for ServiceNow incident ID with retry logic
Scan DynamoDB table for ServiceNow incident ID with optional retry logic

Args:
service_now_incident_id: The ServiceNow incident ID
skip_retry: If True, perform a single lookup without retries (for IncidentCreated events)

Returns:
List of matching items
List of matching items or None if not found
"""
max_retries = 5
wait_time = 30
time.sleep(wait_time)
max_retries = 1 if skip_retry else 5
wait_time = 0 if skip_retry else 30

if wait_time > 0:
time.sleep(wait_time)

for attempt in range(max_retries):
try:
Expand All @@ -383,6 +386,11 @@ def __get_incident_by_id(
items.extend(response.get("Items", []))

if not items:
if skip_retry:
logger.info(
f"ServiceNow incident for {service_now_incident_id} not found in database (new incident)"
)
return None
logger.info(
f"ServiceNow incident for {service_now_incident_id} not found in database on attempt {attempt + 1}"
)
Expand All @@ -399,7 +407,12 @@ def __get_incident_by_id(
)
return item["serviceNowIncidentDetails"]

# If no item has serviceNowIncidentDetails, retry
# If no item has serviceNowIncidentDetails, retry (unless skip_retry)
if skip_retry:
logger.info(
f"ServiceNow incident for {service_now_incident_id} exists but missing serviceNowIncidentDetails (new incident)"
)
return None
logger.info(
f"ServiceNow incident for {service_now_incident_id} missing serviceNowIncidentDetails key in database on attempt {attempt + 1}"
)
Expand All @@ -411,30 +424,38 @@ def __get_incident_by_id(
logger.info(
f"ServiceNow incident for {service_now_incident_id} not found in database on attempt {attempt + 1}. Error encountered: str{e}"
)
if skip_retry:
return None
if not self.__should_retry(attempt, max_retries, wait_time):
return []
wait_time = max(5, wait_time - 5) # Decrease by 5s, minimum 5s
continue
return None

def _get_incident_details(self, service_now_incident_id: str) -> Optional[str]:
def _get_incident_details(self, service_now_incident_id: str, skip_retry: bool = False) -> Optional[str]:
"""
Get ServiceNow incident details from the database

Args:
service_now_incident_id: The ServiceNow incident ID
skip_retry: If True, perform a single lookup without retries

Returns:
ServiceNow incident details or None if not found
"""
try:
service_now_incident_details = self.__get_incident_by_id(
service_now_incident_id
service_now_incident_id, skip_retry=skip_retry
)
if not service_now_incident_details:
logger.info(
f"All retries completed. Incident details for {service_now_incident_id} not found in database."
)
if skip_retry:
logger.info(
f"Incident details for {service_now_incident_id} not found in database (quick check)."
)
else:
logger.info(
f"All retries completed. Incident details for {service_now_incident_id} not found in database."
)
return None

logger.info(
Expand Down Expand Up @@ -543,13 +564,18 @@ def __init__(self, instance_id, **kwargs):
self.service_now_client = ServiceNowClient(instance_id, **kwargs)

def _get_incident_details(
self, service_now_incident_id: str
self, service_now_incident_id: str, max_retries: int = 5, initial_delay: float = 2.0
) -> Optional[Dict[str, Any]]:
"""
Get incident details from ServiceNow
Get incident details from ServiceNow with retry logic.

Retries are needed because the business rule fires during the insert transaction,
and the incident may not be committed yet when the webhook is received.

Args:
service_now_incident_id: The ServiceNow incident ID
max_retries: Maximum number of retry attempts (default: 5)
initial_delay: Initial delay in seconds between retries (default: 2.0)

Returns:
Dictionary of incident details or None if retrieval fails
Expand All @@ -563,13 +589,35 @@ def _get_incident_details(
if not self.service_now_client:
logger.info("Service Now Client failed to initialize")

service_now_incident = self.service_now_client.get_incident_with_display_values(
service_now_incident_id, integration_module
)
# Add initial delay to allow transaction to commit
logger.info(f"Waiting {initial_delay}s before first query to allow transaction commit")
time.sleep(initial_delay)

# Retry logic to handle race condition where business rule fires before transaction commits
service_now_incident = None
delay = initial_delay

for attempt in range(max_retries):
service_now_incident = self.service_now_client.get_incident_with_display_values(
service_now_incident_id, integration_module
)

if service_now_incident:
if attempt > 0:
logger.info(f"Successfully retrieved incident {service_now_incident_id} on attempt {attempt + 1}")
break

if attempt < max_retries - 1:
logger.info(
f"Incident {service_now_incident_id} not found on attempt {attempt + 1}, "
f"retrying in {delay}s (transaction may not be committed yet)"
)
time.sleep(delay)
delay = min(delay * 1.5, 5.0) # Exponential backoff, max 5 seconds

if not service_now_incident:
logger.error(
f"Failed to get incident {service_now_incident_id} from ServiceNow"
f"Failed to get incident {service_now_incident_id} from ServiceNow after {max_retries} attempts"
)
return None

Expand Down Expand Up @@ -781,7 +829,6 @@ def __process_incident(self, incident_number: str, event_type: str) -> bool:
True if processing was successful, False otherwise
"""
try:
# TODO: instead of searching for Incident in DDB using scan to see if the Incident was created or updated, simply use the Event type
logger.info("Get incident details from SNOW")
# Get ServiceNow incident details
service_now_incident_details = self.service_now_service._get_incident_details(
Expand All @@ -793,9 +840,14 @@ def __process_incident(self, incident_number: str, event_type: str) -> bool:
)
return False

# For IncidentCreated events, do a quick check without retries
# This handles the case where ServiceNow creates the incident first (not from AWS SIR)
# For IncidentUpdated events, use retries to wait for the record to be populated
skip_retry = (event_type == "IncidentCreated")

# Get existing incident details from database
service_now_incident_details_ddb = self.db_service._get_incident_details(
incident_number
incident_number, skip_retry=skip_retry
)

# Skip processing if event_type is IncidentCreated and incident already exists in DDB
Expand Down
Loading
Loading