Repository: elastic/synthetics
Pull request #189 (2021-01-18 11:56:21)

Fix linted shellcheck defects
### What

- Add pre-commit
- Fix linted defects from https://github.com/koalaman/shellcheck/
- Use shebang with `#!/usr/bin/env bash`
- Update the contributions with the pre-commit

### Tests

- UI test output for the pre-commit linting 

![image](https://user-images.githubusercontent.com/2871786/104913169-228b2980-5985-11eb-9cea-d9d8fc00b8c8.png)

![image](https://user-images.githubusercontent.com/2871786/104913239-36369000-5985-11eb-9466-f97db10e4488.png)


apmmachine: 
## :green_heart: Build Succeeded


<!-- BUILD BADGES-->
> _the below badges are clickable and redirect to their specific view in the CI or DOCS_
[![Pipeline View](https://img.shields.io/badge/pipeline-pipeline%20-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-189/detail/PR-189/1//pipeline) [![Test View](https://img.shields.io/badge/test-test-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-189/detail/PR-189/1//tests) [![Changes](https://img.shields.io/badge/changes-changes-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-189/detail/PR-189/1//changes) [![Artifacts](https://img.shields.io/badge/artifacts-artifacts-yellow)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-189/detail/PR-189/1//artifacts) [![preview](https://img.shields.io/badge/docs-preview-yellowgreen)](http://synthetics_189.docs-preview.app.elstc.co/diff)

<!-- BUILD SUMMARY-->
<details><summary>Expand to view the summary</summary>
<p>

#### Build stats


* Build Cause: `Pull request #189 opened`



* * Start Time: 2021-01-18T11:56:31.242+0000


* Duration: 15 min 50 sec


* Commit: 3e0d2a4c0462b47895af2773855e2d7c40809e33



#### Test stats :test_tube:

| Test         | Results                         |
| ------------ | :-----------------------------: |
| Failed       | 0  |
| Passed       | 56  |
| Skipped      | 5 |
| Total        | 61   |


</p>
</details>

<!-- TEST RESULTS IF ANY-->


<!-- STEPS ERRORS IF ANY -->








<!--COMMENT_GENERATED_WITH_ID_comment.id-->

Patch:
diff --git a/.ci/Jenkinsfile b/.ci/Jenkinsfile
index a2662b5..19dff78 100644
--- a/.ci/Jenkinsfile
+++ b/.ci/Jenkinsfile
@@ -62,6 +62,7 @@ pipeline {
           withNodeEnv(){
             dir("${BASE_DIR}"){
               sh(label: 'Checks linting',script: 'npm run-script lint')
+              preCommit(commit: "${GIT_BASE_COMMIT}", junit: true)
             }
           }
         }
diff --git a/.gitignore b/.gitignore
index 12a443c..0e6b6f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -111,3 +111,4 @@ tmp
 __tests__/e2e/tmp/
 
 .idea/
+seccomp/build
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..7a72b9a
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,22 @@
+repos:
+-   repo: git://github.com/pre-commit/pre-commit-hooks
+    rev: v2.2.3
+    hooks:
+    -   id: check-case-conflict
+    -   id: check-executables-have-shebangs
+    -   id: check-json
+    -   id: check-merge-conflict
+    -   id: check-xml
+    -   id: check-yaml
+    -   id: end-of-file-fixer
+
+-   repo: git@github.com:elastic/apm-pipeline-library
+    rev: current
+    hooks:
+    -   id: check-bash-syntax
+    -   id: check-en-dashes
+    -   id: check-jenkins-pipelines
+    -   id: check-jjbb
+    -   id: check-unicode-non-breaking-spaces
+    -   id: remove-en-dashes
+    -   id: remove-unicode-non-breaking-spaces
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f887281..9e13f68 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -98,6 +98,17 @@ npm run watch
 npm run test
 ```
 
+### pre-commit
+
+This project uses [pre-commit](https://pre-commit.com/) so, after installing it, please install the already configured pre-commit hooks we support, to enable pre-commit in your local git repository:
+
+```shell
+$ pre-commit install
+pre-commit installed at .git/hooks/pre-commit
+```
+
+To understand more about the hooks we use, please take a look at pre-commit's [configuration file](./.pre-commit-config.yml).
+
 ### Testing
 
 This project currently has both unit and integration tests.
diff --git a/examples/docker/bash-build-local.sh b/examples/docker/bash-build-local.sh
index da151d2..d033de5 100755
--- a/examples/docker/bash-build-local.sh
+++ b/examples/docker/bash-build-local.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 # Build a local docker image using the latest source in this repo
 # and run a bash prompty on the container
 pushd ../../
diff --git a/examples/docker/bash.sh b/examples/docker/bash.sh
index fdabc9b..df95454 100755
--- a/examples/docker/bash.sh
+++ b/examples/docker/bash.sh
@@ -1,11 +1,11 @@
-#!/bin/bash
+#!/usr/bin/env bash
 VERSION=${1:-7.10.0}
-if [[ $VERSION =~ ^[0-9] ]]; then
-	IMAGE=docker.elastic.co/experimental/synthetics:$VERSION-synthetics
+if [[ ${VERSION} =~ ^[0-9] ]]; then
+	IMAGE="docker.elastic.co/experimental/synthetics:${VERSION}-synthetics"
 else
-	IMAGE=$VERSION
+	IMAGE=${VERSION}
 fi
-echo "Using image $IMAGE"
+echo "Using image ${IMAGE}"
 docker run \
   -it \
   --rm \
@@ -15,5 +15,5 @@ docker run \
   --security-opt seccomp=seccomp_profile.json \
   --volume="$(pwd)/heartbeat.docker.yml:/usr/share/heartbeat/heartbeat.yml:ro" \
   --volume="$(pwd)/../../:/opt/elastic-synthetics:rw" \
-  $IMAGE \
+  "${IMAGE}" \
   bash
diff --git a/examples/docker/run-build-local.sh b/examples/docker/run-build-local.sh
index 208846b..8838f28 100755
--- a/examples/docker/run-build-local.sh
+++ b/examples/docker/run-build-local.sh
@@ -1,7 +1,7 @@
-#!/bin/bash
+#!/usr/bin/env bash
 # Build a local docker image using the latest source in this repo
 # and run heartbeat with the config in this directory
 pushd ../../
-./build-docker.sh $1
-popd
+./build-docker.sh "${1}"
+popd || exit
 ./run.sh heartbeat-synthetics-local "${@:2}"
diff --git a/examples/docker/run.sh b/examples/docker/run.sh
index 62a9ca2..3c0beb7 100755
--- a/examples/docker/run.sh
+++ b/examples/docker/run.sh
@@ -1,36 +1,34 @@
-#!/bin/bash
-set -e
-set -x
+#!/usr/bin/env bash
+set -ex
 VERSION=${1:-7.10.0}
-if [ ! -z $1 ]; then
+if [ -n "${1}" ]; then
   shift # discard first arg
 fi
-HEARTBEAT_ARGS=$@
 
-if [ -z $1 ]; then
-  HEARTBEAT_ARGS="-E output.elasticsearch.hosts=["localhost:9200"] -E output.elasticsearch.username=elastic -E output.elasticsearch.password=changeme"
+if [ -z "${1}" ]; then
+  HEARTBEAT_ARGS="-E output.elasticsearch.hosts=[\"localhost:9200\"] -E output.elasticsearch.username=elastic -E output.elasticsearch.password=changeme"
 else
-  HEARTBEAT_ARGS="$@"
+  HEARTBEAT_ARGS="$*"
 fi
 
 # Set Image based on version
-if [[ $VERSION =~ ^[0-9] ]]; then
-	IMAGE=docker.elastic.co/experimental/synthetics:$VERSION-synthetics
+if [[ ${VERSION} =~ ^[0-9] ]]; then
+	IMAGE="docker.elastic.co/experimental/synthetics:${VERSION}-synthetics"
 else
-	IMAGE=$VERSION
+	IMAGE=${VERSION}
 fi
 
 SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
 
-echo "Using image '$IMAGE' with extra args: $HEARTBEAT_ARGS"
+echo "Using image '${IMAGE}' with extra args: ${HEARTBEAT_ARGS}"
 docker run \
   --rm \
   --name=heartbeat \
   --user=heartbeat \
   --net=host \
-  --security-opt seccomp=$SCRIPT_DIR/seccomp_profile.json \
+  --security-opt seccomp="${SCRIPT_DIR}"/seccomp_profile.json \
   --volume="$(pwd)/heartbeat.docker.yml:/usr/share/heartbeat/heartbeat.yml:ro" \
   --volume="$(pwd)/../../:/opt/elastic-synthetics:rw" \
-  $IMAGE \
+  "${IMAGE}" \
   --strict.perms=false -e \
-  $HEARTBEAT_ARGS
+  "${HEARTBEAT_ARGS}"
diff --git a/jest.config.js b/jest.config.js
index b0356e5..308afdd 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -30,5 +30,7 @@ module.exports = {
   coverageDirectory: 'coverage',
   coverageProvider: 'v8',
   testPathIgnorePatterns: ['dist'],
-  modulePathIgnorePatterns: ['/e2e/']
+  modulePathIgnorePatterns: ['/e2e/', '/utils/'],
+  globalSetup: './__tests__/utils/jest-global-setup.ts',
+  globalTeardown: './__tests__/utils/jest-global-teardown.ts',
 };
diff --git a/package.json b/package.json
index 3cd3cad..fecfc24 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,9 @@
     "watch": "tsc -w",
     "lint": "eslint . --rulesdir utils/eslint-rules",
     "lint:fix": "npm run lint -- --fix",
-    "test": "jest",
+    "test": "npm run test:unit && npm run test:browser-service",
+    "test:unit": "jest",
+    "test:browser-service": "WSENDPOINT=ws://localhost:9322 npm run test:unit",
     "coverage": "jest --coverage",
     "docker": "./build-docker.sh"
   },
@@ -62,4 +64,4 @@
     "prettier": "^2.2.1",
     "ts-jest": "^26.4.4"
   }
-}
+}
\ No newline at end of file
diff --git a/seccomp/.dockerignore b/seccomp/.dockerignore
new file mode 100644
index 0000000..5064824
--- /dev/null
+++ b/seccomp/.dockerignore
@@ -0,0 +1,106 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and *not* Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+tmp
+
+# Vim temp files
+*.swp
diff --git a/seccomp/Dockerfile b/seccomp/Dockerfile
new file mode 100644
index 0000000..fc873b6
--- /dev/null
+++ b/seccomp/Dockerfile
@@ -0,0 +1,14 @@
+FROM mcr.microsoft.com/playwright
+
+USER root
+RUN apt-get update
+RUN apt-get -y install rsyslog
+
+RUN apt-get -y install golang
+RUN go get github.com/elastic/go-seccomp-bpf github.com/elastic/go-ucfg/yaml
+
+COPY ./ /home/synthetics
+WORKDIR /home/synthetics
+
+RUN npm install
+RUN npm run build
\ No newline at end of file
diff --git a/seccomp/docker-compose.yml b/seccomp/docker-compose.yml
new file mode 100644
index 0000000..e98d093
--- /dev/null
+++ b/seccomp/docker-compose.yml
@@ -0,0 +1,13 @@
+version: "3.7"
+services:
+  seccomp-profiler:
+    build:
+      context: ../
+      dockerfile: seccomp/Dockerfile
+    working_dir: /home/synthetics/seccomp
+    command: bash -c "sh /home/synthetics/seccomp/profile.sh"
+    stdin_open: true
+    tty: true
+    privileged: true
+    volumes:
+      - ./:/home/synthetics/seccomp/
diff --git a/seccomp/parse-syslog.js b/seccomp/parse-syslog.js
new file mode 100644
index 0000000..77778a9
--- /dev/null
+++ b/seccomp/parse-syslog.js
@@ -0,0 +1,78 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2020-present, Elastic NV
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+/* eslint-disable @typescript-eslint/no-var-requires */
+const fs = require('fs');
+const syscallTable = require('./x86_64_table.json');
+
+const syslogFile = process.argv[2] || './syslog';
+
+console.log(`# Seccomp profile generated from ${syslogFile}`);
+console.log(`
+seccomp:
+  default_action: errno
+  syscalls:
+`);
+const data = fs.readFileSync(syslogFile, 'UTF-8');
+const lines = data.split(/\r?\n/);
+
+const parseLine = line => {
+  const tuples = line.split(' ');
+  const log = {};
+  tuples.forEach(tuple => {
+    const parts = tuple.split('=');
+    log[parts[0]] = parts[1];
+  });
+  return log;
+};
+
+const seccomp = {};
+
+lines.forEach(line => {
+  const { syscall, exe } = parseLine(line);
+  if (syscall && exe) {
+    if (!seccomp[exe]) {
+      seccomp[exe] = {};
+    }
+    if (!seccomp[exe][syscall]) {
+      seccomp[exe][syscall] = syscallTable[syscall] || syscall;
+    }
+  }
+});
+
+for (const key in seccomp) {
+  if (seccomp.hasOwnProperty(key)) {
+    const syscalls = seccomp[key];
+    console.log(`  - \n  # ${key}`);
+    console.log(`    action: allow`);
+    console.log(`    names:`);
+    for (const k in syscalls) {
+      if (syscalls.hasOwnProperty(k)) {
+        const syscall = syscalls[k];
+        console.log(`    - ${syscall} #${k}`);
+      }
+    }
+  }
+}
diff --git a/seccomp/profile.sh b/seccomp/profile.sh
new file mode 100644
index 0000000..816b7be
--- /dev/null
+++ b/seccomp/profile.sh
@@ -0,0 +1,16 @@
+# TODO: use docker syslog driver instead
+service rsyslog start
+# npm run build
+
+go build -o ./build/seccomp_exec seccomp_exec.go
+
+node ../dist/browser-service.js &
+tail -f /var/log/syslog > ./build/syslog &
+
+./build/seccomp_exec -policy=./seccomp_log_agent.yml node ../dist/cli.js ../examples/todos --ws-endpoint=ws://localhost:9322
+
+./build/seccomp_exec -policy=./seccomp_log_agent.yml cat ../examples/inline/sample-inline-journey.js | node ../dist/cli.js --inline --ws-endpoint=ws://localhost:9322
+
+service rsyslog stop
+
+node parse-syslog.js ./build/syslog > synthetics_agent_profile.yml
\ No newline at end of file
diff --git a/seccomp/seccomp_agent_runtime.yml b/seccomp/seccomp_agent_runtime.yml
new file mode 100644
index 0000000..f8d1f7e
--- /dev/null
+++ b/seccomp/seccomp_agent_runtime.yml
@@ -0,0 +1,32 @@
+# Seccomp profile generated from ./build/syslog
+
+seccomp:
+  default_action: errno
+  syscalls:
+  - 
+    action: allow
+    names:
+      - clock_gettime
+  -
+    # "/usr/bin/node"
+    action: allow
+    names:
+      - read #0
+      - write #1
+      - close #3
+      - mprotect #10
+      - writev #20
+      - getpid #39
+      - epoll_wait #232
+      - epoll_ctl #233
+      - openat #257
+      - statx #332
+  -
+    # "/home/synthetics/seccomp/seccomp_exec"
+    action: allow
+    names:
+      - rt_sigprocmask #14
+      - getpid #39
+      - clone #56
+      - newfstatat #262
+      - pipe2 #293
\ No newline at end of file
diff --git a/seccomp/seccomp_agent_static_analysis.yml b/seccomp/seccomp_agent_static_analysis.yml
new file mode 100644
index 0000000..ad6183a
--- /dev/null
+++ b/seccomp/seccomp_agent_static_analysis.yml
@@ -0,0 +1,105 @@
+seccomp:
+  default_action: log
+  syscalls:
+  - names:
+    - stat #4
+    - mprotect #10
+    - brk #12
+    - access #21
+    - socket #41
+    - connect #42
+    - bind #49
+    - getsockname #51
+    - gettimeofday #96
+    - getuid #102
+    - getgid #104
+    - geteuid #107
+    - getegid #108
+    - set_tid_address #218
+    - clock_gettime #228
+    - epoll_wait #232
+    - set_robust_list #273
+    - prlimit64 #302
+    - statx #332
+    action: allow
+  - names:
+    - accept
+    - accept4
+    - arch_prctl
+    - capget
+    - capset
+    - chdir
+    - chroot
+    - clone
+    - close
+    - copy_file_range
+    - dup
+    - dup2
+    - dup3
+    - epoll_create
+    - epoll_create1
+    - epoll_ctl
+    - epoll_pwait
+    - execve
+    - exit
+    - exit_group
+    - fchdir
+    - fchmod
+    - fchown
+    - fcntl
+    - fstat
+    - fsync
+    - ftruncate
+    - futex
+    - getdents64
+    - getpid
+    - getsockopt
+    - gettid
+    - ioctl
+    - kill
+    - lseek
+    - madvise
+    - mincore
+    - mlock
+    - mmap
+    - mount
+    - munmap
+    - nanosleep
+    - newfstatat
+    - openat
+    - pipe
+    - pipe2
+    - prctl
+    - pread64
+    - ptrace
+    - pwrite64
+    - read
+    - readlinkat
+    - recvfrom
+    - recvmsg
+    - rt_sigaction
+    - rt_sigprocmask
+    - rt_sigreturn
+    - sched_getaffinity
+    - sched_yield
+    - seccomp
+    - sendmsg
+    - sendto
+    - setgid
+    - setgroups
+    - setitimer
+    - setpgid
+    - setsid
+    - setsockopt
+    - setuid
+    - shutdown
+    - sigaltstack
+    - tgkill
+    - uname
+    - unshare
+    - wait4
+    - waitid
+    - write
+    - writev
+    action: allow
+
diff --git a/seccomp/seccomp_exec.go b/seccomp/seccomp_exec.go
new file mode 100644
index 0000000..e194f5c
--- /dev/null
+++ b/seccomp/seccomp_exec.go
@@ -0,0 +1,95 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"os/exec"
+
+	"github.com/elastic/go-ucfg/yaml"
+
+	seccomp "github.com/elastic/go-seccomp-bpf"
+)
+
+var (
+	policyFile string
+	noNewPrivs bool
+)
+
+func main() {
+	flag.StringVar(&policyFile, "policy", "seccomp.yml", "seccomp policy file")
+	flag.BoolVar(&noNewPrivs, "no-new-privs", true, "set no new privs bit")
+	flag.Parse()
+
+	args := flag.Args()
+	if len(args) == 0 {
+		fmt.Fprintf(os.Stderr, "You must specify a command and args to execute.\n")
+		os.Exit(1)
+	}
+
+	policy, err := parsePolicy(policyFile)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "error: %v\n", err)
+		os.Exit(1)
+	}
+
+	filter := seccomp.Filter{
+		NoNewPrivs: noNewPrivs,
+		Flag:       seccomp.FilterFlagTSync,
+		Policy:     *policy,
+	}
+
+	if err = seccomp.LoadFilter(filter); err != nil {
+		fmt.Fprintf(os.Stderr, "error loading filter: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Execute the specified command
+	cmd := exec.Command(args[0], args[1:]...)
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	cmd.Stdin = os.Stdin
+
+	/*
+	 * cmd.Start doesn't apply the seccomp rules
+	 */
+	if err = cmd.Run(); err != nil {
+		fmt.Fprintf(os.Stderr, "error: %v\n", err)
+		os.Exit(1)
+	}
+}
+
+func parsePolicy(policyFile string) (*seccomp.Policy, error) {
+	conf, err := yaml.NewConfigWithFile(policyFile)
+	if err != nil {
+		return nil, err
+	}
+
+	type Config struct {
+		Seccomp seccomp.Policy
+	}
+
+	var config Config
+	if err = conf.Unpack(&config); err != nil {
+		return nil, err
+	}
+
+	return &config.Seccomp, nil
+}
diff --git a/seccomp/seccomp_log_agent.yml b/seccomp/seccomp_log_agent.yml
new file mode 100644
index 0000000..4f261a2
--- /dev/null
+++ b/seccomp/seccomp_log_agent.yml
@@ -0,0 +1,6 @@
+seccomp:
+  default_action: log
+  syscalls:
+  - names:
+    - clock_gettime
+    action: allow
\ No newline at end of file
diff --git a/seccomp/seccomp_profile_playwright.yml b/seccomp/seccomp_profile_playwright.yml
new file mode 100644
index 0000000..df5ed34
--- /dev/null
+++ b/seccomp/seccomp_profile_playwright.yml
@@ -0,0 +1,663 @@
+seccomp:
+  defaultAction: SCMP_ACT_ERRNO
+  archMap:
+    - architecture: SCMP_ARCH_X86_64
+      subArchitectures:
+        - SCMP_ARCH_X86
+        - SCMP_ARCH_X32
+    - architecture: SCMP_ARCH_AARCH64
+      subArchitectures:
+        - SCMP_ARCH_ARM
+    - architecture: SCMP_ARCH_MIPS64
+      subArchitectures:
+        - SCMP_ARCH_MIPS
+        - SCMP_ARCH_MIPS64N32
+    - architecture: SCMP_ARCH_MIPS64N32
+      subArchitectures:
+        - SCMP_ARCH_MIPS
+        - SCMP_ARCH_MIPS64
+    - architecture: SCMP_ARCH_MIPSEL64
+      subArchitectures:
+        - SCMP_ARCH_MIPSEL
+        - SCMP_ARCH_MIPSEL64N32
+    - architecture: SCMP_ARCH_MIPSEL64N32
+      subArchitectures:
+        - SCMP_ARCH_MIPSEL
+        - SCMP_ARCH_MIPSEL64
+    - architecture: SCMP_ARCH_S390X
+      subArchitectures:
+        - SCMP_ARCH_S390
+  syscalls:
+    - comment: Allow create user namespaces
+      names:
+        - clone
+        - setns
+        - unshare
+      action: SCMP_ACT_ALLOW
+      args: []
+      includes: {}
+      excludes: {}
+    - names:
+        - accept
+        - accept4
+        - access
+        - adjtimex
+        - alarm
+        - bind
+        - brk
+        - capget
+        - capset
+        - chdir
+        - chmod
+        - chown
+        - chown32
+        - clock_adjtime
+        - clock_adjtime64
+        - clock_getres
+        - clock_getres_time64
+        - clock_gettime
+        - clock_gettime64
+        - clock_nanosleep
+        - clock_nanosleep_time64
+        - close
+        - connect
+        - copy_file_range
+        - creat
+        - dup
+        - dup2
+        - dup3
+        - epoll_create
+        - epoll_create1
+        - epoll_ctl
+        - epoll_ctl_old
+        - epoll_pwait
+        - epoll_wait
+        - epoll_wait_old
+        - eventfd
+        - eventfd2
+        - execve
+        - execveat
+        - exit
+        - exit_group
+        - faccessat
+        - fadvise64
+        - fadvise64_64
+        - fallocate
+        - fanotify_mark
+        - fchdir
+        - fchmod
+        - fchmodat
+        - fchown
+        - fchown32
+        - fchownat
+        - fcntl
+        - fcntl64
+        - fdatasync
+        - fgetxattr
+        - flistxattr
+        - flock
+        - fork
+        - fremovexattr
+        - fsetxattr
+        - fstat
+        - fstat64
+        - fstatat64
+        - fstatfs
+        - fstatfs64
+        - fsync
+        - ftruncate
+        - ftruncate64
+        - futex
+        - futex_time64
+        - futimesat
+        - getcpu
+        - getcwd
+        - getdents
+        - getdents64
+        - getegid
+        - getegid32
+        - geteuid
+        - geteuid32
+        - getgid
+        - getgid32
+        - getgroups
+        - getgroups32
+        - getitimer
+        - getpeername
+        - getpgid
+        - getpgrp
+        - getpid
+        - getppid
+        - getpriority
+        - getrandom
+        - getresgid
+        - getresgid32
+        - getresuid
+        - getresuid32
+        - getrlimit
+        - get_robust_list
+        - getrusage
+        - getsid
+        - getsockname
+        - getsockopt
+        - get_thread_area
+        - gettid
+        - gettimeofday
+        - getuid
+        - getuid32
+        - getxattr
+        - inotify_add_watch
+        - inotify_init
+        - inotify_init1
+        - inotify_rm_watch
+        - io_cancel
+        - ioctl
+        - io_destroy
+        - io_getevents
+        - io_pgetevents
+        - io_pgetevents_time64
+        - ioprio_get
+        - ioprio_set
+        - io_setup
+        - io_submit
+        - io_uring_enter
+        - io_uring_register
+        - io_uring_setup
+        - ipc
+        - kill
+        - lchown
+        - lchown32
+        - lgetxattr
+        - link
+        - linkat
+        - listen
+        - listxattr
+        - llistxattr
+        - _llseek
+        - lremovexattr
+        - lseek
+        - lsetxattr
+        - lstat
+        - lstat64
+        - madvise
+        - membarrier
+        - memfd_create
+        - mincore
+        - mkdir
+        - mkdirat
+        - mknod
+        - mknodat
+        - mlock
+        - mlock2
+        - mlockall
+        - mmap
+        - mmap2
+        - mprotect
+        - mq_getsetattr
+        - mq_notify
+        - mq_open
+        - mq_timedreceive
+        - mq_timedreceive_time64
+        - mq_timedsend
+        - mq_timedsend_time64
+        - mq_unlink
+        - mremap
+        - msgctl
+        - msgget
+        - msgrcv
+        - msgsnd
+        - msync
+        - munlock
+        - munlockall
+        - munmap
+        - nanosleep
+        - newfstatat
+        - _newselect
+        - open
+        - openat
+        - pause
+        - pipe
+        - pipe2
+        - poll
+        - ppoll
+        - ppoll_time64
+        - prctl
+        - pread64
+        - preadv
+        - preadv2
+        - prlimit64
+        - pselect6
+        - pselect6_time64
+        - pwrite64
+        - pwritev
+        - pwritev2
+        - read
+        - readahead
+        - readlink
+        - readlinkat
+        - readv
+        - recv
+        - recvfrom
+        - recvmmsg
+        - recvmmsg_time64
+        - recvmsg
+        - remap_file_pages
+        - removexattr
+        - rename
+        - renameat
+        - renameat2
+        - restart_syscall
+        - rmdir
+        - rseq
+        - rt_sigaction
+        - rt_sigpending
+        - rt_sigprocmask
+        - rt_sigqueueinfo
+        - rt_sigreturn
+        - rt_sigsuspend
+        - rt_sigtimedwait
+        - rt_sigtimedwait_time64
+        - rt_tgsigqueueinfo
+        - sched_getaffinity
+        - sched_getattr
+        - sched_getparam
+        - sched_get_priority_max
+        - sched_get_priority_min
+        - sched_getscheduler
+        - sched_rr_get_interval
+        - sched_rr_get_interval_time64
+        - sched_setaffinity
+        - sched_setattr
+        - sched_setparam
+        - sched_setscheduler
+        - sched_yield
+        - seccomp
+        - select
+        - semctl
+        - semget
+        - semop
+        - semtimedop
+        - semtimedop_time64
+        - send
+        - sendfile
+        - sendfile64
+        - sendmmsg
+        - sendmsg
+        - sendto
+        - setfsgid
+        - setfsgid32
+        - setfsuid
+        - setfsuid32
+        - setgid
+        - setgid32
+        - setgroups
+        - setgroups32
+        - setitimer
+        - setpgid
+        - setpriority
+        - setregid
+        - setregid32
+        - setresgid
+        - setresgid32
+        - setresuid
+        - setresuid32
+        - setreuid
+        - setreuid32
+        - setrlimit
+        - set_robust_list
+        - setsid
+        - setsockopt
+        - set_thread_area
+        - set_tid_address
+        - setuid
+        - setuid32
+        - setxattr
+        - shmat
+        - shmctl
+        - shmdt
+        - shmget
+        - shutdown
+        - sigaltstack
+        - signalfd
+        - signalfd4
+        - sigprocmask
+        - sigreturn
+        - socket
+        - socketcall
+        - socketpair
+        - splice
+        - stat
+        - stat64
+        - statfs
+        - statfs64
+        - statx
+        - symlink
+        - symlinkat
+        - sync
+        - sync_file_range
+        - syncfs
+        - sysinfo
+        - tee
+        - tgkill
+        - time
+        - timer_create
+        - timer_delete
+        - timer_getoverrun
+        - timer_gettime
+        - timer_gettime64
+        - timer_settime
+        - timer_settime64
+        - timerfd_create
+        - timerfd_gettime
+        - timerfd_gettime64
+        - timerfd_settime
+        - timerfd_settime64
+        - times
+        - tkill
+        - truncate
+        - truncate64
+        - ugetrlimit
+        - umask
+        - uname
+        - unlink
+        - unlinkat
+        - utime
+        - utimensat
+        - utimensat_time64
+        - utimes
+        - vfork
+        - vmsplice
+        - wait4
+        - waitid
+        - waitpid
+        - write
+        - writev
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes: {}
+      excludes: {}
+    - names:
+        - ptrace
+      action: SCMP_ACT_ALLOW
+      args: null
+      comment: ''
+      includes:
+        minKernel: '4.8'
+      excludes: {}
+    - names:
+        - personality
+      action: SCMP_ACT_ALLOW
+      args:
+        - index: 0
+          value: 0
+          valueTwo: 0
+          op: SCMP_CMP_EQ
+      comment: ''
+      includes: {}
+      excludes: {}
+    - names:
+        - personality
+      action: SCMP_ACT_ALLOW
+      args:
+        - index: 0
+          value: 8
+          valueTwo: 0
+          op: SCMP_CMP_EQ
+      comment: ''
+      includes: {}
+      excludes: {}
+    - names:
+        - personality
+      action: SCMP_ACT_ALLOW
+      args:
+        - index: 0
+          value: 131072
+          valueTwo: 0
+          op: SCMP_CMP_EQ
+      comment: ''
+      includes: {}
+      excludes: {}
+    - names:
+        - personality
+      action: SCMP_ACT_ALLOW
+      args:
+        - index: 0
+          value: 131080
+          valueTwo: 0
+          op: SCMP_CMP_EQ
+      comment: ''
+      includes: {}
+      excludes: {}
+    - names:
+        - personality
+      action: SCMP_ACT_ALLOW
+      args:
+        - index: 0
+          value: 4294967295
+          valueTwo: 0
+          op: SCMP_CMP_EQ
+      comment: ''
+      includes: {}
+      excludes: {}
+    - names:
+        - sync_file_range2
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        arches:
+          - ppc64le
+      excludes: {}
+    - names:
+        - arm_fadvise64_64
+        - arm_sync_file_range
+        - sync_file_range2
+        - breakpoint
+        - cacheflush
+        - set_tls
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        arches:
+          - arm
+          - arm64
+      excludes: {}
+    - names:
+        - arch_prctl
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        arches:
+          - amd64
+          - x32
+      excludes: {}
+    - names:
+        - modify_ldt
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        arches:
+          - amd64
+          - x32
+          - x86
+      excludes: {}
+    - names:
+        - s390_pci_mmio_read
+        - s390_pci_mmio_write
+        - s390_runtime_instr
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        arches:
+          - s390
+          - s390x
+      excludes: {}
+    - names:
+        - open_by_handle_at
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        caps:
+          - CAP_DAC_READ_SEARCH
+      excludes: {}
+    - names:
+        - bpf
+        - clone
+        - fanotify_init
+        - lookup_dcookie
+        - mount
+        - name_to_handle_at
+        - perf_event_open
+        - quotactl
+        - setdomainname
+        - sethostname
+        - setns
+        - syslog
+        - umount
+        - umount2
+        - unshare
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        caps:
+          - CAP_SYS_ADMIN
+      excludes: {}
+    - names:
+        - clone
+      action: SCMP_ACT_ALLOW
+      args:
+        - index: 0
+          value: 2114060288
+          valueTwo: 0
+          op: SCMP_CMP_MASKED_EQ
+      comment: ''
+      includes: {}
+      excludes:
+        caps:
+          - CAP_SYS_ADMIN
+        arches:
+          - s390
+          - s390x
+    - names:
+        - clone
+      action: SCMP_ACT_ALLOW
+      args:
+        - index: 1
+          value: 2114060288
+          valueTwo: 0
+          op: SCMP_CMP_MASKED_EQ
+      comment: s390 parameter ordering for clone is different
+      includes:
+        arches:
+          - s390
+          - s390x
+      excludes:
+        caps:
+          - CAP_SYS_ADMIN
+    - names:
+        - reboot
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        caps:
+          - CAP_SYS_BOOT
+      excludes: {}
+    - names:
+        - chroot
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        caps:
+          - CAP_SYS_CHROOT
+      excludes: {}
+    - names:
+        - delete_module
+        - init_module
+        - finit_module
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        caps:
+          - CAP_SYS_MODULE
+      excludes: {}
+    - names:
+        - acct
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        caps:
+          - CAP_SYS_PACCT
+      excludes: {}
+    - names:
+        - kcmp
+        - process_vm_readv
+        - process_vm_writev
+        - ptrace
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        caps:
+          - CAP_SYS_PTRACE
+      excludes: {}
+    - names:
+        - iopl
+        - ioperm
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        caps:
+          - CAP_SYS_RAWIO
+      excludes: {}
+    - names:
+        - settimeofday
+        - stime
+        - clock_settime
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        caps:
+          - CAP_SYS_TIME
+      excludes: {}
+    - names:
+        - vhangup
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        caps:
+          - CAP_SYS_TTY_CONFIG
+      excludes: {}
+    - names:
+        - get_mempolicy
+        - mbind
+        - set_mempolicy
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        caps:
+          - CAP_SYS_NICE
+      excludes: {}
+    - names:
+        - syslog
+      action: SCMP_ACT_ALLOW
+      args: []
+      comment: ''
+      includes:
+        caps:
+          - CAP_SYSLOG
+      excludes: {}
diff --git a/seccomp/x86_64_table.json b/seccomp/x86_64_table.json
new file mode 100644
index 0000000..1ad06c4
--- /dev/null
+++ b/seccomp/x86_64_table.json
@@ -0,0 +1,349 @@
+{
+    "0": "read",
+    "1": "write",
+    "2": "open",
+    "3": "close",
+    "4": "stat",
+    "5": "fstat",
+    "6": "lstat",
+    "7": "poll",
+    "8": "lseek",
+    "9": "mmap",
+    "10": "mprotect",
+    "11": "munmap",
+    "12": "brk",
+    "13": "rt_sigaction",
+    "14": "rt_sigprocmask",
+    "15": "rt_sigreturn",
+    "16": "ioctl",
+    "17": "pread",
+    "18": "pwrite",
+    "19": "readv",
+    "20": "writev",
+    "21": "access",
+    "22": "pipe",
+    "23": "select",
+    "24": "sched_yield",
+    "25": "mremap",
+    "26": "msync",
+    "27": "mincore",
+    "28": "madvise",
+    "29": "shmget",
+    "30": "shmat",
+    "31": "shmctl",
+    "32": "dup",
+    "33": "dup2",
+    "34": "pause",
+    "35": "nanosleep",
+    "36": "getitimer",
+    "37": "alarm",
+    "38": "setitimer",
+    "39": "getpid",
+    "40": "sendfile",
+    "41": "socket",
+    "42": "connect",
+    "43": "accept",
+    "44": "sendto",
+    "45": "recvfrom",
+    "46": "sendmsg",
+    "47": "recvmsg",
+    "48": "shutdown",
+    "49": "bind",
+    "50": "listen",
+    "51": "getsockname",
+    "52": "getpeername",
+    "53": "socketpair",
+    "54": "setsockopt",
+    "55": "getsockopt",
+    "56": "clone",
+    "57": "fork",
+    "58": "vfork",
+    "59": "execve",
+    "60": "exit",
+    "61": "wait4",
+    "62": "kill",
+    "63": "uname",
+    "64": "semget",
+    "65": "semop",
+    "66": "semctl",
+    "67": "shmdt",
+    "68": "msgget",
+    "69": "msgsnd",
+    "70": "msgrcv",
+    "71": "msgctl",
+    "72": "fcntl",
+    "73": "flock",
+    "74": "fsync",
+    "75": "fdatasync",
+    "76": "truncate",
+    "77": "ftruncate",
+    "78": "getdents",
+    "79": "getcwd",
+    "80": "chdir",
+    "81": "fchdir",
+    "82": "rename",
+    "83": "mkdir",
+    "84": "rmdir",
+    "85": "creat",
+    "86": "link",
+    "87": "unlink",
+    "88": "symlink",
+    "89": "readlink",
+    "90": "chmod",
+    "91": "fchmod",
+    "92": "chown",
+    "93": "fchown",
+    "94": "lchown",
+    "95": "umask",
+    "96": "gettimeofday",
+    "97": "getrlimit",
+    "98": "getrusage",
+    "99": "sysinfo",
+    "100": "times",
+    "101": "ptrace",
+    "102": "getuid",
+    "103": "syslog",
+    "104": "getgid",
+    "105": "setuid",
+    "106": "setgid",
+    "107": "geteuid",
+    "108": "getegid",
+    "109": "setpgid",
+    "110": "getppid",
+    "111": "getpgrp",
+    "112": "setsid",
+    "113": "setreuid",
+    "114": "setregid",
+    "115": "getgroups",
+    "116": "setgroups",
+    "117": "setresuid",
+    "118": "getresuid",
+    "119": "setresgid",
+    "120": "getresgid",
+    "121": "getpgid",
+    "122": "setfsuid",
+    "123": "setfsgid",
+    "124": "getsid",
+    "125": "capget",
+    "126": "capset",
+    "127": "rt_sigpending",
+    "128": "rt_sigtimedwait",
+    "129": "rt_sigqueueinfo",
+    "130": "rt_sigsuspend",
+    "131": "sigaltstack",
+    "132": "utime",
+    "133": "mknod",
+    "134": "uselib",
+    "135": "personality",
+    "136": "ustat",
+    "137": "statfs",
+    "138": "fstatfs",
+    "139": "sysfs",
+    "140": "getpriority",
+    "141": "setpriority",
+    "142": "sched_setparam",
+    "143": "sched_getparam",
+    "144": "sched_setscheduler",
+    "145": "sched_getscheduler",
+    "146": "sched_get_priority_max",
+    "147": "sched_get_priority_min",
+    "148": "sched_rr_get_interval",
+    "149": "mlock",
+    "150": "munlock",
+    "151": "mlockall",
+    "152": "munlockall",
+    "153": "vhangup",
+    "154": "modify_ldt",
+    "155": "pivot_root",
+    "156": "_sysctl",
+    "157": "prctl",
+    "158": "arch_prctl",
+    "159": "adjtimex",
+    "160": "setrlimit",
+    "161": "chroot",
+    "162": "sync",
+    "163": "acct",
+    "164": "settimeofday",
+    "165": "mount",
+    "166": "umount2",
+    "167": "swapon",
+    "168": "swapoff",
+    "169": "reboot",
+    "170": "sethostname",
+    "171": "setdomainname",
+    "172": "iopl",
+    "173": "ioperm",
+    "174": "create_module",
+    "175": "init_module",
+    "176": "delete_module",
+    "177": "get_kernel_syms",
+    "178": "query_module",
+    "179": "quotactl",
+    "180": "nfsservctl",
+    "181": "getpmsg",
+    "182": "putpmsg",
+    "183": "afs_syscall",
+    "184": "tuxcall",
+    "185": "security",
+    "186": "gettid",
+    "187": "readahead",
+    "188": "setxattr",
+    "189": "lsetxattr",
+    "190": "fsetxattr",
+    "191": "getxattr",
+    "192": "lgetxattr",
+    "193": "fgetxattr",
+    "194": "listxattr",
+    "195": "llistxattr",
+    "196": "flistxattr",
+    "197": "removexattr",
+    "198": "lremovexattr",
+    "199": "fremovexattr",
+    "200": "tkill",
+    "201": "time",
+    "202": "futex",
+    "203": "sched_setaffinity",
+    "204": "sched_getaffinity",
+    "205": "set_thread_area",
+    "206": "io_setup",
+    "207": "io_destroy",
+    "208": "io_getevents",
+    "209": "io_submit",
+    "210": "io_cancel",
+    "211": "get_thread_area",
+    "212": "lookup_dcookie",
+    "213": "epoll_create",
+    "214": "epoll_ctl_old",
+    "215": "epoll_wait_old",
+    "216": "remap_file_pages",
+    "217": "getdents64",
+    "218": "set_tid_address",
+    "219": "restart_syscall",
+    "220": "semtimedop",
+    "221": "fadvise64",
+    "222": "timer_create",
+    "223": "timer_settime",
+    "224": "timer_gettime",
+    "225": "timer_getoverrun",
+    "226": "timer_delete",
+    "227": "clock_settime",
+    "228": "clock_gettime",
+    "229": "clock_getres",
+    "230": "clock_nanosleep",
+    "231": "exit_group",
+    "232": "epoll_wait",
+    "233": "epoll_ctl",
+    "234": "tgkill",
+    "235": "utimes",
+    "236": "vserver",
+    "237": "mbind",
+    "238": "set_mempolicy",
+    "239": "get_mempolicy",
+    "240": "mq_open",
+    "241": "mq_unlink",
+    "242": "mq_timedsend",
+    "243": "mq_timedreceive",
+    "244": "mq_notify",
+    "245": "mq_getsetattr",
+    "246": "kexec_load",
+    "247": "waitid",
+    "248": "add_key",
+    "249": "request_key",
+    "250": "keyctl",
+    "251": "ioprio_set",
+    "252": "ioprio_get",
+    "253": "inotify_init",
+    "254": "inotify_add_watch",
+    "255": "inotify_rm_watch",
+    "256": "migrate_pages",
+    "257": "openat",
+    "258": "mkdirat",
+    "259": "mknodat",
+    "260": "fchownat",
+    "261": "futimesat",
+    "262": "newfstatat",
+    "263": "unlinkat",
+    "264": "renameat",
+    "265": "linkat",
+    "266": "symlinkat",
+    "267": "readlinkat",
+    "268": "fchmodat",
+    "269": "faccessat",
+    "270": "pselect6",
+    "271": "ppoll",
+    "272": "unshare",
+    "273": "set_robust_list",
+    "274": "get_robust_list",
+    "275": "splice",
+    "276": "tee",
+    "277": "sync_file_range",
+    "278": "vmsplice",
+    "279": "move_pages",
+    "280": "utimensat",
+    "281": "epoll_pwait",
+    "282": "signalfd",
+    "283": "timerfd_create",
+    "284": "eventfd",
+    "285": "fallocate",
+    "286": "timerfd_settime",
+    "287": "timerfd_gettime",
+    "288": "accept4",
+    "289": "signalfd4",
+    "290": "eventfd2",
+    "291": "epoll_create1",
+    "292": "dup3",
+    "293": "pipe2",
+    "294": "inotify_init1",
+    "295": "preadv",
+    "296": "pwritev",
+    "297": "rt_tgsigqueueinfo",
+    "298": "perf_event_open",
+    "299": "recvmmsg",
+    "300": "fanotify_init",
+    "301": "fanotify_mark",
+    "302": "prlimit64",
+    "303": "name_to_handle_at",
+    "304": "open_by_handle_at",
+    "305": "clock_adjtime",
+    "306": "syncfs",
+    "307": "sendmmsg",
+    "308": "setns",
+    "309": "getcpu",
+    "310": "process_vm_readv",
+    "311": "process_vm_writev",
+    "312": "kcmp",
+    "313": "finit_module",
+    "314": "sched_setattr",
+    "315": "sched_getattr",
+    "316": "renameat2",
+    "317": "seccomp",
+    "318": "getrandom",
+    "319": "memfd_create",
+    "320": "kexec_file_load",
+    "321": "bpf",
+    "322": "execveat",
+    "323": "userfaultfd",
+    "324": "membarrier",
+    "325": "mlock2",
+    "326": "copy_file_range",
+    "327": "preadv2",
+    "328": "pwritev2",
+    "329": "pkey_mprotect",
+    "330": "pkey_alloc",
+    "331": "pkey_free",
+    "332": "statx",
+    "333": "io_pgetevents",
+    "334": "rseq",
+    "424": "pidfd_send_signal",
+    "425": "io_uring_setup",
+    "426": "io_uring_enter",
+    "427": "io_uring_register",
+    "428": "open_tree",
+    "429": "move_mount",
+    "430": "fsopen",
+    "431": "fsconfig",
+    "432": "fsmount",
+    "433": "fspick",
+    "434": "pidfd_open",
+    "435": "clone3"
+}
\ No newline at end of file
diff --git a/src/browser-service.ts b/src/browser-service.ts
index 99470cc..91cd2f0 100644
--- a/src/browser-service.ts
+++ b/src/browser-service.ts
@@ -31,6 +31,12 @@ import { chromium } from 'playwright-chromium';
 const proxy = createProxyServer();
 const proxyServer = createServer();
 
+/**
+ * TODO: refactor browser service into a class
+ * to manage this state with init/dispose.
+ */
+let closeCallbacks = [];
+
 proxyServer.on('upgrade', async function (req, socket, head) {
   const browserServer = await chromium.launchServer({ headless: true });
   const wsEndpoint = browserServer.wsEndpoint();
@@ -39,9 +45,14 @@ proxyServer.on('upgrade', async function (req, socket, head) {
   console.log(`New browser: ${wsEndpoint}`);
   proxy.ws(req, socket, head, { target: origin });
   const closeBrowser = async () => {
+    const index = closeCallbacks.indexOf(closeBrowser);
+    if (index >= 0) {
+      closeCallbacks.splice(index, 1);
+    }
     await browserServer.close();
     console.log(`Socket closed: ${wsEndpoint}`);
   };
+  closeCallbacks.push(closeBrowser);
   socket.on('close', closeBrowser);
   socket.on('error', closeBrowser);
 });
@@ -50,3 +61,12 @@ const port = 9322;
 proxyServer.listen(port, () => {
   console.log(`Listening on port: ${port}`);
 });
+
+process.on('SIGTERM', async () => {
+  for (let i = 0; i < closeCallbacks.length; i++) {
+    const callback = closeCallbacks[i];
+    await callback();
+  }
+  closeCallbacks = [];
+  process.exit(0);
+});


Test patch:
diff --git a/__tests__/core/gatherer.test.ts b/__tests__/core/gatherer.test.ts
index b095dcc..adfc062 100644
--- a/__tests__/core/gatherer.test.ts
+++ b/__tests__/core/gatherer.test.ts
@@ -26,18 +26,19 @@
 import { Gatherer } from '../../src/core/gatherer';
 import { PluginManager } from '../../src/plugins';
 import { NetworkManager } from '../../src/plugins/network';
+import { wsEndpoint } from '../utils/test-config';
 
 jest.mock('../../src/plugins/network');
 
 describe('Gatherer', () => {
   it('boot and dispose driver', async () => {
-    const driver = await Gatherer.setupDriver(true);
+    const driver = await Gatherer.setupDriver(true, wsEndpoint);
     expect(typeof driver.page.goto).toBe('function');
     await Gatherer.dispose(driver);
   });
 
   it('begin recording based on flags', async () => {
-    const driver = await Gatherer.setupDriver(true);
+    const driver = await Gatherer.setupDriver(true, wsEndpoint);
     const pluginManager = await Gatherer.beginRecording(driver, {
       network: true,
     });
diff --git a/__tests__/core/runner.test.ts b/__tests__/core/runner.test.ts
index 6722490..9d6cbf8 100644
--- a/__tests__/core/runner.test.ts
+++ b/__tests__/core/runner.test.ts
@@ -28,8 +28,9 @@ import { Gatherer } from '../../src/core/gatherer';
 import Runner from '../../src/core/runner';
 import { step, journey } from '../../src/core';
 import { Journey } from '../../src/dsl';
-import { Server } from '../../utils/server';
+import { Server } from '../utils/server';
 import { generateTempPath } from '../../src/helpers';
+import { wsEndpoint } from '../utils/test-config';
 
 describe('runner', () => {
   let runner: Runner, server: Server;
@@ -104,7 +105,7 @@ describe('runner', () => {
         end: expect.any(Number),
       });
     });
-    const result = await runner.runJourney(journey, {});
+    const result = await runner.runJourney(journey, { wsEndpoint });
     expect(result.status).toBe('succeeded');
   });
 
@@ -115,7 +116,7 @@ describe('runner', () => {
     journey.addStep('step2', async () => {
       throw error;
     });
-    const result = await runner.runJourney(journey, {});
+    const result = await runner.runJourney(journey, { wsEndpoint });
     expect(result).toEqual({
       status: 'failed',
       error,
@@ -126,7 +127,7 @@ describe('runner', () => {
     const journey = new Journey({ name: 'with hooks' }, noop);
     journey.addHook('before', noop);
     journey.addHook('after', noop);
-    const result = await runner.runJourney(journey, {});
+    const result = await runner.runJourney(journey, { wsEndpoint });
     expect(result).toEqual({
       status: 'succeeded',
     });
@@ -139,7 +140,7 @@ describe('runner', () => {
     journey.addHook('after', () => {
       throw error;
     });
-    const result = await runner.runJourney(journey, {});
+    const result = await runner.runJourney(journey, { wsEndpoint });
     expect(result).toEqual({
       status: 'failed',
       error,
@@ -221,6 +222,7 @@ describe('runner', () => {
     const journey = new Journey({ name }, noop);
     runner.addJourney(journey);
     const result = await runner.run({
+      wsEndpoint,
       outfd: fs.openSync(dest, 'w'),
     });
     expect(result).toEqual({
@@ -232,6 +234,7 @@ describe('runner', () => {
     runner.addJourney(new Journey({ name: 'j1' }, noop));
     runner.addJourney(new Journey({ name: 'j2' }, noop));
     const result = await runner.run({
+      wsEndpoint,
       outfd: fs.openSync(dest, 'w'),
       journeyName: 'j2',
     });
@@ -249,6 +252,7 @@ describe('runner', () => {
     });
     runner.addJourney(j2);
     const result = await runner.run({
+      wsEndpoint,
       outfd: fs.openSync(dest, 'w'),
     });
     expect(result).toEqual({
@@ -263,6 +267,7 @@ describe('runner', () => {
     let count = 0;
     runner.on('journey:register', () => count++);
     const result = await runner.run({
+      wsEndpoint,
       outfd: fs.openSync(dest, 'w'),
       dryRun: true,
     });
@@ -288,6 +293,7 @@ describe('runner', () => {
     runner.addJourney(j2);
 
     await runner.run({
+      wsEndpoint,
       outfd: fs.openSync(dest, 'w'),
     });
     expect(result).toEqual([
diff --git a/__tests__/plugins/browser-console.test.ts b/__tests__/plugins/browser-console.test.ts
index bd26be0..0abb670 100644
--- a/__tests__/plugins/browser-console.test.ts
+++ b/__tests__/plugins/browser-console.test.ts
@@ -25,7 +25,8 @@
 
 import { Gatherer } from '../../src/core/gatherer';
 import { BrowserConsole } from '../../src/plugins';
-import { Server } from '../../utils/server';
+import { Server } from '../utils/server';
+import { wsEndpoint } from '../utils/test-config';
 
 describe('BrowserConsole', () => {
   let server: Server;
@@ -37,7 +38,7 @@ describe('BrowserConsole', () => {
   });
 
   it('should capture browser console logs', async () => {
-    const driver = await Gatherer.setupDriver();
+    const driver = await Gatherer.setupDriver(true, wsEndpoint);
     const browserConsole = new BrowserConsole(driver.page);
     browserConsole.start();
     await driver.page.goto(server.TEST_PAGE);
diff --git a/__tests__/plugins/network.test.ts b/__tests__/plugins/network.test.ts
index 9f8a71e..4991817 100644
--- a/__tests__/plugins/network.test.ts
+++ b/__tests__/plugins/network.test.ts
@@ -25,7 +25,8 @@
 
 import { Gatherer } from '../../src/core/gatherer';
 import { NetworkManager } from '../../src/plugins/network';
-import { Server } from '../../utils/server';
+import { Server } from '../utils/server';
+import { wsEndpoint } from '../utils/test-config';
 
 describe('network', () => {
   let server: Server;
@@ -36,8 +37,8 @@ describe('network', () => {
     await server.close();
   });
 
-  it('capture network info', async () => {
-    const driver = await Gatherer.setupDriver();
+  it('should capture network info', async () => {
+    const driver = await Gatherer.setupDriver(true, wsEndpoint);
     const network = new NetworkManager();
     await network.start(driver.client);
     await driver.page.goto(server.TEST_PAGE);
@@ -53,7 +54,7 @@ describe('network', () => {
   });
 
   it('produce distinct events for redirects', async () => {
-    const driver = await Gatherer.setupDriver();
+    const driver = await Gatherer.setupDriver(true, wsEndpoint);
     const network = new NetworkManager();
     await network.start(driver.client);
     /**
diff --git a/__tests__/plugins/performance.test.ts b/__tests__/plugins/performance.test.ts
index c5d34ee..179854b 100644
--- a/__tests__/plugins/performance.test.ts
+++ b/__tests__/plugins/performance.test.ts
@@ -25,7 +25,8 @@
 
 import { Gatherer } from '../../src/core/gatherer';
 import { PerformanceManager } from '../../src/plugins/performance';
-import { Server } from '../../utils/server';
+import { Server } from '../utils/server';
+import { wsEndpoint } from '../utils/test-config';
 
 describe('performance', () => {
   let server: Server;
@@ -37,7 +38,7 @@ describe('performance', () => {
   });
 
   it('should capture page metrics', async () => {
-    const driver = await Gatherer.setupDriver();
+    const driver = await Gatherer.setupDriver(true, wsEndpoint);
     const performance = new PerformanceManager(driver.client);
     await performance.start();
     await driver.page.goto(server.TEST_PAGE);
diff --git a/__tests__/plugins/tracing.test.ts b/__tests__/plugins/tracing.test.ts
index dc70691..de50e7a 100644
--- a/__tests__/plugins/tracing.test.ts
+++ b/__tests__/plugins/tracing.test.ts
@@ -25,7 +25,8 @@
 
 import { Tracing, filterFilmstrips } from '../../src/plugins/tracing';
 import { Gatherer } from '../../src/core/gatherer';
-import { Server } from '../../utils/server';
+import { Server } from '../utils/server';
+import { wsEndpoint } from '../utils/test-config';
 
 describe('tracing', () => {
   let server: Server;
@@ -37,7 +38,7 @@ describe('tracing', () => {
   });
 
   it('should capture filmstrips', async () => {
-    const driver = await Gatherer.setupDriver();
+    const driver = await Gatherer.setupDriver(true, wsEndpoint);
     const tracer = new Tracing();
     await tracer.start(driver.client);
     await driver.page.goto(server.TEST_PAGE);
diff --git a/__tests__/utils/jest-global-setup.ts b/__tests__/utils/jest-global-setup.ts
new file mode 100644
index 0000000..6cae183
--- /dev/null
+++ b/__tests__/utils/jest-global-setup.ts
@@ -0,0 +1,51 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2020-present, Elastic NV
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+import { spawn } from 'child_process';
+import { wsEndpoint } from './test-config';
+
+module.exports = async () => {
+  if (wsEndpoint) {
+    return new Promise((resolve, reject) => {
+      console.log(`\nRunning BrowserService ${wsEndpoint}`);
+      const process = spawn('node', ['./dist/browser-service.js']);
+      (global as any).__browserServiceProcess = process;
+      process.stdout.on('data', data => {
+        if (data.indexOf('Listening on port: 9322') >= 0) {
+          console.log(`${data}`);
+          resolve(process);
+        }
+      });
+
+      process.stderr.on('data', data => {
+        const message = `BrowserService: ${data}`;
+        process.kill();
+        reject(message);
+      });
+    });
+  } else {
+    console.log(`\nRunning without BrowserService`);
+  }
+};
diff --git a/__tests__/utils/jest-global-teardown.ts b/__tests__/utils/jest-global-teardown.ts
new file mode 100644
index 0000000..0e8ece4
--- /dev/null
+++ b/__tests__/utils/jest-global-teardown.ts
@@ -0,0 +1,31 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2020-present, Elastic NV
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+module.exports = async () => {
+  const browserService = (global as any).__browserServiceProcess;
+  if (browserService) {
+    browserService.kill();
+  }
+};
diff --git a/utils/server.ts b/__tests__/utils/server.ts
similarity index 100%
rename from utils/server.ts
rename to __tests__/utils/server.ts
diff --git a/__tests__/utils/test-config.ts b/__tests__/utils/test-config.ts
new file mode 100644
index 0000000..ee2397d
--- /dev/null
+++ b/__tests__/utils/test-config.ts
@@ -0,0 +1,26 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2020-present, Elastic NV
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+export const wsEndpoint = process.env.WSENDPOINT;
diff --git a/examples/docker/pull-latest.sh b/examples/docker/pull-latest.sh
index efdf88c..d8654d0 100755
--- a/examples/docker/pull-latest.sh
+++ b/examples/docker/pull-latest.sh
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/usr/bin/env bash
 docker pull docker.elastic.co/experimental/synthetics:7.10.0-synthetics
 docker pull docker.elastic.co/experimental/synthetics:8.0.0-synthetics
 docker pull docker.elastic.co/observability-ci/heartbeat:7.10.0-synthetics