Detecting Cloud Native Anti-Patterns with Semgrep

To realise the promise of cloud including auto-scaling, high availability and fault tolerance requires more than a lift and shift for legacy workloads. Applications require a tactical refactor to ensure components are stateless, retrieve configuration through appropriate means such as environment variables or platform configuration sources such as AWS Parameter Store / Secrets Manager, output logs appropriately so they may be consumed into centralised logging mechanisms such as CloudWatch or Splunk rather than written to local files and much more.

Whilst static analysis is traditionally used for security scanning its power extends beyond finding vulnerabilities. I’ve created a Cloud Anti-Patterns Ruleset to solve a problem I have repeatedly hit when working with clients, rapidly analysing their code bases to determine cloud suitability and highlight potential areas for refactor. To understand why static analysis is a great fit for this it helps to understand where such tooling came from.

Semgrep – A Brief History

You may have heard of or used Semgrep before for static code analysis purposes. Semgrep was inspired by Coccinelle, a matching and transformation engine developed by C devs to detect dangerous code patterns such as null pointer risks. Coccinelle evolved to allow semantic patching to search and replace coding patterns safely and has been part of the management of massive codebases such as the Linux kernel for many years. In 2009 Facebook required a similar tool for PHP but found nothing suitable off the shelf. Yoann Padioleau, a member of the team, created sgrep, an internal but open-sourced tool, which opened up the pattern matching and transformation of Coccinelle to all languages using dynamically defined rule sets. In the present day sgrep has been rebranded to Semgrep and is maintained by Semgrep Inc. who have scaled it into a major commercial and open-source code scanning platform.

Cloud Anti-Pattern Ruleset

Given the flexibility of Semgrep rulesets I decided to apply Semgrep to my cloud suitability analysis and refactor problem and have released the Semgrep Cloud Anti-Patterns Ruleset. It supports most major languages used in modern cloud applications including Typescript, NodeJS, Python, Go, Java, C# and Kotlin and popular libraries and frameworks such as Flask, SQLAlchemy, ExpressJS etc… The ruleset is released under the GNU GPL v3 open-source license.

You may be wondering if rulesets and static analysis such as this are worth it in the age of generative AI? I believe that there is still a strong case for static analysis tools due to the following reasons:

  • Idempotency – Static analysis is consistent between executions which cannot be guaranteed with AI powered methods making static analysis a strong choice for Continuous Integration (CI) gates which require repeatability.
  • Zero hallucinations – Unlike AI, static analysis cannot make up phantom functionality or misread context, it either matches something in the Abstract Syntax Tree or it doesn’t.
  • Extremely fast – Semgrep is able to process thousands of lines of code per second on a modest CPU unlike Gen AI which could take tens of minutes to analyse a medium to large size code base.
  • Low cost – static analysis compute requirements are minimal and have negligible cost consideration as your codebase grows. Conversely, AI token costs scale in a linear fashion at best, or exponentially with large context windows at worst, making it prohibitively expensive at scale on fast iterating projects.
  • Highly explainable – static analysis rules are explicit and can be deconstructed by humans, if the team disagrees with a rule or identifies a false positive it can be disregarded, allowing careful tweaking and control. Meanwhile AI’s reasoning may not be easy to discover or evaluate.
  • Portability – Due to its modest compute requirements, static analysis can be run on easily sourced commodity hardware, unlike modern large language models and Model as a Service delivery preferred by some vendors, and can be run entirely in-house making it deployable to air gapped systems or suitable for use in cases where data sovereignty is of importance.

This isn’t to say Generative AI doesn’t have a place in refactoring for cloud. The two tools can co-exist and both complement each other well. Here are a few examples of how Generative AI can work alongside static analysis to accelerate the cloud modernisation journey:

  • Automated refactoring – Semgrep and other static analysis utilities can identify anti-patterns quickly and reliably, but lack the ability to refactor beyond simple replacement. LLMs excel at generating actual refactored code (e.g. rewriting local file storage to cloud object storage such as AWS S3 / Azure Blob).
  • Custom rule creation – AI’s deep reasoning can help discover and write custom rules for Semgrep, provide it with some pre-cloud and modernised code examples and it will draft matching patterns for you to further enhance the ruleset already provided or account for custom cases.
  • Contextual remediation – During CI, AI can read the Semgrep output providing a smaller surface area for AI analysis during the pipeline ensuring CI execution times are still reasonable and token consumption and cost is reduced whilst still adding value to the developers by commenting Pull Requests (PRs) with further insights or suggesting automated code fixes.

Usage

Here is a simple example of running the rules locally. First, you’ll probably want to create a temporary directory and Python virtual environment to experiment in so you can easily cleanup once finished evaluating. Install Semgrep, and clone the ruleset, it works with the latest version and is maintained as such.

mkdir semgrep-experiment
cd semgrep-experiment
python3 -m venv .venv
source .venv/bin/activate
pip install semgrep
git clone https://github.com/robputt/semgrep-rules-cloud.git

To demonstrate, I ran the ruleset against two well-known repositories. The first is Jenkins’ source code. Despite the nature of scaling agents in Jenkins the core control node is not a cloud native application and features significant amounts of state stored within the local filesystem and in-memory, making it a poor candidate for highly available deployment. It is hypothesised that the repository should return a great number of cloud anti-patterns.

git clone https://github.com/jenkinsci/jenkins.git
semgrep --config semgrep-rules-cloud jenkins/.

The output here is quite extensive so I just share a few examples and the summary.

### State stored on local filesystem

