Accelerating incident response with AI

How we leveraged GitHub Copilot to accelerate Typeform's incident response process — from generating one-off scripts during live attacks to promoting them into production Go services.

Posted Feb 11, 2025 in Posts

This post was originally published on Typeform’s Engineering blog on Medium: Accelerating incident response with AI

Photo by Ian Taylor on Unsplash
Photo by Ian Taylor on Unsplash

Any sufficiently well-known SaaS product eventually suffers attacks and abuse, and Typeform is no exception — that’s why we have dedicated Security Engineers embedded into our DevTools and SRE teams who work tirelessly to protect our customers’ data and our platform. However, incidents happen, and when they do, we need to respond quickly and effectively to ensure minimal impact on our customers’ experience. Having an effective Incident Response Process is crucial to making this happen. At Typeform we follow a process similar to that outlined by the UK’s National Cybersecurity Council: we break down Incident Response into a series of stages that see an Incident from initial triage through to its conclusion.


In this blog post, we’ll explore how we’ve leveraged AI, specifically GitHub Copilot, to accelerate our incident response process during the Analyse, Mitigate and Contain stages — in other words, when an incident is happening and we need to react quickly and effectively. We’ll discuss how this approach has allowed us to quickly develop ad-hoc tooling for data extraction, transformation, and simple automations. By harnessing AI’s power, we’ve significantly reduced our time to Recovery, even when dealing with novel and complex attacks.

Sophisticated attacks need tailored responses

While most of the attacks we face use known patterns, occasionally a few of them are more complex or purpose-built to target Typeform and a dedicated mitigation must be used. Generally speaking, this can either be a time-consuming, non-scalable manual process or the development of automated abuse management tooling; if an attack is novel, this latter tooling doesn’t yet exist.

Sometimes preventing abuse of a feature also requires making changes to it, like limiting its capabilities or implementing new or improved guardrails such as account verification requirements, in order to make it more secure for everyone. However, these require careful consideration and design, as we take user experience very seriously at Typeform. Any changes to our product must undergo thorough testing to ensure they don’t negatively impact our users.

This post will also look at how we use AI during the Review stage to ease the promotion of our ad-hoc tools to more mature features that help Typeform remain secure in the face of future threats.

Generating one-off scripts using Copilot

One of the key ways we’ve accelerated our incident response process is by using GitHub Copilot to generate one-off scripts. These scripts are invaluable for quickly extracting and transforming data, as well as creating simple automations. By leveraging Copilot’s AI-powered code suggestions, we can rapidly prototype and implement solutions tailored to the specific attack we’re facing.


For example, let’s imagine we want to reset a bunch of users’ passwords using Okta’s REST API, given a list of email addresses; we could start with this mostly-empty bash script:

#!/bin/bash

OKTA_BASE_URL="https://typeform.okta.com"
OKTA_API_TOKEN="<to be filled in>"
USER_AGENT="typeform/password-reset-script"

# Print statements for debugging
set -x

while read email; do
  echo "Processing user $email"
done

Let’s ask Copilot to implement our desired behavior with the following prompt:

Using cURL and the credentials above, call the Okta API to fetch each user by email address, then call the API again to reset their password

 #!/bin/bash
 
 OKTA_BASE_URL="https://typeform.okta.com"
 OKTA_API_TOKEN="<to be filled in>"
 USER_AGENT="typeform/password-reset-script"
 
 # Print statements for debugging
 set -x
 
 while read email; do
   echo "Processing user $email"
+  # Fetch user by email
+  response=$(curl -s -X GET "$OKTA_BASE_URL/api/v1/users/$email" \
+    -H "Authorization: SSWS $OKTA_API_TOKEN" \
+    -H "Accept: application/json" \
+    -H "User-Agent: $USER_AGENT")
+
+  # Extract user ID from response
+  user_id=$(echo $response | jq -r '.id')
+
+  if [ "$user_id" != "null" ]; then
+    # Reset user password
+    curl -s -X POST "$OKTA_BASE_URL/api/v1/users/$user_id/lifecycle/reset_password" \
+      -H "Authorization: SSWS $OKTA_API_TOKEN" \
+      -H "Accept: application/json" \
+      -H "User-Agent: $USER_AGENT"
+    echo "Password reset for user $email"
+  else
+    echo "User $email not found"
+  fi
 done

Finally, let’s ask Copilot to add a “dry run” command line option to the script, so we can verify what calls would be made without actually executing them:

