Awesome Reviewers

Security & Access Control

Static analysis, cloud posture scanning, authentication, authorization and secrets handling.

197 instructions from 8 repositories. Last updated 2025-09-08.


Comprehensive function documentation

Functions, methods, and non-trivial constants should include comprehensive documentation that explains their purpose, behavior, parameters, and relationships to similar functionality. Documentation should be precise about what the code does and when errors may occur, avoiding misleading statements.

Key documentation elements to include:

Example of comprehensive documentation:

// fromWhereExpr translates a WhereExpr to a database filter expression.
// The condFilterParams serves as both input and output, where attrValues 
// maps placeholder names to their values and attrNames maps field names 
// to their database column equivalents.
func fromWhereExpr(cond *types.WhereExpr, params *condFilterParams) (string, error) {
    // implementation
}

// yamuxTunnelALPN is the ALPN (Application-Layer Protocol Negotiation) 
// identifier used for Yamux multiplexed tunnel connections in Teleport's 
// relay tunnel protocol.
const yamuxTunnelALPN = "teleport-relaytunnel"

For deprecated functions, include specific migration guidance and deletion timelines when available. Focus documentation on explaining what problems the code solves rather than just describing implementation details.


Safe null handling

Always handle potentially null or undefined values defensively using appropriate JavaScript patterns. Choose the right technique based on your specific needs:

Examples:

// Optional chaining for safe property access
i.description?.toLowerCase().includes(s)

// Explicit ternary for reliable fallbacks
const desktop = desktop_name ? desktop_name : desktop_addr;

// When nullish coalescing works as expected
const value = input ?? defaultValue;

Choose the pattern that provides the clearest intent and most reliable behavior for your specific use case. Test edge cases to ensure your chosen approach handles empty strings, null, and undefined values as expected.


thoughtful configuration design

When designing configuration schemas and fields, carefully consider their placement, validation, and long-term implications to prevent conflicts, maintain compatibility, and ensure good user experience.

Key principles to follow:

Field Placement: Avoid adding new fields to shared types like metadata that could cause unintended side effects across multiple resource types. Place configuration fields at appropriate levels where they can be validated independently.

Conflict Prevention: Design configuration to avoid ambiguous states where resources could match multiple conflicting criteria. Use explicit validation to catch potential conflicts early rather than relying on implicit resolution.

Backwards Compatibility: Consider how configuration changes affect existing tooling like Terraform providers that check for drift. Provide sensible defaults and migration paths for new fields.

Size and Constraint Management: Implement appropriate limits for configuration size and complexity. For example, limit configuration payloads to stay within backend storage constraints (e.g., 320KB for DynamoDB).

Graceful Validation: Handle invalid or unrecognized configuration gracefully by providing clear error messages and, where appropriate, warnings about ignored fields rather than silent failures.

Complexity Reduction: Simplify configuration schemas to reduce the likelihood of user misconfigurations. Provide sensible defaults and clear documentation about field interactions.

Example of thoughtful field placement:

# Good: Top-level field with clear scope
kind: example_resource
metadata:
  name: example
scope: /staging/west  # Clear, independent field

# Avoid: Adding to shared metadata type
metadata:
  name: example
  scope: /staging/west  # Could affect all resource types

This approach prevents configuration-related issues that can lead to security vulnerabilities, operational problems, and poor user experience.


Configuration requirements clarity

Configuration requirements and dependencies should be made explicit and prominent in documentation, not buried in notes or admonitions. When introducing new configuration options or requirements, ensure they are clearly documented as prerequisites or explicit steps rather than side notes that users might miss.

Key practices:

Example of good practice:

# Clear prerequisite documentation
## Prerequisites
- Teleport role version 8 requires app_labels configuration
- Ensure your role allows access with proper label matching

