Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
57 changes: 41 additions & 16 deletions content/response_integrations/power_ups/git_sync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ environments = ["Production"]

**Selection Logic:**
1. **Primary Instance**: Search for instance with similar 'display name' configured in the environment
3. **Display Name Matching**: In the case there was not match, look for similar display name in the shared environment ("*")
2. **Display Name Matching**: In the case there was not match, look for similar display name in the shared environment ("*")
3. **First Available**: If display name resolution fails, use the first available instance (configured instances are preferred)
4. **Fallback**: If no instance found, leave unassigned

**Configuration Result:**
Expand Down Expand Up @@ -164,26 +165,36 @@ This allows for shared instances that can be used across multiple environments,
- **Cached Results**: Instance lists are cached per environment for performance

#### Instance Validation
- Only **configured instances** (`isConfigured: true`) are considered
- Instances are sorted by name for consistent selection
- Invalid or unconfigured instances are filtered out
- Both **configured and unconfigured** instances are now considered
- Instances are sorted with **configured instances first**, then alphabetically by name — this ensures a configured instance is always preferred when selecting the first available
- Unconfigured instances serve as fallbacks when no configured instance is available

### Existing Step Reuse Logic

When updating an existing playbook, the system attempts to preserve instance assignments:
When updating an existing playbook, the system attempts to preserve instance assignments, but now **validates** them first:

```python
if existing_step:
# Copy instance configuration from existing step
instance_id = existing_step.parameters["IntegrationInstance"].value
fallback_id = existing_step.parameters["FallbackIntegrationInstance"].value

# Apply to new step
new_step.parameters["IntegrationInstance"].value = instance_id
new_step.parameters["FallbackIntegrationInstance"].value = fallback_id

# Determine which instance to validate
# For "AutomaticEnvironment" mode, validate the fallback instance instead
instance_to_validate = instance_id
if instance_id == "AutomaticEnvironment":
instance_to_validate = fallback_id

# Only reuse if the instance actually exists on the target platform
if _is_valid_existing_instance(integration, instance_to_validate, environments):
new_step.parameters["IntegrationInstance"].value = instance_id
new_step.parameters["FallbackIntegrationInstance"].value = fallback_id
else:
# Fall through to instance-discovery logic
...
```

This ensures that manual instance assignments are preserved during playbook updates.
This prevents broken playbooks caused by copying instance IDs from prior failed imports or from instances that no longer exist on the target platform. If the existing instance is invalid, the system falls through to the standard instance-discovery logic described above.

### Display Name Resolution

Expand All @@ -202,8 +213,9 @@ The system stores and attempts to resolve instance display names:

#### Scenario 2: Single Environment with No Direct Instance
- **Environment**: "Development"
- **Available Instances**: ["Shared_VirusTotal"] (in shared environment)
- **Result**: Uses "Shared_VirusTotal" due to shared prefix matching
- **Available Instances in Development**: None
- **Available Instances in Shared ("*")**: ["Shared_VirusTotal"]
- **Result**: Falls back to the shared environment and uses "Shared_VirusTotal"

#### Scenario 3: Multi-Environment Playbook
- **Environment**: ["*"] (All Environments)
Expand All @@ -212,7 +224,20 @@ The system stores and attempts to resolve instance display names:
- `IntegrationInstance`: "AutomaticEnvironment"
- `FallbackIntegrationInstance`: "Shared_VirusTotal"

#### Scenario 4: Missing Environment on Target System
#### Scenario 4: Multi-Environment Playbook with No Shared Instance
- **Environments**: ["Env1", "Env2"]
- **Available Shared Instances**: None
- **Available in Env1**: ["VirusTotal_Env1"]
- **Result**:
- `IntegrationInstance`: "AutomaticEnvironment"
- `FallbackIntegrationInstance`: "VirusTotal_Env1" (found by iterating individual environments)

#### Scenario 5: Existing Step with Invalid Instance
- **Existing Step Instance**: "old-uuid-from-source-server"
- **Available Instances on Target**: ["VirusTotal_Prod"]
- **Result**: Validation fails for the old instance ID → falls through to discovery logic → assigns "VirusTotal_Prod"