Check for a --dry-run command line argument at the start of the script, and if set only print the actions that would be taken without executing them.

 #!/bin/bash
 
 OKTA_BASE_URL="https://typeform.okta.com"
 OKTA_API_TOKEN="<to be filled in>"
 USER_AGENT="typeform/password-reset-script"
 
 # Print statements for debugging
 set -x
 
+# Check for --dry-run argument
+DRY_RUN=false
+if [ "$1" == "--dry-run" ]; then
+  DRY_RUN=true
+  shift
+fi
 
 while read email; do
   echo "Processing user $email"
   # Fetch user by email
   response=$(curl -s -X GET "$OKTA_BASE_URL/api/v1/users/$email" \
     -H "Authorization: SSWS $OKTA_API_TOKEN" \
     -H "Accept: application/json" \
     -H "User-Agent: $USER_AGENT")
 
   # Extract user ID from response
   user_id=$(echo $response | jq -r '.id')
 
   if [ "$user_id" != "null" ]; then
+    if [ "$DRY_RUN" == true ]; then
+      echo "Would reset password for user $email (user ID: $user_id)"
+    else
       # Reset user password
       curl -s -X POST "$OKTA_BASE_URL/api/v1/users/$user_id/lifecycle/reset_password" \
         -H "Authorization: SSWS $OKTA_API_TOKEN" \
         -H "Accept: application/json" \
         -H "User-Agent: $USER_AGENT"
       echo "Password reset for user $email"
+    fi
   else
     echo "User $email not found"
   fi
 done

This approach allows us to respond to novel threats with agility, even when we don’t have pre-existing tools in place. Copilot’s contextual code generation means it can provide useful code snippets based on similar API usage; as we document our APIs with OpenAPI specs, Copilot generates mostly-perfect API client code. In a time-sensitive context, this drastically cuts down the development time for scripts that would otherwise require manual coding from scratch. We’ve been able to apply this pattern to third-party APIs, too, and automating tasks like resetting user passwords, sending out compliance emails, triggering alerts, and more.

Evolving scripts into internal utilities and Go services

Writing one-off scripts is incredibly useful during the initial stages of incident response, but it doesn’t make our platform inherently more secure. To address that, and to protect our customers from repeated attacks or similar threats in the longer term, we like to “promote” the automations developed for incident response to first-class internal Go services, so that they can be integrated with our other tooling.

Again, Copilot has proven to be extremely useful for this too: since at Typeform we write all our Go services following a standard template (and whenever possible harness code generation tools instead of wasting time writing boilerplate), it’s relatively straightforward for Copilot to infer how the new Go code should look like — generating not just idiomatic Go code, but idiomatic Typeform Go service code.


Of course, LLMs aren’t perfect and often need directions to get the code just right; that said, we’ve found that documenting code inline (as opposed to documenting library APIs in markdown, for example) works best, as Copilot has an easier time understanding the context, and providing usage examples goes a very long way too. It’s also extremely helpful to manually add other files to Copilot prompts using #file:<filename>, which immensely helps Copilot maintain the desired code style by imitating existing content.

Consider this example:

package transport

import "net/http"

// InternalGetSSOPolicy is the HTTP API handler for getting the SSO policy for an account
// given their ID, which is passed in the request URL as a route param.
func (t *Transport) InternalGetSSOPolicy(w http.ResponseWriter, r *http.Request) {
	// TODO: implement
}

Implement this method with the same style as #file:handler_internal_get_sso_domains.go

Results in the following:

 package transport
 
 import (
+  "errors"
   "net/http"
+
+  "github.com/gorilla/mux"
+
+  "github.com/Typeform/my-service/internal/errors"
 )
 
 // InternalGetSSOPolicy is the HTTP API handler for getting the SSO policy for an account
 // given their ID, which is passed in the request URL as a route param.
 func (t *Transport) InternalGetSSOPolicy(w http.ResponseWriter, r *http.Request) {
-  // TODO: implement
+  ctx := r.Context()
+  accountID := mux.Vars(r)["account_id"]
+
+  ssoPolicy, err := t.service.GetSSOPolicy(ctx, accountID)
+  if err != nil {
+    if errors.Is(err, errors.ErrAccountNotFound) {
+      t.handleError(w, r, errors.NewNotFoundError(err).WithDetails(errors.ErrorDetail{
+        Code:        "ACCOUNT_NOT_FOUND",
+        Description: "Account not found",
+      }))
+
+      return
+    }
+
+    t.handleError(w, r, err)
+
+    return
+  }
+
+  t.writeJSONResponse(w, r, http.StatusOK, ssoPolicy)
 }