jenkins/core/src/main/java/jenkins/security/ConfidentialKey.java
❯❯❱ semgrep-rules-cloud.java.lang.filesystem.state-serialized-to-local-file
    ❰❰ Blocking ❱❱
    Application state is being serialised to a local file. This is the strongest form of the            
    statefulness problem: the process is now the system of record, so it cannot be replicated (two      
    instances hold divergent state), cannot be restarted without a volume, and cannot be rolled back    
    independently of its data. It also makes deploys risky, because the on-disk format is coupled to the
    running version. Move the record to a database or object store and keep the process disposable.     
                                                                                                              
    50┆ ConfidentialStore.get().store(this, payload);


### Session state stored in memory

jenkins/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
❯❱ semgrep-rules-cloud.java.spring.statefulness.http-session-attribute
    ❰❰ Blocking ❱❱
    Server-side session state written to `HttpSession`. By default Tomcat keeps this in the JVM heap, so
    the next request can land on another replica and the attribute is gone, and a rolling deploy logs   
    everyone out. Either move the state client-side into a signed token, or add Spring Session with     
    Redis (`spring-session-data-redis`) so all replicas share the same session store.                   
                                                                                                              
    267┆ Stapler.getCurrentRequest2().getSession().setAttribute(FEDERATED_IDENTITY_SESSION_KEY,
               identity);  


### Application state stored in memory

jenkins/core/src/main/java/hudson/Functions.java
❯❱ semgrep-rules-cloud.java.lang.statefulness.static-mutable-collection
    ❰❰ Blocking ❱❱
    Static mutable collection `iota` is JVM-wide state. Each replica holds its own copy, so reads are   
    inconsistent between instances and everything is lost when the pod restarts. It also grows unbounded
    unless something evicts, which eventually trips the container memory limit. Move the data to a      
    shared store (Redis, the database) or to an explicitly bounded cache such as Caffeine that is backed
    by that store.                                                                                      
                                                                                                              
    208┆ private static final AtomicLong iota = new AtomicLong();


### Summary

┌──────────────┐
│ Scan Summary │
└──────────────┘
✅ Scan completed successfully.
 • Findings: 113 (113 blocking)
 • Rules run: 33
 • Targets scanned: 1400
 • Parsed lines: ~99.9%
 • Scan skipped: 
   ◦ Files matching .semgrepignore patterns: 1160
 • Scan was limited to files tracked by git
 • For a detailed list of skipped files and lines, run semgrep with the --verbose flag
Ran 33 rules on 1400 files: 113 findings.

As predicted many cloud anti-patterns are identified within the Jenkins code base. For comparison, here is the output summary of a code base which is regarded as much more cloud aligned, Authentik.

git clone https://github.com/goauthentik/authentik.git
semgrep --config semgrep-rules-cloud authentik/.

There are still a few findings, mostly in memory caching, whilst this is not a showstopper for cloud deployment it will affect the cache hit rate when clients round-robin nodes, this could be remediated by moving the cache out to a service such as AWS ElastiCache using memcached or Valkey. Most notably the summary compared to Jenkins is greatly reduced in total findings.

### In memory caching    

authentik/authentik/lib/expression/evaluator.py
❯❱ semgrep-rules-cloud.python.lang.caching.local-in-memory-cache
    ❰❰ Blocking ❱❱
    Process-local cache. Each replica builds and evicts its own copy, so hit rates fall as you scale 
    out, cold starts hit the origin, and invalidation on one instance leaves the others serving stale
    data. Use a shared cache (Redis, Memcached, or a managed equivalent) so all replicas see the same
    entries and a single delete actually invalidates.                                                
                                                                                                           
    91┆ @cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180))
    92┆ @staticmethod
    93┆ def expr_resolve_dns(host: str, ip_version: int | None = None) -> list[str]:
    94┆     """Resolve host to a list of IPv4 and/or IPv6 addresses."""
    95┆     # Although it seems to be fine (raising OSError), docs warn
    96┆     # against passing `None` for both the host and the port
    97┆     # https://docs.python.org/3/library/socket.html#socket.getaddrinfo
    98┆     host = host or ""
    99┆ 
    100┆     ip_list = []
    [hid 13 additional lines, adjust with --max-lines-per-finding] 
    91┆ @cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180))
    ⋮┆----------------------------------------
    91┆ @cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180))
    ⋮┆----------------------------------------
    115┆ @cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180))
    116┆ @staticmethod
    117┆ def expr_reverse_dns(ip_addr: str) -> str:
    118┆     """Perform a reverse DNS lookup."""
    119┆     try:
    120┆         return socket.getfqdn(ip_addr)
    121┆     except OSError:
    122┆         return ip_addr
    ⋮┆----------------------------------------
    115┆ @cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180))
    ⋮┆----------------------------------------
    115┆ @cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180))


### Summary

┌──────────────┐
│ Scan Summary │
└──────────────┘
✅ Scan completed successfully.
 • Findings: 22 (22 blocking)
 • Rules run: 15
 • Targets scanned: 1821
 • Parsed lines: ~99.9%
 • Scan skipped: 
   ◦ Files larger than  files 1.0 MB: 3
   ◦ Files matching .semgrepignore patterns: 547
 • Scan was limited to files tracked by git
 • For a detailed list of skipped files and lines, run semgrep with the --verbose flag
Ran 15 rules on 1821 files: 22 findings.

Useful Links

Semgrep – https://github.com/semgrep/semgrep
Semgrep Cloud Anti-Pattern Ruleset – https://github.com/robputt/semgrep-rules-cloud

Leave a comment

Your email address will not be published.