diff --git a/crowdsec-docs/docs/local_api/notification_plugins/elasticsearch.md b/crowdsec-docs/docs/local_api/notification_plugins/elasticsearch.mdx similarity index 98% rename from crowdsec-docs/docs/local_api/notification_plugins/elasticsearch.md rename to crowdsec-docs/docs/local_api/notification_plugins/elasticsearch.mdx index a0b691ed6..b4267bfee 100644 --- a/crowdsec-docs/docs/local_api/notification_plugins/elasticsearch.md +++ b/crowdsec-docs/docs/local_api/notification_plugins/elasticsearch.mdx @@ -3,6 +3,8 @@ id: elastic title: Elasticsearch --- +import TryTemplateButton from "@site/src/components/TryTemplateButton"; + CrowdSec can forward Alerts to Elasticsearch using the HTTP plugin. This guide will show you how to configure the plugin to send alerts to your Elasticsearch instance. ## Configuring the plugin @@ -38,6 +40,14 @@ headers: Content-Type: "application/json" ``` + + ### Authentication diff --git a/crowdsec-docs/docs/local_api/notification_plugins/email.md b/crowdsec-docs/docs/local_api/notification_plugins/email.mdx similarity index 90% rename from crowdsec-docs/docs/local_api/notification_plugins/email.md rename to crowdsec-docs/docs/local_api/notification_plugins/email.mdx index 2ff9cc0f1..9a07d0ae4 100644 --- a/crowdsec-docs/docs/local_api/notification_plugins/email.md +++ b/crowdsec-docs/docs/local_api/notification_plugins/email.mdx @@ -3,6 +3,8 @@ id: email title: Email Plugin --- +import TryTemplateButton from '@site/src/components/TryTemplateButton'; + The Email plugin is shipped by default with CrowdSec. The following guide shows how to configure, test and enable it. ## Configuring the plugin @@ -74,6 +76,17 @@ encryption_type: "ssltls" # ... ``` + +{{range . -}} + {{$alert := . -}} + {{range .Decisions -}} +

{{.Value}} will get {{.Type}} for next {{.Duration}} for triggering {{.Scenario}} on machine {{$alert.MachineID}}.

CrowdSec CTI

+ {{end -}} +{{end -}} +`} +/> + The `format` configuration directive is a [go template](https://pkg.go.dev/text/template), which receives a list of [Alert](https://pkg.go.dev/github.com/crowdsecurity/crowdsec@master/pkg/models#Alert) objects. Typical port and TLS/SSL settings diff --git a/crowdsec-docs/docs/local_api/notification_plugins/file.md b/crowdsec-docs/docs/local_api/notification_plugins/file.mdx similarity index 89% rename from crowdsec-docs/docs/local_api/notification_plugins/file.md rename to crowdsec-docs/docs/local_api/notification_plugins/file.mdx index cf09d1bd5..79501bbaa 100644 --- a/crowdsec-docs/docs/local_api/notification_plugins/file.md +++ b/crowdsec-docs/docs/local_api/notification_plugins/file.mdx @@ -3,6 +3,8 @@ id: file title: File Plugin --- +import TryTemplateButton from '@site/src/components/TryTemplateButton'; + The File plugin is by default shipped with your CrowdSec installation and allows you to write Alerts to an external file that can be monitored by external applications. The following guide shows how to configure, test and enable it. ## Configuring the plugin @@ -43,6 +45,12 @@ rotate: compress: true # Compress rotated files using gzip ``` + + **Note** that the `format` is a [go template](https://pkg.go.dev/text/template), which is fed a list of [Alert](https://pkg.go.dev/github.com/crowdsecurity/crowdsec@master/pkg/models#Alert) objects. :::warning @@ -65,6 +73,13 @@ format: | { "time": "{{.StopAt}}", "source": "crowdsec", "alert": {{. | toJson }} } {{ end -}} ``` + + + #### Wazuh Wazuh has set of reserved top level keys and may cause logs not to be sent by the agent. The following format can be used to be compatible with Wazuh: @@ -76,6 +91,12 @@ format: | {{ end -}} ``` + + ## Testing the plugin Before enabling the plugin it is best to test the configuration so the configuration is validated and you can see the output of the plugin. diff --git a/crowdsec-docs/docs/local_api/notification_plugins/gotify.md b/crowdsec-docs/docs/local_api/notification_plugins/gotify.mdx similarity index 87% rename from crowdsec-docs/docs/local_api/notification_plugins/gotify.md rename to crowdsec-docs/docs/local_api/notification_plugins/gotify.mdx index e35dd33ee..8afae5d39 100644 --- a/crowdsec-docs/docs/local_api/notification_plugins/gotify.md +++ b/crowdsec-docs/docs/local_api/notification_plugins/gotify.mdx @@ -3,6 +3,8 @@ id: gotify title: Gotify --- +import TryTemplateButton from "@site/src/components/TryTemplateButton" + CrowdSec can forward Alerts to Gotify via the HTTP plugin. This guide will show you how to configure the HTTP plugin to send alerts to your Gotify instance. ## Configuring the plugin @@ -64,7 +66,24 @@ headers: Content-Type: application/json # skip_tls_verification: # true or false. Default is false ``` - + ## Testing the plugin Before enabling the plugin it is best to test the configuration so the configuration is validated and you can see the output of the plugin. diff --git a/crowdsec-docs/docs/local_api/notification_plugins/helpers.mdx b/crowdsec-docs/docs/local_api/notification_plugins/helpers.mdx new file mode 100644 index 000000000..48dfbd8b6 --- /dev/null +++ b/crowdsec-docs/docs/local_api/notification_plugins/helpers.mdx @@ -0,0 +1,69 @@ +--- +id: template_helpers +title: Templating helpers +--- + +In order to simplify some operation in the templates, we provide some custom helpers. + +## Sprig + +The [Sprig](https://masterminds.github.io/sprig/) library is available in the templates, and provides a lot of useful functions. Refer to the sprig documentation for more information. + +## CrowdSec specific helpers + +### `HTMLEscape` + +:::info +When displaying untrusted data sources, such as metadata (for example, URIs), it is best to use this function to prevent the data from being rendered in notifications that support HTML format, such as emails. +::: + +The string is processed through the [`html.EscapeString`](https://pkg.go.dev/html#EscapeString) function, which converts special characters into their HTML-encoded equivalents. + +```go +{{ "I am not escaped" }} // I am not escaped +{{ "I am escaped" | HTMLEscape }} // I am <img src=x /> escaped +``` + +:::note +This function only escapes five specific characters: + +| Character | Escape Sequence | +|-----------|-----------------| +| `<` | `<` | +| `>` | `>` | +| `&` | `&` | +| `'` | `'` | +| `"` | `"` | + +It does not provide comprehensive sanitization. +::: + +### `Hostname` + +Returns the hostname of the machine running crowdsec. + +### `GetMeta(alert, key)` + +Return the list of meta values for the given key in the specified alert. + +```go +{{ range . }} +{{ $alert := .}} +{{ GetMeta $alert "username"}} +{{ end }} +``` + +### `CrowdsecCTI` + +Queries the crowdsec CTI API to get information about an IP based on the smoke database. + +Documentation on the available fields and methods is [here](https://pkg.go.dev/github.com/crowdsecurity/crowdsec/pkg/cticlient#SmokeItem). + +```go + {{range . -}} + {{$alert := . -}} + :flag-{{$alert.Source.Cn}}: triggered *{{$alert.Scenario}}* ({{$alert.Source.AsName}}) : Maliciousness Score is + {{- $cti := $alert.Source.IP | CrowdsecCTI -}} + {{" "}}{{mulf $cti.GetMaliciousnessScore 100 | floor}} % + {{- end }} +``` diff --git a/crowdsec-docs/docs/local_api/notification_plugins/http.md b/crowdsec-docs/docs/local_api/notification_plugins/http.mdx similarity index 96% rename from crowdsec-docs/docs/local_api/notification_plugins/http.md rename to crowdsec-docs/docs/local_api/notification_plugins/http.mdx index 075c6fca7..6af363b14 100644 --- a/crowdsec-docs/docs/local_api/notification_plugins/http.md +++ b/crowdsec-docs/docs/local_api/notification_plugins/http.mdx @@ -3,6 +3,8 @@ id: http title: HTTP Plugin --- +import TryTemplateButton from '@site/src/components/TryTemplateButton'; + The HTTP plugin is by default shipped with your CrowdSec installation. The following guide shows how to configure, test and enable it. Every alert which would pass the profile's filter would be dispatched to `http_default` plugin. @@ -53,6 +55,10 @@ method: POST # eg either of "POST", "GET", "PUT" and other http verbs is valid v ``` + + :::info `format` is a [go template](https://pkg.go.dev/text/template), which is fed a list of [Alert](https://pkg.go.dev/github.com/crowdsecurity/crowdsec@master/pkg/models#Alert) objects. ::: diff --git a/crowdsec-docs/docs/local_api/notification_plugins/sentinel.md b/crowdsec-docs/docs/local_api/notification_plugins/sentinel.mdx similarity index 96% rename from crowdsec-docs/docs/local_api/notification_plugins/sentinel.md rename to crowdsec-docs/docs/local_api/notification_plugins/sentinel.mdx index 159a387fd..4f7e73882 100644 --- a/crowdsec-docs/docs/local_api/notification_plugins/sentinel.md +++ b/crowdsec-docs/docs/local_api/notification_plugins/sentinel.mdx @@ -3,6 +3,8 @@ id: sentinel title: Sentinel Plugin --- +import TryTemplateButton from '@site/src/components/TryTemplateButton'; + The sentinel plugin is by default shipped with your CrowdSec installation. The following guide shows how to configure, test and enable it. ## Configuring the plugin @@ -47,6 +49,10 @@ log_type: crowdsec ``` + + **Note** that the `format` is a [go template](https://pkg.go.dev/text/template), which is fed a list of [Alert](https://pkg.go.dev/github.com/crowdsecurity/crowdsec@master/pkg/models#Alert) objects. ### Configuration options diff --git a/crowdsec-docs/docs/local_api/notification_plugins/slack.md b/crowdsec-docs/docs/local_api/notification_plugins/slack.mdx similarity index 83% rename from crowdsec-docs/docs/local_api/notification_plugins/slack.md rename to crowdsec-docs/docs/local_api/notification_plugins/slack.mdx index ec6f66ef5..6b306c66e 100644 --- a/crowdsec-docs/docs/local_api/notification_plugins/slack.md +++ b/crowdsec-docs/docs/local_api/notification_plugins/slack.mdx @@ -3,6 +3,8 @@ id: slack title: Slack Plugin --- +import TryTemplateButton from '@site/src/components/TryTemplateButton'; + The slack plugin is by default shipped with your CrowdSec installation. The following guide shows how to enable it. ## Configuring the plugin: @@ -39,6 +41,18 @@ webhook: https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxxxxxxxxxxx # Replace ``` + will get {{.Type}} for next {{.Duration}} for triggering {{.Scenario}} on machine '{{$alert.MachineID}}'. {{end}} +{{if not $alert.Source.Cn -}} +:pirate_flag: will get {{.Type}} for next {{.Duration}} for triggering {{.Scenario}} on machine '{{$alert.MachineID}}'. {{end}} +{{end -}} +{{end -}}`} +/> + **Don't forget to replace the webhook with your own webhook** See [slack guide](https://slack.com/intl/en-in/help/articles/115005265063-Incoming-webhooks-for-Slack) for instructions to obtain webhook. diff --git a/crowdsec-docs/docs/local_api/notification_plugins/splunk.md b/crowdsec-docs/docs/local_api/notification_plugins/splunk.mdx similarity index 96% rename from crowdsec-docs/docs/local_api/notification_plugins/splunk.md rename to crowdsec-docs/docs/local_api/notification_plugins/splunk.mdx index 2360c34c7..49db8043d 100644 --- a/crowdsec-docs/docs/local_api/notification_plugins/splunk.md +++ b/crowdsec-docs/docs/local_api/notification_plugins/splunk.mdx @@ -3,6 +3,8 @@ id: splunk title: Splunk Plugin --- +import TryTemplateButton from '@site/src/components/TryTemplateButton'; + The splunk plugin is by default shipped with your CrowdSec installation. The following guide shows how to enable it. ## Configuring the plugin: @@ -40,6 +42,9 @@ url: https://xxx.yyyy.splunkcloud.com:8088/services/collector # timeout: # duration to wait for response from plugin before considering this attempt a failure. eg "10s" ``` + See [splunk guide](https://docs.splunk.com/Documentation/Splunk/8.2.1/Data/UsetheHTTPEventCollector) for instructions to obtain the token and url. diff --git a/crowdsec-docs/docs/local_api/notification_plugins/teams.md b/crowdsec-docs/docs/local_api/notification_plugins/teams.mdx similarity index 62% rename from crowdsec-docs/docs/local_api/notification_plugins/teams.md rename to crowdsec-docs/docs/local_api/notification_plugins/teams.mdx index 6eaeacf4b..cb9e77d83 100644 --- a/crowdsec-docs/docs/local_api/notification_plugins/teams.md +++ b/crowdsec-docs/docs/local_api/notification_plugins/teams.mdx @@ -3,6 +3,8 @@ id: teams title: Microsoft Teams --- +import TryTemplateButton from '@site/src/components/TryTemplateButton'; + The following guide shows how to configure, test and enable HTTP plugin to forward Alerts to Microsoft Teams. ## Configuring the plugin @@ -145,6 +147,109 @@ headers: # timeout: # duration to wait for response from plugin before considering this attempt a failure. eg "10s" ``` + + ### Additional Alert Context If you have enabled [Alert Context](/u/user_guides/alert_context/) you can add additional fields to the alert, the following `format` loops over all context that is available within the Alert. So simply following the previous linked guide will be enough to enable these fields to show within the template. @@ -283,6 +388,117 @@ headers: # timeout: # duration to wait for response from plugin before considering this attempt a failure. eg "10s" ``` + + **Note** * Don't forget to replace the webhook with your own webhook diff --git a/crowdsec-docs/docs/local_api/notification_plugins/telegram.md b/crowdsec-docs/docs/local_api/notification_plugins/telegram.mdx similarity index 82% rename from crowdsec-docs/docs/local_api/notification_plugins/telegram.md rename to crowdsec-docs/docs/local_api/notification_plugins/telegram.mdx index 8548e3bbc..a3c86fe7f 100644 --- a/crowdsec-docs/docs/local_api/notification_plugins/telegram.md +++ b/crowdsec-docs/docs/local_api/notification_plugins/telegram.mdx @@ -3,6 +3,8 @@ id: telegram title: Telegram --- +import TryTemplateButton from '@site/src/components/TryTemplateButton'; + CrowdSec can forward Alerts to telegram via the HTTP plugin. This guide will show you how to configure the HTTP plugin to send alerts to your Telegram chat. ## Configuring the plugin @@ -77,6 +79,37 @@ headers: Content-Type: "application/json" ``` + + ## Testing the plugin Before enabling the plugin it is best to test the configuration so the configuration is validated and you can see the output of the plugin. diff --git a/crowdsec-docs/docusaurus.config.js b/crowdsec-docs/docusaurus.config.js index 6d6e29a05..e0c491c60 100644 --- a/crowdsec-docs/docusaurus.config.js +++ b/crowdsec-docs/docusaurus.config.js @@ -167,6 +167,16 @@ module.exports = { position: "left", label: "Console", }, + { + label: "Playground", + position: "left", + items: [ + { + label: "Notifications", + to: "/playground/notification", + }, + ], + }, { to: `https://academy.crowdsec.net/courses?${ process.env.NODE_ENV === "production" diff --git a/crowdsec-docs/package-lock.json b/crowdsec-docs/package-lock.json index 9e182b4ae..2024033c4 100644 --- a/crowdsec-docs/package-lock.json +++ b/crowdsec-docs/package-lock.json @@ -8,6 +8,8 @@ "name": "crowdsec-docs", "version": "0.0.0", "dependencies": { + "@codemirror/lang-go": "^6.0.1", + "@codemirror/lang-json": "^6.0.2", "@coreui/icons": "^3.0.1", "@coreui/icons-react": "2.3.0", "@docusaurus/core": "^3.8.1", @@ -23,6 +25,7 @@ "@mui/material": "^5.13.4", "@mui/x-date-pickers": "^6.18.0", "@radix-ui/react-tooltip": "^1.1.2", + "@uiw/react-codemirror": "^4.23.14", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "docusaurus-plugin-zooming": "^1.0.0", @@ -1973,6 +1976,122 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.6", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", + "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", + "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-go": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz", + "integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/go": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz", + "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", + "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", + "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.0.tgz", + "integrity": "sha512-yvSchUwHOdupXkd7xJ0ob36jdsSR/I+/C+VbY0ffBiL5NiSTEBDfB1ZGWbbIlDd5xgdUkody+lukAdOxYrOBeg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -4522,6 +4641,58 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "license": "MIT" }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "license": "MIT" + }, + "node_modules/@lezer/go": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz", + "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@mdx-js/mdx": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz", @@ -6854,6 +7025,59 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "license": "MIT" }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.23.14", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.14.tgz", + "integrity": "sha512-lCseubZqjN9bFwHJdQlZEKEo2yO1tCiMMVL0gu3ZXwhqMdfnd6ky/fUCYbn8aJkW+cXKVwjEVhpKjOphNiHoNw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.23.14", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.14.tgz", + "integrity": "sha512-/CmlSh8LGUEZCxg/f78MEkEMehKnVklqJvJlL10AXXrO/2xOyPqHb8SK10GhwOqd0kHhHgVYp4+6oK5S+UIEuQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.23.14", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -8112,6 +8336,21 @@ "node": ">=6" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/collapse-white-space": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", @@ -8487,6 +8726,12 @@ } } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -18829,6 +19074,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.16", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", @@ -19872,6 +20123,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/watchpack": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", diff --git a/crowdsec-docs/package.json b/crowdsec-docs/package.json index ec0bd2cad..dd5897b4a 100644 --- a/crowdsec-docs/package.json +++ b/crowdsec-docs/package.json @@ -14,6 +14,8 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { + "@codemirror/lang-go": "^6.0.1", + "@codemirror/lang-json": "^6.0.2", "@coreui/icons": "^3.0.1", "@coreui/icons-react": "2.3.0", "@docusaurus/core": "^3.8.1", @@ -29,6 +31,7 @@ "@mui/material": "^5.13.4", "@mui/x-date-pickers": "^6.18.0", "@radix-ui/react-tooltip": "^1.1.2", + "@uiw/react-codemirror": "^4.23.14", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "docusaurus-plugin-zooming": "^1.0.0", diff --git a/crowdsec-docs/src/components/Playground.js b/crowdsec-docs/src/components/Playground.js new file mode 100644 index 000000000..8d36e33d5 --- /dev/null +++ b/crowdsec-docs/src/components/Playground.js @@ -0,0 +1,130 @@ +import { useState, useEffect } from 'react'; +import "@site/static/wasm/wasm_exec.js" + +// Global state to manage WASM loading across all component instances +let wasmState = { + isLoaded: false, + isLoading: false, + instance: null, + error: null, + subscribers: new Set() +}; + +// Custom hook to manage WASM loading +function useWasm() { + const [isLoading, setIsLoading] = useState(!wasmState.isLoaded && !wasmState.error); + const [error, setError] = useState(wasmState.error); + + useEffect(() => { + // Subscribe to state changes + const updateState = () => { + setIsLoading(wasmState.isLoading); + setError(wasmState.error); + }; + + wasmState.subscribers.add(updateState); + + // If WASM is already loaded, update state immediately + if (wasmState.isLoaded) { + setIsLoading(false); + setError(null); + } else if (wasmState.error) { + setIsLoading(false); + setError(wasmState.error); + } else if (!wasmState.isLoading) { + // Start loading if not already in progress + loadWasm(); + } + + // Cleanup subscription on unmount + return () => { + wasmState.subscribers.delete(updateState); + }; + }, []); + + return { isLoading, error, isLoaded: wasmState.isLoaded }; +} + +// Function to load WASM (called only once) +async function loadWasm() { + if (wasmState.isLoading || wasmState.isLoaded) { + return; + } + + wasmState.isLoading = true; + notifySubscribers(); + + try { + const go = new window.Go(); + const result = await WebAssembly.instantiateStreaming( + fetch("/wasm/main.wasm"), + go.importObject + ); + + go.run(result.instance); + window.grokInit(); + + wasmState.isLoaded = true; + wasmState.isLoading = false; + wasmState.instance = result.instance; + wasmState.error = null; + } catch (err) { + wasmState.isLoading = false; + wasmState.error = err; + console.error('Failed to load WASM:', err); + } + + notifySubscribers(); +} + +// Notify all subscribers of state changes +function notifySubscribers() { + wasmState.subscribers.forEach(callback => callback()); +} + +export default function Playground({ + children, + component: Component, + componentProps = {}, + title = "Playground", + loadingTitle = "Loading...", + subtitle = "" +}) { + const { isLoading, error, isLoaded } = useWasm(); + + if (error) { + return ( +
+

Error loading playground

+

Failed to load WASM: {error.message}

+
+ ); + } + + if (isLoading) { + return ( +
+

{loadingTitle}

+
+ ); + } + + return ( +
+

{title}

+

+ {subtitle} +

+ {isLoaded && ( +
+ {/* Render either a specific component or children */} + {Component ? ( + + ) : ( + children ||

WASM loaded successfully! Ready to use.

+ )} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/crowdsec-docs/src/components/TryTemplateButton.js b/crowdsec-docs/src/components/TryTemplateButton.js new file mode 100644 index 000000000..9c9c1c1c0 --- /dev/null +++ b/crowdsec-docs/src/components/TryTemplateButton.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { useHistory } from '@docusaurus/router'; + +export default function TryTemplateButton({ + template, + children = "Try in Playground", + className = "", + variant = "primary" +}) { + const history = useHistory(); + + const handleClick = () => { + try { + // Base64 encode the template + const encodedTemplate = btoa(template); + + // Navigate to the playground page with the template query parameter + const playgroundUrl = `/playground/notification?template=${encodeURIComponent(encodedTemplate)}`; + history.push(playgroundUrl); + } catch (err) { + console.error('Failed to encode template or navigate:', err); + // Fallback: navigate without template + history.push('/playground/notification'); + } + }; + + // Define button styles based on variant + const getButtonClasses = () => { + const baseClasses = "tw-cursor-pointer tw-my-2 tw-inline-flex tw-items-center tw-px-4 tw-py-2 tw-text-sm tw-font-medium tw-rounded-md tw-transition-colors tw-duration-200 focus:tw-outline-none focus:tw-ring-2 focus:tw-ring-offset-2"; + + const variantClasses = { + primary: "tw-bg-blue-600 hover:tw-bg-blue-700 tw-text-white focus:tw-ring-blue-500", + secondary: "tw-bg-gray-200 hover:tw-bg-gray-300 tw-text-gray-900 focus:tw-ring-gray-500 dark:tw-bg-gray-700 dark:hover:tw-bg-gray-600 dark:tw-text-white", + outline: "tw-border tw-border-blue-600 tw-text-blue-600 hover:tw-bg-blue-50 focus:tw-ring-blue-500 dark:tw-border-blue-400 dark:tw-text-blue-400 dark:hover:tw-bg-blue-900/20" + }; + + return `${baseClasses} ${variantClasses[variant] || variantClasses.primary} ${className}`; + }; + + return ( + + ); +} \ No newline at end of file diff --git a/crowdsec-docs/src/pages/playground/notification.js b/crowdsec-docs/src/pages/playground/notification.js new file mode 100644 index 000000000..60cd4bb46 --- /dev/null +++ b/crowdsec-docs/src/pages/playground/notification.js @@ -0,0 +1,365 @@ +import Playground from "@site/src/components/Playground"; +import CodeMirror, { EditorView } from "@uiw/react-codemirror"; +import { useState, useEffect, useRef } from "react"; +import { json } from "@codemirror/lang-json"; +import { go } from "@codemirror/lang-go"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@site/src/ui/tooltip"; +import Layout from '@theme/Layout'; + +function Notifications() { + // Default test alert based on CrowdSec example + const defaultAlert = [{ + "capacity": 0, + "decisions": [{ + "duration": "4h", + "scope": "Ip", + "value": "10.10.10.10", + "type": "ban", + "scenario": "test alert", + "origin": "cscli" + }], + "events": [], + "events_count": 1, + "leakspeed": "0", + "message": "test alert", + "scenario_hash": "", + "scenario": "test alert", + "scenario_version": "", + "simulated": false, + "source": { + "as_name": "", + "as_number": "", + "cn": "", + "ip": "10.10.10.10", + "range": "", + "scope": "Ip", + "value": "10.10.10.10" + }, + "start_at": new Date().toISOString(), + "stop_at": new Date().toISOString(), + "created_at": new Date().toISOString() + }]; + + const [alert, setAlert] = useState(JSON.stringify(defaultAlert, null, 2)); + + const defaultTemplate = `{{range . -}} +Subject: CrowdSec Alert - {{.Message}} + +Alert Details: +- Scenario: {{.Scenario}} +- Source IP: {{.Source.Value}} +- Country: {{.Source.Cn}} +- Events Count: {{.EventsCount}} +- Time Range: {{.StartAt}} to {{.StopAt}} + +This alert was triggered by suspicious activity from {{.Source.Value}}. +{{end -}}`; + + const [template, setTemplate] = useState(defaultTemplate); + + const [output, setOutput] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const timeoutRef = useRef(null); + + // Check for template query parameter on component mount + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const templateParam = urlParams.get('template'); + + if (templateParam) { + try { + // Base64 decode the template + const decodedTemplate = atob(templateParam); + setTemplate(decodedTemplate); + } catch (err) { + console.error('Failed to decode template from query parameter:', err); + // Keep the default template if decoding fails + } + } + }, []); + + // Handle alert input changes and auto-wrap single objects in arrays + const handleAlertChange = (value) => { + try { + // Try to parse the JSON + const parsed = JSON.parse(value); + + // If it's an object (not array), wrap it in an array + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + const wrappedValue = JSON.stringify([parsed], null, 2); + setAlert(wrappedValue); + } else { + // If it's already an array or other valid JSON, use as is + setAlert(value); + } + } catch (err) { + // If it's not valid JSON, just set the value as is + // This allows users to type/edit without constant parsing errors + setAlert(value); + } + }; + + const formatAlert = () => { + // Clear any existing timeout + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + setLoading(true); + setError(""); + setOutput(""); + + // Add a delay for better UX + timeoutRef.current = setTimeout(() => { + try { + // Call WASM function + if (window.formatAlert) { + const result = window.formatAlert(alert, template); + + // Handle WASM function result structure + if (result && result.error) { + setError(`WASM Error: ${result.error}`); + setOutput(""); + } else if (result && result.out) { + setOutput(String(result.out)); + setError(""); + } else { + setError("Unexpected result format from WASM function"); + setOutput(""); + } + } else { + setError("WASM function formatAlert not available"); + setOutput(""); + } + } catch (err) { + setError(`Error: ${err.message}`); + setOutput(""); + } finally { + setLoading(false); + } + }, 800); // 800ms delay for better UX + }; + + // Auto-format when inputs change + useEffect(() => { + if (alert && template) { + formatAlert(); + } + + // Cleanup timeout on unmount + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, [alert, template]); + + // Loading spinner component + const LoadingSpinner = () => ( +
+

Generating notification...

+
+ ); + + return ( +
+ {/* Main Layout with 3 columns */} +
+ {/* Left Column - Inputs */} +
+ {/* Alert JSON Input */} +
+
+
+ + + + +
+ ? +
+
+ +
+

How to get your own alert:

+
+

• Use cscli alerts list to find an alert ID

+

• Use cscli alerts inspect <alert_id> -o json to get the alert details in JSON

+

• Pipe the output to jq -c to compact the output

+

• Paste the alert details - it will be auto-wrapped in an array

+
+
+
+
+
+
+ + {alert.split('\n').length} lines + +
+
+ +
+
+ + {/* Template Input */} +
+
+
+ + + + +
+ ? +
+
+ + + +
+
+
+
+ + {template.split('\n').length} lines + + + +
+
+
+ +
+
+
+ + {/* Middle Column - Arrow */} +
+
+ + + +
+
+ + {/* Right Column - Output */} +
+
+ +
+ + {loading ? ( +
+ +
+ ) : error ? ( +
+

{error}

+
+ ) : ( +
+ +
+ )} +
+
+
+ ); +} + +export default function PlaygroundNotifications() { + return ( + + + + ); +} \ No newline at end of file diff --git a/crowdsec-docs/static/wasm/main.wasm b/crowdsec-docs/static/wasm/main.wasm new file mode 100755 index 000000000..c8752832e Binary files /dev/null and b/crowdsec-docs/static/wasm/main.wasm differ diff --git a/crowdsec-docs/static/wasm/wasm_exec.js b/crowdsec-docs/static/wasm/wasm_exec.js new file mode 100644 index 000000000..d71af9e97 --- /dev/null +++ b/crowdsec-docs/static/wasm/wasm_exec.js @@ -0,0 +1,575 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +"use strict"; + +(() => { + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!globalThis.fs) { + let outputBuf = ""; + globalThis.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substring(0, nl)); + outputBuf = outputBuf.substring(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + chmod(path, mode, callback) { callback(enosys()); }, + chown(path, uid, gid, callback) { callback(enosys()); }, + close(fd, callback) { callback(enosys()); }, + fchmod(fd, mode, callback) { callback(enosys()); }, + fchown(fd, uid, gid, callback) { callback(enosys()); }, + fstat(fd, callback) { callback(enosys()); }, + fsync(fd, callback) { callback(null); }, + ftruncate(fd, length, callback) { callback(enosys()); }, + lchown(path, uid, gid, callback) { callback(enosys()); }, + link(path, link, callback) { callback(enosys()); }, + lstat(path, callback) { callback(enosys()); }, + mkdir(path, perm, callback) { callback(enosys()); }, + open(path, flags, mode, callback) { callback(enosys()); }, + read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, + readdir(path, callback) { callback(enosys()); }, + readlink(path, callback) { callback(enosys()); }, + rename(from, to, callback) { callback(enosys()); }, + rmdir(path, callback) { callback(enosys()); }, + stat(path, callback) { callback(enosys()); }, + symlink(path, link, callback) { callback(enosys()); }, + truncate(path, length, callback) { callback(enosys()); }, + unlink(path, callback) { callback(enosys()); }, + utimes(path, atime, mtime, callback) { callback(enosys()); }, + }; + } + + if (!globalThis.process) { + globalThis.process = { + getuid() { return -1; }, + getgid() { return -1; }, + geteuid() { return -1; }, + getegid() { return -1; }, + getgroups() { throw enosys(); }, + pid: -1, + ppid: -1, + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } + } + + if (!globalThis.path) { + globalThis.path = { + resolve(...pathSegments) { + return pathSegments.join("/"); + } + } + } + + if (!globalThis.crypto) { + throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); + } + + if (!globalThis.performance) { + throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); + } + + if (!globalThis.TextEncoder) { + throw new Error("globalThis.TextEncoder is not available, polyfill required"); + } + + if (!globalThis.TextDecoder) { + throw new Error("globalThis.TextDecoder is not available, polyfill required"); + } + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + + globalThis.Go = class { + constructor() { + this.argv = ["js"]; + this.env = {}; + this.exit = (code) => { + if (code !== 0) { + console.warn("exit code:", code); + } + }; + this._exitPromise = new Promise((resolve) => { + this._resolveExitPromise = resolve; + }); + this._pendingEvent = null; + this._scheduledTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const setInt64 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); + } + + const setInt32 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + } + + const getInt64 = (addr) => { + const low = this.mem.getUint32(addr + 0, true); + const high = this.mem.getInt32(addr + 4, true); + return low + high * 4294967296; + } + + const loadValue = (addr) => { + const f = this.mem.getFloat64(addr, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = this.mem.getUint32(addr, true); + return this._values[id]; + } + + const storeValue = (addr, v) => { + const nanHead = 0x7FF80000; + + if (typeof v === "number" && v !== 0) { + if (isNaN(v)) { + this.mem.setUint32(addr + 4, nanHead, true); + this.mem.setUint32(addr, 0, true); + return; + } + this.mem.setFloat64(addr, v, true); + return; + } + + if (v === undefined) { + this.mem.setFloat64(addr, 0, true); + return; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = this._values.length; + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 0; + switch (typeof v) { + case "object": + if (v !== null) { + typeFlag = 1; + } + break; + case "string": + typeFlag = 2; + break; + case "symbol": + typeFlag = 3; + break; + case "function": + typeFlag = 4; + break; + } + this.mem.setUint32(addr + 4, nanHead | typeFlag, true); + this.mem.setUint32(addr, id, true); + } + + const loadSlice = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + return new Uint8Array(this._inst.exports.mem.buffer, array, len); + } + + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (addr) => { + const saddr = getInt64(addr + 0); + const len = getInt64(addr + 8); + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); + } + + const testCallExport = (a, b) => { + this._inst.exports.testExport0(); + return this._inst.exports.testExport(a, b); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + _gotest: { + add: (a, b) => a + b, + callExport: testCallExport, + }, + gojs: { + // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) + // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported + // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). + // This changes the SP, thus we have to update the SP used by the imported function. + + // func wasmExit(code int32) + "runtime.wasmExit": (sp) => { + sp >>>= 0; + const code = this.mem.getInt32(sp + 8, true); + this.exited = true; + delete this._inst; + delete this._values; + delete this._goRefCounts; + delete this._ids; + delete this._idPool; + this.exit(code); + }, + + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) + "runtime.wasmWrite": (sp) => { + sp >>>= 0; + const fd = getInt64(sp + 8); + const p = getInt64(sp + 16); + const n = this.mem.getInt32(sp + 24, true); + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); + }, + + // func resetMemoryDataView() + "runtime.resetMemoryDataView": (sp) => { + sp >>>= 0; + this.mem = new DataView(this._inst.exports.mem.buffer); + }, + + // func nanotime1() int64 + "runtime.nanotime1": (sp) => { + sp >>>= 0; + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); + }, + + // func walltime() (sec int64, nsec int32) + "runtime.walltime": (sp) => { + sp >>>= 0; + const msec = (new Date).getTime(); + setInt64(sp + 8, msec / 1000); + this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); + }, + + // func scheduleTimeoutEvent(delay int64) int32 + "runtime.scheduleTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this._nextCallbackTimeoutID; + this._nextCallbackTimeoutID++; + this._scheduledTimeouts.set(id, setTimeout( + () => { + this._resume(); + while (this._scheduledTimeouts.has(id)) { + // for some reason Go failed to register the timeout event, log and try again + // (temporary workaround for https://github.com/golang/go/issues/28975) + console.warn("scheduleTimeoutEvent: missed timeout event"); + this._resume(); + } + }, + getInt64(sp + 8), + )); + this.mem.setInt32(sp + 16, id, true); + }, + + // func clearTimeoutEvent(id int32) + "runtime.clearTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this.mem.getInt32(sp + 8, true); + clearTimeout(this._scheduledTimeouts.get(id)); + this._scheduledTimeouts.delete(id); + }, + + // func getRandomData(r []byte) + "runtime.getRandomData": (sp) => { + sp >>>= 0; + crypto.getRandomValues(loadSlice(sp + 8)); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (sp) => { + sp >>>= 0; + const id = this.mem.getUint32(sp + 8, true); + this._goRefCounts[id]--; + if (this._goRefCounts[id] === 0) { + const v = this._values[id]; + this._values[id] = null; + this._ids.delete(v); + this._idPool.push(id); + } + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (sp) => { + sp >>>= 0; + storeValue(sp + 24, loadString(sp + 8)); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (sp) => { + sp >>>= 0; + const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 32, result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (sp) => { + sp >>>= 0; + Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (sp) => { + sp >>>= 0; + storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const m = Reflect.get(v, loadString(sp + 16)); + const args = loadSliceOfValues(sp + 32); + const result = Reflect.apply(m, v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, result); + this.mem.setUint8(sp + 64, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, err); + this.mem.setUint8(sp + 64, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.apply(v, undefined, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.construct(v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (sp) => { + sp >>>= 0; + setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (sp) => { + sp >>>= 0; + const str = encoder.encode(String(loadValue(sp + 8))); + storeValue(sp + 16, str); + setInt64(sp + 24, str.length); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (sp) => { + sp >>>= 0; + const str = loadValue(sp + 8); + loadSlice(sp + 16).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (sp) => { + sp >>>= 0; + this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (sp) => { + sp >>>= 0; + const dst = loadSlice(sp + 8); + const src = loadValue(sp + 32); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + // func copyBytesToJS(dst ref, src []byte) (int, bool) + "syscall/js.copyBytesToJS": (sp) => { + sp >>>= 0; + const dst = loadValue(sp + 8); + const src = loadSlice(sp + 16); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + "debug": (value) => { + console.log(value); + }, + } + }; + } + + async run(instance) { + if (!(instance instanceof WebAssembly.Instance)) { + throw new Error("Go.run: WebAssembly.Instance expected"); + } + this._inst = instance; + this.mem = new DataView(this._inst.exports.mem.buffer); + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + globalThis, + this, + ]; + this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map([ // mapping from JS values to reference ids + [0, 1], + [null, 2], + [true, 3], + [false, 4], + [globalThis, 5], + [this, 6], + ]); + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. + let offset = 4096; + + const strPtr = (str) => { + const ptr = offset; + const bytes = encoder.encode(str + "\0"); + new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); + offset += bytes.length; + if (offset % 8 !== 0) { + offset += 8 - (offset % 8); + } + return ptr; + }; + + const argc = this.argv.length; + + const argvPtrs = []; + this.argv.forEach((arg) => { + argvPtrs.push(strPtr(arg)); + }); + argvPtrs.push(0); + + const keys = Object.keys(this.env).sort(); + keys.forEach((key) => { + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); + }); + argvPtrs.push(0); + + const argv = offset; + argvPtrs.forEach((ptr) => { + this.mem.setUint32(offset, ptr, true); + this.mem.setUint32(offset + 4, 0, true); + offset += 8; + }); + + // The linker guarantees global data starts from at least wasmMinDataAddr. + // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. + const wasmMinDataAddr = 4096 + 8192; + if (offset >= wasmMinDataAddr) { + throw new Error("total length of command line and environment variables exceeds limit"); + } + + this._inst.exports.run(argc, argv); + if (this.exited) { + this._resolveExitPromise(); + } + await this._exitPromise; + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } +})();