Which matches the style of the referenced file (handler_internal_get_sso_domains.go)!

A real-world example: password spraying attacks

Password spraying attacks consist of spread-over-time bruteforcing of user credentials in an attempt to gain access to them. They’re very common — and, if performed well enough, very hard to detect. For example, an attacker will use a static password and many variations on the username in an attempt to catch accounts with common passwords.

At Typeform, we protect our customer’s accounts using a variety of techniques and solutions, and suspicious login attempts already set off alarms that our Security Engineers very quickly respond with and investigate. Here’s an example of a recent attack: password spraying using valid credentials, freshly leaked from another website.

What a simplified password spraying attack looks like: an attacker trying to log in to various different Typeform accounts using the same password.
What a simplified password spraying attack looks like.

The user accounts that the attackers were trying to gain access to were unfortunately sharing the same credentials (email and password) across multiple services, which is an insecure practice; they also did not have MFA enabled, which is also not recommended.

Typeform recommends at least the following measures in order to keep your account secure:

  1. use a unique, long and complex password; even better, a Password Manager
  2. do not use the same password you use for Typeform on other websites
  3. configure and enable Multi-Factor Authentication.

Following an attack on another website and the subsequent leaking of user credentials, attackers tried using these credentials on Typeform in a password spraying attack.

Alarms went off: we ingest all events from our IDP’s firehose in realtime into our home-grown Security data platform, in this specific case backed by an ElasticSearch cluster; the alerting system periodically (as well as on-trigger) runs a series of queries to identify both common recognized patterns and suspicious metric values, such as a high number of failed logins.

This early detection system paged our Security team, who promptly mitigated the password spraying attack using restrictive network policies based on the attacker’s network: first off, we started identifying patterns in suspicious login attempts, speeding up our analysis by using Copilot to generate ElasticSearch/Kibana queries and dashboards to visually spot correlations as well as extract relevant identifiers (IP addresses, ASNs, email addresses the attackers attempted to use, etc.).

Copilot was also extremely useful for optimizing some of these ElasticSearch queries: not all members of the team are ElasticSearch experts, but do have a deep understanding of the data; the use of AI enabled them to bridge the gap and deploy working, performant queries without spending hours sifting through documentation to learn about database internals.

It also significantly sped up the work required to integrate the new queries into the data platform, as “glue” code would readily be autocompleted.

Once a series of patterns to identify malicious traffic was ready and armed with a comprehensive list of filters, we applied much more restrictive access rules — slowing down attackers and starting to flag user accounts that potentially had compromised credentials. Again, Copilot proved very useful in generating data conversion scripts, from input data points extracted from our logs to Terraform code (network/access rules).

Once traffic from the attackers had been completely blocked at our firewall, we started to look into addressing the accounts at risk: using the OpenAPI spec of our Identity Provider’s API, we instructed Copilot to generate scripts to identify the affected users, set stricter security controls for suspicious logins, and reset their passwords.

Closing thoughts

While AI tools like Copilot have proven invaluable in accelerating our incident response capabilities, it’s crucial to understand that AI is not a silver bullet that automatically solves security challenges. The real power of AI emerges when it’s wielded by domain experts who already possess deep understanding of security processes, infrastructure, and incident response protocols.

These tools act as force multipliers — they don’t replace human expertise, but rather enhance it by automating repetitive tasks, generating boilerplate code faster, and suggesting solutions based on patterns. Our security engineers still need to understand API specifications, validate generated code, and most importantly, know exactly what actions need to be taken during an incident.

Organizations looking to leverage AI should view it as an enhancement to their existing toolset rather than a replacement for expertise. Just as a powerful IDE makes developers more productive but doesn’t write programs by itself, AI assists and accelerates but relies on the knowledge and judgment of the professionals using it.

AI enables us Engineers to automate workflows that were previously too complex or time-consuming to tackle programmatically: tasks that might have required extensive manual intervention, advanced NLP or in general processed free-form text or data can now be partially or fully automated, allowing our teams to focus on higher-level strategic work.

What we shared above is just a little example of how we leverage AI at Typeform Engineering; from speeding up Product prototyping, to improved Engineering velocity and faster Incident Response across both Engineering and Security, we have been investing in AI since the start — but more importantly, into making our product better and more secure and providing solutions for our customers efficiently.

If you’re passionate about leveraging cutting-edge AI to drive innovation, work with us to build the future of Typeform!