## Step X: Configure Access
Configure the required app_labels for RBAC:
```yaml
allow:
  app_labels:
    'teleport.dev/origin': 'aws-identity-center'

This approach ensures users understand configuration requirements upfront rather than discovering them through error messages or buried documentation notes.


Use appropriate testify assertions

Choose the most specific and appropriate testify assertion methods for your test scenarios to improve test clarity, error reporting, and maintainability.

Key guidelines:

  1. Use require in EventuallyWithT functions: As of testify v1.10.0, require can be used within EventuallyWithT and will cause early return on failure, triggering a retry on the next tick.
require.EventuallyWithT(t, func(t *assert.CollectT) {
    trackers, err := auth.GetActiveSessionTrackers(ctx)
    require.NoError(t, err)  // Use require, not assert
    require.Len(t, trackers, 1)
    require.Equal(t, helpers.HostID, trackers[0].GetAddress())
})
  1. Use specialized error assertion methods: Instead of combining generic assertions, use specific methods like require.ErrorContains for error message validation.
// Instead of:
require.Error(t, err)
require.Contains(t, err.Error(), tt.errMsg)

// Use:
require.ErrorContains(t, err, tt.errMsg)
  1. Use structured data assertions: For JSON and YAML comparisons, use semantic equality assertions rather than string matching.
// Instead of string comparison:
require.Contains(t, captureStdout.String(), tc.wantOutput())

// Use semantic comparison:
require.JSONEq(t, expectedJSON, actualJSON)
require.YAMLEq(t, expectedYAML, actualYAML)

These specialized assertions provide better error messages, handle formatting differences, and make test intentions clearer to other developers.


Add contextual logging

Add informative log messages with sufficient context to help operators diagnose issues, understand system state, and trace complex operations. Include relevant error details, system configuration status, and operational flow information.

Key practices:

Example from user accounting initialization:

utmp, utmpErr := NewUtmpBackend(cfg.UtmpFile, cfg.WtmpFile, cfg.BtmpFile)
if utmpErr == nil {
    uacc.utmp = utmp
    slog.DebugContext(ctx, "utmp user accounting is active")
}
wtmpdb, wtmpdbErr := NewWtmpdbBackend(cfg.WtmpdbFile)
if wtmpdbErr == nil {
    uacc.wtmpdb = wtmpdb
    slog.DebugContext(ctx, "wtmpdb user accounting is active")
}
if uacc.utmp == nil && uacc.wtmpdb == nil {
    slog.DebugContext(
        ctx, "no user accounting backends are available, sessions will not be logged locally",
        "utmp_error", utmpErr, "wtmpdb_error", wtmpdbErr,
    )
}

This approach helps operators understand what’s happening in the system and provides the context needed to resolve issues when they occur.


Authenticated health checks

When implementing health checks for external services, verify the complete communication stack rather than just basic connectivity. Health checks should validate that your service can successfully authenticate and perform actual operations, not merely establish network connections.

Basic TCP or TLS connectivity checks only confirm network reachability but miss critical configuration issues like insufficient credentials, improper RBAC setup, or API compatibility problems. Instead, implement health checks that make authenticated requests to meaningful endpoints that exercise the same permissions your service requires during normal operation.

For example, when health checking a Kubernetes cluster, use authenticated API calls like SelfSubjectAccessReview to verify proper RBAC configuration:

// Instead of just TCP connectivity
conn, err := net.Dial("tcp", kubernetesEndpoint)

// Use authenticated API calls that verify actual functionality
sarClient := authzapi.NewSelfSubjectAccessReviewInterface(client)
resp, err := sarClient.Create(ctx, &authzapi.SelfSubjectAccessReview{
    Spec: authzapi.SelfSubjectAccessReviewSpec{
        ResourceAttributes: &authzapi.ResourceAttributes{
            Verb:     "get",
            Resource: "pods",
        },
    },
}, metav1.CreateOptions{})

This approach catches configuration problems early, such as when “the agent was deployed with insufficient credentials and is unable to talk to the Kubernetes API.” It also provides more meaningful health status that reflects whether your service can actually perform its intended operations rather than just reach the target system.

Consider the network characteristics of your health check endpoints - some may be exempt from rate limiting (like Kubernetes /readyz) while others may not. Design your health check frequency and retry logic accordingly to avoid overwhelming the target service while still providing timely health status updates.


Database query parameter hygiene

Ensure all database query parameters serve a clear purpose and are properly utilized. Remove unused filter parameters that create confusion and verify that necessary filtering conditions are not accidentally omitted during refactoring.

When working with database queries and filters:

  1. Verify filter usage: Ensure all filter parameters are actually consumed by the query logic
  2. Remove unused parameters: Clean up vestigial parameters from previous iterations that no longer serve a purpose
  3. Validate filter conditions: Double-check that important filtering conditions (like membership type checks) are not accidentally removed during code changes
  4. Clarify filtering responsibilities: Be explicit about whether filtering should occur at the database/cache layer or the RPC/service layer

Example of problematic code:

// Bad: unused filter parameter
func (c *Cache) RangeAccessLists(ctx context.Context, start string, end string, filter *accesslistv1.AccessListsFilter, sort *types.SortBy) {
    // filter parameter is never used in the implementation
}

// Bad: missing important filter condition
for _, owner := range existingAccessList.Spec.Owners {
    // Missing check for owner.MembershipKind == accesslist.MembershipKindList
    if err := a.updateAccessListOwnerOf(ctx, accessList.GetName(), owner.Name, false); err != nil {
        return trace.Wrap(err)
    }
}

This practice prevents both performance issues from unused parameters and correctness issues from missing filter conditions.


defensive error handling

Implement defensive programming practices to prevent runtime errors and provide meaningful error information when failures occur. This includes validating object state before operations and ensuring error messages contain specific contextual details.

Key practices:

  1. State validation: Check if objects are in a valid state before performing operations, especially for resources that can be destroyed or become invalid
  2. Informative error messages: Include specific values, versions, or context in error messages rather than generic references

Example from window management:

// Bad - can crash if window is destroyed
showWindow() {
  this.window.show(); // TypeError: Object has been destroyed
}

// Good - validate state first
showWindow() {
  if (!this.window.isDestroyed()) {
    this.window.show();
  }
}

Example from error construction:

// Bad - vague error message
throw new Error(`Cannot update to this version`);

// Good - specific error message
throw new Error(`Cannot update to version ${actualVersion}. Minimum supported version is ${minVersion}`);

This approach prevents crashes from invalid operations and provides developers with actionable information when errors do occur.


Environment-specific configuration identifiers

Use distinct, environment-specific identifiers in configuration to prevent conflicts between different application contexts (development vs production, different versions, etc.) and ensure semantic clarity.

When configuration values like IDs, state names, or identifiers could be ambiguous or cause conflicts across environments, make them explicitly environment-aware and semantically meaningful.

Examples:

This prevents runtime conflicts, reduces debugging complexity, and makes the codebase more maintainable by eliminating ambiguity about configuration intent and scope.


Network address clarity

When documenting or configuring network addresses, DNS names, and IP addresses, be explicit about traffic direction and ensure uniqueness to prevent conflicts and ambiguity.

For IP addresses, specify whether they are for ingress or egress traffic:

# Good: Explicit about traffic direction
Teleport Cloud maintains a list of IP addresses it uses for ingress. If your environment restricts outbound traffic,

# Avoid: Ambiguous traffic direction
Teleport Cloud maintains a list of IP addresses it uses. If your environment restricts outbound traffic,

For DNS names and public addresses, ensure they are unique and don’t conflict with existing cluster addresses:

- name: "jira"
  uri: "https://localhost:8001"
  # The public address must be a unique DNS name and not conflict with the Teleport cluster's public addresses.

This prevents network configuration errors, reduces troubleshooting time, and makes network architecture clearer for both developers and operators.


Use semantic HTML elements

When creating interactive components in React, use proper HTML semantic elements as the foundation, even when using styled-components. Interactive elements should be actual buttons, links, or form controls rather than generic divs or spans with click handlers.

Attach event handlers directly to the interactive element itself, not to child elements like icons or text. This ensures proper accessibility, keyboard navigation, and screen reader support.

Example of the issue:

// Problematic - styled div with onClick on child
const PlayButton = styled.div`...`;
<PlayButton>
  <AdjustedPlay size="extra-large" onClick={handlePlay} />
</PlayButton>

Corrected approach:

// Better - actual button element with onClick on the button
const PlayButton = styled.button`...`;
<PlayButton onClick={handlePlay}>
  <AdjustedPlay size="extra-large" />
</PlayButton>

This pattern applies to all interactive elements: use <button> for actions, <a> for navigation, <input> for form controls, etc. Styled-components should enhance semantic HTML, not replace it with generic containers.


defer authentication prompts

Defer mounting components that trigger authentication prompts (MFA, per-session authentication) until they are actually visible or needed by the user. This prevents multiple concurrent authentication requests that can cause failures due to hardware token limitations or daemon mutex conflicts.

Components requiring authentication should implement lazy loading patterns where authentication is only triggered when the component becomes visible or when the user actively interacts with it. This is particularly important for applications that restore multiple tabs or sessions on startup.

Example implementation:

function renderDocuments(documentsService: DocumentsService) {
  return documentsService.getDocuments().map(doc => {
    const isActiveDoc = workspacesService.isDocumentActive(doc.uri);
    const { kind } = doc;
    
    // Only mount authentication-requiring components when visible
    switch (kind) {
      case 'doc.terminal':
      case 'doc.desktop_session':
        return (
          <MountOnVisible visible={isActiveDoc}>
            <MemoizedDocument doc={doc} visible={isActiveDoc} />
          </MountOnVisible>
        );
      default:
        return <MemoizedDocument doc={doc} visible={isActiveDoc} />;
    }
  });
}

This approach improves both security reliability and user experience by avoiding unnecessary authentication prompts and preventing authentication mechanism failures caused by concurrent requests.


Enforce scope boundaries

When implementing hierarchical permission systems with scopes, ensure that permissions granted at a specific scope cannot be used to access resources or escalate privileges outside that scope’s boundaries. This prevents lateral movement and privilege escalation attacks.

Key validation requirements:

Example implementation:

# Secure: Role created in /dev/lab scope with proper constraints
kind: role
metadata:
  name: lab-admin
spec:
  grantable_scopes: ['/dev/lab']  # Cannot grant broader than creation scope
  parent_resource_group: /dev/lab
  allow:
    rules:
    - resources: [node, app]
      verbs: [create, read, update, delete]
      # Implicitly scoped to /dev/lab and descendants only

# Insecure: Would allow privilege escalation
kind: role
spec:
  grantable_scopes: ['/']  # Broader than creation scope - should be rejected
  parent_resource_group: /dev/lab

This principle ensures that compromised credentials or roles cannot be used to affect resources outside their intended domain, maintaining proper security boundaries in multi-tenant or hierarchical systems.


Guide structure consistency

Documentation guides should follow a consistent structure and provide clear, descriptive content to improve user experience and comprehension.

Structure Requirements:

Content Clarity:

Example:

# Configure the update schedule by defining groups and timing
kind: autoupdate_config
metadata:
  name: autoupdate-config
spec:
  agents:
    mode: enabled           # Enable automatic updates
    strategy: halt-on-error # Stop if any group fails
    schedules:
      regular:
        - name: development     # Group name (string)
          days: ["Mon","Tue"]   # Days of week (array)
          start_hour: 4         # UTC hour (0-23)

This approach reduces user confusion, maintains consistency across documentation, and provides the context needed for users to successfully complete tasks without guessing at requirements or structure.


minimize unnecessary allocations

Avoid unnecessary memory allocations in performance-critical code paths, especially in frequently executed functions or loops that process large datasets. Common allocation sources to eliminate include:

  1. Intermediate collections: Iterate directly over data structures instead of creating temporary slices or maps
  2. Buffer reuse: Reuse existing buffers or pass io.Writer interfaces to avoid extra copies
  3. Map/slice reuse: Swap and clear existing collections instead of allocating new ones
  4. String operations: Use direct string concatenation or efficient formatting instead of fmt.Sprintf in hot paths
  5. Preallocation: Only preallocate collections when you know they will be populated in the common case

Example of eliminating intermediate slice:

// Before: Creates unnecessary intermediate slice
proxyAddrs := make([]string, 0, len(proxyServers))
for _, proxyServer := range proxyServers {
    proxyAddrs = append(proxyAddrs, proxyServer.GetPublicAddr())
}
for _, proxyAddr := range proxyAddrs {
    // process proxyAddr
}

// After: Direct iteration
for _, proxyServer := range proxyServers {
    proxyAddr := proxyServer.GetPublicAddr()
    // process proxyAddr directly
}

Example of avoiding fmt.Sprintf in index functions:

// Before: Expensive formatting in frequently called function
return fmt.Sprintf("%s/%s", date.Format(time.RFC3339), name)

// After: Direct string concatenation
return date.Format("20060102") + "/" + name

This optimization is particularly important for functions called frequently, index operations on large collections, or code paths processing thousands of items.


Optimize computational complexity

Always analyze the computational complexity of algorithms and choose efficient implementations to avoid performance bottlenecks and security vulnerabilities. Pay special attention to avoiding O(N²) algorithms that can create DoS risks, eliminate unnecessary computational overhead, and select appropriate data structures.

Key practices:

Example of problematic O(N²) code:

// Avoid: O(N²) complexity with DoS risk
for _, v := range bval {
    if !slices.Contains(aval, v) {
        return false
    }
}

// Prefer: O(N) complexity using map lookup
aMap := make(map[string]bool, len(aval))
for _, v := range aval {
    aMap[v] = true
}
for _, v := range bval {
    if !aMap[v] {
        return false
    }
}

Consider the security implications of algorithmic complexity, especially in user-facing APIs where input size may be controlled by attackers. When dealing with large datasets or user-controlled input, always prefer linear or logarithmic algorithms over quadratic ones.


Use descriptive semantic names

Choose descriptive, semantic names that clearly convey purpose and meaning rather than generic or ambiguous identifiers. Avoid generic method names like apply or handle that don’t indicate their specific function. When existing semantic references are available (such as theme palette names), prefer them over hardcoded values. Add clarifying comments when names might be ambiguous or when distinguishing between similar concepts.

Examples:

This practice improves code readability, reduces cognitive load, and makes the codebase more maintainable by making intent explicit.


Use structured API parameters

When designing APIs with multiple related parameters, especially optional ones like filters, sorting, and pagination options, bundle them into structured types rather than using long parameter lists. This prevents parameter confusion, reduces the likelihood of argument order mistakes, and makes APIs more extensible without breaking changes.

Problems with long parameter lists:

Recommended approach:

// Instead of:
func ListBotInstances(ctx context.Context, botName string, pageSize int, lastToken string, search string, sort *types.SortBy, query string) ([]*machineidv1.BotInstance, string, error)

// Use structured parameters:
type ListBotInstancesRequest struct {
    BotName   string
    PageSize  int
    LastToken string
    Search    string
    Sort      *types.SortBy
    Query     string
}

func ListBotInstances(ctx context.Context, req *ListBotInstancesRequest) ([]*machineidv1.BotInstance, string, error)

For APIs with many optional parameters, consider using functional options pattern:

func RangeAccessLists(ctx context.Context, start, end string, opts ...RangeOption) iter.Seq2[*accesslist.AccessList, error]

This approach makes APIs self-documenting, prevents argument order mistakes, and allows for backward-compatible extension of functionality.


Design intuitive query interfaces

When designing algorithms for querying, filtering, or data manipulation, prioritize user-friendly function names and interfaces over technically precise but obscure terminology. Users should be able to understand and use your algorithmic interfaces without deep technical knowledge of the underlying implementation.

Consider how your algorithm’s public interface will be consumed by developers who may not be familiar with technical concepts like semantic versioning, predicate logic, or domain-specific terminology. Choose function names that align with users’ mental models and existing familiar tools.

For example, when designing version comparison functions:

This principle applies to all algorithmic interfaces: sorting functions, search predicates, data transformation operations, and filtering mechanisms. The goal is to make complex algorithms accessible through simple, intuitive interfaces that reduce cognitive load and minimize the need for documentation lookup.


Follow platform naming conventions

Always use the correct naming conventions for the platform you’re working with, particularly React and HTML standards. React has specific prop naming requirements that differ from HTML attributes, and using incorrect names can cause functionality to break or behave unexpectedly.

Key examples:

Example from the codebase:

// ❌ Incorrect - invalid prop name
{props.readonly ? (
  <ReadOnlyCheckboxInternal />
) : (
  <CheckboxInternal />
)}

// ✅ Correct - follows React naming convention  
{props.readOnly ? (
  <ReadOnlyCheckboxInternal />
) : (
  <CheckboxInternal />
)}

When in doubt, consult the official React documentation or MDN for the correct prop names and supported attributes for specific HTML elements.


validate before access

Always check for null, empty, or unset values before accessing or processing them, and return meaningful errors when validation fails. This prevents runtime panics and provides clear feedback about missing required data.

When accessing potentially null or empty values, implement explicit validation:

// Check for empty parameters
params := q.Get("params")
if params == "" {
    return nil, trace.BadParameter("missing params")
}

// Check for nil context values  
func (ctx *Context) GetRole() (types.Role, error) {
    if ctx.role == nil {
        return nil, trace.NotFound("role is not set in the context")
    }
    return ctx.role, nil
}

This pattern ensures that null or empty states are caught early with descriptive error messages, rather than allowing them to propagate and cause unexpected behavior or panics later in the code execution.


Organize code by functionality

Structure code based on what it does rather than how it’s implemented. Group related functionality together, extract reusable components, and place code in appropriate packages or files to improve maintainability and readability.

Key principles:

Example of good organization:

// Instead of having everything in one large function
func (s *recordingPlayback) streamEvents(ctx context.Context, req *fetchRequest, ...) {
    // 200+ lines of complex logic with nested processEvent function
}

// Extract for better readability
func (s *recordingPlayback) streamEvents(ctx context.Context, req *fetchRequest, ...) {
    // Main orchestration logic
    for event := range eventsChan {
        if !s.processEvent(event, req) {
            break
        }
    }
}

func (s *recordingPlayback) processEvent(evt apievents.AuditEvent, req *fetchRequest) bool {
    // Focused event processing logic
}

This approach makes code easier to understand, test, and maintain by creating clear boundaries between different responsibilities.


Use descriptive names

Choose names that clearly indicate their purpose and behavior rather than using generic or ambiguous terms. Avoid internal abbreviations in user-facing content, ensure function names accurately reflect what they do, and prefer specific descriptive terms over generic ones.

Examples of improvements:

This prevents confusion about functionality, makes code self-documenting, and improves maintainability by making the intent clear to future developers.


API consistency patterns

Maintain consistent API patterns and structures across different resource types and access methods within the same system. When designing new API fields or resources, follow established patterns from existing similar functionality rather than creating divergent approaches.

For example, when adding Kubernetes access controls, follow the same pattern used for SSH access with logins traits and wildcard selectors, rather than creating Kubernetes-specific preset roles:

# Consistent pattern - reuse existing access role with traits
tctl users add hugo --logins root --kubernetes-group teleport:preset:editor --roles=access

# Instead of creating new kube-specific roles
roles: [kube-access, kube-editor, kube-auditor]

Similarly, when extending existing resources, prefer embedding or reusing established field structures over creating entirely new schemas. If you must create new structures, ensure they follow the same naming conventions, required/optional field patterns, and default value behaviors as existing APIs.

This approach reduces cognitive load for users, ensures feature parity across interfaces (CLI, Web UI, API), and maintains a cohesive system architecture. Always check if similar functionality already exists before designing new API patterns.


prefer simpler patterns

Choose simpler, more readable code patterns over complex alternatives to improve maintainability and reduce cognitive load. This includes avoiding unnecessary complexity in variable assignment, function parameters, and styling approaches.

Key practices:

Examples:

Instead of IIFE for complex logic:

const hasAccess = (() => {
  if (spec.kind === IntegrationKind.ExternalAuditStorage) {
    return hasIntegrationAccess && hasExternalAuditStorage && enabled;
  }
  return hasIntegrationAccess;
})();

Use simple assignment:

let hasAccess = hasIntegrationAccess;
if (spec.kind === IntegrationKind.ExternalAuditStorage) {
  hasAccess &&= hasExternalAuditStorage && enabled;
}

Instead of conditional assignment:

let primaryButtonText = 'Browse Existing Resources';
if (props.primaryButtonText) {
  primaryButtonText = props.primaryButtonText;
}

Use destructuring defaults:

export function Finished({
  primaryButtonText = 'Browse Existing Resources',
  ...props
}: Props) {

This approach reduces indentation, eliminates unnecessary complexity, and makes code intent clearer at first glance.


Use semantically clear names

Choose names that clearly convey their purpose, scope, and meaning to reduce ambiguity and improve code maintainability. Avoid generic or potentially confusing terms when more specific alternatives exist.

Key principles:

Examples:

// Instead of generic 'role' which could mean many things
string role = 4;

// Use specific 'system_role' to clarify the type of role
string system_role = 4;
// Instead of 'node_name' which implies SSH nodes only
string node_name = 3;

// Use 'friendly_name' or 'host_name' to reflect broader applicability
string friendly_name = 3;
// Instead of "App to App" which is less common
title: App to App mTLS

// Use "Service to Service" which aligns with industry terminology
title: Service to Service mTLS

This practice prevents confusion during code reviews, reduces onboarding time for new developers, and makes the codebase more self-documenting.


parameterize configuration values

Avoid hardcoded values in configuration files and templates. Make settings parameterizable with sensible defaults to accommodate different environments and use cases. Even when you’re unsure if a setting needs to be configurable, err on the side of flexibility - most users will stick with defaults, but those who need customization will appreciate the option.

Example of problematic hardcoded configuration:

# Bad: hardcoded values
proxy_server: lukeo.teleport-test.com:443
discovery_interval: 10m  # fixed, no way to change

Example of properly parameterized configuration:

# Good: parameterized with defaults
proxy_server: {{ .Values.proxyServer | default "example.teleport-test.com:443" }}
discovery_interval: {{ .Values.discoveryInterval | default "10m" }}

This approach ensures configurations work across different environments while maintaining reasonable defaults for common scenarios. As one reviewer noted: “Hard to think of a default that is good for everyone” - parameterization solves this by letting each deployment choose what works best.


API parameter encapsulation

When designing API methods that accept multiple related parameters (especially filters, search criteria, or configuration options), encapsulate them in dedicated message types rather than adding individual fields directly to the request message. This pattern improves future extensibility by allowing new parameters to be added without breaking existing method signatures.

For example, instead of adding individual filter fields:

message ListAccessListsRequest {
  int32 page_size = 1;
  string next_token = 2;
  string search = 3;           // Avoid this
  types.SortBy sort_by = 4;    // Avoid this
}

Encapsulate related parameters in a dedicated message:

message ListAccessListsRequest {
  int32 page_size = 1;
  string next_token = 2;
  ListAccessListsFilter filter = 3;  // Preferred approach
}

message ListAccessListsFilter {
  string search = 1;
  repeated string owners = 2;
  repeated string roles = 3;
}

This approach has several benefits: it makes the API more organized and self-documenting, enables adding new filter options without changing method signatures, allows for generic helper functions that can work with filter types, and provides a clear upgrade path when backward compatibility concerns require creating new API versions (e.g., ListAccessListsV2). When extending existing APIs would break backward compatibility, consider creating versioned methods rather than modifying existing ones.


Assess optimization necessity

Evaluate whether performance optimizations are actually needed based on your specific context, data size, and operation cost. Avoid implementing optimizations that add complexity without meaningful benefit.

For small datasets (< 100 items), techniques like debouncing may be unnecessary unless they prevent expensive operations such as API calls, URL parameter updates, or complex computations. Consider the trade-off between code complexity and actual performance gains.

Example of contextual evaluation:

// Question: Do we need debouncing for 30 items?
const DebouncedSearchInput = ({ onSearch }) => {
  // If onSearch only filters local data: debouncing may be overkill
  // If onSearch triggers URL updates or API calls: debouncing is valuable
  
  useEffect(() => {
    const timer = setTimeout(() => {
      onSearch(searchTerm); // Expensive operation?
    }, 350);
    return () => clearTimeout(timer);
  }, [searchTerm]);
};

Before adding performance optimizations, ask: What specific problem am I solving? Is the current performance actually problematic? Will this optimization provide measurable benefit given my data size and use case?


Minimize authentication complexity

Avoid implementing redundant authentication steps when secure, direct methods are already available. Multiple authentication layers can introduce unnecessary complexity and potential security vulnerabilities. When identity files or tokens are already being used, evaluate whether additional login steps are truly required.

For example, if you’re already using an identity file from a trusted source like tbot, avoid adding unnecessary login commands:

# Avoid this - redundant authentication
tsh login --proxy=${PROXY} -i /identity-output/identity
tctl create --identity=/identity-output/identity resource.yaml

# Prefer this - direct identity file usage
tctl create --identity=/identity-output/identity resource.yaml

Before adding authentication steps, ask: “Is this authentication method necessary given the existing secure credentials?” Simpler authentication flows are often more secure and less prone to configuration errors.


Follow platform naming standards

Adhere to established naming conventions specific to each platform, framework, or technology being used. Different platforms have different naming requirements and best practices that should be followed to ensure compatibility, avoid linter warnings, and maintain consistency with ecosystem expectations.

For Prometheus metrics, avoid suffixes like _total for gauge metrics since they are reserved for counter types that only increase:

// Incorrect - _total implies counter type
teleport_health_resources_total{type="kubernetes"}

// Correct - no suffix for gauge metrics  
teleport_health_resources{type="kubernetes"}

For Kubernetes CRDs, avoid underscores in resource names as they are not allowed:

# Incorrect
kind: resource_group

# Correct  
kind: resourcegroup

For Protocol Buffer enums, use proper prefixes and avoid generic names like “UNSET”:

// Incorrect
enum BotKind {
    UNSET = 0;
    TBOT_BINARY = 1;
}

// Correct
enum BotKind {
    BOT_KIND_UNSPECIFIED = 0;
    BOT_KIND_TBOT_BINARY = 1;
}

Research and follow the naming conventions for each technology stack in your project. This prevents compatibility issues, reduces confusion for other developers familiar with those platforms, and ensures tooling works correctly.


Avoid overly broad permissions

Use specific, scoped permissions instead of wildcards to prevent unintended access to sensitive resources. Wildcard permissions can grant access to critical applications beyond the intended scope, creating security vulnerabilities.

When security controls like MFA or device trust must be bypassed for technical reasons, explicitly document these limitations and consider renaming functions to highlight the security implications.

Example of problematic wildcard usage:

// BAD: Grants access to all applications, including sensitive ones
AppLabels: map[string]apiutils.Strings{
    types.Wildcard: []string{types.Wildcard},
}

// GOOD: Scope to specific application types using labels
AppLabels: map[string]apiutils.Strings{
    types.TeleportInternalAppType: []string{"mcp"},
}

When bypassing security controls, make the limitation explicit:

// BAD: Unclear that MFA is bypassed
func hasAccess() bool { /* ... */ }

// GOOD: Clear about security limitations  
func canViewUncheckedMFA() bool { /* ... */ }

Always validate that role permissions match their intended purpose and document any security control bypasses to prevent future misuse.


Plan encryption key recovery

Always implement recovery mechanisms and plan for key rotation when designing encryption systems. Encryption key failures, compromises, or rotation issues can lead to permanent data loss or service disruption.

Key recovery strategies include:

Example configuration showing recovery key setup:

auth_service:
  session_recording_config:
    encryption:
      enabled: yes
      recovery_key: "backup-hsm-key"
      key_rotation_policy: "automatic"

Without proper key recovery planning, a single HSM failure or incorrect key deletion can make all encrypted session recordings permanently inaccessible, creating both security and compliance risks.


Environment variable precedence

When working with environment variables in configuration management, ensure proper precedence handling and use clean fallback patterns. Environment variables can be inherited by subprocesses unexpectedly, potentially overriding command-line flags or other configuration sources.

Key practices:

  1. Explicit precedence control: When spawning subprocesses, explicitly control which environment variables are passed to prevent unintended inheritance that could override intended configuration
  2. Clean fallback patterns: Use utilities like cmp.Or() for cleaner environment variable fallback chains instead of verbose loops

Example of clean fallback pattern:

// Instead of verbose loops:
editor := "vi"
for _, v := range []string{"TELEPORT_EDITOR", "VISUAL", "EDITOR"} {
    if value := os.Getenv(v); value != "" {
        editor = value
        break
    }
}

// Use clean fallback:
editor := cmp.Or(os.Getenv("TELEPORT_EDITOR"), os.Getenv("VISUAL"), os.Getenv("EDITOR"), "vi")

This prevents configuration conflicts where subprocess environment variables might override parent process flags, and makes environment variable fallback logic more readable and maintainable.


consistent configuration handling

Ensure configuration options are handled uniformly across all API endpoints and providers. Configuration should have clear precedence rules, support dynamic values where appropriate, and avoid hardcoded fallbacks.

Key principles:

  1. Clear precedence: When multiple configuration sources exist (payload, options, defaults), document and implement a consistent precedence order
  2. Dynamic configuration: Allow functions for configuration values that need to be determined at runtime based on context
  3. Uniform option processing: Handle configuration options consistently across similar endpoints and providers
  4. Avoid hardcoded values: Use configuration options instead of hardcoded paths, domains, or other values

Example of good configuration handling:

// Bad: Unclear precedence and hardcoded fallback
const consentURI = `${options.consentPage}?client_id=${client.clientId}&scope=${requestScope.join(" ")}`;

// Good: Clear precedence and proper encoding
const url = new URL(options.consentPage);
url.searchParams.set('consent_code', encodeURIComponent(code));
url.searchParams.set('client_id', encodeURIComponent(client.clientId));
url.searchParams.set('scope', encodeURIComponent(requestScope.join(' ')));

// Bad: Static configuration only
team: string;

// Good: Dynamic configuration support  
team?: string | ((ctx: Context) => string);

This ensures developers have predictable behavior, proper flexibility, and consistent patterns across the entire API surface.


Prefer simple solutions

Choose existing utilities, simpler patterns, and cleaner implementations over manual or complex approaches. This improves code maintainability and reduces potential bugs.

Key practices:

Example of preferred approach:

// Instead of manual parsing:
const cookies = cookieString.split(";").map(cookie => cookie.trim());

// Use existing utility:
const cookieValue = ctx.getCookie(cookieName);

// Instead of else-if chains:
if (authentication === "basic") {
    requestHeaders["authorization"] = `Basic ${encodedCredentials}`;
}
if (authentication !== "basic" && options.clientSecret) {
    body.set("client_secret", options.clientSecret);
}

This approach reduces complexity, leverages tested utilities, and makes code more readable and maintainable.


Synchronization primitive selection

Choose appropriate synchronization primitives and ensure proper protection of shared state to avoid race conditions and deadlocks. Prefer regular mutexes over RWMutex unless read-heavy workloads justify the complexity, as RWMutex can be a performance trap. When accessing shared state, ensure the entire read-modify-write operation is atomic by keeping it within the same lock scope.

For goroutine coordination, consider using channels instead of mutexes when possible to avoid deadlock scenarios. When using sync.WaitGroup, understand the reuse patterns - you can reuse the same WaitGroup with Add/Wait/Done cycles, but ensure proper synchronization during the transition between tasks.

Example of proper shared state protection:

// Bad - race condition between check and use
s.mu.Lock()
alreadySent := s.closeSent
s.mu.Unlock()

if !alreadySent {
    // closeSent could have changed here
    s.sendCloseMessage()
}

// Good - atomic check and action
s.mu.Lock()
if !s.closeSent {
    s.closeSent = true
    s.sendCloseMessage()
}
s.mu.Unlock()

Avoid holding locks during blocking operations like network I/O. Instead, use dedicated goroutines with channels for coordination:

// Bad - lock held during read blocks all writes
s.websocket.Lock()
msgType, data, err := s.ws.ReadMessage()
s.websocket.Unlock()

// Good - separate goroutines with channel communication
go s.writeLoop() // handles all writes via channel
s.readLoop()     // reads without blocking writes

comprehensive test coverage

When adding new functionality, configuration options, or plugins, always include corresponding tests that verify the feature works as expected. Tests should comprehensively cover all aspects of the functionality, including input/output validation, data transformations, and edge cases. Additionally, structure tests as focused, single-responsibility test cases rather than combining multiple concerns into one test.

For example, when adding a bearer token plugin to a client configuration:

// Bad: Adding plugin without tests
const client = createAuthClient({
  plugins: [bearerClient()], // No tests verify this works
});

// Good: Adding plugin with comprehensive tests
describe('Bearer Token Plugin', () => {
  test('should authenticate with bearer token', async () => {
    // Test the plugin functionality
  });
  
  test('should handle invalid bearer tokens', async () => {
    // Test error cases
  });
});

When testing data transformations, verify both the transformation process and the final output. For array-to-string conversions, test that ["medium", "large"] transforms to '["medium","large"]' during storage and back to the original array when retrieved.


Provide contextual error messages

Error messages should be specific, contextual, and use appropriate error creation functions to help users understand what went wrong and how to fix it.

Key principles:

  1. Include relevant context: Error messages should contain specific identifiers, values, or context that help identify the source of the problem.

  2. Use appropriate error creation functions:
    • Use errors.New() for static error messages without formatting
    • Use fmt.Errorf() only when you need to include dynamic values
    • Use trace-specific functions like trace.BadParameter() when appropriate
  3. Be specific about the problem: Avoid generic messages that don’t help users understand what went wrong.

Examples:

// Bad: Generic error without context
return trace.BadParameter("target is not an IC plugin")

// Good: Specific error with context
return trace.BadParameter("%q is not an AWS Identity Center integration", p.edit.awsIC.pluginName)

// Bad: Using fmt.Errorf for static messages
return fmt.Errorf("end time before start time")

// Good: Using errors.New for static messages
return errors.New("end time before start time")

// Good: Using fmt.Errorf when formatting is needed
return fmt.Errorf("invalid request size: expected %d bytes, got %d bytes", requestHeaderSize, len(data))

This approach improves debugging experience by providing actionable information that helps users identify and resolve issues quickly.


Design comprehensive metrics

When designing observability metrics, ensure they are explicit about all possible states, comprehensive in scope, and clearly documented to avoid ambiguity. Metrics should accurately represent what is being monitored and make their limitations transparent.

Key principles:

  1. Explicit state representation: Use separate metrics for each distinct state rather than requiring calculations. For example, use teleport_resources_health_status_healthy, teleport_resources_health_status_unhealthy, and teleport_resources_health_status_unknown instead of forcing users to calculate unknown states.

  2. Clear scope documentation: Explicitly document what resources are included in metrics. If metrics only count resources with health checks enabled, this limitation must be clearly stated to prevent false assumptions about system coverage.

  3. Comprehensive coverage: When implementing health checks for a resource type, always include all instances in the monitoring system, even if they’re disabled. Mark disabled instances with appropriate status (e.g., status: unknown, reason: disabled) rather than excluding them entirely.

Example implementation:

// Good: Explicit metrics for each state
resourcesHealthyGauge = prometheus.NewGaugeVec(
    prometheus.GaugeOpts{
        Name: "teleport_resources_health_status_healthy",
        Help: "Number of resources in healthy state with health checks enabled",
    },
    []string{"type"}, // db|kubernetes|etc
)

resourcesUnhealthyGauge = prometheus.NewGaugeVec(
    prometheus.GaugeOpts{
        Name: "teleport_resources_health_status_unhealthy", 
        Help: "Number of resources in unhealthy state with health checks enabled",
    },
    []string{"type"},
)

This approach prevents confusion about metric scope, enables accurate alerting, and provides clear visibility into system state without requiring complex calculations.


API consistency standards

Maintain consistent parameter formats and requirements across all API providers and endpoints. When documentation conflicts with implementation, resolve discrepancies to ensure alignment. Avoid breaking changes by designing API modifications as unions or optional parameters rather than replacing existing structures.

For example, when adding new authentication methods, extend the existing interface rather than replacing it:

// Instead of breaking change:
const { data } = await authClient.twoFactor.enable({
    verification: { // This breaks existing code
        password: "password"
    }
});

// Use union types for backward compatibility:
const { data } = await authClient.twoFactor.enable({
    password: "password" // Existing format still works
    // OR otp: "123456" // New option available
});

Ensure parameter formats match official provider documentation (e.g., Google’s select_account consent vs select_account+consent) and maintain consistency with established patterns across similar providers within the same system.


Document configuration requirements

Ensure that configuration requirements, dependencies, and options are clearly documented and communicated to users. When introducing features that require external configuration or have configurable behavior, explicitly state what must be configured and provide clear guidance on the implications of different settings.

For configuration dependencies, use strong emphasis to highlight mandatory requirements:

This is an advanced feature. Configuration outside of this plugin **MUST** be provided.

For configuration options, clearly explain the conditions and effects:

If you want your users to link a social account with a different email address than their existing one, you'll need to enable this in the account linking settings.

This practice prevents configuration-related issues, reduces support burden, and helps developers understand the full scope of setup requirements for features they implement.


Use consistent error types

Always use the appropriate error classes and throwing mechanisms for consistent error handling across the codebase. Prefer BetterAuthError or APIError over generic Error classes, use throw instead of returning error responses in endpoints, and structure error codes consistently.

Key practices:

Example:

// ❌ Avoid
throw new Error("unable to set a future iat time");
return ctx.json({ error: "invalid_grant" }, { status: 400 });

// ✅ Prefer  
throw new BetterAuthError("unable to set a future iat time");
throw new APIError("BAD_REQUEST", {
  message: ERROR_CODES.INVALID_GRANT,
  code: "INVALID_GRANT"
});

This ensures proper error type inference, consistent error handling patterns, and better debugging experience across the application.


Context-aware algorithm selection

Choose algorithms and data structures based on operational requirements and usage context rather than defaulting to familiar patterns. Consider factors like compilation timing, API constraints, and different execution contexts when making design decisions.

Key principles:

  1. Analyze operational requirements: Store data in forms that support required operations. For example, if you need compilation flags later, store the full compiled object rather than just the raw string.

  2. Eliminate algorithmic redundancy: Consolidate redundant pattern matching cases and prefer functional approaches over imperative ones when appropriate.

  3. Respect API constraints: Understand the limitations of your tools and APIs. For instance, if Fpath.add_seg only accepts filenames, design your interface accordingly.

  4. Context-dependent processing: Use different algorithms for different contexts. The same syntax may require different parsing strategies in patterns versus programs.

Example of context-aware data structure choice:

(* Instead of storing just the pattern string *)
| MvarRegexp (mvar, re_str, const_prop) ->
    let re = Pcre2_.pcre_compile re_str in
    (* Store the compiled object to avoid recompilation *)

(* Consider compilation context and flags *)
let re = match Pcre2_.remove_end_of_string_assertions compiled_re with
  | None -> raise GeneralPattern  
  | Some re -> re

This approach leads to more efficient, maintainable code that adapts appropriately to different operational contexts and constraints.


Use specific descriptive names

Choose names that clearly indicate the specific purpose, scope, or domain of functions, variables, and types rather than using generic terms. Generic names like is_start, is_middle, enable_ignore can be ambiguous and require readers to examine implementation details to understand their meaning.

Prefer specific, descriptive names that immediately convey what the identifier does or represents:

(* Instead of generic names *)
let is_start (l : single_line) : bool = ...
let is_middle (l : single_line) : bool = ...
let enable_ignore = false

(* Use specific, descriptive names *)
let is_if_start (l : single_line) : bool = ...
let is_else_start (l : single_line) : bool = ...
let apply_ignore_pattern = false

This principle also applies to type names - ensure they clearly communicate their purpose and distinguish them from similar types in the codebase. When names might still be unclear despite being specific, add clear documentation explaining the rationale behind the naming choice.


derive from session context

Always derive sensitive identifiers and permissions from the authenticated session context rather than accepting them as user input. This prevents privilege escalation attacks where users could manipulate request parameters to access unauthorized resources or perform actions on behalf of other users.

Instead of accepting sensitive IDs like stripeCustomerId or organizationId directly from request bodies, retrieve them from the authenticated session or validate that the user has permission to access the specified resource. Use session middleware to automatically reject requests without valid sessions.

Example of vulnerable pattern:

// BAD: Trusting user input for sensitive operations
body: z.object({
  stripeCustomerId: z.string(), // User could provide any customer ID
  organizationId: z.string(),   // User could access any organization
})

// GOOD: Derive from session context
const session = await getSessionFromCtx(ctx);
const user = session.user;
const stripeCustomerId = user.stripeCustomerId; // From authenticated user
const organizationId = await getUserOrganization(user.id); // Validated access

This approach ensures that all operations are properly scoped to the authenticated user’s permissions and prevents unauthorized access to resources belonging to other users.


Expose essential configurations

Carefully evaluate which configuration options should be exposed to users versus kept internal. Not all configuration parameters need to be user-controllable - expose only those that provide meaningful customization while keeping implementation details private.

For user-facing configurations, provide multiple access methods appropriate to the use case:

Consider the deployment context when designing configuration storage. For libraries that may run in serverless environments, avoid persistent global configuration files that won’t work across ephemeral instances.

Example of appropriate configuration exposure:

// Good: Essential user configurations exposed
interface DeviceAuthorizationOptions {
  expiresIn?: number;           // User should control
  interval?: number;            // User should control  
  verificationUri?: string;     // User should control
}

// Good: Internal implementation details not exposed
const opts = {
  deviceCodeLength: 40,         // Internal default
  enableRateLimiting: true,     // Internal behavior
  ...options,                   // User overrides
};

// Avoid: Exposing internal endpoints
telemetry?: {
  endpoint?: string;  // Internal implementation detail
}

Provide environment variable alternatives for settings that affect behavior across projects, allowing users to disable or configure features globally when appropriate.


Use descriptive naming

Choose specific, contextual names over generic terms to improve code clarity and maintainability. Generic names become ambiguous when combined into larger APIs or codebases, making it difficult for developers to understand functionality at a glance.

When naming functions, variables, or properties, include enough context to make their purpose clear without requiring additional documentation or code inspection.

Examples of improvements:

// ❌ Generic, unclear in larger context
export const verify = async (input) => { ... }
// ✅ Specific, self-documenting
export const verifySiweMessage = async (input) => { ... }

// ❌ Ambiguous scope
skipConsent?: boolean;
// ✅ Clear scope and intent  
skipConsentForTrustedClients?: boolean;

// ❌ Generic endpoint naming
export const realEndpoint: TelemetryEndpoint = async (event) => { ... }
// ✅ Descriptive function naming
export const sendTelemetryEvent = async (event) => { ... }

This practice becomes especially important in plugin systems or large APIs where multiple modules contribute functionality under a common namespace. A name like auth.api.verify provides little context, while auth.api.verifySiweMessage immediately communicates its specific purpose.


Validate before usage

Always explicitly check for null or undefined values before using them in operations. Don’t assume values exist - validate their presence and handle missing cases appropriately with early returns, error throwing, or fallback logic.

When working with potentially nullable values, implement explicit checks rather than relying on implicit behavior:

// ❌ Avoid - assumes value exists
const result = await handleOAuthUserInfo(ctx, {
  userInfo: { email: userInfo.email } // email could be null/undefined
});

// ✅ Good - validate before usage  
if (!userInfo.email) {
  throw new APIError("BAD_REQUEST", {
    message: "Email is required"
  });
}

// ❌ Avoid - proceeding without validation
const key = getIp(req, ctx.options) + path;

// ✅ Good - check and handle missing values
const ip = getIp(req, ctx.options);
if (!ip) {
  return; // Skip rate limiting if IP unavailable
}
const key = ip + path;

This prevents runtime errors, makes code behavior predictable, and ensures proper error handling when required values are missing. Consider whether missing values should trigger errors, fallbacks, or early returns based on your application’s requirements.


API endpoint correctness

API endpoints must behave according to their intended design, properly handling special values and executing expected operations like validation. This includes preserving meaningful null values (e.g., null representing “no limit”) rather than replacing them with default values that change the intended behavior, and ensuring validation runs when expected.

For example, when creating API keys where remaining: null signifies unlimited usage, the endpoint should preserve this null value rather than defaulting to a capped value. Similarly, validation endpoints like isUsernameAvailable should actually execute validation logic rather than bypassing it.

// Good: Preserve meaningful null values
const apiKey = {
  remaining: null // Correctly represents "no cap"
};

// Bad: Replace null with default that changes behavior  
const apiKey = {
  remaining: remaining || defaultLimit // Unintentionally caps usage
};

Intentional configuration management

Configuration choices should be intentional, balancing production requirements with developer experience while keeping configuration logic centralized in dedicated files rather than embedded inline.

When making configuration decisions:

Example of moving from inline to centralized configuration:

// Instead of inline filters in package.json:
"format": "biome format . --write && pnpm --filter @better-auth/svelte-kit-example format"

// Define filters in biome.json and use simple commands:
"format": "biome format . --write"

For build configurations, provide separate commands for development and production needs:

{
  "scripts": {
    "build": "tsup --clean --dts",
    "build:prod": "tsup --clean --dts --minify"
  }
}

This approach ensures configurations are maintainable, discoverable, and support both development debugging and production requirements.


Extract duplicated logic

When you notice the same code pattern appearing in multiple places, extract it into a reusable function rather than copy-pasting. Duplicated code is error-prone and harder to maintain. Look for repeated logic, data structure creation patterns, or similar operations that can be abstracted.

For example, instead of repeating fold operations:

(* Duplicated pattern *)
taints_and_shapes
|> List.fold_left (fun acc (taints, shape) ->
     acc |> Taints.union taints |> Taints.union (Shape.gather_all_taints_in_shape shape))

Extract it into a helper function that can be reused across multiple locations.

Similarly, instead of copy-pasting record definitions:

(* Instead of duplicating this structure *)
{
  source_kind = "semgrepignore";
  filename = custom_filename;
  format = Gitignore.Legacy_semgrepignore;
}

Create a parameterized function:

let semgrepignore_files ~filename = {
  source_kind = "semgrepignore";
  filename;
  format = Gitignore.Legacy_semgrepignore;
}

This approach improves code organization, reduces maintenance burden, and prevents inconsistencies that arise from manual copying.


Meaningful consistent naming

Use names that clearly describe the purpose and behavior of variables, methods, classes, and files, while maintaining consistency across the codebase. Follow these guidelines:

  1. Be semantically accurate: Names should reflect their exact purpose and behavior.
    • Name methods according to what they do: use ‘get_’ for retrieval and ‘set_’ for modification.
    • Example: Rename get_required_permissions to set_required_permissions if the method is setting values.
  2. Maintain consistency: Use consistent patterns and terminology throughout the codebase.
    • Apply consistent suffixes for related functions (e.g., all task functions should end with _task).
    • Use consistent placeholder values (e.g., always use ‘N/A’ for missing values instead of arbitrary values like ‘NoName’).
    • Stick to the same terminology for similar concepts (use either ‘report’ or ‘output’ consistently, not both).
  3. Follow established conventions: Adhere to language and framework-specific naming standards.
    • Use singular nouns for model class names (e.g., ‘SAMLConfiguration’ not ‘SAMLConfigurations’).
    • Choose descriptive variable names that distinguish similar concepts (e.g., api_token vs generic token).
    • Avoid ambiguous names that could cause confusion (e.g., prefer legacy_auth_enabled over modern_authentication when the value represents legacy authentication).

Document configuration variables

Always provide complete documentation for configuration variables, including their exact format, purpose, and behavior. For environment variables and connection strings, specify the full expected format with examples and explain why specific formats are required.

When documenting configuration options:

  1. Specify the complete format - Use the exact format needed, especially for connection strings that may appear to have redundant parts
    # Good: Full connection string format explained
    # export PROWLER_DB_CONNECTION=sqlite://
    # Format includes "://" to support future DB integrations like MongoDB or Redis
    
  2. Clarify when configuration changes take effect - Document whether changes apply immediately, on restart, or only for new operations
    ???+ note
        The Mutelist configuration takes effect on the next scans.
    
  3. Document automatic detection behavior - When environment variables can be detected automatically, specify the order of precedence
    If no token is explicitly provided, Prowler will automatically attempt to authenticate using environment variables in the following order of precedence:
       
    1. `GITHUB_PERSONAL_ACCESS_TOKEN`
    2. `OAUTH_APP_TOKEN`
    3. `GITHUB_APP_ID` and `GITHUB_APP_KEY`
    
  4. Include activation requirements - If additional steps are needed to apply configuration, document them clearly
    The necessary modules will not be installed automatically by Prowler. Nevertheless, if you want Prowler to install them for you, you can execute the provider with the flag `--init-modules`, which will run the script to install and import them.
    

Thorough configuration documentation prevents confusion, reduces support requests, and ensures users can correctly configure the system for their specific needs.


Maintain component consistency

Use a consistent approach when working with components to improve code maintainability and ensure visual coherence throughout the application:

  1. Reuse existing components rather than creating new ones for similar functionality.
    // Good: Using the existing ContentLayout component
    <ContentLayout title="Configure Chatbot" icon="lucide:settings">
      {/* Content here */}
    </ContentLayout>
       
    // Avoid: Creating new custom layouts for similar purposes
    
  2. Centralize styling through component props (color, variant, size) rather than adding custom classes:
    // Good: Using standard props for styling
    <CustomLink
      path="/manage-groups"
      ariaLabel="Manage Groups"
      variant="dashed"
      color="secondary"
    />
       
    // Avoid: Adding custom classes that override the component's design system
    <CustomLink
      path="/manage-groups"
      className="rounded-md px-4 py-2 !font-bold hover:border-solid hover:bg-default-100"
    />
    
  3. Extract reusable code into helper files when functionality is needed across multiple components:
    // Good: Create shared helpers for reusable functionality
    // In a helper file:
    export const PermissionIcon = ({ enabled }: { enabled: boolean }) => (
      // Icon implementation
    );
       
    // Then import in multiple components:
    import { PermissionIcon } from "@/helpers/permission-helpers";
    
  4. Maintain separation between different component types rather than merging them into one:
    // Good: Separate components for different HTML elements
    <CustomInput type="text" {...props} />
    <CustomTextArea {...props} />
       
    // Avoid: Conditionally rendering different elements within one component
    <CustomInput type={type === "textarea" ? "textarea" : "text"} {...props} />
    

Following these principles ensures consistent UI patterns, reduces technical debt, and makes the codebase easier to maintain as it grows.


Write objectively

Use an impersonal, objective tone in technical documentation. Avoid using second-person pronouns (“you”) and subjective terms like “simple” or “easy” which make assumptions about reader knowledge.

Instead of:

You can see full details about Mutelist here

Prefer:

See full details about Mutelist here

Similarly, when describing functionality, avoid characterizing it as “simple” or “straightforward” as this can be discouraging to readers who may struggle with the concept. Rather than describing how complex or easy something is, focus on clear, factual descriptions of what the feature does and how to use it. This creates more inclusive, professional documentation that respects diverse experience levels.


Format AI code identifiers

When documenting AI systems, always format code identifiers, function names, agent names, model names, and other technical references with backticks (`) to improve readability and clearly distinguish them from regular text. This is particularly important in AI documentation where specialized components like agents, models, and functions need to be visually distinct.

// Incorrect
The overview_agent provides a summary of connected cloud accounts.

// Correct
The `overview_agent` provides a summary of connected cloud accounts.

This formatting practice helps developers quickly identify technical components within documentation, making it easier to understand the architecture and implementation details of AI systems. Consistent formatting of identifiers also improves searchability of documentation and reduces confusion when discussing specific system components.


contextual failure handling

Implement intelligent failure handling that makes contextual decisions about retry, cleanup, and error propagation based on the type and likelihood of recovery from specific failures.

Consider these key principles:

Example implementation:

#!/usr/bin/env bash
set -euo pipefail

retry_with_context() {
    local url="$1"
    local temp_file="$2"
    local max_attempts=3
    
    for attempt in $(seq 1 $max_attempts); do
        if curl --fail --location "$url" > "$temp_file"; then
            return 0
        fi
        
        # Get HTTP code to decide if retry makes sense
        http_code=$(curl -s -o /dev/null -w "%{http_code}" "$url")
        case $http_code in
            404|403) 
                echo "Resource not found (HTTP $http_code), not retrying"
                return 1
                ;;
            429|5*) 
                echo "Temporary failure (HTTP $http_code), retrying in 2s..."
                sleep 2
                ;;
        esac
    done
    
    # Cleanup on final failure
    rm -f "$temp_file"
    return 1
}

This approach balances user experience with system reliability by making informed decisions about when failures are recoverable versus when they indicate fundamental problems that shouldn’t be retried.


Ensure cross-platform compatibility

Write code that works consistently across different platforms and environments. Avoid using Unicode symbols or special characters that may not render properly in all terminals or logging systems, as they can cause display issues or interfere with automated processing. Use portable system paths and commands that work across different operating systems.

For shell scripts, use #!/usr/bin/env bash instead of #!/bin/bash to ensure the script works on systems where bash is installed in different locations. Avoid fancy Unicode symbols like ⚠️ in output messages, opting instead for plain ASCII characters that display consistently everywhere.

Example:

# Good - portable and compatible
#!/usr/bin/env bash -e
echo "WARNING: attempt failed, retrying..."

# Avoid - may not work on all systems
#!/bin/bash -e  
echo "⚠️ attempt failed, retrying..."

This ensures your code maintains consistent behavior and appearance regardless of the target environment, improving reliability and user experience across different systems and tools.


Safe attribute access patterns

Implement consistent patterns for safely accessing potentially null or undefined attributes and dictionary values. This prevents runtime errors and makes code more robust.

Key patterns to follow:

  1. For dictionary access, use .get() with default value:
    # Instead of dict["key"]
    location = project_info["source"].get("location", "")
    
  2. For object attributes, use getattr():
    # Instead of obj.attribute
    name = getattr(contact, "name", "default")
    
  3. For complex fallback chains, use clear progressive checks:
    # Multiple fallbacks with clear intent
    resource_id = (
     explicit_id or 
     getattr(resource_metadata, "id", None) or 
     getattr(resource_metadata, "name", None) or 
     ""
    )
    
  4. For nested null checks, combine patterns:
    # Safe nested access
    is_mfa_capable = getattr(registration_details.get(user.id), "is_mfa_capable", False)
    

These patterns ensure graceful handling of missing values while maintaining code readability. Choose the appropriate pattern based on the data structure you’re working with (dictionary vs object) and the complexity of the fallback logic needed.


Use configurable default values

Make configuration values flexible and robust by avoiding hardcoded values and providing sensible defaults. This ensures your application works correctly even with incomplete configuration and supports different environments without code changes.

Key practices:

  1. Use environment variables with default values: ```python

    Better: Provide a sensible default

    prowler_db_connection = os.environ.get(‘PROWLER_DB_CONNECTION’, “memory://”)

Better: Use framework utilities for type conversion

FINDINGS_BATCH_SIZE = env.int(“DJANGO_FINDINGS_BATCH_SIZE”, 1000)


2. Extract magic numbers into configurable settings:
```python
# Problematic: Hardcoded threshold
if months_inactive >= 6:

# Better: Use a configurable setting
inactive_threshold_days = settings.REPOSITORY_INACTIVE_THRESHOLD_DAYS
months_inactive = (now - latest_activity).days / 30.44
if months_inactive >= inactive_threshold_days / 30.44:
  1. Make version requirements configurable to avoid code changes for policy updates: ```python

    Problematic: Hardcoded version check

    if sql_server.minimal_tls_version in (“1.2”, “1.3”):

Better: Use configuration setting

if sql_server.minimal_tls_version in settings.RECOMMENDED_TLS_VERSIONS:


4. Implement graceful fallbacks when configurations are missing:
```python
try:
    s3_client = boto3.client(
        "s3",
        aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
        aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
        region_name=settings.AWS_DEFAULT_REGION,
    )
except (ClientError, NoCredentialsError, ParamValidationError, ValueError):
    # Fallback to default credentials provider chain
    s3_client = boto3.client("s3")

This approach makes your application more maintainable, environment-agnostic, and future-proof.


Centralize environment variables

All environment variables should be defined in a centralized location (checkov/common/util/env_vars_config.py) rather than scattered throughout the codebase. This approach enhances maintainability, promotes consistency, and simplifies tracking of configuration settings.

When adding a new environment variable:

  1. Define it in the centralized config file with proper type conversion
  2. Add a descriptive comment explaining its purpose
  3. Import and use the variable in your code instead of direct os.getenv() calls

Always use strtobool() for boolean environment variables since bool('False') evaluates to True.

Example:

# In checkov/common/util/env_vars_config.py
from distutils.util import strtobool

# Controls whether to ignore hidden directories (default: True)
IGNORE_HIDDEN_DIRECTORY_ENV = strtobool(os.getenv("CKV_IGNORE_HIDDEN_DIRECTORIES", "True"))

# In your code
from checkov.common.util.env_vars_config import IGNORE_HIDDEN_DIRECTORY_ENV

if IGNORE_HIDDEN_DIRECTORY_ENV:
    # Skip hidden directories logic here

Specific exception handling

Handle exceptions with specificity rather than using broad catch-all blocks. Catch specific exception types, provide clear error messages, and respond appropriately based on the error type. This improves error diagnosis and enables targeted recovery strategies.

For example, instead of:

try:
    decrypted_key = fernet.decrypt(self.api_key)
    return decrypted_key.decode()
except Exception:
    return None

Use specific exception types with proper logging:

try:
    decrypted_key = fernet.decrypt(self.api_key)
    return decrypted_key.decode()
except InvalidToken:
    logger.warning("Failed to decrypt API key: invalid token.")
    return None
except Exception as e:
    logger.error(f"Unexpected error while decrypting API key: {e}")
    return None

For service-specific errors (like AWS S3), handle known error codes separately:

try:
    s3_object = s3_client.get_object(Bucket=bucket_name, Key=key)
except ClientError as e:
    error_code = e.response.get("Error", {}).get("Code")
    if error_code == "NoSuchKey":
        return Response(
            {"detail": "The scan has no reports."},
            status=status.HTTP_404_NOT_FOUND,
        )
    return Response(
        {"detail": "There is a problem with credentials."},
        status=status.HTTP_403_FORBIDDEN,
    )

Also, ensure that error messages are consistent with the actual state of the system and don’t report success when errors occur.


Prioritize code readability

Write code that optimizes for human readability and understanding. Complex expressions, while technically correct, can become difficult to understand and maintain over time.

When facing complex logic, prefer explicit, step-by-step approaches over condensed one-liners. This applies particularly to nested expressions, complex conditionals, and list comprehensions.

For example, instead of:

klass = next(
    (c for cond, c in COMPLIANCE_CLASS_MAP.get(provider_type, []) if cond(name)),
    GenericCompliance,
)

Consider a more readable approach:

klass = GenericCompliance  # Default value
for condition, cls in COMPLIANCE_CLASS_MAP.get(provider_type, []):
    if condition(name):
        klass = cls
        break

Other readability practices to follow:

Remember that code is read many more times than it’s written. Optimizing for readability leads to fewer bugs and easier maintenance.


Use pytest best practices

Adopt modern pytest features to create maintainable, isolated, and comprehensive tests. This includes:

  1. Prefer pytest over unittest for new test development to leverage pytest’s rich feature set and simpler syntax.

  2. Use parameterization for testing multiple scenarios instead of duplicating test code: ```python

    Instead of manual iteration over test cases:

    for graph_framework in [“NETWORKX”, “RUSTWORKX”]: # test code with graph_framework…

Use pytest’s parameterization:

@pytest.mark.parametrize(“graph_framework”, [“NETWORKX”, “RUSTWORKX”]) def test_connected_node_with_framework(graph_framework): # test code with graph_framework…


3. **Maintain test isolation** by using fixtures like `monkeypatch` to modify environment variables or dependencies for a single test:
```python
# Instead of modifying environment directly:
def test_get_folder_definitions_do_not_ignore_hidden(self):
    os.environ["CKV_IGNORE_HIDDEN_DIRECTORIES"] = "False"
    # test code...

# Use monkeypatch for isolated modifications:
def test_get_folder_definitions_do_not_ignore_hidden(monkeypatch):
    monkeypatch.setenv("CKV_IGNORE_HIDDEN_DIRECTORIES", "False")
    # test code...
  1. Write comprehensive assertions that validate specific properties, not just existence or counts, to ensure complete verification of expected behavior.

Following these practices will result in tests that are easier to maintain, less prone to side effects, and provide clearer failure information.


Service layer abstraction

Separate business logic and external service interactions from API views by using dedicated service layers. This improves testability, maintainability, and enforces separation of concerns in your API design.

For example, instead of embedding S3 interactions directly in views:

# Don't do this in your views
@action(detail=True, methods=["get"])
def report(self, request, pk=None):
    s3_client = boto3.client("s3")
    # Direct S3 interaction code...

Create a service module:

# services/storage.py
class StorageService:
    def get_report(self, tenant_id, report_id):
        s3_client = self._get_client()
        # S3 interaction code...
        
    def _get_client(self):
        # Client initialization logic

Then use it in your views:

@action(detail=True, methods=["get"])
def report(self, request, pk=None):
    storage_service = StorageService()
    report_data = storage_service.get_report(
        request.tenant_id, pk
    )
    # Handle response...

When extending existing functionality, reuse common methods rather than creating duplicates with similar behavior. This maintains consistency and reduces maintenance burden across your API.


Task signature methods

When chaining Celery tasks, choose the correct signature method:

Incorrect signature methods can cause task failures or unexpected data flow between tasks.

# Example of proper signature method usage:
chain(
    initial_task.si(param1=value1),  # First task or doesn't need previous output
    process_results.s(),  # Needs output from initial_task
    save_to_database.si(param2=value2)  # Independent task with static parameters
)

prefer authentication wrappers

Use authentication wrapper functions or higher-order functions that automatically handle unauthenticated calls rather than manually checking authentication status. These wrappers provide built-in security by ensuring that authentication edge cases are handled consistently and reducing the risk of forgetting to validate authentication in handlers.

For example, prefer using withMcpAuth which handles session retrieval and unauthenticated calls automatically:

// Preferred: Using authentication wrapper
const handler = withMcpAuth((session, req) => {
  // Handler logic with guaranteed authenticated session
});

// Avoid: Manual authentication checking
const handler = (req) => {
  const session = auth.api.getMcpSession(req);
  // Risk of forgetting to handle null/undefined session
};

This approach centralizes authentication logic, reduces code duplication, and minimizes security vulnerabilities from inconsistent authentication handling across your application.


Precise CSP configuration

When integrating third-party services into your web application, carefully configure Content Security Policy (CSP) headers by explicitly specifying only the necessary domains for each directive. Understand the complete technical requirements of each service, including resources they might dynamically load. Add domains to appropriate directives based on how they’re used (script-src, connect-src, img-src, etc.) to maintain strong security while preventing runtime CSP violations.

For example, when adding Google Tag Manager support:

const cspHeader = `
  default-src 'self';
  script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com https://www.googletagmanager.com;
  connect-src 'self' https://api.iconify.design https://api.simplesvg.com https://api.unisvg.com 
    https://js.stripe.com https://www.googletagmanager.com;
  img-src 'self' https://www.google-analytics.com https://www.googletagmanager.com;
  // Other directives...
`

This approach enhances security by following the principle of least privilege while ensuring functionality isn’t broken by CSP violations. Document any non-obvious additions (like tracking pixels that might be injected by services) to help team members understand security decisions.


Pin GitHub Actions dependencies

Always pin GitHub Actions to specific commit SHAs rather than using major version references to prevent supply chain attacks and ensure workflow stability. This practice ensures your CI/CD pipelines remain consistent and secure even if the referenced action is updated or compromised.

Example:

# Instead of this (vulnerable to supply chain attacks):
- name: Find existing changelog comment
  uses: peter-evans/find-comment@v3

# Use this (pinned to specific SHA and version):
- name: Find existing changelog comment
  uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e #v3.1.0

Additionally, when implementing workflows that require temporary workarounds (like installing special system dependencies), document these with TODOs and create tickets to revisit them when the underlying issues are resolved. This helps manage technical debt in your CI/CD configurations.


Document dependency versioning

Use consistent patterns for specifying dependency versions in configuration files and document reasoning behind version constraints.

Guidelines:

  1. Use exact versions (==) for stability-critical dependencies
  2. Use version ranges only when there’s a specific compatibility requirement
  3. Document the reason for version constraints, especially when using ranges or preventing upgrades
  4. Only add dependencies to the root configuration when the constraint applies project-wide

Example:

# Exact version for stability
some-critical-package = "1.2.3"

# Version range with documented reason
marshmallow = ">=3.15.0,<4.0.0"  # Safety tool incompatible with v4.0+

# Development dependency with more flexibility
pytest = ">=7.0.0"

When adding version constraints that differ from the project norm or prevent upgrades, add a comment explaining why, either in the PR description or inline with the configuration change.


Ensure migration compatibility

When modifying database schemas through migrations, ensure compatibility with existing data. For new required fields, use null=True or provide appropriate default values to prevent failures on existing records. Always keep migration files synchronized with model changes, and test migrations against a representative dataset before deploying to production.

For fields added to models that may already have data:

# Good practice:
first_seen_at = models.DateTimeField(null=True, editable=False)
# Or with a default value:
first_seen_at = models.DateTimeField(default=timezone.now, editable=False)

# Problematic - will fail on existing data:
first_seen_at = models.DateTimeField(editable=False)  # No null or default

For complex migrations involving raw SQL or custom Python functions, define and test reverse operations (RunSQL or RunPython). When true reversal is not possible (like adding enum values), use migrations.RunSQL.noop to explicitly indicate this limitation. Consider performance impacts when migrations affect large datasets, as this may block application startup.


Endpoints for evolving data

Design dedicated API endpoints for data that changes frequently or grows over time, rather than hardcoding such information in frontend files. This pattern ensures that applications remain maintainable as backend data evolves.

When to apply:

Example: Instead of:

// ui/lib/lighthouse/helpers/complianceframeworks.ts
export const complianceFrameworks = {
  aws: [
    { id: "cis-aws", name: "CIS AWS Benchmark" },
    { id: "aws-foundational", name: "AWS Foundational Security Best Practices" },
    // Adding new frameworks requires code changes and deployment
  ]
};

Implement an API endpoint:

// API service
app.get('/api/compliance-frameworks/:provider', (req, res) => {
  const provider = req.params.provider;
  return res.json(getComplianceFrameworksByProvider(provider));
});

// Frontend usage
const fetchFrameworks = async (provider) => {
  const response = await fetch(`/api/compliance-frameworks/${provider}`);
  return await response.json();
};

This approach eliminates maintenance overhead when adding new frameworks and allows for dynamic filtering and presentation options.


Consistent naming patterns

Follow systematic naming conventions for types, interfaces, and functions to enhance code readability and navigability:

Type/Interface Naming

Function Naming

Use Typed Constants

// ❌ Avoid
interface Role { id: string; type: "roles"; attributes: { /* ... */ }; }
const getFindingsLatest = async () => { /* ... */ };
type CredentialsForm = { aws_access_key_id: string; /* string literals */ };

// ✅ Better
interface RoleData { id: string; type: "roles"; attributes: RoleAttributes; }
interface RoleAttributes { /* ... */ }
const getLatestFindings = async () => { /* ... */ };
enum CredentialKeys { AwsAccessKey = "aws_access_key_id" /* ... */ }

This systematic naming makes the code more self-documenting and helps developers quickly distinguish between different types of interfaces when reading or searching through the codebase.


Flexible AI model versions

When integrating LLM or AI models into your application, implement model selection in a way that accommodates the rapid evolution of AI capabilities without requiring frequent code deployments.

Use Django enums or similar in-code definitions for validation while avoiding database-level constraints like Postgres enums that are difficult to modify:

# Recommended approach:
class AIModelConfig(models.Model):
    # Define choices in code for validation
    MODEL_CHOICES = [
        "gpt-4o",
        "gpt-4o-mini",
        # Add new models here without database migrations
    ]
    
    # Use CharField with validation in clean() instead of ChoiceField
    model = models.CharField(
        max_length=50,
        blank=False,
        null=False,
        help_text="Must be one of the supported model names"
    )
    
    def clean(self):
        # Validate model name
        if self.model not in self.MODEL_CHOICES:
            raise ValidationError(
                f"Model must be one of: {', '.join(self.MODEL_CHOICES)}"
            )

This pattern allows you to add support for new AI models by updating the MODEL_CHOICES list without requiring database migrations or application updates, while still maintaining validation to ensure only supported models are used.


Consistent environment variable naming

When naming environment variables, ensure that names are both technically accurate and follow established project conventions:

  1. Use terminology that precisely reflects the actual content and purpose of the variable, especially for security-related configurations.

  2. Follow established prefixing conventions in your project to indicate which system or component uses the variable.

Example:

# Bad: Inconsistent prefixing and potentially unclear naming
ARTIFACTS_STORAGE_PATH="/path/to/artifacts"
SAML_PUBLIC_CERT=""

# Good: Consistent prefixing and accurate naming
DJANGO_ARTIFACTS_STORAGE_PATH="/path/to/artifacts"
DJANGO_SAML_CERT=""  # or DJANGO_SAML_X509_CERT if more precision is needed

This approach improves code maintainability by making the purpose of variables immediately clear and ensuring consistency across the codebase.


Test authentication dependencies

When updating authentication-related dependencies or adding new authentication capabilities (like SAML, OAuth, etc.), thoroughly test all authentication flows to prevent security vulnerabilities. Authentication components often have complex interdependencies that can lead to unexpected failures when versions change.

Example:

# When updating authentication libraries like this:
dependencies = [
  "dj-rest-auth[with_social,jwt] (==7.0.1)",
  "django-allauth[saml] (>=65.8.0,<66.0.0)",  # Updated with new SAML capability
]

Always:

  1. Test all existing authentication methods (social logins, JWT, etc.)
  2. Verify the new authentication capabilities work as expected
  3. Document interdependencies between authentication libraries
  4. Consider using version ranges that balance security updates with stability

This is critical for security as broken authentication can lead to unauthorized access or account takeovers.


Extract focused functions

Break down complex logic into small, well-named functions that each do one thing well. This improves code readability, maintainability, and testability by making the code’s intent clearer and reducing cognitive load.

Consider this example from discussion #6:

# Before
if "virtual_resources" in vertex.config:
    for i, v in enumerate(self.vertices):
        if v.name in vertex.config["virtual_resources"]:
            self.create_edge(i, origin_node_index, "virtual_resource")

# After
def create_virtual_resources_edges(self, origin_node_index, vertex):
    if "virtual_resources" in vertex.config:
        for i, v in enumerate(self.vertices):
            if v.name in vertex.config["virtual_resources"]:
                self.create_edge(i, origin_node_index, VIRTUAL_RESOURCE_EDGE)

Similarly, complex conditions should be extracted into functions with descriptive names:

# Before
if self.bc_integration.repo_matches(file_suppression['repositoryName']) \
        and (suppression_file_path == record_file_path
             or suppression_file_path == convert_to_unix_path(record_file_path)):
    # Do something

# After
def is_matching_suppression_path(self, file_suppression, record_file_path):
    suppression_file_path = normalize_path(file_suppression['filePath'])
    return (self.bc_integration.repo_matches(file_suppression['repositoryName']) 
            and (suppression_file_path == record_file_path
                 or suppression_file_path == convert_to_unix_path(record_file_path)))

Apply this principle to:

This practice makes code more self-documenting and easier to test, modify, and debug.


centralize configuration management

Configuration values should be defined in a single location and passed through proper data structures rather than duplicated across modules or stored in global state. This prevents inconsistencies, reduces error-prone code duplication, and improves maintainability.

Avoid defining default values in multiple places:

(* Bad: Duplicating default value *)
let semgrepignore_filename = Option.value conf.semgrepignore_filename ~default:".semgrepignore" in

(* Good: Use centralized default from Semgrepignore.ml *)
Semgrepignore.create ?semgrepignore_filename:conf.semgrepignore_filename

Replace global configuration state with proper data structures:

(* Bad: Global mutable state *)
let custom_ignore_pattern : string option ref = ref None

(* Good: Pass configuration through data types *)
type config = { custom_ignore_pattern: string option; ... }
let process_with_config config = ...

Ensure configuration consistency across different implementations (e.g., Python and OCaml components) by maintaining a single source of truth for feature flags and their requirements.


Parameterize similar tests

Write maintainable tests by using pytest parametrization for similar test cases instead of duplicating test logic. This makes tests easier to read, maintain, and extend when requirements change.

For example, instead of writing multiple similar test functions:

# Before: Repetitive test functions
def test_invalid_name(self, client, payload):
    payload["data"]["attributes"]["name"] = "T"  # Too short
    response = client.post("/endpoint/", data=payload)
    assert response.status_code == 400
    assert "name" in response.json()["errors"][0]["source"]["pointer"]

def test_invalid_api_key(self, client, payload):
    payload["data"]["attributes"]["api_key"] = "invalid"  # Invalid format
    response = client.post("/endpoint/", data=payload)
    assert response.status_code == 400
    assert "api_key" in response.json()["errors"][0]["source"]["pointer"]

Use pytest parametrization to consolidate test logic:

# After: Parametrized test
@pytest.mark.parametrize(
    "field,value,expected_error",
    [
        ("name", "T", "name"),  # Too short
        ("api_key", "invalid", "api_key"),  # Invalid format
        ("temperature", 2.0, "temperature"),  # Out of range
        ("max_tokens", -1, "max_tokens"),  # Invalid value
    ],
)
def test_invalid_field_validation(self, client, valid_payload, field, value, expected_error):
    """Test validation failures for various invalid fields"""
    payload = copy.deepcopy(valid_payload)
    payload["data"]["attributes"][field] = value
    
    response = client.post("/endpoint/", data=payload, content_type=API_JSON_CONTENT_TYPE)
    assert response.status_code == 400
    errors = response.json()["errors"]
    assert any(expected_error in error["source"]["pointer"] for error in errors)

Also use fixtures to setup test data properly instead of relying on other tests:

@pytest.fixture
def lighthouse_config_fixture(db):
    """Create a test configuration in the database"""
    return LighthouseConfig.objects.create(
        name="Test Config",
        api_key="sk-test1234567890T3BlbkFJtest1234567890",
        model="gpt-4o"
    )

This approach reduces code duplication, improves readability, and makes it easier to add test cases as requirements evolve.


Least privilege principle

Always follow the principle of least privilege by granting the minimum permissions necessary for functionality. This applies to various security contexts:

  1. OAuth scopes: Request only the specific permissions needed: ```python

    BAD: Requesting excessive permissions

    SOCIALACCOUNT_PROVIDERS = { “github”: { “SCOPE”: [ “user”, “repo”, # Grants full access to repositories ], }, }

GOOD: Request only what’s needed

SOCIALACCOUNT_PROVIDERS = { “github”: { “SCOPE”: [ “user”, # Only user information ], }, }


2. **Default to secure values**: When uncertain about security settings, default to the more secure option:
```python
# BAD: Defaulting to insecure value
allow_security_end_user_reporting=global_meeting_policy.get(
    "AllowSecurityEndUserReporting", True
)

# GOOD: Default to secure value
allow_security_end_user_reporting=global_meeting_policy.get(
    "AllowSecurityEndUserReporting", False
)
  1. Authentication flows: Implement proper validation with clear error paths that provide minimal information to unauthenticated users.

  2. Access control: Avoid public access by default for all resources. For AWS services specifically, check and remove any policy statements with wildcard principals:

    # Replace public access policies with specific account-based access
    trusted_policy = {
     "Version": "2012-10-17",
     "Statement": [{
         "Effect": "Allow",
         "Principal": {"AWS": f"arn:{audited_partition}:iam::{account_id}:root"},
         "Action": "sqs:*",
         "Resource": resource_arn
     }]
    }
    

Regularly audit permissions and be vigilant about actions that could lead to privilege escalation, such as iam:PassRole or other powerful IAM actions.


Prefer server components

Default to server components in Next.js applications to maximize the benefits of server-side rendering. Avoid adding “use client” directives unnecessarily, especially at the page level.

When building features:

  1. Start with server components
  2. Only add “use client” when you need client-side interactivity
  3. Keep “use client” directives as low in the component tree as possible

For configurations or data that doesn’t require client-side interactivity, handle them directly in Next.js layouts or server components rather than introducing additional client components.

Example - Avoid:

// ui/app/(prowler)/lighthouse/config/page.tsx
"use client"; // Unnecessary - prevents server-side rendering

Example - Prefer:

// Keep pages as server components
// Only mark specific interactive components with "use client"
// ui/components/interactive-feature.tsx
"use client"; // Only where needed for client-side functionality

This approach leverages Next.js 14’s SSR capabilities, improves performance, and aligns with the project’s architecture.


Avoid localStorage for credentials

Do not store user profiles, authentication tokens, or other sensitive user information in client-side storage mechanisms like localStorage or sessionStorage. This creates security vulnerabilities as these storage mechanisms are accessible to any JavaScript running on the page, including potential XSS attacks.

Instead:

  1. Use server-side session management
  2. Utilize memory-only state for the current session
  3. Pass user data via React Context when needed across components
  4. Consider established authentication libraries that implement security best practices

Example - Instead of:

export const useUser = create(
  persist<UserStore>(
    (set) => ({
      user: null,
      setUser: (user) => set({ user }),
      clearUser: () => set({ user: null }),
    }),
    { name: 'user-storage', storage: createJSONStorage(() => localStorage) }
  )
);

Use a context-based approach:

// In a shared layout or provider
export const UserContext = createContext<UserProfileProps | null>(null);

export function UserProvider({ children }) {
  const { data } = useSession();  // Using an existing session management solution
  return <UserContext.Provider value={data?.user ?? null}>{children}</UserContext.Provider>;
}

// In components
export function useUser() {
  return useContext(UserContext);
}

Secure authentication flows

Implement authentication flows that protect sensitive information and follow secure credential management practices:

  1. Prevent domain enumeration attacks by returning consistent responses regardless of whether the input (like domain name) is valid:
// INSECURE: Different responses reveal valid domains
if (domainExists) {
  return redirect('/saml_login');
} else {
  return notFound();
}

// SECURE: Consistent responses prevent enumeration
// Always redirect to SAML endpoint, handle invalid domains later
return redirect('/saml_login'); 
// Handle invalid domains in the login page itself
  1. Use short-lived credentials instead of long-term static access keys when possible:
    • For AWS, prefer temporary session tokens or role assumption over permanent API keys
    • Use service principal authentication with appropriate permission scopes
    • Consider identity federation (SSO) where available
  2. Verify tenant identity in multi-tenant environments to ensure users and applications belong to the same tenant:
    • Validate domains match between service principals and user accounts
    • Use default/primary domains when tenant verification is critical
  3. Use correct environment variables when implementing authentication:
    • For Azure/M365: AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID
    • Document security implications and proper usage of authentication methods

These practices help mitigate common authentication vulnerabilities while maintaining proper security boundaries between tenants and users.


Use configuration placeholders

Avoid hardcoding specific values like regions, timeframes, or identifiers in configuration commands, examples, or settings. Instead, use descriptive placeholders that clearly indicate the type of value required. This practice:

  1. Makes configuration examples more reusable across different environments
  2. Prevents users from mistakenly using example values in production
  3. Supports parametrization of values that may change across environments or over time

Example - Instead of:

aws dms modify-replication-instance --region us-east-1 --replication-instance-arn arn:aws:dms:us-east-1:123456789012:rep:ABCDABCDABCDABCDABCDABCDAB --auto-minor-version-upgrade --apply-immediately

Use:

aws dms modify-replication-instance --region <REGION> --replication-instance-arn <REPLICATION_INSTANCE_ARN> --auto-minor-version-upgrade --apply-immediately

Similarly, when defining configurable timeframes or identifiers in code, make them parameters rather than hardcoding values like “6 months” or specific domain GUIDs. This approach improves flexibility, reusability, and reduces potential confusion about whether example values are required literals.


prefer built-in error utilities

Use existing error handling utilities and appropriate error mechanisms instead of implementing manual error handling logic. This reduces code complexity, improves maintainability, and leverages well-tested library functions.

For regex operations, prefer functions like pmatch_noerr that handle errors automatically:

(* Instead of manual error handling *)
match Pcre2_.pmatch ~rex:url_regex config_path with
| Ok true -> true
| Ok false -> false 
| Error err ->
    Log.warn (fun m -> m "Error Validation URL: %a" pp_error err);
    false

(* Use built-in error handling *)
let is_url config_path = Pcre2_.pmatch_noerr ~rex:url_regex config_path

For impossible cases, choose appropriate mechanisms: use assert false for truly impossible conditions, provide clear error messages for invalid input (“at least one target required” rather than “bug: no valid target”), or refactor to eliminate impossible cases entirely.

For conditional error handling, structure logic clearly with explicit branching rather than complex nested error propagation.


avoid platform-specific CI features

Avoid using CI platform-specific features in workflows to prevent vendor lock-in and ensure portability. Instead of relying on GitHub Actions artifacts, Docker Hub registries, or other platform-specific storage mechanisms, use cloud-agnostic alternatives like S3 for artifact storage and transfer between jobs.

This approach ensures that workflows can be easily migrated between different CI platforms (GitHub Actions, GitLab CI, Jenkins, etc.) without requiring significant rewrites. It also provides better control over retention policies, access permissions, and integration with existing infrastructure.

Example of what to avoid:

- name: Upload artifacts
  uses: actions/upload-artifact@v4
  with:
    name: build-artifacts
    path: |
      ${{ github.workspace }}/build/artifacts

Preferred approach:

- name: Upload artifacts to S3
  run: |
    aws s3 cp ${{ github.workspace }}/build/artifacts s3://your-bucket/artifacts/ --recursive

Additionally, consider consolidating sequential jobs to eliminate the need for intermediate artifact storage altogether, as this reduces both platform dependency and improves workflow efficiency.


Environment variables for configurations

Store all configurable values such as URLs, API endpoints, and external service locations in environment variables rather than hardcoding them in the codebase. This makes the application more maintainable, secure, and environment-adaptable.

Good practice:

// Define the environment variable in your .env file
// NEXT_PUBLIC_RSS_FEED_URL=https://prowler.com/blog/rss

// Then access it directly in your code
const feedUrl = process.env.NEXT_PUBLIC_RSS_FEED_URL;

Avoid:

// Hardcoding configuration values
const RSS_FEED_URL = "https://prowler.com/blog/rss";

For values that combine environment variables with runtime data, access the environment variables directly rather than creating unnecessary intermediate variables:

// Better approach
const cloudFormationUrl = `${process.env.NEXT_PUBLIC_AWS_CLOUDFORMATION_QUICK_LINK}${session?.tenantId}`;

// Rather than
const CLOUDFORMATION_QUICK_LINK = process.env.NEXT_PUBLIC_AWS_CLOUDFORMATION_QUICK_LINK;
const cloudFormationUrl = `${CLOUDFORMATION_QUICK_LINK}${session?.tenantId}`;

This approach improves maintainability, simplifies deployment across different environments, and reduces security risks associated with hardcoded values.


Restrict public access

Cloud resources should be configured to restrict public network access by default to minimize potential attack surfaces. Always explicitly disable public-facing endpoints and network interfaces unless absolutely necessary for the service to function.

When implementing security checks for cloud resources:

  1. Use appropriate base classes like BaseResourceValueCheck or BaseResourceNegativeValueCheck to verify public access is disabled
  2. Check the correct property paths in configuration (e.g., public_network_access_enabled, associate_public_ip_address)
  3. Handle default values correctly - many cloud resources enable public access by default
  4. Validate all network-facing attributes (ports, protocols, IP ranges)

Example for AWS EMR:

from checkov.terraform.checks.resource.base_resource_value_check import BaseResourceValueCheck
from checkov.common.models.enums import CheckCategories

class EMRPubliclyAccessible(BaseResourceValueCheck):
    def __init__(self):
        name = "Ensure AWS EMR block public access setting is enabled"
        id = "CKV_AWS_390"
        supported_resources = ['aws_emr_block_public_access_configuration']
        categories = [CheckCategories.NETWORKING]
        super().__init__(name=name, id=id, categories=categories, supported_resources=supported_resources)

    def get_inspected_key(self):
        return "block_public_security_group_rules"

For VM instances, database services, storage accounts, and container services, ensure you’re checking all network access points including public IPs, public endpoints, network rules, and default access policies.


Restrict public network

Always configure cloud resources to restrict or disable public network access unless explicitly required for the application’s functionality. Public network exposure increases the attack surface and should be minimized.

Key implementation patterns to follow:

  1. For cloud VMs and container instances:
    • Disable public IP address assignment
    • Example: Set associate_public_ip_address to false in AWS launch configurations
  2. For storage and database services:
    • Enable block public access settings
    • Use private endpoints instead of public access
    • Example: Ensure block_public_security_group_rules is true for AWS EMR
  3. For API and service endpoints:
    • Set public network access flags to ‘disabled’
    • Configure ‘deny’ as the default network access action
    • Example: For Azure resources, set public_network_access_enabled to false

Code example showing proper implementation:

from checkov.common.models.enums import CheckCategories
from checkov.terraform.checks.resource.base_resource_negative_value_check import BaseResourceNegativeValueCheck

class AutoScalingGroupWithPublicAccess(BaseResourceNegativeValueCheck):
    def __init__(self):
        name = "Ensure AWS Auto Scaling group launch configuration doesn't have public IP address assignment enabled"
        id = "CKV_AWS_389"
        supported_resources = ['aws_launch_configuration']
        categories = [CheckCategories.NETWORKING]
        super().__init__(name=name, id=id, categories=categories, supported_resources=supported_resources)

    def get_forbidden_values(self):
        return [True]

    def get_inspected_key(self):
        return "associate_public_ip_address"

Document authentication requirements

When creating or modifying APIs, always provide comprehensive documentation for authentication methods and prerequisites. For each authentication approach:

  1. Clearly state all supported authentication methods with explicit code examples
  2. Document the exact permissions required for each method
  3. Highlight any limitations or differences between authentication approaches
  4. Specify all prerequisite APIs or services that need to be enabled
  5. Include configuration requirements for billing and resource allocation

For example:

### Authentication Methods

#### Service Principal Authentication
```console
prowler microsoft365 --sp-env-auth

Browser Authentication

prowler microsoft365 --browser-auth --tenant-id "XXXXXXXX"

Prerequisites

Complete authentication documentation helps users successfully implement your API while avoiding unexpected permission issues or missing dependencies.


Parameterize configuration values

Always extract configuration values (like versions, paths, or other changeable settings) into parameters rather than hardcoding them directly in multiple places. This creates a single source of truth, simplifies updates, and makes your code more maintainable.

For Dockerfiles, use ARG directives:

# Good practice
ARG POWERSHELL_VERSION=7.5.0

# Later used as:
RUN curl -L https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/powershell-${POWERSHELL_VERSION}-linux-x64.tar.gz

Consider whether configurations should be controlled through environment variables or build arguments, based on when and how they need to be modified. For critical components like dependency versions, implementing a single source of truth ensures consistency across your application and simplifies future updates.


Next.js directory structure

Keep the Next.js /app directory strictly for routing purposes as per framework conventions. Non-routing related code like tools, utilities, and services should be moved to appropriate directories such as /lib/. This improves code organization, maintainability, and follows Next.js best practices.

For example, instead of:

// ui/app/(ai)/analyst/(tools)/checks.ts
import { tool } from "@langchain/core/tools";
import { aiGetProviderChecks } from "@/lib/lighthouse/helperChecks";
import { checkSchema } from "@/types/ai/checks";

export const getProviderChecksTool = tool(
  // tool implementation
);

Use:

// ui/lib/lighthouse/tools/checks.ts
import { tool } from "@langchain/core/tools";
import { aiGetProviderChecks } from "@/lib/lighthouse/helperChecks";
import { checkSchema } from "@/types/ai/checks";

export const getProviderChecksTool = tool(
  // tool implementation
);

This organization makes the codebase more maintainable and follows the intended use of the Next.js app directory structure.


Tenant-aware query optimization

Always include tenant_id filters in database queries for multi-tenant systems to maintain data isolation and improve query performance. In ORM queries, explicitly filter by tenant_id even when using Row Level Security (RLS) as a defense-in-depth measure and to help the query optimizer.

When fetching data in multi-tenant contexts:

# AVOID: Missing tenant filter can leak data across tenants
return LighthouseConfig.objects.all()

# RECOMMENDED: Always filter by tenant_id 
return LighthouseConfig.objects.filter(tenant_id=self.request.tenant_id)

Additionally, optimize database access by:

  1. Consolidating related queries to reduce round trips
  2. Caching results when appropriate
  3. Moving logic to avoid redundant queries for the same data

Example of optimizing repeated queries:

# AVOID: Extra query when delta is not NEW
if delta != Finding.DeltaChoices.NEW:
    first_seen = (
        Finding.objects.filter(uid=finding_uid, delta=Finding.DeltaChoices.NEW.value)
        .values("first_seen")
        .first()["first_seen"]
    )

# RECOMMENDED: Retrieve first_seen during delta calculation to avoid extra query
# In _create_finding_delta function:
def _create_finding_delta(last_status, current_status, last_finding=None):
    # Calculate delta...
    first_seen = datetime.now(tz=timezone.utc)
    if delta != Finding.DeltaChoices.NEW and last_finding:
        first_seen = last_finding.first_seen
    return delta, first_seen

Support all target environments

When managing dependency configurations (Pipfiles, requirements.txt, lock files), ensure compatibility with all supported environments in your project. Package version requirements should work across all supported Python versions and deployment environments.

Adding version constraints in dependency configurations:

  1. Verify compatibility with all supported Python versions
  2. Test configurations in all CI environments before committing
  3. If version locking is necessary, document the reason as a comment
  4. Consider separate PRs for dependency updates that require code changes

Example:

# Bad practice - locks to specific version that may not support all environments
asteval = "==1.0.6"  # Only supports Python 3.10+

# Good practice - explicitly specify a compatible version range
asteval = ">=0.9.27,<1.1.0"  # Supports Python 3.8 and above

# If version locking is necessary, include a comment explaining why
mypy = "==1.13.0"  # Locked temporarily to avoid breaking changes, see PR #1234 for update plan

Ensure dependency compatibility

When adding or updating dependencies in configuration files (Pipfile, requirements.txt), ensure compatibility with all supported Python versions in the project. Generated lock files and specific version constraints should work with the minimum supported Python version to prevent CI failures or production issues.

Example:

# If your project supports Python 3.8+, avoid dependencies like:
asteval = "==1.0.6"  # This version may not support Python 3.8

# Instead use:
asteval = "<1.0.6"  # Or a version known to be compatible with Python 3.8+

When generating lock files, use the minimum supported Python version environment to ensure all CI jobs and production environments can install dependencies successfully. Before committing changes to dependency configurations, verify compatibility by testing with the lowest supported Python version.


Write pythonic code

Embrace Python’s idiomatic programming style to make your code more readable, concise, and maintainable. Follow these Pythonic patterns:

  1. Use truthiness checks instead of explicit length comparisons:
    # Instead of this:
    if len(mod['Key']):
       
    # Do this:
    if mod['Key']:
    
  2. Access dictionaries directly without unnecessary calls:
    # Instead of this:
    if 'source_arn' in conf.keys():
       
    # Do this:
    if 'source_arn' in conf:
    
  3. Use dictionary comprehensions for building mappings:
    # Instead of this:
    for vertex in self.tf_graph.vertices:
        if vertex.block_type == BlockType.RESOURCE:
            self._address_to_tf_vertex_map[vertex.attributes[TF_PLAN_RESOURCE_ADDRESS]] = vertex
       
    # Do this:
    self._address_to_tf_vertex_map = {
        vertex.attributes[TF_PLAN_RESOURCE_ADDRESS]: vertex
        for vertex in self.tf_graph.vertices
        if vertex.block_type == BlockType.RESOURCE
    }
    
  4. Use context managers for resource handling:
    # Instead of this:
    f = open(f"{sast_policies_dir}/{policy.get('id')}.yaml", "a")
    # ...operations with f...
    f.close()
       
    # Do this:
    with open(f"{sast_policies_dir}/{policy.get('id')}.yaml", "a") as f:
        # ...operations with f...
    
  5. Prefer built-in type annotations over imported ones:
    # Instead of this:
    from typing import Dict, List
    def function(param: Dict[str, List[int]]) -> None:
       
    # Do this:
    def function(param: dict[str, list[int]]) -> None:
    
  6. Use fail-fast patterns to avoid deeply nested conditionals:
    # Instead of this:
    if source_bc_id:
        ckv_id = self.get_ckv_id_from_bc_id(source_bc_id)
        if ckv_id:
            ckv_ids.append(ckv_id)
       
    # Do this:
    if not source_bc_id:
        continue
    ckv_id = self.get_ckv_id_from_bc_id(source_bc_id)
    if not ckv_id:
        continue
    ckv_ids.append(ckv_id)
    
  7. Use static methods appropriately without unnecessary instantiation:
    # Instead of this:
    k8s_validator = K8sValidator()
    if k8s_validator.is_valid_template(t):
       
    # Do this:
    if K8sValidator.is_valid_template(t):
    

These practices will result in cleaner, more maintainable code that follows Python’s design philosophy of readability and simplicity.


Lambda CORS protection

When implementing network security checks for AWS Lambda functions, ensure that CORS (Cross-Origin Resource Sharing) policies are verified on the core Lambda function resources, not just on function URL endpoints. This prevents open CORS policies that could expose your Lambda functions to unauthorized cross-origin requests.

For proper implementation, target these resource types:

Example configuration for a security check:

metadata:
  name: "Ensure no open CORS policy"
  id: "CKV2_AWS_XX"
  category: "NETWORKING"
scope:
  provider: "aws"
definition:
  and:
    - cond_type: "filter"
      attribute: "resource_type"
      value:
        - "AWS::Lambda::Function"  # Core Lambda resource
        # - "AWS::Lambda::Url"     # Not sufficient alone

Target core resources

When implementing network security checks for serverless functions like AWS Lambda, ensure you target the core function resources rather than just their endpoints. Security checks for configurations like CORS policies should be applied to the primary Lambda resources (AWS::Lambda::Function, aws_lambda_function) rather than only their URL interfaces (AWS::Lambda::Url, aws_lambda_function_url).

This ensures comprehensive coverage of potential security vulnerabilities across your serverless architecture.

Example correction:

# Instead of:
value:
  - "AWS::Lambda::Url"

# Use:
value:
  - "AWS::Lambda::Function"

This approach applies across infrastructure-as-code platforms including CloudFormation and Terraform.


Improve documentation clarity

Write clear, accessible, and consistent documentation by using descriptive link text and providing complete, realistic examples. Avoid generic link text like “here” or “click here” - instead, describe what the link leads to. Ensure code examples are comprehensive and consistent across all documentation.

For links, replace generic text:

// Avoid
Read more [here](https://redis.io/).

// Prefer  
Read more [about Redis here](https://redis.io/).

For code examples, provide realistic, complete configurations:

// Avoid incomplete examples
connectionString: "redis://localhost:6379"

// Prefer complete, realistic examples
connectionString: "redis://user:password@localhost:6379"

This approach improves accessibility for screen readers and other assistive technologies while ensuring developers have practical, working examples they can adapt to their needs.


Use descriptive names

Choose names that clearly communicate purpose, content, and intent. Avoid generic or ambiguous identifiers that require additional context to understand.

Function names should describe what the function does:

// Avoid: generic or misleading names
function depGraphData(dg: DepGraphData, targetName: string): string
function throwCodeClientError(error) // doesn't actually throw

// Prefer: descriptive names that indicate the action/output
function depGraphToOutputString(dg: DepGraphData, targetName: string): string  
function resolveCodeClientError(error)

Variable names should indicate their content:

// Avoid: misleading or generic names
const dirPath = 'iac/cloudformation/aurora-valid.yml'; // actually a file path
const id = await submitHashes(); // which ID?

// Prefer: specific, descriptive names
const filePath = 'iac/cloudformation/aurora-valid.yml';
const taskId = await submitHashes();

Parameter names should be explicit and self-documenting:

// Avoid: generic objects or unclear parameters
function getCodeDisplayedOutput(testResults, meta, prefix, shouldFilterIgnored)
function getIacCloudContext(testConfig: Partial<TestConfig>)

// Prefer: explicit parameters that communicate intent
function getCodeDisplayedOutput(args: {
  testResults: CodeTestResults,
  meta: string,
  prefix: string,
  shouldFilterIgnored: boolean
})
function getIacCloudContext(snykCloudEnvironment?: string)

This reduces cognitive load, improves code readability, and makes the codebase more maintainable by eliminating the need to infer meaning from context.


validate environment variables early

Environment variables should be validated early in the application lifecycle with descriptive, actionable error messages. Use specific, namespaced variable names to avoid conflicts in CI/CD environments and other systems. When possible, avoid hardcoding configuration values and instead use configuration files or well-defined environment variables.

For required environment variables, implement early validation checks that fail fast with clear guidance on what the user needs to do:

// Good: Early validation with descriptive error
if iacRulesURL := os.Getenv("IAC_RULES_URL"); iacRulesURL == "" {
    return fmt.Errorf("IAC_RULES_URL environment variable is required. Please set it to the appropriate rules bundle URL")
}

Avoid generic environment variable names that could conflict with system or CI/CD variables:

# Avoid: Generic name that could conflict
ifeq ($(DEBUG), 1)

# Better: Use namespaced/specific names
ifeq ($(SNYK_DEBUG_BUILD), 1)

When configuration values are needed across multiple environments, prefer configuration files over hardcoded values:

# Avoid: Hardcoded version
$(PKG) -t node16-alpine-x64 -o $(OUTPUT)

# Better: Use configuration file
NODE_VERSION = $(shell cat .nvmrc | cut -d'v' -f2 | cut -d'.' -f1)
$(PKG) -t node$(NODE_VERSION)-alpine-x64 -o $(OUTPUT)

validate environment variable usage

Always validate environment variables with proper fallback logic and avoid unnecessary usage in tests or when defaults suffice. Check for truthiness rather than explicit None comparisons to handle empty strings, and ensure configuration values match their declared types and intended usage.

Key practices:

Example from XDG cache directory handling:

cache_home = os.getenv("XDG_CACHE_HOME")
if cache_home and Path(cache_home).is_dir():
    parent_dir = Path(cache_home)
else:
    parent_dir = Path.home() / ".cache"

This approach handles both None values and empty strings while providing a clear fallback path.


Pin dependency versions

Always pin dependencies to exact versions in package.json instead of using semantic version ranges (^, ~) to ensure reproducible builds and prevent unintentional version drift during deployments.

This practice is especially critical for applications deployed as units, where consistency across all dependency versions is essential. Using exact versions prevents scenarios where different environments might resolve to different dependency versions, leading to inconsistent behavior.

Use npm’s --save-exact flag when installing dependencies:

# Instead of: npm install some-package
npm install some-package --save-exact

# Or use the shorthand:
npm install some-package -E

This will add the dependency to package.json without version range operators:

{
  "dependencies": {
    "snyk-gradle-plugin": "4.1.0",  //  Exact version
    "snyk-cpp-plugin": "2.24.0"     //  Exact version
  }
}

Rather than:

{
  "dependencies": {
    "snyk-gradle-plugin": "^4.1.0",  //  Range allows 4.x.x
    "snyk-cpp-plugin": "^2.24.0"     //  Range allows 2.x.x
  }
}

While package-lock.json provides some protection against version drift, pinning to exact versions in package.json provides an additional layer of certainty and makes dependency management intentions explicit to all team members.


Parameterize security commands

When writing security-related CLI commands or remediation steps, always use standardized parameter placeholders (e.g., <REGION>, <RESOURCE_NAME>) instead of hardcoded values. This practice ensures commands are adaptable across different environments, prevents implementation errors, and makes security remediation steps more reliable. Properly parameterized commands also improve documentation and enable automation scripts to be more flexible and reusable.

Example:

# Incorrect (hardcoded values)
aws lambda remove-permission --region us-east-1 --function-name cc-process-app-queue --statement-id FullAccess

# Correct (parameterized)
aws lambda remove-permission --region <REGION> --function-name <FUNCTION_NAME> --statement-id FullAccess

Enable database resilience

Always configure appropriate resilience features for database services to ensure data durability and high availability. This includes:

  1. Enable regular backups with sufficient retention periods for all database clusters to prevent data loss
  2. Configure multi-AZ deployments for critical database services to ensure high availability

When implementing these features through infrastructure as code or CLI commands, use placeholders instead of hardcoded values for better reusability and documentation.

Example for configuring DocumentDB backups:

aws docdb modify-db-cluster --region <REGION> --db-cluster-identifier <DB_CLUSTER_ID> --backup-retention-period 7 --apply-immediately

Example for enabling Multi-AZ for DMS instances:

aws dms modify-replication-instance --region <REGION> --replication-instance-arn <REPLICATION_INSTANCE_ARN> --multi-az --apply-immediately

Log exceptions with context

When handling exceptions in your code, ensure they are properly logged with sufficient context to aid debugging. For critical exceptions, use Sentry to track them alongside regular logging. For handled exceptions, include both the object being processed and the exception details in your log message.

Example:

try:
    result = process_item(item)
except Exception as exc:
    # For critical exceptions, capture in Sentry
    sentry_sdk.capture_exception(exc)
    
    # Always log with context information
    logger.warning(f"Failed to process '{item}': {exc}")
    
    # Then handle the exception appropriately
    # Don't silently continue without logging

Choose the appropriate log level based on severity - use error for application failures and warning for handled exceptions. Proper exception logging improves troubleshooting capabilities and helps identify recurring issues in production.


avoid failwith patterns

Avoid using failwith, assert false, and similar blunt error handling mechanisms that can turn recoverable errors into unrecoverable ones. Instead, prefer more robust approaches:

Use pattern matching over exception handling:

(* Avoid *)
let params = if is_method then
    let (lb, ps', rb) = def.G.fparams in
    (lb, (try List.tl ps' with Failure _ -> ps'), rb)

(* Prefer *)
let params = if is_method then
    let (lb, ps', rb) = def.G.fparams in
    match ps' with
    | [] -> (lb, ps', rb)
    | _ :: tl -> (lb, tl, rb)

Use Result types for error-prone operations:

(* Avoid Either.t, prefer Result.t *)
val load_rules_from_file : ... -> (rules_and_origin, Rule.error) Result.t

Choose appropriate exception types:

Handle dangerous operations safely:

(* Avoid *)
let repository_path = url |> Git_wrapper.temporary_remote_checkout_path |> Option.get

(* Prefer *)
match Git_wrapper.temporary_remote_checkout_path url with
| Some path -> path
| None -> Error.abort "Failed to create temporary checkout path"

This approach prevents crashes from supposedly “impossible” cases (especially after error recovery), allows for better error propagation, and makes code more maintainable by explicitly handling edge cases.


comprehensive test coverage

Ensure thorough test coverage by extracting complex logic into small, testable functions and using appropriate testing utilities. When encountering complex code blocks, break them into smaller functions that can be tested in isolation. Use Go’s built-in testing utilities like t.Setenv() instead of os.Setenv() for environment variables, and assert.Eventually() instead of time.Sleep() for timing-dependent assertions. Cover multiple scenarios in your tests, including edge cases and different input conditions.

Example of extracting testable logic:

// Instead of testing complex inline logic
func (c *CLI) getErrorFromFile(errFilePath string) (data error, err error) {
    // ... complex logic here
    if len(jsonErrors) != 0 {
        useSTDIO := c.globalConfig.GetBool(configuration.WORKFLOW_USE_STDIO)
        // more complex processing...
    }
}

// Extract into testable function
func shouldUseSTDIO(config Config, jsonErrors []Error) bool {
    if len(jsonErrors) != 0 {
        return config.GetBool(configuration.WORKFLOW_USE_STDIO)
    }
    return false
}

// Test the extracted function
func Test_shouldUseSTDIO(t *testing.T) {
    t.Run("with errors", func(t *testing.T) {
        // test logic here
    })
    t.Run("without errors", func(t *testing.T) {
        // test logic here  
    })
}

Use proper testing utilities:

// Prefer this
func TestWithEnv(t *testing.T) {
    t.Setenv("SNYK_TOKEN", "invalidToken")
    // test logic
}

// Over this
func TestWithEnv(t *testing.T) {
    os.Setenv("SNYK_TOKEN", "invalidToken")
    defer os.Unsetenv("SNYK_TOKEN")
    // test logic
}

Configure observability variables

Always define all necessary environment variables for observability tools in configuration files, even if using default values. For error tracking services like Sentry, include both connection strings and contextual variables to ensure comprehensive error reporting. This improves troubleshooting by providing environment and release information with each error.

# Error tracking configuration
DJANGO_SENTRY_DSN=
SENTRY_ENVIRONMENT=local
SENTRY_RELEASE=local

Use proper URI parsing

When working with URLs and URIs in networking code, use proper URI parsing libraries and types instead of string manipulation or regex validation. This approach is more robust, handles edge cases correctly, and provides better type safety.

Instead of using regex patterns like ^https?:// to validate URLs, use the URI library’s parsing capabilities:

(* Avoid this *)
let url_regex = Pcre2_.regexp "^https?://"
let is_url config_path = Pcre2_.pmatch_noerr ~rex:url_regex config_path

(* Prefer this *)
let is_url config_path =
  match Option.bind Uri.scheme (of_string_opt config_path) with
  | Some "http"
  | Some "https" -> true
  | Some _
  | None -> false

Additionally, use Uri.t types instead of strings when representing URLs or URIs in function signatures and data structures. This makes the intent clearer and leverages the type system to catch errors at compile time.


Prevent sensitive data exposure

Never log or store sensitive information (passwords, tokens, secrets) in clear text. This common security vulnerability can lead to credential leaks and unauthorized access.

When logging:

  1. Remove sensitive data from logs entirely
  2. If logging is necessary, hash or mask the sensitive values
  3. Use specialized logging frameworks that automatically redact sensitive fields

For example, instead of:

logging.info(f"cloning {git_url} to {clone_dir}")  # git_url may contain credentials

Use:

# Extract and mask sensitive parts
safe_url = git_url if '@' not in git_url else git_url.split('@')[1]  
logging.info(f"cloning {safe_url} to {clone_dir}")

When handling secrets:

  1. Never store secrets in clear text variables
  2. Use environment variables or dedicated secret management services
  3. Implement proper filtering for references to vault secrets
  4. Add validation to prevent accidental logging of secrets

Remember that exposing sensitive data in logs is a common finding in security audits and can lead to significant security breaches. Consistently audit your code for instances of clear-text logging or storage of sensitive information.


Protect sensitive data

Never log, display, or store sensitive information like passwords, tokens, or secrets in clear text. This creates significant security vulnerabilities that could lead to unauthorized access and data breaches.

When working with sensitive data:

  1. Mask or hash sensitive values before logging
  2. Use secure logging frameworks that automatically redact sensitive data
  3. Implement proper encryption for data at rest and in transit
  4. Use dedicated secure storage solutions for secrets

Example of vulnerable code:

# Insecure - logs sensitive data in clear text
logging.info(f"cloning {git_url} to {clone_dir}")
print(f'attempting to load module {module_params.module_source} via git loader')

Secure alternative:

# Secure - masks sensitive information
safe_url = re.sub(r'(https?://)([^:]+)(:.+@|@)(.*)', r'\1\2@\4', git_url)
logging.info(f"cloning {safe_url} to {clone_dir}")

# For debugging, avoid logging sensitive parameters directly
logging.info(f"attempting to load module via git loader")

Memory usage optimization

Optimize memory usage in high-performance applications by implementing explicit memory management techniques. This includes:

  1. Configure cache limits to prevent excessive memory consumption:
    # Set appropriate cache size limits for database operations
    def _configure_cache(self):
     with self.conn:
         self.conn.execute(f'PRAGMA cache_size = {-self.cache_size}')
    
  2. Use context managers for garbage collection when processing large datasets:
    @contextmanager
    def force_gc(disable_gc=False):
     if disable_gc:
         gc.disable()
     try:
         yield
     finally:
         gc.collect()
     if disable_gc:
         gc.enable()
    
  3. Process data in streams rather than loading everything into memory at once: ```python

    Instead of:

    all_findings.extend(findings)

    And then processing all findings at once

Prefer streaming approach:

for finding in findings: process_and_write_to_file(finding)


These techniques are particularly important for long-running processes, applications handling large datasets, or systems with limited resources.

---

## Configuration naming consistency

<!-- source: snyk/cli | topic: Configurations | language: JavaScript | updated: 2025-01-29 -->

Ensure configuration elements (environment variables, feature flags, config files) maintain consistent naming and clear documentation across code, logs, and documentation. Mismatched names between implementation and logging/documentation create confusion and debugging difficulties.

When working with configuration:
- Use identical names in code, environment variables, logs, and documentation
- Add clear comments explaining the purpose of configuration wrappers or compatibility functions
- Ensure feature flags and configuration parameters are properly propagated through function calls

Example of inconsistent naming to avoid:
```javascript
// Code uses SNYK_CONFIG_FILE but logs reference TEST_CONFIG_FILE
if (!process.env.SNYK_CONFIG_FILE && !process.env.TEST_CONFIG_FILE) {
  // ...
}
console.info('Snyk configuration [TEST_CONFIG_FILE] ...' + process.env.SNYK_CONFIG_FILE);

Example of proper configuration handling:

// Wrapper for CommonJS compatibility
async function callModule(mod, args) {
  // Clear documentation of purpose
}

function run(root, options, featureFlags) {
  // Properly propagate configuration parameters
  validateProjectType(options, projectType, featureFlags);
  return runTest(projectType, root, options, featureFlags);
}

Document intent and reasoning

Add comments that explain the reasoning, intent, or context behind code decisions, especially for non-obvious logic, workarounds, or business requirements. Focus on the “why” rather than just the “what” to help future maintainers understand the decision-making process.

Examples of good explanatory comments:

Avoid comments that simply restate what the code does. Instead, provide context about why certain approaches were taken, what constraints influenced the design, or what future considerations should be kept in mind.


Optimize database queries

Avoid inefficient database query patterns by using batch operations and appropriate query types. Instead of making multiple individual queries, use batch queries with in clauses. When you only need counts, use count queries rather than fetching full records.

Examples of improvements:

// Use a single batch query const roleIds = userRoles.map(ur => ur.roleId); const roles = await ctx.context.adapter.findMany({ model: “role”, where: [{ field: “id”, operator: “in”, value: roleIds }] });


- Use count queries when you don't need the actual data:
```typescript
// Instead of fetching all records
const allUsers = await db.findMany({ model: "user" });
const userCount = allUsers.length;

// Use count directly
const userCount = await db.count({ model: "user" });

This approach reduces database load, improves response times, and minimizes memory usage by avoiding unnecessary data transfer.


Safe dictionary navigation

Always use the .get() method with appropriate default values when accessing dictionaries instead of direct key access with brackets. This prevents KeyError exceptions when keys don’t exist and provides predictable default values. For nested dictionaries, chain .get() calls with empty dictionaries as defaults for intermediate levels.

Example of problematic code:

# May raise KeyError if 'properties' or 'siteConfig' keys don't exist
if conf['properties']['siteConfig'] is not None:
    # process configuration

Improved version:

# Safely handles missing keys with default values
if conf.get('properties', {}).get('siteConfig') is not None:
    # process configuration

For collections that should be lists, provide an empty list as the default:

# Safe access for list-type values
for item in conf.get('items', []):
    # process each item

When type checking is necessary before access:

# Verify both existence and type
if conf.get('properties') and isinstance(conf.get('properties'), dict):
    # safely work with the dictionary value

Always verify that a value is the expected type before accessing its attributes or performing operations, especially after retrieving it with .get().


Fix dependency vulnerabilities

Security vulnerabilities in dependencies must be addressed immediately rather than bypassed with flags like --no-verify. When security scans identify vulnerabilities in libraries like the one detected in pyjwt:

VULNERABILITIES REPORTED
Vulnerability found in pyjwt version 2.9.0
Vulnerability ID: 74429
Affected spec: <2.10.1
ADVISORY: Affected versions of pyjwt are vulnerable to Partial
Comparison (CWE-187). This flaw allows attackers to bypass issuer (iss)...
CVE-2024-53861

Prioritize updating to a secure version before continuing development. Create a dedicated PR to address the vulnerability rather than working around security checks. For critical security issues, establish a policy that prevents merging code with known high-severity vulnerabilities and define clear remediation timeframes based on severity levels.


Handle all errors explicitly

Always explicitly handle errors rather than assuming they are handled by dependencies or ignoring them. Every function that returns an error should have that error checked and handled appropriately. Use proper error wrapping with fmt.Errorf and the %w verb to maintain error chains, and ensure resource cleanup with defer statements even when errors occur.

Key practices:

Example:

// Bad - assuming error is handled elsewhere
match, _ := regexp.MatchString("^[a-zA-Z0-9-]+$", name)

// Good - explicit error handling
match, err := regexp.MatchString("^[a-zA-Z0-9-]+$", name)
if err != nil {
    log.Fatal(err)
}

// Bad - complex defer with ignored error
defer func(Body io.ReadCloser) {
    err := Body.Close()
    if err != nil {
        // empty
    }
}(resp.Body)

// Good - simple defer
defer resp.Body.Close()

// Good - proper error wrapping
if err != nil {
    return fmt.Errorf("failed to create cache directory: %w", err)
}

This approach prevents silent failures, makes debugging easier, and ensures robust error handling throughout the codebase.


prevent silent test failures

Tests must explicitly fail when expected conditions are not met, rather than silently exiting early or using weak assertions that can hide real issues. Silent test failures create false confidence in test suites and can mask regressions.

Key practices:

  1. Fail explicitly on missing preconditions: Instead of early returns, throw errors or use explicit assertions
  2. Use strict assertions: Prefer exact matches over loose checks like “greater than 0” unless specifically needed
  3. Validate actual content: Test the substance of outputs, not just their existence
  4. Assert before accessing: Check conditions before attempting to access files or data structures

Example of problematic pattern:

it('should create sarif result with ignored issues omitted', async () => {
  const sarifWithoutIgnores = resultWithoutIgnores?.analysisResults.sarif.runs[0].results;
  if (!sarifWithoutIgnores) return; // Silent failure - test passes but validates nothing
  // ... rest of test
});

Better approach:

it('should create sarif result with ignored issues omitted', async () => {
  const sarifWithoutIgnores = resultWithoutIgnores?.analysisResults.sarif.runs[0].results;
  expect(sarifWithoutIgnores).toBeDefined(); // Explicit failure if condition not met
  // ... rest of test with confidence that data exists
});

For output validation:

// Weak - only checks existence
expect(stdoutBuffer).toBeDefined();

// Better - validates actual structure and content
expect(JSON.parse(stdout)).toMatchSchema(expectedSchema);
expect(backendRequests).toHaveLength(2); // Exact expectation, not just > 0

This prevents tests from appearing to pass when they’re actually not testing anything meaningful.


Choose optimal algorithms

Always select appropriate data structures and algorithms based on their performance characteristics and the specific operations your code needs to perform. This can significantly improve both code readability and execution efficiency.

For data structures:

Use:

secret_suppressions_id = {suppression[‘policyId’] for suppression in suppressions}


- Use defaultdict when initializing dictionaries that need default values for missing keys:
```python
# Instead of:
if dir_path in dirs_to_definitions:
    dirs_to_definitions[dir_path].append({tf_definition_key: tf_value})
else:
    dirs_to_definitions[dir_path] = [{tf_definition_key: tf_value}]

# Use:
from collections import defaultdict
dirs_to_definitions = defaultdict(list)
dirs_to_definitions[dir_path].append({tf_definition_key: tf_value})

For algorithms:

Use:

for field, value in each[“change”][“before”].items(): if value != each[“change”][“after”].get(field):


- Avoid regular expressions with potential for exponential backtracking, especially on large inputs

When in doubt, use specialized libraries that implement optimized algorithms (e.g., for version comparison) rather than implementing your own solutions.

---

## prefer simple readable code

<!-- source: semgrep/semgrep | topic: Code Style | language: Other | updated: 2024-12-02 -->

Choose clarity and simplicity over clever or complex constructs. Avoid unnecessary abstractions, complex function compositions, and "clever" code patterns that sacrifice readability for brevity.

**Key principles:**
- Use direct, explicit code over complex function compositions like `Option.map (Fun.const true)`
- Prefer simple conditionals over polymorphic variants with unnecessary indirection
- Choose appropriate built-in functions (`List.exists` for boolean checks, `String.starts_with` over manual substring comparison)
- Avoid polymorphic equality operators when specific ones are available
- Extract helper functions to eliminate code duplication rather than repeating complex logic

**Example:**
```ocaml
(* Avoid: clever but hard to read *)
code_config = Option.map (Fun.const true) code_config

(* Prefer: explicit and clear *)
match code_config with 
| None -> false 
| Some _ -> true

(* Or even simpler *)
Option.is_some code_config

The goal is code that any team member can quickly understand and modify. When choosing between a clever one-liner and a few clear lines, prefer clarity. This improves maintainability and reduces the cognitive load for future developers working with the code.


debug logging reliability

Ensure debug logging behaves correctly by only activating when debug flags are explicitly enabled, while providing comprehensive coverage of all relevant cases when active. Debug logs should not cause application failures and should be printed for all error conditions, not just specific subsets.

Key principles:

Example of proper debug setup:

// Correct: Debug only enabled when intended
const debug = debugLib('snyk-code');

// Incorrect: Always enabling debug
debugLib.enable('snyk-code');
const debug = debugLib('snyk-code');

Example of comprehensive debug coverage:

// Print debug logs for all cases, not just specific conditions
if (debugLogs[fileData.filePath]) {
  debug(
    'File %s failed to parse with: %s',
    fileData.filePath,
    debugLogs[fileData.filePath],
  );
}

Preserve API compatibility

When modifying existing APIs, function signatures, or return structures, always maintain backward compatibility to prevent breaking dependent code. This applies to both internal interfaces between components and external API integrations.

Key practices:

  1. Make new function parameters optional with sensible defaults
  2. Support multiple versions of external APIs when needed
  3. Maintain consistent return structures or provide migration paths

Example of adding an optional parameter correctly:

# Before
def serialize_to_json(igraph: Graph) -> Dict[str, Any]:
    # implementation

# After - Added parameter is optional
def serialize_to_json(igraph: Graph, absolute_root_folder: str = None) -> Dict[str, Any]:
    # implementation with handling for when absolute_root_folder is None

Example of supporting multiple API versions:

def check_azure_container_registry():
    # Support both v2 and v3 attribute paths
    if "retention_policy/enabled" in resource:
        # Handle v2 API format
        return resource["retention_policy/enabled"]
    elif "data_retention_policy/days" in resource:
        # Handle v3 API format
        return resource["data_retention_policy/days"] > 0
    # Handle missing case appropriately

When changes to return values are necessary, consider using tuple returns that can be extended while keeping backward position-compatibility, or use dictionaries/objects that can be enhanced without breaking existing code.


Graceful error handling

Implement robust error handling that provides fallback mechanisms, ensures proper resource cleanup, and delivers user-friendly error messages without exposing internal details.

Key principles:

  1. Use finally blocks for cleanup: When modifying global state or resources, always reset them in finally blocks to prevent resource leaks when errors occur
  2. Provide fallback mechanisms: When primary operations fail (like JSON serialization), implement fallback strategies rather than failing completely
  3. Check error types, not messages: Use instanceof Error or specific error types rather than fragile string matching on error messages
  4. Handle unexpected scenarios gracefully: When encountering unexpected conditions, log debug information and provide user-friendly error messages
  5. Avoid exposing internal details: Don’t leak internal error messages or stack traces to external users; provide sanitized, actionable error messages

Example implementation:

// Good: Proper cleanup and fallback
export async function processWithCleanup(config: Config): Promise<string> {
  let originalValue;
  try {
    originalValue = global.someProperty;
    global.someProperty = newValue;
    
    const result = await riskyOperation();
    
    // Fallback for serialization
    try {
      return JSON.stringify(result, null, 2);
    } catch (serializationError) {
      debug('JSON.stringify failed, trying without pretty print', serializationError);
      return JSON.stringify(result);
    }
  } catch (error) {
    // Type-based error handling
    if (error instanceof CustomError) {
      return handleCustomError(error);
    }
    
    // Graceful handling of unexpected errors
    debug(`Unexpected error: ${error}`);
    throw new UserFriendlyError('Operation failed. Please try again or contact support.');
  } finally {
    // Always cleanup, regardless of success or failure
    if (originalValue !== undefined) {
      global.someProperty = originalValue;
    }
  }
}

This approach ensures applications remain stable and user-friendly even when encountering unexpected conditions or failures.


Use appropriate logging levels

Select the correct logging level based on the importance and visibility requirements of the information. Choose WARNING for issues that should be visible by default and may require attention, INFO for general operational messages, and DEBUG for detailed information needed only during troubleshooting. Additionally, ensure log messages include sufficient context to be meaningful on their own.

Example:

# Good - Using warning for issues that should be visible by default
logging.warning(f"Resource dependency {processed_dep} not found in {dep}")

# Good - Using debug for detailed information with context
logging.debug(f"OpenAI request returned: {completion}")

# Good - Using info with specific context about what's being processed
logging.info(f"Done persisting {len(maps)} maps to {bucket}/{key}")

# Good - Using logging framework instead of print statements
logging.info(f"Authentication failed for user {username}")

# Avoid - Using info for detailed debug information
# logging.info(f"[COMPLETION]{completion}")

# Avoid - Vague messages without context
# logging.warning("Dependency not found")

# Avoid - Using print statements
# print(f"Authentication failed for user {username}")

When deciding between log levels, consider both the importance of the message and how it will be used. Warning logs should be used for situations that aren’t errors but may require attention, while debug logs should contain information helpful for troubleshooting without cluttering normal operation logs.


Use centralized loggers

Always use centrally supplied loggers from the invocation context or configuration instead of creating new logger instances, using log.Default(), or direct output methods like fmt.Println(). This ensures consistent debug configuration, proper log level control, and centralized logging behavior across the entire application.

Creating new loggers bypasses the centralized configuration and can result in logs appearing even when debugging is disabled, or missing important configuration like log levels and output destinations.

Preferred approach:

// Use logger from invocation context
debugLogger := invocation.GetEnhancedLogger()
debugLogger.Printf("Command timeout set for %d seconds", timeout)

// Or use centrally configured logger with wrapper when needed
p.DebugLogger = log.New(&gafUtils.ToZeroLogDebug{Logger: debugLogger}, "", 0)

Avoid:

// Don't create new loggers
zerologLogger := zerolog.New(os.Stderr).With().Timestamp().Logger()

// Don't use defaults that bypass central configuration  
p.authenticator = httpauth.NewProxyAuthenticator(p.authMechanism, p.upstreamProxy, log.Default())

// Don't use direct output methods
fmt.Println("Error deleting an old version directory")

This practice ensures that all logging respects the debug configuration and logger settings that are centrally managed, providing consistent behavior and proper control over log output.


Write actionable documentation

Documentation should be written in active voice with concrete examples and clear context. Avoid passive constructions and vague references that leave readers guessing about implementation details or expectations.

Use active voice to make instructions direct and actionable:

❌ Should be used when using `--sarif`. Will remove the `markdown` field...
✅ Removes the `markdown` field from the `result.message` object. Should be used when using `--sarif`.

Provide concrete examples when referencing technical concepts:

❌ Add the replace directives you need in `cliv2/go.mod`
✅ Add the replace directives you need in `cliv2/go.mod`:
    replace github.com/snyk/go-application-framework => ../go-application-framework

Include context about purpose and expectations, especially in templates and guides:

❌ ## Pull Request Submission
✅ ## Pull Request Submission
    Thanks for your interest in improving Snyk CLI. To ensure all our users have a great experience, please carefully complete all following fields.

This approach ensures documentation is immediately useful and reduces back-and-forth clarification requests during reviews.


Use centralized configuration access

Always access configuration values through the configuration object rather than direct helper functions or environment variables. This ensures consistent behavior across the application and provides proper abstraction from the underlying value sources.

Instead of calling utility functions directly or accessing environment variables, use the configuration object to retrieve values. This approach decouples the implementation from knowing where values come from and how they need to be named, making the code more maintainable and testable.

Example of what to avoid:

// Don't use helper functions directly
tmpDirectory := utils.GetTemporaryDirectory(cacheDirectory, cliVersion)

// Don't access environment variables directly  
integration := os.Getenv("SNYK_INTEGRATION_NAME")

Preferred approach:

// Use configuration object instead
tmpDirectory := config.GetString(configuration.TEMP_DIR_PATH)

// Access through configuration abstraction
integration := globalConfiguration.GetString(configuration.INTEGRATION_NAME)

This pattern ensures that configuration values are consistently accessed, properly validated, and can be easily mocked or overridden for testing. It also enables centralized configuration management where alternative keys and default values can be handled uniformly.


Use file locks

When multiple processes or goroutines need to access shared file system resources concurrently, use file locks (flock) to prevent race conditions and ensure mutual exclusion. This is particularly important for operations like certificate file management, directory creation, and other file system modifications that could conflict.

Consider placing lock files in appropriate locations - for resources being created, use a lock file in an existing directory (like temp dir) rather than within the directory being created. This prevents the chicken-and-egg problem of needing to create a directory to place a lock file for that same directory creation.

Example approach:

// For certificate file operations
if _, existsError := os.Stat(caSingleton.CertFile); errors.Is(existsError, fs.ErrNotExist) {
    // Any error should trigger regeneration, use file lock during creation
    // Set exclusive flock on cert file while process is running
}

// For directory creation with subdirectories
// Use flock in conjunction with MkdirAll, place lock file outside target directory
lockFile := filepath.Join(os.TempDir(), "cache-creation.lock")
// Acquire lock, then safely create directory structure

This pattern ensures that concurrent operations on shared file resources are properly synchronized and prevents corruption or inconsistent state.


Meaningful identifier names

Choose descriptive and semantic identifiers that clearly convey purpose, role, and behavior. Avoid vague, generic names in favor of specific ones that enhance code readability and self-documentation.

Key practices:

  1. Use descriptive names - Replace generic identifiers with specific ones that reveal intent:
    # Poor naming
    new_value = [*vertex.attributes[property], *value]
       
    # Better naming
    list_updated_value = [*vertex.attributes[property], *value]
    
  2. Function names should reflect behavior - Functions should have names that indicate what they do or return:
    # Misleading (returns boolean)
    def _prioritise_secrets(secret_records, secret_key, check_id):
           
    # Clear and accurate
    def _should_prioritise_secrets(secret_records, secret_key, check_id):
    
  3. Avoid name collisions - Use prefixes or qualifiers to distinguish similar components:
    # Ambiguous - multiple "Runner" classes in codebase
    class Runner(TerraformRunner):
           
    # Clear and specific
    class TerraformJsonRunner(TerraformRunner):
    
  4. Use consistent naming patterns - Maintain consistent naming styles for similar concepts:
    # Inconsistent
    self.dataflow = data_flow
       
    # Consistent
    self.data_flow = data_flow
    
  5. Disambiguate from standard library - When naming functions that might conflict with standard functions, use clarifying prefixes:
    # Potentially confusing with standard library
    def deepcopy(obj: _T) -> _T:
           
    # Clear it's a custom implementation
    def pickle_deepcopy(obj: _T) -> _T:
    

When iterating over collections, use descriptive variable names instead of generic ones:

# Vague
for each in resource_changes:
    # ...

# Descriptive
for changed_resource in resource_changes:
    # ...

maintain CI/CD boundaries

Ensure clear separation between different CI/CD concerns and maintain accurate documentation of automated processes. Release artifacts like release notes should not be committed to main development branches, and testing workflows should be clearly documented with accurate branch-specific behaviors.

For release management, keep release notes and similar artifacts in dedicated release branches or automated release processes rather than cluttering the main development branch. For testing documentation, ensure accuracy in describing when and how different test suites execute, including branch-specific triggers and scheduling.

Example of proper smoke test documentation:

### Smoke Tests

Smoke tests typically don't run on branches unless the branch is specifically prefixed with `smoke/`. They usually run on an hourly basis against the latest published version of the CLI.

This maintains clear boundaries between development work, release processes, and testing workflows, making the CI/CD pipeline more predictable and maintainable.


Document configuration consistently

Ensure all configuration options are clearly documented and follow consistent naming and syntax conventions. This includes:

  1. Explicitly document prerequisites - Clearly state all required tools, dependencies, and environment variables needed for configuration. For example, when jq is required for processing:
    terraform show -json tfplan.binary | jq > tfplan.json
    
  2. Use proper syntax for configuration files - When defining multiple options with the same key in JSON configurations, use arrays:
    "//": [
      "checkov:skip=CVE-2023-123: ignore this CVE for this file",
      "checkov:skip=express[BC_LIC_2]: ignore license violations"
    ]
    
  3. Follow consistent naming conventions - For configuration flags and environment variables:
    • Use prefixes like DISABLE_* for turning off features that are enabled by default
    • Document environment variables in a structured format with clear descriptions
    • Maintain consistent enumeration values (e.g., severity levels: INFO, LOW, MEDIUM, HIGH, CRITICAL)
    • Use proper capitalization for product names (e.g., “Terraform Cloud / Enterprise”)
  4. Place configurations in appropriate locations - Environment variables should be documented in Settings blocks, and related configuration options should be grouped together.

Consistent naming conventions

Maintain consistent naming patterns throughout the codebase to improve readability and reduce confusion:

  1. Use descriptive names that reflect purpose:
    • Boolean functions should use prefixes like is_, has_, or should_ (e.g., _should_prioritise_secrets instead of _prioritise_secrets)
    • Loop variables should be descriptive (e.g., resource or changed_resource instead of each)
    • Classes should have specific names that avoid ambiguity (e.g., TerraformJsonRunner instead of Runner)
  2. Follow project style conventions:
    • Use snake_case for variables and functions (e.g., self.data_flow not self.dataflow)
    • Use underscores in file names instead of spaces (e.g., a_example_skip not a example skip)
    • Prefix environment variables with CHECKOV_ for internal vars
    • Use distinctive names for utility functions to avoid IDE confusion (e.g., pickle_deepcopy instead of just deepcopy)
  3. Use consistent type hint syntax:
    • Prefer modern Union syntax: str | None instead of Optional[str]
    • Use lowercase type aliases: tuple instead of Tuple

These conventions make code easier to understand, maintain, and debug while reducing the cognitive load for developers working across different parts of the codebase.


Thorough test assertions

Ensure tests thoroughly validate functionality through comprehensive assertions. When writing tests:

  1. Assert all relevant aspects of the expected behavior, not just presence/absence or simple success/failure
  2. Verify specific resource properties, not just counts or general outcomes
  3. Test critical logic with dedicated unit tests, especially for complex or core functionality
  4. Include both normal and edge cases in your test suite

Example:

# Incomplete test - only checks summary counts
def test_resource_skips_incomplete():
    report = Runner().run(root_folder=test_files_dir, runner_filter=RunnerFilter(checks=[]))
    summary = report.get_summary()
    self.assertEqual(summary["skipped"], 3)  # ❌ Missing specific assertions

# Better test - verifies specific resources and their properties
def test_resource_skips_thorough():
    report = Runner().run(root_folder=test_files_dir, runner_filter=RunnerFilter(checks=[]))
    
    # Assert overall summary
    summary = report.get_summary()
    self.assertEqual(summary["skipped"], 3)
    
    # Assert specific resources and their skip counts
    skipped_resources = report.get_skipped_resources()
    self.assertEqual(len(skipped_resources["default"]), 1)
    self.assertEqual(len(skipped_resources["skip_invalid"]), 1)
    self.assertEqual(len(skipped_resources["skip_more_than_one"]), 2)  # ✅ Thorough validation

When implementing new functionality, always add corresponding tests that validate the specific behavior, not just general outcomes. For critical logic, create dedicated unit tests even if the component is tested indirectly elsewhere.


Safe dictionary access

Always use safe dictionary access patterns when handling potentially null or missing values. This prevents KeyError and AttributeError exceptions that can crash your application.

Key practices to follow:

  1. Use .get() with default values when accessing dictionaries:
    # Instead of conf["properties"]["siteConfig"]
    # Use this pattern:
    conf.get("properties", {}).get("siteConfig")
    
  2. Validate types before performing operations:
    # Before accessing nested attributes or methods:
    if isinstance(nic_bloc, dict) and nic_bloc.get('allow_ip_spoofing', [False])[0]:
        # Process nic_bloc
    
  3. Use explicit checks for None vs. empty collections:
    # For dictionaries with empty values vs None
    if each["change"]["before"] is None:  # Check specifically for None
        each["change"]["before"] = {}
    
  4. Handle deeply nested structures safely:
    # For complex nested access:
    inline_suppressions_by_cve = inline_suppressions.get("cves", {}).get("byCve", [])
    for cve_suppression in inline_suppressions_by_cve:
        cve_id = cve_suppression.get("cveId")
        if cve_id:
            cve_by_cve_map[cve_id] = cve_suppression
    

This pattern reduces bugs, improves code readability, and eliminates the need for multiple conditional checks.


Minimize workflow complexity

Keep GitHub Actions workflows efficient and maintainable by eliminating redundant configurations and ensuring comprehensive test coverage:

  1. Avoid redundant environment variables - Use the minimum number of variables needed to achieve your workflow goals. When one variable can be derived from another, don’t store both.

Example refactoring:

# Not recommended
- name: Filter files
  run: |
    YAML_JSON_FILES=$(echo ${{ steps.changed-files.outputs.all_changed_files }} | grep -E '\.ya?ml$|\.json$')
    echo "YAML_JSON_FILES=$YAML_JSON_FILES" >> "$GITHUB_ENV"
    echo "RELEVANT_FILES_CHANGED=true" >> "$GITHUB_ENV"

# Recommended
- name: Filter files
  run: |
    YAML_JSON_FILES=$(echo ${{ steps.changed-files.outputs.all_changed_files }} | grep -E '\.ya?ml$|\.json$')
    echo "YAML_JSON_FILES=$YAML_JSON_FILES" >> "$GITHUB_ENV"
    
- name: Next step
  if: env.YAML_JSON_FILES != ''
  # Use YAML_JSON_FILES directly instead of a redundant flag
  1. Define comprehensive test matrices - Ensure matrix strategies include all relevant versions of languages, tools, or environments needed for thorough testing.
# Not recommended
strategy:
  matrix:
    python: ["3.8"]  # Incomplete coverage

# Recommended
strategy:
  matrix:
    python: ["3.8", "3.9", "3.10", "3.11"]  # Complete coverage

Keeping workflows streamlined improves maintainability, reduces debugging time, and makes your CI processes more reliable.


consistent readable patterns

Maintain consistent coding patterns and prioritize readability to make code easier to understand and maintain. This includes using consistent approaches for similar operations, avoiding overly complex constructions, and ensuring code follows predictable patterns.

Key practices:

Example of improving consistency:

// Inconsistent - different patterns for similar operations
const upgrades = Object.keys(upgrade);
const pins = Object.keys(pin);
if (Object.keys(patch).length) { // Different pattern

// Consistent - same pattern for all similar operations  
const upgrades = Object.keys(upgrade);
const pins = Object.keys(pin);
const patches = Object.keys(patch);
if (patches.length) {

Example of extracting complex conditions:

// Hard to read when repeated
if (options.iac && options.report && !options.legacy) {

// Extract to descriptive function
const isIacReportFlow = () => options.iac && options.report && !options.legacy;
if (isIacReportFlow()) {

This approach reduces cognitive load, makes code more predictable, and helps maintain consistency across the codebase.


Azure encryption property names

When configuring encryption settings for Azure resources in ARM templates, always use the correct property names as documented in the Azure API specifications. Specifically for Azure Compute disks, use enableDoubleEncryption instead of doubleEncryptionEnabled to configure double encryption. Using incorrect property names can result in security configurations silently failing to be applied, potentially leaving resources vulnerable.

Example:

// INCORRECT
{
  "properties": {
    "doubleEncryptionEnabled": true  // Wrong property name
  }
}

// CORRECT
{
  "properties": {
    "enableDoubleEncryption": true  // Correct property name
  }
}

Use correct encryption properties

When configuring security features in Azure ARM templates, always use the correct property names as specified in the Azure documentation. For double encryption of Azure Compute disks, use “enableDoubleEncryption” rather than “doubleEncryptionEnabled”. Using incorrect property names may result in security features not being properly applied, potentially leaving resources vulnerable even when you intended to secure them.

// INCORRECT
"properties": {
  "doubleEncryptionEnabled": true
}

// CORRECT
"properties": {
  "enableDoubleEncryption": true
}

Balance concurrent operations

When implementing concurrent operations, carefully balance performance gains with resource consumption and system stability. Set conservative concurrency limits initially and adjust based on performance metrics rather than assuming higher parallelism is always better.

Key considerations:

Example of optimized async workflow:

// Instead of waiting for all inspects to complete before processing
const results = await Promise.all(
  args.map((arg) => detectInspectMonitor(arg, options))
);

// Process incrementally to start dependent operations sooner
const snapshots: Array<Promise<Result>> = [];
for (const path of args) {
  const inspectResult = await inspect(path);
  // Start snapshot saving immediately, don't wait
  snapshots.push(saveSnapshot(inspectResult, options));
}
const results = await Promise.all(snapshots);

This approach reduces overall execution time while maintaining controlled resource usage and predictable behavior.


Implement pre-commit hooks

Automate quality and security checks by implementing pre-commit hooks in your Git repositories to catch issues early in the development process. Pre-commit hooks run specified checks automatically when files change, before code is committed, helping to enforce standards without manual intervention.

To set up pre-commit hooks:

  1. Install the pre-commit framework: https://pre-commit.com/#install
  2. Create a .pre-commit-config.yaml file in your repository root with configurations for tools like Checkov:
# .pre-commit-config.yaml
repos:
-   repo: https://github.com/bridgecrewio/checkov.git
    rev: 2.0.556  # Use the latest version
    hooks:
    -   id: checkov
        args: [--quiet]
  1. Install the hooks with pre-commit install

This practice improves CI/CD pipelines by shifting quality control left, reducing failed builds, and allowing developers to fix issues before they enter the main pipeline. Configure hooks for linting, security scanning, formatting, and other quality checks relevant to your project.


Choose optimal data structures

Select the most appropriate data structure based on the specific operations your algorithm needs to perform. This choice can dramatically impact performance and code clarity:

  1. Use sets instead of lists when you need unique values or fast membership testing: ```python

    Instead of checking membership in a list (O(n) operation):

    ENTROPY_CHECK_IDS = (‘CKV_SECRET_6’, ‘CKV_SECRET_19’, ‘CKV_SECRET_80’) if check_id in ENTROPY_CHECK_IDS: # Linear search

Use a set for O(1) lookups:

ENTROPY_CHECK_IDS = {‘CKV_SECRET_6’, ‘CKV_SECRET_19’, ‘CKV_SECRET_80’} if check_id in ENTROPY_CHECK_IDS: # Constant-time lookup


2. Consider defaultdict to simplify code that manages collections:
```python
# Instead of manually checking if keys exist:
dirs_to_definitions = {}
for tf_definition_key, tf_value in tf_definitions.items():
    dir_path = os.path.dirname(tf_definition_key.file_path)
    if dir_path in dirs_to_definitions:
        dirs_to_definitions[dir_path].append({tf_definition_key: tf_value})
    else:
        dirs_to_definitions[dir_path] = [{tf_definition_key: tf_value}]

# Use defaultdict for automatic initialization:
from collections import defaultdict
dirs_to_definitions = defaultdict(list)
for tf_definition_key, tf_value in tf_definitions.items():
    dir_path = os.path.dirname(tf_definition_key.file_path)
    dirs_to_definitions[dir_path].append({tf_definition_key: tf_value})
  1. When implementing search algorithms, use early termination when possible: ```python

    Instead of collecting all matches and then choosing one:

    target_variables = [ index for index in variables_map.get(vertex.name, []) if conditions_match(index) ] if len(target_variables) >= 1: use_variable(target_variables[0])

Stop once you find what you need:

target_variable = 0 for index in variables_map.get(vertex.name, []): if conditions_match(index): target_variable = index break if target_variable: use_variable(target_variable)


Choosing the right data structure is fundamental to writing efficient algorithms and clear code.

---

## Add CI safety checks

<!-- source: semgrep/semgrep | topic: CI/CD | language: Other | updated: 2024-05-22 -->

Implement defensive checks and error handling in CI/CD workflows to prevent failures and ensure robust operations. This includes validating preconditions, checking for edge cases, and using appropriate tools that preserve important information.

Key practices:
- Add validation checks before critical operations to prevent duplicate or conflicting actions
- Use proper tools that preserve metadata (e.g., `git am` instead of `patch` to maintain author information)
- Verify platform compatibility when adding new dependencies
- Implement comprehensive testing that covers all supported environments

Example from sync workflow:
```bash
# Check if commit already synced to prevent duplicates
if git show --stat HEAD | grep -q "synced from"; then
    echo "error: HEAD commit already comes from Pro and cannot be synced"
    exit 1
fi

# Use git am to preserve author info instead of patch
sed -E 's:( a| b)/:\1OSS/:g' 0001-*.patch >pro.patch
git am pro.patch

This approach prevents common CI failures, reduces debugging time, and ensures workflows handle edge cases gracefully while maintaining data integrity.


Use typed interfaces

Prefer typed interfaces over dictionary access to enable static type checking and prevent null reference errors. When accessing data structures, use typed objects with defined fields rather than dictionary key access, as this allows mypy to detect typos and missing fields at compile time.

This practice is especially important for optional and nullable fields, where dictionary access can lead to KeyError exceptions or unexpected None values that aren’t caught until runtime.

Example:

# Avoid: Dictionary access that mypy cannot validate
show_dataflow_traces = extra["dataflow_traces"]  # Prone to typos, no null safety

# Prefer: Typed interface access
show_dataflow_traces = cli_output_extra.dataflow_traces  # Mypy validates field exists

# For optional fields, be explicit about the type
repo_display_name: str | None = self.repo_display_name  # Clear nullable intent

When designing APIs, clearly define whether fields are required or optional (str | None) rather than relying on fallback logic, as this makes null handling explicit and prevents unexpected runtime behavior.


Use descriptive names

Names should clearly and accurately describe their purpose, avoiding abbreviations, acronyms, and misleading terms. This applies to variables, functions, types, constructors, and modules.

Key principles:

Example:

(* Avoid *)
type sca_mode = [ `SCA of dependency_formula ]
let code_of_origin origin = ...
let parse_csv_from_env_vars vars = ...

(* Prefer *)
type dependency_mode = [ `Dependency of dependency_formula ]  
let mk_code_target xlang products origin = ...
let read_comma_separated_from_env_vars vars = ...

Names are the primary way developers understand code intent. Descriptive names reduce cognitive load, prevent misunderstandings, and make code self-documenting.


Ensure comprehensive test coverage

Tests should validate all expected behaviors, variants, and edge cases rather than covering only the happy path. When implementing tests, ensure you include scenarios for all relevant configurations, types, or levels that your code supports.

Key practices:

Example from rule testing:

# Instead of only testing one severity level:
rules:
- id: critical
  severity: CRITICAL

# Test multiple levels:
rules:
- id: critical-test
  severity: CRITICAL
- id: high-test  
  severity: HIGH
- id: medium-test
  severity: MEDIUM

This approach prevents silent failures when mappings or configurations change and ensures robust validation of your code’s behavior across all supported scenarios.


use consistent JavaScript syntax

Maintain consistent use of modern JavaScript syntax throughout networking code to improve reliability and maintainability. Use const or let instead of var for variable declarations, and prefer strict equality (===) over loose equality (==) for comparisons.

This is particularly important in networking code where subtle bugs from variable scoping issues or type coercion can cause runtime errors that are difficult to debug. Consistent syntax also makes the codebase more predictable for team members.

Examples:

// Preferred
const proxy = process.env[ssl ? "HTTPS_PROXY" : "HTTP_PROXY"];
if (globalThis.process.platform === "win32" && path === "NUL") {

// Avoid  
var proxy = process.env[ssl ? "HTTPS_PROXY" : "HTTP_PROXY"];
if (globalThis.process.platform === "win32" && path == "NUL") {

Design consistent tracing interfaces

When implementing observability features like tracing, prioritize consistent and extensible API design. Use structured annotations that can accommodate future configuration options rather than simple flags. Place tracing instrumentation directly on the functions being measured rather than creating wrapper functions. Follow established naming conventions that align with other similar tools in your ecosystem.

For tracing annotations, prefer extensible syntax like [@@trace { level = Debug }] over simple flags like [@@trace_debug], as this provides a more consistent design with other ppx extensions and allows for future expansion of configuration options.

Place tracing annotations directly on the target functions rather than creating wrapper functions:

(* Preferred: Direct annotation *)
let rules_from_rule_source config = 
  (* implementation *)
[@@trace]

(* Avoid: Wrapper function *)
let get_rules config =
  Common.with_time (fun () -> rules_from_rule_source config)
[@@trace]

For API parameters, use direct parameters instead of optional ones when the value is always computed:

(* Preferred: Direct parameter *)
let config = Opentelemetry_client_ocurl.Config.make ~url () in
Opentelemetry_client_ocurl.with_setup ~config () @@ fun () ->

(* Avoid: Unnecessary optional *)
let config = Some (Opentelemetry_client_ocurl.Config.make ~url ()) in
Opentelemetry_client_ocurl.with_setup ?config () @@ fun () ->

This approach ensures your observability instrumentation remains maintainable, follows predictable patterns, and can evolve with changing requirements.


Document configuration troubleshooting

Configuration files and scripts should include comprehensive documentation that helps developers understand their purpose, scope, and how to troubleshoot common issues. This includes adding comments that explain what the configuration does, what errors might occur, and specific steps to resolve them.

Key elements to include:

Example from a build configuration script:

#!/usr/bin/env bash
# Setup the opam environment under MacOS to build and release semgrep-core.
# All other MacOS-related setup should be in `osx-setup-post-opam-for-release.sh`

# If you added a dependency that's causing a linking error:
# 1. Use `opam depext` to find required system libraries
# 2. Add the library to the appropriate CCLIB array below
# 3. For language dependencies, add to the LANGS array

This approach prevents developers from having to reverse-engineer configuration files and reduces time spent debugging environment-specific issues. When errors occur, developers can quickly identify what needs to be modified and how to make the necessary changes.


maintain build environment parity

Ensure that local development and CI/CD environments use identical build commands and dependency management strategies to prevent deployment issues and environment drift. When introducing local-specific build targets, consider whether the same approach should be adopted in CI pipelines to maintain consistency.

Avoid creating separate build paths that could diverge over time. Instead of having different commands like build vs build-local, standardize on a single approach that works across all environments.

For dependency management, be consistent about when to use clean installs vs incremental installs:

# Instead of different targets:
build-local: pre-build
	source .venv/bin/activate; make build-full

# Prefer unified approach:
build: pre-build
	@if [ -f .venv/bin/activate ]; then source .venv/bin/activate; fi
	make build-full

When optimizing for performance (like using npm i instead of npm ci), ensure the optimization logic is consistent between local and CI environments, or clearly document why they differ.


Document configuration choices

Add explanatory comments for non-obvious configuration settings, especially when they deviate from defaults or serve specific purposes. Configuration choices that aren’t immediately clear should include comments explaining their rationale, impact, or relationship to other settings.

This helps prevent accidental changes, aids debugging, and makes maintenance easier for future developers who need to understand why specific settings exist.

Examples of when to add configuration documentation:

(* Default libraries to skip in logging - you can use regexps *)
let default_skip_libs = [
  "cohttp.lwt.client";
  "dns*";  (* Skip all DNS-related logging *)
]

The comment should explain both what the configuration does and why it’s necessary, making the codebase more maintainable and reducing the likelihood of configuration-related bugs.


Simplify complex logic

Break down complex code structures into simpler, more readable forms. Long indented blocks, verbose conditionals, and repetitive patterns should be simplified to improve code clarity and maintainability.

Key practices:

Examples:

Instead of a verbose loop:

for value in validation_state_metadata.values():
    if value == "block":
        return True
return False

Use a concise expression:

return "block" in validation_state_metadata.values()

Instead of confusing conditional order:

*(
    (self.start.offset, self.end.offset)
    if not (self.extra.get("sca_info") and not self.extra["sca_info"].reachable)
    else (self.start.line, self.end.line)
),

Reorder for clarity:

*(
    (self.start.line, self.end.line)
    if (self.extra.get("sca_info") and not self.extra["sca_info"].reachable)
    else (self.start.offset, self.end.offset)
),

This approach reduces cognitive load and makes code easier to understand and maintain.


Document code purpose

Ensure that code elements have clear documentation explaining their purpose, behavior, and context. This includes function parameters (especially non-obvious ones), complex business logic, and magic values or constants.

Key areas to document:

Example from the discussions:

def add_engine_config(
    self, engineType: "EngineType", secrets_origins: Optional[str]
) -> None:
    """
    Configure engine with type and secrets information.
    
    Args:
        engineType: The type of engine to configure
        secrets_origins: Source information for secrets detection, 
                        e.g., "github" or "local_scan"
    """

This practice helps future developers (including yourself) understand the intent behind code decisions and reduces the cognitive load when maintaining or extending the codebase.


Benchmark performance assumptions

Always validate performance concerns with actual measurements before making optimization decisions. Avoid making assumptions about what is “too slow” without benchmarking evidence.

When performance issues are confirmed through benchmarks, optimize strategically by:

Example of evidence-based decision making:

(* Initial assumption: "We cannot afford to call Unix.realpath for each path because it's too slow" *)
(* After benchmarking: "running Semgrep with all free and Pro rules on 38 repos, and seeing no difference in perf wrt baseline" *)
(* Conclusion: "clearly we can afford Unix.realpath" *)

Example of strategic optimization:

(* Inefficient: constructing unnecessary intermediate lists *)
let trace_tokens = trace |> List.concat_map tokens_of_trace_item in
let filenames = trace_tokens |> List.map get_filename in

(* Better: direct iteration without intermediate allocations *)
let filenames = trace |> List.fold_left (fun acc item -> 
  get_filename_from_item item :: acc) [] |> List.rev in

This approach prevents both premature optimization and performance regressions by ensuring decisions are grounded in measurable data.


Validate CI pipeline inputs

Always validate and sanitize dynamic inputs in CI/CD pipelines before using them in URLs, file operations, or other system interactions. This includes environment variables, repository names, file paths, and user-provided parameters that could contain special characters, spaces, or unexpected formats.

Key validation practices:

Example of problematic URL construction:

# Problematic - can create broken URLs like /orgs/semgrep/findings?repo=semgrep/semgrep/semgrep
f"https://semgrep.dev/orgs/{scan_handler.deployment_name}/findings?repo={scan_handler.deployment_name}/{metadata.repo_display_name}"

# Better - validate and sanitize inputs
f"https://semgrep.dev/orgs/{scan_handler.deployment_name}/findings?repo={sanitized_repo_name}"

This prevents CI pipeline failures, broken reporting links, and unexpected behavior when processing dynamic content or state changes between pipeline runs.


require module documentation

All modules containing functions should have corresponding .mli files with comprehensive documentation. The .mli file must include:

  1. Module purpose: A clear, high-level description of what the module does and why it exists
  2. Function documentation: Comments explaining what each function does, its parameters, return values, and any special behaviors
  3. Type documentation: Clear explanations of custom types and their fields
  4. API behavior notes: Documentation of important behaviors like error handling, redirects, or resource management

Example structure:

(** High-level module purpose and goals.
    
    This module handles X by doing Y, and is primarily used for Z.
    See also {!Related_module} for similar functionality.
*)

(** Custom type representing... *)
type analysis_flags = {
  secrets_validators : bool; (** Enable secrets validation *)
  historical_scan : bool;    (** Scan historical data *)
  (* ... *)
}

(** Execute a command request.
    
    @param request The command to execute
    @param context Current execution context
    @return Result of execution or error
    
    Note: Automatically handles 307 redirects for binary downloads.
*)
val handle_execute_request : request -> context -> (result, error) Result.t

This ensures that module interfaces are self-documenting and that ocamldoc can generate useful documentation. Avoid vague function names without proper documentation, and always explain the module’s role in the larger system.


Add comprehensive test coverage

Ensure all code changes include appropriate unit tests and address any identified testing gaps immediately. When reviewing code, verify that:

  1. New functionality has corresponding tests: Every new feature or significant code change should include unit tests that exercise the new behavior.

  2. Existing untested code gets tests when modified: If you’re working on code that lacks test coverage, add tests as part of your changes rather than deferring them.

  3. Test TODOs are addressed, not accumulated: Don’t leave comments like “TODO: This has a bug!!!” in test code. Either fix the issue immediately or create a proper ticket to track it.

  4. Mark missing test areas explicitly: When you identify code that needs tests but can’t add them immediately, add clear TODO comments like (* TODO: test this *) to acknowledge the gap and help future developers.

Example from the discussions:

(* Instead of leaving this unaddressed: *)
(* TODO: This has a bug!!!
   Suppose we exclude `test.py` and are given `tests2/test.py`.
   This code will not exclude properly... *)

(* Do this: *)
(* Add unit test for path exclusion edge cases - see issue #1234 *)
let test_path_exclusion_with_nested_dirs () = 
  (* Test the specific bug case mentioned *)
  assert (excludes_file "test.py" "tests2/test.py" = true)

The goal is to build confidence in code changes through comprehensive testing rather than relying on manual verification like “run -cfg_il and looks like it’s ok”.


avoid hardcoded configuration values

Replace hardcoded URLs, ports, file paths, and environment-specific strings with configurable parameters. Hardcoded values make code difficult to test, deploy across different environments, and maintain over time.

Use configuration objects, environment variables, or dependency injection to make values configurable:

// Bad: Hardcoded URL
message += `  - ${vulnId} (See https://security.snyk.io/vuln/${vulnId})`;

// Good: Use configuration value
message += `  - ${vulnId} (See ${config.PUBLIC_VULN_DB_URL}/vuln/${vulnId})`;

// Bad: Hardcoded port
const options = new URL(downloadUrl);

// Good: Dynamic port allocation
deepCodeServer = fakeDeepCodeServer();
deepCodeServer.listen(() => {});
env = {
  ...initialEnvVars,
  SNYK_CODE_CLIENT_PROXY_URL: `http://localhost:${deepCodeServer.getPort()}`,
};

This approach improves testability by allowing dynamic port allocation, enables environment-specific deployments, and makes the codebase more maintainable. Always prefer configuration injection over hardcoded constants, especially for URLs, API endpoints, file paths, and environment-dependent values.


Defensive authorization checks

Always implement explicit authentication and authorization checks before granting access to premium features or sensitive operations, even when other conditions might imply proper authorization. Use defensive programming to prevent accidental privilege escalation.

This practice prevents security vulnerabilities where changes to one part of the system could inadvertently bypass authorization controls elsewhere. Always validate both the user’s authentication state and their specific permissions for the requested operation.

Example implementation:

# Bad: Relying on implicit authorization
if scan_handler and scan_handler.deepsemgrep:
    requested_engine = cls.PRO_INTERFILE

# Good: Explicit defensive checks
if scan_handler and scan_handler.deepsemgrep and logged_in:
    requested_engine = cls.PRO_INTERFILE
elif not logged_in and requested_engine != cls.OSS:
    raise SemgrepError("Premium features require authentication")

Key principles:


Use descriptive variable names

Choose variable and parameter names that clearly communicate their purpose and avoid misleading or cryptic naming. Names should be self-documenting and reduce cognitive load for other developers.

Key principles:

Example of improvement:

# Before: Confusing relationship between parameters
def decide_engine_type(requested_engine, engine_flag):
    requested_engine = engine_flag  # Unclear why both exist

# After: Clear purpose and relationship
def decide_engine_type(engine_flag):
    requested_engine = engine_flag  # Or just use engine_flag directly

This reduces confusion during code reviews and makes the codebase more maintainable by ensuring names accurately reflect their semantic meaning.


Use optional chaining

Always use optional chaining (?.) and null checks when accessing properties or methods on objects that might be null or undefined. This prevents runtime crashes and makes code more robust.

Before accessing properties, especially on objects from external sources, API responses, or optional parameters, check for null/undefined values or use optional chaining to safely navigate object properties.

Examples of unsafe access:

// Unsafe - can crash if vulnerabilities is null
if (jsonData.vulnerabilities.length === 0) { ... }

// Unsafe - can crash if vuln.cvssScore is null  
'security-severity': vuln.cvssScore // pushes "undefined" if null

// Unsafe - can crash if nodes is undefined
for (let i = 0; i < attributes?.dep_graph_data?.graph?.nodes.length; i++) { ... }

Safe alternatives:

// Safe with optional chaining
if (jsonData.vulnerabilities?.length === 0) { ... }

// Safe with null check and conversion
'security-severity': vuln.cvssScore ? String(vuln.cvssScore) : undefined

// Safe with fallback value
const nodes = attributes?.dep_graph_data?.graph?.nodes || [];
for (let i = 0; i < nodes.length; i++) { ... }

// Safe with explicit null check
if (upgradePath && upgradePath.length >= 2) { ... }

This pattern is especially critical when the absence of null checks can cause the application to “choke” or crash, as seen with CLI tools processing external data.


Conservative security assumptions

When implementing security measures, always err on the side of caution by making conservative assumptions about potential risks. This principle applies across multiple security contexts:

Taint Analysis: If there’s any execution path where data could become tainted, treat it as tainted. Use may-analysis approaches that assume the worst-case scenario rather than trying to prove safety.

Capability Restriction: Limit access rights to the absolute minimum necessary. Don’t pass unnecessary capabilities to callbacks or sub-components, even if they’re currently unused.

Resource Protection: Implement defensive measures like timeouts to prevent resource exhaustion, even in contexts where attacks seem unlikely.

Example from taint analysis:

def process_data(user_input):
    try:
        safe_data = validate(user_input)
        result = process(safe_data)
    except ValidationError:
        result = user_input  # This path taints result
    
    # Treat result as tainted due to exception path
    return sanitize(result)

This conservative approach helps prevent security vulnerabilities by assuming potential attack vectors exist rather than trying to prove they don’t.


optimize CI resource allocation

Match CI/CD resource allocation to actual job requirements rather than using oversized configurations globally. Create specialized executors and resource classes for different job types to optimize cost and performance.

This practice prevents unnecessary resource waste when jobs with different computational needs share the same executor configuration. Instead of applying large resource classes universally, analyze each job’s actual requirements and provision accordingly.

Implementation approach:

Example:

executors:
  docker-amd64:
    docker:
      - image: bastiandoetsch209/cli-build:20240214-145818
    working_directory: /mnt/ramdisk/snyk
    resource_class: large
    
  docker-amd64-xl:  # Only for resource-intensive jobs
    docker:
      - image: bastiandoetsch209/cli-build:20240214-145818
    working_directory: /mnt/ramdisk/snyk
    resource_class: xlarge

  noop:
    docker:
      - image: cimg/base:current
    resource_class: small  # Minimal resources for no-op jobs

jobs:
  acceptance-test:
    executor: docker-amd64-xl  # Use xlarge only where needed
    
  simple-validation:
    executor: noop  # Use small resources for lightweight tasks

This approach reduces infrastructure costs while maintaining performance where it matters most, and prevents the common anti-pattern of over-provisioning resources across all pipeline jobs.


synchronize configuration values

Ensure configuration values remain consistent and aligned across all related files, environments, and systems. When updating versions, parameters, or settings in one location, verify and update corresponding values in other configuration files to prevent mismatches that can lead to build failures or environment pollution.

Key practices:

Example from CircleCI config:

# Instead of hardcoding versions that may drift apart
executors:
  circle-go:
    docker:
      - image: cimg/go:1.20  # This should match Dockerfile version

# Consider using parameters for consistency
parameters:
  go_version:
    type: string
    default: "1.20"

This prevents issues where different parts of the system use incompatible versions or where test configurations accidentally affect production environments.


defensive shell script configuration

Configure shell scripts with defensive practices to ensure reliability and debuggability. Use appropriate set options for error handling and debugging, implement safe defaults that require explicit opt-in for risky operations, and preserve execution state when changing directories.

Key practices:

Example of defensive directory handling:

# Instead of:
cd "${PROJECT_PATH}"
pipenv install

# Use:
pushd "${PROJECT_PATH}"
pipenv install
popd

This approach prevents unintended side effects on subsequent script execution and makes scripts more predictable in different environments.


Choose efficient algorithms

When implementing algorithms, prioritize computational efficiency by selecting appropriate data structures and avoiding unnecessarily complex approaches. Consider the time complexity of your solution and whether built-in functions or better data structures could improve performance.

Key principles:

Examples of improvements:

Instead of manual exception handling for early exit:

(* Avoid *)
try
  List.fold_left (fun _ x -> 
    if condition x then raise (Found x)) () lst
with Found x -> Some x

(* Prefer *)
List.find_opt condition lst

Instead of manual folding for set operations:

(* Avoid *)
List.fold_left (fun acc m -> StringSet.add m.path acc) StringSet.empty matches

(* Prefer *)
matches |> List.map (fun m -> m.path) |> Set_.of_list

Instead of expensive equality comparisons:

(* Avoid *)
let a_json = a |> to_json |> to_string in
let b_json = b |> to_json |> to_string in
String.equal a_json b_json

(* Prefer *)
Common2.on String.equal extract_key a b

When dealing with potentially large datasets, document the time complexity and consider whether O(N²) algorithms need optimization. For stack-intensive operations, prefer iterative approaches or tail-recursive functions to avoid stack overflow issues.


CI workflow optimization

Optimize CI/CD workflows by prioritizing reliability over convenience and leveraging built-in platform features. When workflows generate files or have complex dependencies, use brute-force approaches like make clean; make to ensure reproducible builds, even if it seems inefficient. Simplify workflow steps by using native GitHub Actions features instead of manual workarounds - for example, use submodules: true in checkout actions rather than separate submodule checkout steps.

Look for opportunities to consolidate similar workflows that perform redundant operations, as this can reduce maintenance overhead and improve build times. When making dependency changes, consider the overall impact on workflow performance and whether jobs can be parallelized or merged.

Example of reliable build verification:

- name: Check GitHub workflow files are up to date
  working-directory: .github/workflows
  run: |
    sudo apt-get update
    sudo apt-get install jsonnet
    make clean  # Ensure clean state
    make        # Regenerate all files

Example of using built-in features:

- uses: actions/checkout@v3
  with:
    submodules: true  # Instead of separate checkout steps

Defensive optional handling

Always handle optional and nullable values defensively by checking for null/empty states before use and handling multiple optional values independently. Use explicit null coalescing patterns to provide safe defaults, and avoid nesting optional value logic when they should be processed independently.

Key practices:

  1. Check for empty/null values before operations that could fail
  2. Handle multiple optional parameters independently rather than nesting their logic
  3. Use null coalescing operators or patterns for safe defaults

Example from the codebase:

(* Good: Independent handling of multiple optionals *)
match opt1 with
| None -> []
| Some (_, st1) -> [st1]
@
match opt2 with  (* Handle opt2 independently *)
| None -> []
| Some (_, st2) -> [st2]

(* Good: Defensive check before use *)
let loc =
  if not String.(equal file "") then Tok.first_loc_of_file file
  else default_loc

(* Good: Null coalescing pattern *)
let result = optional_value ||| default_value

This prevents runtime errors from unexpected null/undefined values and ensures all optional cases are properly handled.


use appropriate test markers

Test markers and annotations should match the test type and context they’re applied to. Different test categories (unit tests, e2e tests, integration tests) require different markers to ensure proper test organization and execution.

For example, markers like osemfail and no_semgrep_cli are typically meant for end-to-end tests and should not be applied to unit tests. When adding new functionality, use markers that indicate the test’s purpose and any special requirements for porting or execution.

# Incorrect - using e2e markers on unit tests
@pytest.mark.quick
@pytest.mark.no_semgrep_cli  # Remove - not relevant for unit tests
@pytest.mark.osemfail       # Remove - not relevant for unit tests
def test_clean_project_url():
    pass

# Correct - using appropriate markers for the test context
@pytest.mark.quick
def test_clean_project_url():
    pass

# Correct - using osemfail for tests that need porting
@pytest.mark.osemfail  # Appropriate when test needs to be ported
def test_metrics_filtering():
    pass

This ensures tests are properly categorized, executed in the right contexts, and that CI/CD pipelines can correctly identify and run the appropriate test suites.


Use appropriate log levels

Choose log levels based on the intended audience to prevent user-visible noise. Most logging should use Logs.debug rather than Logs.info or Logs.err, since info-level and above messages are visible to end users with --verbose or by default.

Guidelines:

Example:

(* Bad - creates noise for users *)
Logs.err (fun m -> m ~tags "BUG PARSING LOCAL DECL PROBABLY");
Logs.info (fun m -> m "skipping: %s" (Tok.content_of_tok tok));

(* Good - appropriate for debugging *)  
Logs.debug (fun m -> m ~tags "BUG PARSING LOCAL DECL PROBABLY");
Logs.debug (fun m -> m "skipping: %s" (Tok.content_of_tok tok));

The principle is that no log message should appear to command-line users unless they explicitly enable debugging or the message requires user action.


Strategic error handling

Choose appropriate error handling strategies based on the nature of the error and recovery potential:

  1. Raise exceptions only for truly exceptional conditions:
    • Use exceptions for invalid states that indicate logic errors or when recovery is impossible
    • For recoverable errors, consider returning default values or graceful fallbacks
# Instead of always raising exceptions:
def get_sso_prismacloud_url(self, report_url: str) -> str:
    request = self.http.request("GET", url_saml_config, headers=headers, timeout=10)
    if request.status >= 300:
        # Return a fallback value instead of raising an exception
        return report_url
    # Normal processing continues...
  1. Implement appropriate recovery mechanisms:
    • For transient errors (e.g., network issues), implement retries
    • For permanent errors, provide graceful degradation
# Retry for transient errors:
for i in range(retries):
    request = self.http.request("POST", f"{self.prisma_api_url}/login", 
                              body=json.dumps({"username": username, "password": password}),
                              headers=headers)
    
    if request.status == 200:
        return json.loads(request.data.decode("utf8"))['token']
    elif request.status >= 500:  # Server errors are transient and should be retried
        continue
    elif request.status in [401, 403]:  # Authentication errors won't be resolved by retrying
        self.raise_bridgecrew_auth_error(request.status, request.data)
  1. Catch specific exceptions rather than using broad exception handlers:
try:
    module_name_index = len(full_definition_path) - full_definition_path[::-1][1:].index(BlockType.MODULE) - 1
except ValueError as e:
    # Handle the specific error case
    logging.warning(f"Could not determine module name index: {str(e)}")
# Instead of using a generic "except Exception" which catches everything
  1. Make deliberate decisions about when to log errors versus raising exceptions:
    • Raise exceptions for conditions that indicate developer errors
    • Log and recover for operational issues that might resolve themselves

Following these principles leads to more robust code that fails gracefully when possible and provides clear error information when necessary.


Strategic exception management

Choose the appropriate error handling strategy based on the context and severity of potential failures. Use exceptions for truly exceptional conditions that represent invalid states or unrecoverable errors, while providing graceful fallbacks for non-critical failures.

When to fail fast (raise exceptions):

# Good: Raising exceptions for invalid program states
def get_record_file_line_range(package: dict[str, Any], file_line_range: list[int] | None) -> list[int]:
    package_line_range = get_package_lines(package)
    if package_line_range and file_line_range:
        raise Exception('Both \'package_line_range\' and \'file_line_range\' are not None. Conflict.')
    # Continue processing with valid state

When to degrade gracefully:

# Good: Providing graceful fallback for non-critical failures
def get_sso_prismacloud_url(self, report_url: str) -> str:
    # If authentication fails, return the regular URL instead of raising an exception
    request = self.http.request("GET", url_saml_config, headers=headers, timeout=10)
    if request.status >= 300:  # Any error response
        return report_url  # Fallback to standard URL

Best practices:

  1. Use specific exception types rather than generic ones (e.g., except ValueError: instead of except Exception:)
  2. Add validation before operations that might throw exceptions
  3. Provide informative error messages that help diagnose the root cause
  4. Consider the impact on the user experience when choosing your error handling strategy

API response consistency

Ensure consistent patterns for API response handling, error management, and type safety across all API interactions. This includes standardizing error response structures based on status codes, properly defining response types to match actual API behavior, and implementing robust error handling with appropriate fallbacks.

Key principles:

  1. Status-specific error handling: Different HTTP status codes should have consistent response body handling (e.g., 400 errors with JSON bodies vs 401/403 with string responses)
  2. Type accuracy: Response types should reflect actual API behavior, including union types when fields can have multiple formats
  3. Proper HTTP response patterns: Always return after sending responses to prevent multiple response attempts
  4. Structured response access: Use consistent patterns for accessing nested API response data
  5. Timeout and retry logic: Implement proper polling mechanisms with clear timeout handling

Example of consistent error handling:

if (res.statusCode === 400) {
  return reject({
    code: res.statusCode,
    body: JSON.parse(body as any), // JSON response for user feedback
  });
} else if (res.statusCode >= 401) {
  return reject({
    code: res.statusCode,
    // No body parsing for non-JSON responses
  });
}

Example of proper response type definition:

interface ApiResponse {
  remediation: string | { resolve: string }; // Union type reflecting actual API behavior
  target?: GitTarget | ContainerTarget | {}; // Optional with fallback
}

This ensures APIs are predictable, type-safe, and handle edge cases gracefully while maintaining consistency across different endpoints and response scenarios.


Document configuration options

Always provide comprehensive documentation for all configuration options, including environment variables, feature flags, and version overrides. For each configuration option:

  1. Specify default values and all possible values
  2. Include examples showing both default and custom settings
  3. Document the correct syntax for the configuration format
  4. Group related configurations together for better discoverability

For environment variables used for authentication:

| Variable Name     | Description                                                    | Default Value    |
|-------------------|----------------------------------------------------------------|------------------|
| TF_HOST_NAME      | Terraform Enterprise host name (example.com)                   | app.terraform.io |
| TF_REGISTRY_TOKEN | Private registry access token for Terraform Cloud/Enterprise   | None             |

When defining multiple configuration options in JSON files, use arrays for multiple values with the same key:

{
  "name": "my-package",
  "version": "1.0.0",
  "//": ["checkov:skip=express[BC_LIC_2]: ignore license violations",
         "checkov:skip=CVE-2023-123: ignore this CVE"]
}

For container-based tools, document version pinning options:

# Default configuration uses 'latest' tag
hooks:
  - id: checkov_container
    entry: bridgecrew/checkov -d .

# Override with specific version
hooks:
  - id: checkov_container
    entry: bridgecrew/checkov:2.4.2 -d .

Secure API endpoints

Always configure proper authorization for API endpoints to prevent unauthorized access to back-end resources. Avoid combinations that create open access, such as using AuthorizationType.NONE together with api_key_required=False in API Gateway configurations.

Remember that keyword argument order can vary in function calls, so ensure your security checks account for all possible parameter arrangements. For example, in AWS CDK:

# Insecure configuration - will create an open endpoint
aws_cdk.aws_apigateway.Method(
    resource,
    http_method="GET",
    integration=some_integration,
    authorization_type=aws_cdk.aws_apigateway.AuthorizationType.NONE,
    api_key_required=False
)

# Secure configuration
aws_cdk.aws_apigateway.Method(
    resource,
    http_method="GET",
    integration=some_integration,
    authorization_type=aws_cdk.aws_apigateway.AuthorizationType.IAM,  # Or other appropriate type
    api_key_required=True  # When applicable
)

Additionally, define security configurations at the appropriate level in API specifications to avoid redundancy while ensuring comprehensive protection.


Avoid hardcoded secrets

Never include hardcoded secrets or credentials in your infrastructure as code files. Secrets in configuration files pose significant security risks as they may be inadvertently exposed through version control systems or shared repositories.

Instead:

When writing Terraform configurations for Azure Container Instances, use variable references instead of hardcoded values:

# BAD PRACTICE - Hardcoded secret
resource "azurerm_container_group" "example" {
  # other configuration...
  
  container {
    # other settings...
    
    secure_environment_variables = {
      API_KEY = "actual-secret-value-here"  # Security risk!
    }
  }
}

# GOOD PRACTICE - Referenced secret
variable "api_key" {
  description = "API key for the container"
  sensitive   = true  # Marks as sensitive in Terraform
}

resource "azurerm_container_group" "example" {
  # other configuration...
  
  container {
    # other settings...
    
    secure_environment_variables = {
      API_KEY = var.api_key
    }
  }
}

For testing scenarios where you need placeholder secrets, use clearly marked test values and include security check exceptions:

secure_environment_variables = {
  TEST_API_KEY = "test-value-only"  # checkov:skip=CKV_SECRET_6 test-only secret
}

justify error handling design

When implementing error handling mechanisms, ensure that design decisions are well-justified and properly reasoned. This applies to both architectural choices (like global state for error correlation) and technical analysis (like exception flow paths).

For error correlation systems, clearly document why specific approaches are necessary. For example, when adding global state for tracking failures across system boundaries, explain the correlation requirements that justify the design choice.

For exception handling analysis, ensure proper understanding of control flow and reachability. When exceptions can exit functions early, carefully consider which code paths are actually reachable.

Example from error correlation:

class SemgrepState:
    # Justified: UUID needed for correlating backend requests with failures
    # in fail-open endpoint when semgrep ci fails
    request_id: UUID = Factory(uuid4)

Always provide clear reasoning for error handling design decisions, especially when they involve trade-offs like global state or complex control flow analysis.


Precise configuration validation

Ensure configuration validation logic accurately reflects security intentions and best practices. When writing validation checks:

  1. Use precise operators that directly verify the intended state:
    • For prohibited settings, check they “not_exist” rather than checking existence and then value
    • For boolean flags, verify the exact expected value (e.g., not_equals_ignore_case "true")
    • For resources that should have valid values, check “not_empty” rather than just “exists”
  2. Account for dependencies between configuration settings:
    • If one setting makes another irrelevant, handle that case explicitly
    • Use logical operators (AND, OR) to represent these relationships accurately

Example:

# GOOD: Directly validates the intended state
- cond_type: attribute
  attribute: "enable_feature"
  operator: "not_equals_ignore_case"
  value: "true"

# BAD: Unnecessarily complex and error-prone
- cond_type: attribute
  attribute: "enable_feature"
  operator: "exists"
- cond_type: attribute
  attribute: "enable_feature"
  operator: "equals_ignore_case"
  value: "false"

# GOOD: Handles configuration dependencies
- or:
  - cond_type: attribute
    attribute: "shared_access_key_enabled"
    operator: "equals_ignore_case" 
    value: "false"
  - and:
    - cond_type: attribute
      attribute: "shared_access_key_enabled"
      operator: "equals_ignore_case"
      value: "true"
    - cond_type: attribute
      attribute: "sas_policy.expiration_period"
      operator: "exists"

Comprehensive security scanning

Always configure security scanning tools with comprehensive coverage and readable output to maximize vulnerability detection. This includes:

  1. Enable scanning across all files: Use flags like --enable-secret-scan-all-files to ensure no potential vulnerability is missed.

  2. Configure Docker-based scanners with proper TTY support: When running security tools in containers, add the --tty flag for better output handling and readability.

  3. Enable appropriate protection mechanisms: Ensure protective measures like Web Application Firewalls are properly configured in your infrastructure code.

Example:

# Pre-commit hook with proper configuration
-   id: checkov_secrets
    name: Checkov Secrets
    description: This hook looks for secrets with checkov.
    entry: checkov -d . --framework secrets --enable-secret-scan-all-files

-   id: checkov_container
    name: Checkov
    description: This hook runs checkov.
    entry: bridgecrew/checkov:latest --tty -d .

When writing infrastructure as code, ensure that protection mechanisms like WAF are explicitly enabled:

aws_cdk.aws_cloudfront.CfnDistribution(
    distribution_config={"webAclId": my_web_acl_id}
)

These practices help ensure that security vulnerabilities are consistently detected and addressed before deployment.


Backward compatible parameters

When evolving API function signatures, maintain backward compatibility by making new parameters optional with sensible defaults. This allows existing code to continue working while supporting new functionality. When modifying parameters, carefully consider the complete data structure being passed, especially for collections and complex objects.

Example:

# Bad: Breaking change - adds required parameter
def run_graph_checks_results(self, runner_filter: RunnerFilter, report_type: str, graph: Graph) -> dict:
    # Implementation

# Good: Backward compatible - new parameter is optional
def run_graph_checks_results(self, runner_filter: RunnerFilter, report_type: str, graph: Graph | None = None) -> dict:
    # Implementation with fallback if graph is None
    
# Bad: Passing incomplete data structure
completion = await openai.ChatCompletion.acreate(
    engine=self.AZURE_OPENAI_DEPLOYMENT_NAME,
    messages=messages[0],  # Only passes first message
)

# Good: Passing complete data structure
completion = await openai.ChatCompletion.acreate(
    engine=self.AZURE_OPENAI_DEPLOYMENT_NAME,
    messages=messages,  # Passes all messages
)

Validate configurations correctly

When writing validation logic for configurations, ensure you’re using the appropriate operators that correctly test for the intended state. Incorrect validation operators can lead to false results and security vulnerabilities.

Common mistakes include:

  1. Using exists when you should check that a value is not empty
  2. Using length_greater_than when you should use not_exists to verify absence
  3. Using equals_ignore_case with the wrong comparison value

Example - Incorrect:

- cond_type: attribute
  resource_types: "google_container_cluster"
  attribute: "enable_kubernetes_alpha"
  operator: "equals_ignore_case"  # This doesn't specify what value it should equal

Correct:

- cond_type: attribute
  resource_types: "google_container_cluster"
  attribute: "enable_kubernetes_alpha"
  operator: "not_equals_ignore_case"
  value: "true"  # Now it correctly checks that alpha cluster feature is disabled

When validating network configurations, similarly ensure your logic matches your security intent:

# Incorrect - checks length rather than absence
- cond_type: attribute
  resource_types: ["azurerm_network_interface"]
  attribute: "ip_configuration.public_ip_address_id"
  operator: "length_greater_than"  # Wrong operator for checking absence
  value: 0

# Correct - properly checks for absence
- cond_type: attribute
  resource_types: ["azurerm_network_interface"]
  attribute: "ip_configuration.public_ip_address_id"
  operator: "not_exists"  # Correctly checks that public IP is not configured

Always consider what exactly your validation is trying to check and select operators that precisely match that intention.


Validate security configurations

Always validate security-related configurations and implementations to ensure they follow established security best practices. This includes verifying authentication token formats, communication protocols, and error handling mechanisms.

Key areas to validate:

  1. Authentication tokens: Use proper Authorization header formats instead of plain tokens. Prefer standard formats like token <value> or bearer <jwt> over custom implementations.

  2. Communication protocols: Validate that API URLs use secure HTTPS protocols and warn users when insecure HTTP is detected.

  3. Authentication errors: Properly handle and propagate authentication failures (401/403 errors) with appropriate error types.

Example implementation:

// Validate secure API URL
const apiUrl = config.API_REST_URL;
if (apiUrl.startsWith('http://')) {
  console.warn("You configured the Snyk CLI to use an API URL with an HTTP scheme. This option is insecure and might prevent the Snyk CLI from working correctly.");
}

// Use proper authentication header format
const sessionToken = getAuthHeader(); // Returns "token <value>" format

// Handle authentication errors properly
try {
  return await makeAuthenticatedRequest();
} catch (error) {
  const unauthorized = error.code === 401 || error.code === 403;
  if (unauthorized) {
    throw AuthFailedError(error.error, error.code);
  }
}

This validation approach prevents security vulnerabilities by catching misconfigurations early and ensuring consistent security practices across the codebase.


Optimize variable declarations

Declare variables close to their usage point and choose appropriate types to improve code readability and reduce cognitive load. Avoid declaring variables at the top of functions when they’re only used once later in the code. Remove unused variables and imports to keep the codebase clean.

Key practices:

Example of improvement:

// Instead of declaring at top:
func updateConfigFromParameter(config configuration.Configuration, args []string, rawArgs []string) {
    doubleDashArgs := []string{}
    doubleDashPosition := -1  // could be boolean
    // ... later usage
}

// Declare where used with appropriate type:
func updateConfigFromParameter(config configuration.Configuration, args []string, rawArgs []string) {
    // ... other code
    doubleDashArgs := []string{}
    foundDoubleDash := false  // boolean is clearer than position integer
    // ... immediate usage
}

This practice reduces the mental overhead of tracking variable scope and purpose, making code easier to understand and maintain.


Use descriptive identifier names

Choose specific, descriptive names for variables, functions, types, and parameters that clearly indicate their purpose and avoid generic terms that could be overloaded or ambiguous. Generic names like Command, Config, or Data can become confusing when multiple similar concepts exist in the same codebase.

When naming identifiers, consider:

Example of improvement:

// Instead of generic name that conflicts with other Command types
type Command struct {
    // ...
}

// Use specific, descriptive name
type NodeCLICommandMeta struct {
    // ...
}

// Instead of ambiguous function name
func GetGlobalConfiguration() []ConfigOption {
    // ...
}

// Use name that clearly indicates what is returned
func GetGlobalConfigurationOptions() []ConfigOption {
    // ...
}

This practice prevents naming conflicts, reduces cognitive load when reading code, and makes the codebase more maintainable as it grows.


Avoid wildcard permissions

Implement the principle of least privilege by avoiding wildcard permissions (e.g., ‘*’) in access control policies. Always specify only the exact permissions required for a resource or action. When analyzing existing policies with wildcards, use tools that expand wildcards into specific permissions to understand the full security implications.

For AWS IAM policies, consider using the IAM_ACTION_EXPANSION extension which adds additional data to the Action field by expanding wildcards to actual actions, enabling more precise security analysis:

metadata:
  id: "CKV2_CUSTOM_1"
  extensions:
    - IAM_ACTION_EXPANSION

Example of permission best practices:

# Instead of overly permissive policies like:
Action: 
  - s3:*

# Use specific permissions:
Action:
  - s3:GetObject
  - s3:PutObject

When security tools analyze your policies, extensions like IAM_ACTION_EXPANSION can help by automatically expanding wildcards for more precise permission analysis and vulnerability detection.


Expand IAM wildcards

Always expand IAM wildcard permissions (*) to specific actions for thorough security analysis. Wildcard permissions can unintentionally grant excessive access, creating security vulnerabilities.

Use the IAM_ACTION_EXPANSION extension in your security scanning tools to automatically expand wildcards to specific permissions:

metadata:
  id: "CKV2_CUSTOM_1"
  extensions:
    - IAM_ACTION_EXPANSION

This allows for precise permission auditing and helps identify overly permissive policies that might violate the principle of least privilege.


Use descriptive parameter names

Parameter names should clearly indicate their purpose and avoid ambiguity or conflicts with existing constructs in the codebase context. Names should be self-explanatory without requiring additional comments or context to understand their function.

When naming parameters, consider:

Example of problematic naming:

# Confusing - "executor" has special meaning in CircleCI
- install-deps-python:
    executor: win-something

# Unclear purpose
go_base_url: 'https://go.dev/dl/'

Example of improved naming:

# Clear and doesn't conflict with CircleCI constructs  
- install-deps-python:
    os: win-something

# Descriptive and obvious purpose
go_download_base_url: 'https://go.dev/dl/'

This practice reduces cognitive load for developers and prevents misunderstandings about parameter usage.


Configure security scanners completely

Security scanning tools must be fully configured to maximize detection capabilities. When implementing tools like Checkov in CI/CD pipelines:

  1. For Docker-based scanners, use the --tty flag for better output handling:
    -   id: checkov_container
     name: Checkov
     description: This hook runs checkov.
     entry: bridgecrew/checkov:latest -d . --tty
    
  2. Enable comprehensive file scanning to detect all potential security issues:
    -   id: checkov_secrets
     name: Checkov Secrets
     description: This hook looks for secrets with checkov.
     entry: checkov -d . --framework secrets --enable-secret-scan-all-files
    

Improper configuration of security scanners can lead to false negatives, allowing vulnerabilities like leaked secrets, insecure infrastructure configurations, or compliance violations to go undetected. Always verify that security scanning tools are configured with appropriate flags to ensure complete coverage of your codebase.


verify downloaded file integrity

Always verify the integrity of downloaded files before using them, especially in security-sensitive applications. At minimum, validate downloaded files using checksums (SHA256, etc.) provided by the source. For higher security requirements, implement cryptographic signature verification using GPG keys.

Hash verification should occur immediately after download and before caching or extraction:

def download_and_verify(url, expected_hash, filepath):
    # Download file
    urllib.request.urlretrieve(url, filepath)
    
    # Verify hash
    with open(filepath, 'rb') as f:
        file_hash = hashlib.sha256(f.read()).hexdigest()
    
    if file_hash != expected_hash:
        os.remove(filepath)
        raise ValueError("File integrity check failed")
    
    return filepath

For critical applications, extend this with GPG signature verification using trusted public keys. This prevents supply chain attacks and ensures the authenticity of downloaded binaries, libraries, or configuration files.


separate build from runtime

Dependencies and build requirements should be installed during Docker image creation or environment setup, not in runtime execution scripts. This separation improves pipeline reliability, reduces execution time, and maintains clear boundaries between build and deployment phases.

Build-time operations (during Docker image creation):

Runtime operations (during script execution):

Example of what to avoid:

# In a signing script - BAD
sudo apt-get update && sudo apt-get install -y cmake libssl-dev libcurl4-openssl-dev faketime
# ... then do signing

Instead, install dependencies when building the Docker image, and keep runtime scripts focused on their primary purpose. This also helps avoid issues like Docker buildx tagging problems that can occur when operations are unnecessarily separated across multiple commands.


Explain complex logic

Add explanatory comments for non-obvious code sections, complex business logic, and public methods to clarify their purpose and reasoning. Comments should explain the “why” behind decisions and the “what” for functions that aren’t immediately clear from their implementation.

For conditional logic with business rules, explain the rationale:

// Cobra has its own help mechanism, however since we have help documentation 
// only in legacy CLI, we should fallback to calling it for flag errors
if commandError {
    resultError = handleErrorFallbackToLegacyCLI
} else if flagError {
    resultError = handleErrorShowHelp
}

For public methods, provide high-level documentation describing what the method does:

// ConnectToProxy establishes a connection to the specified proxy server
// and handles the authentication handshake for the target address
func (p *ProxyAuthenticator) ConnectToProxy(ctx context.Context, proxyURL *url.URL, target string, connection net.Conn) error {

Focus on areas where the code’s intent or business logic isn’t immediately apparent to future maintainers.


Use secure hash functions

When hashing sensitive data such as authentication tokens, always use cryptographically secure hash functions like SHA-256 instead of faster but insecure alternatives like MD5 or CRC32. Secure hash functions provide protection against reverse engineering and rainbow table attacks, which is critical for maintaining the confidentiality of sensitive information.

Additionally, consider truncating the hash output to further prevent reverse engineering attempts. For debugging purposes, guard such operations behind debug flags to minimize exposure in production environments.

Example:

func writeLogHeader(config configuration.Configuration) {
    tokenShaSum := []byte{}
    if token := config.GetString(configuration.AUTHENTICATION_TOKEN); len(token) > 0 {
        temp := sha256.Sum256([]byte(token))  // Use SHA-256, not MD5 or CRC32
        tokenShaSum = temp[:16]  // Truncate to 16 bytes to prevent reverse engineering
    }
    // ... rest of logging logic
}

This approach ensures that even if logs are compromised, the original sensitive data remains protected through strong cryptographic practices.


API interface design

Design interfaces that balance simplicity with extensibility. Consolidate related parameters into cohesive data structures to reduce interface complexity, while using interfaces to enable future implementations and maintain flexibility.

When designing function signatures, group related parameters into structured types rather than passing multiple individual parameters. This reduces cognitive load and makes the interface more maintainable.

For extensibility, define interfaces even when only one implementation currently exists if you anticipate future variations. This prevents breaking changes later.

Example:

// Instead of multiple parameters:
func Execute(port int, certLocation string, proxyHost string, proxyPort int, args []string)

// Consolidate related data:
type ProxyInfo struct {
    Port int
    CertificateLocation string
    Host string
    ProxyPort int
}

func Execute(proxyInfo *ProxyInfo, args []string)

// Use interfaces for extensibility:
type AuthenticationHandlerInterface interface {
    Authenticate() error
}

This approach creates cleaner APIs that are easier to use, test, and extend over time.


Configure loggers consistently

Always configure module-level loggers with a consistent pattern across the codebase. Use logging.getLogger(__name__) to create loggers that match the module hierarchy, set appropriate default levels (typically WARNING for library code), use standardized formatting that includes timestamp, logger name, level, and message, and provide mechanisms to adjust log levels at runtime.

Example:

import logging
import sys

# Create module-level logger with consistent naming
logger = logging.getLogger(__name__)
logger.setLevel(logging.WARNING)  # Set default level to WARNING

# Configure handler and formatter consistently
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

# Provide a way to adjust log level at runtime
def change_log_level(log_level):
    logger.setLevel(log_level)

This approach ensures logs are consistent across the application, properly namespaced, and can be controlled for verbosity based on runtime needs (like command line flags).