#### Scenario 6: Missing Environment on Target System
- **Source Environment**: "Staging" (doesn't exist on target)
- **Available Instances**: ["VirusTotal_Prod", "Shared_VirusTotal"]
- **Result**: May fail to find appropriate instance, requiring manual configuration
Expand All @@ -233,9 +258,9 @@ The system stores and attempts to resolve instance display names:
- **Cause**: Environment names differ between source and target systems
- **Solution**: Create matching environments or use shared instances

4. **Playbook Step Unassigned**
- **Cause**: Instance exists but is not configured (`isConfigured: false`)
- **Solution**: Complete integration instance configuration
4. **Playbook Step Using Unconfigured Instance**
- **Cause**: No configured instances available; system assigned an unconfigured instance as fallback
- **Solution**: Complete the integration instance configuration on the target platform

#### Debugging Tips

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -834,17 +834,26 @@ def _assign_integration_instance_to_step(
existing_step,
"IntegrationInstance",
).get("value")
self._set_step_parameter_by_name(step, "IntegrationInstance", instance)
fallback = self._get_step_parameter_by_name(
existing_step,
"FallbackIntegrationInstance",
).get("value")
self._set_step_parameter_by_name(
step,
"FallbackIntegrationInstance",
fallback,
)
return
# Validate the existing instance before copying it.
# If it's invalid (e.g. from a prior failed import), fall through
# to the instance-discovery logic below.
instance_to_validate = fallback if instance == "AutomaticEnvironment" else instance
if instance_to_validate and self._is_valid_existing_instance(
step.get("integration"),
instance_to_validate,
environments,
):
self._set_step_parameter_by_name(
step, "IntegrationInstance", instance,
)
self._set_step_parameter_by_name(
step, "FallbackIntegrationInstance", fallback,
)
return

instance_display_name = self._get_instance_display_name(
step,
Expand Down Expand Up @@ -873,6 +882,13 @@ def _assign_integration_instance_to_step(
step.get("integration"),
environments[0],
)
# Fallback: also check the shared environment if no instances
# found in the playbook's specific environment
if not integration_instances:
integration_instances = self._find_integration_instances_for_step(
step.get("integration"),
ALL_ENVIRONMENTS_IDENTIFIER,
)
if integration_instances:
instance_id = self.api.get_integration_instance_id_by_name(
self.chronicle_soar,
Expand All @@ -896,6 +912,17 @@ def _assign_integration_instance_to_step(
step.get("integration"),
ALL_ENVIRONMENTS_IDENTIFIER,
)
# Fallback: check individual environments if no shared instances
if not integration_instances:
for env in environments:
if env == ALL_ENVIRONMENTS_IDENTIFIER:
continue
integration_instances = self._find_integration_instances_for_step(
step.get("integration"),
env,
)
if integration_instances:
break
self._set_step_parameter_by_name(
step,
"IntegrationInstance",
Expand Down Expand Up @@ -933,30 +960,68 @@ def _find_integration_instances_for_step(
integration_name: str,
environment: str,
) -> list[dict]:
"""Find integration instances available for integration per environment
"""Find integration instances available for integration per environment.

Returns configured instances if any exist; otherwise falls back to
unconfigured instances to preserve backward compatibility while still
enabling a fallback when no configured instances are available.

Args:
integration_name: The integration name to look for
environment: The environment to fetch the integration instances

Returns:
A list of configured integration instances
A list of integration instances, preferring configured ones.

"""
cache_key = f"integration_instances_{environment}"
if cache_key not in self._cache:
self._cache[cache_key] = self.api.get_integrations_instances(environment)
self._cache[cache_key] = self.api.get_integrations_instances(environment) or []

instances = self._cache.get(cache_key)
instances.sort(key=lambda x: x.get("instanceName"))
instances = self._cache.get(cache_key, [])

return [
filtered_instances = [
x
for x in instances
if x.get("integrationIdentifier") == integration_name
and x.get("isConfigured")
]
Comment thread
adarshtwy marked this conversation as resolved.

configured_instances = [x for x in filtered_instances if x.get("isConfigured")]
if configured_instances:
return sorted(configured_instances, key=lambda x: x.get("instanceName") or "")

# Fallback: return unconfigured instances sorted by name when no configured
# instances are available, so callers can still find something to assign.
return sorted(filtered_instances, key=lambda x: x.get("instanceName") or "")

def _is_valid_existing_instance(
self,
integration_name: str,
instance_id: str,
environments: list[str],
) -> bool:
"""Check if an instance ID exists among the available integration instances.

Args:
integration_name: The integration identifier to check.
instance_id: The instance UUID to validate.
environments: Playbook assigned environments to search.

Returns:
True if the instance exists in any of the playbook's environments
or in the shared environment.
"""
envs_to_check = {*environments, ALL_ENVIRONMENTS_IDENTIFIER}
for env in envs_to_check:
instances = self._find_integration_instances_for_step(
integration_name,
env,
)
if any(x.get("identifier") == instance_id for x in instances):
return True

return False

@staticmethod
def _flatten_playbook_steps(steps: list) -> list[dict]:
"""Flatten playbook steps with parallel actions to one list
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "GitSync"
version = "44.0"
version = "45.0"
description = "Sync Google SecOps integrations, playbooks, and settings with a GitHub, BitBucket or GitLab instance"
requires-python = ">=3.11,<3.12"
dependencies = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -479,3 +479,9 @@
item_type: Integration
publish_time: '2026-03-18'
ticket_number: ''
- description: Pull Playbook - Updated fallback assignment mechanism.
integration_version: 45.0
item_name: Pull Playbook
item_type: Jobs
publish_time: '2026-04-17'
ticket_number: '461900756'
2 changes: 1 addition & 1 deletion content/response_integrations/power_ups/git_sync/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading