[9.0] [Security solution][AI assistant] bug: encode security labs content to prevent antivirus false positive (#221656) (#222951)

# Backport

This will backport the following commits from `main` to `9.0`:
- [[Security solution][AI assistant] bug: encode security labs content
to prevent antivirus false positive
(#221656)](https://github.com/elastic/kibana/pull/221656)

<!--- Backport version: 10.0.0 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Kenneth
Kreindler","email":"42113355+KDKHD@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-06-06T09:16:48Z","message":"[Security
solution][AI assistant] bug: encode security labs content to prevent
antivirus false positive (#221656)\n\n## Summary\n\nFixes:
https://github.com/elastic/kibana/issues/202114\n\nSummarize your PR. If
it involves visual changes include a screenshot or\ngif.\n\nThis PR
addresses the
following\n[issue](https://github.com/elastic/kibana/issues/202114).\n\n####
Problem \nThe security labs' content is triggering false positive
anti-virus\nalerts from [ESET
cyber\nsecurity](https://www.eset.com/uk/home/cyber-security/?srsltid=AfmBOorLSTn6FfQXm9h4rm2nDpma91Q9-IfHmeUg4TuRL4TvMF9xB-Cc).\nThis
is because the content contains specific words and YARA rules that\nthe
antivirus picks up as malware. The content is not dangerous.\n\n####
Solution \nEncrypt the content so that the Yara rules do not trigger
alerts. The\ncontent is encrypted with AES-256-ECB and the key
`ELASTIC`. The\nencryption is not secure and does not need to be secure
(we just want to\nobfuscate the content).\n\n#### Considerations\n- An
alternative approach to fixing this issue that was considered
was\nbuilding an integration so that the content is only imported
after\nKibana has been started. As a team, we decided against this
because it\nis convenient for airgapped systems to have the secure labs
content\nshipped with the installation.\n- It would be great to test
this fix against many antivirus providers,\nhowever, I haven't found a
tool that lets me do this quickly.\n\n## How to test\n#### Verify the
security labs content is encoded\n- Check out the PR\n- Run the
following\n```bash\nyarn kbn bootstrap\nyarn build\n```\n- The build may
not fully complete locally; however, you should still be\nable to see
the build files at `/build`. Inspect the content of the\nfollowing
folder:\n`build/kibana/node_modules/@kbn/elastic-assistant-plugin/server/knowledge_base/security_labs`\nand
verify that only files ending with `.encoded.md` exist. The content\nof
these files should not be
human-readable.\n\n\n![image](https://github.com/user-attachments/assets/f8b4977d-2962-4cc5-a737-53cedd7b1dad)\n\n####
Verify the security labs content is encoded in the
cloud/serverless\nbuild (optional)\n- Log into docker.elastic.co in your
terminal. Do this by heading to\nhttps://docker-auth.elastic.co/ (more
info\n[here](https://elasticprod.service-now.com/esc?id=kb_article&table=kb_knowledge&sys_id=e8d361c747abb910ffad4438946d439a&recordUrl=kb_view.do%3Fsysparm_article%3DKB0012946))\n-
Use the latest CI run to get the serverless docker image and start
an\ninteractive shell with it:\n```bash\ndocker run -it --rm
docker.elastic.co/kibana-ci/kibana-serverless:pr-221656-ae41a481bbfc
sh\n```\n- Inspect the contents of the following directory and verify
that only\n`.encoded.md` files exist.\n```bash\ncd
node_modules/@kbn/elastic-assistant-plugin/server/knowledge_base/security_labs/\nls\n```\n-
Repeat the last 2 steps for the cloud deployment image (also found
in\nthe CI)\n\n<img width=\"1233\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/5d308537-b0a9-4bd0-a449-6ddae1ca1bfb\"\n/>\n\n####
Verify security labs content can be installed\n- Start Kibana locally\n-
Head over
to\n`http://localhost:5601/app/management/kibana/securityAiAssistantManagement?tab=knowledge_base`\nand
install the knowledge base.\n\n<img width=\"1454\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/e8c4b557-ea45-4c58-96ff-aacce47e9982\"\n/>\n\n-
Check that the security labs content is being installed\n\n<img
width=\"1456\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/e7c2b128-ee94-436e-b4e0-1d48cb9d19cb\"\n/>\n\n-
Once the security labs content is installed, go to the security
AI\nassistant and ask the following: `Which malware is mentioned in
the\nsecurity labs content. Include citations`\n- Verify the assistant
gives a proper response and the citation links to\nthe Elastic Security
Labs page.\n\n<img width=\"859\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/e89e523c-3054-4b8b-a3c3-2857cb1ed8cd\"\n/>\n\n####
Verify antivirus does not trigger (optional)\n- Download and install
ESET Cyber security trial
from\n[here](https://www.eset.com/uk/home/cyber-security/?srsltid=AfmBOorLSTn6FfQXm9h4rm2nDpma91Q9-IfHmeUg4TuRL4TvMF9xB-Cc)\n-
Open up the ESET\n- Click on `custom scan` and run the scan on the
folder
`build/kibana`\n\n\n![image](https://github.com/user-attachments/assets/4404f7cd-cc42-4476-bc05-24a1b67e80de)\n\n-
Expect no alerts to be
triggered\n\n\n![image](https://github.com/user-attachments/assets/45631134-512a-4a4d-ad4b-0de8074aa5aa)\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [X] Any text
added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n-
[X]\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\nwas
added for features that require explanation or tutorials\n- [X] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [X] If a plugin
configuration key changed, check if it needs to be\nallowlisted in the
cloud and added to the
[docker\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\n-
[X] This was checked for breaking HTTP API changes, and any
breaking\nchanges have been approved by the breaking-change committee.
The\n`release_note:breaking` label should be applied in these
situations.\n- [X] [Flaky
Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\nused on any tests changed\n- [X] The PR description includes the
appropriate Release Notes section,\nand the correct `release_note:*`
label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n\n###
Identify risks\n\nDoes this PR introduce any risks? For example,
consider risks like hard\nto test bugs, performance regression,
potential of data loss.\n\nDescribe the risk, its severity, and
mitigation for each identified\nrisk. Invite stakeholders and evaluate
how to proceed before merging.\n\n- [ ] [See some
risk\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)\n-
[ ] ...\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\nCo-authored-by: Garrett Spong
<garrett.spong@elastic.co>\nCo-authored-by: Garrett Spong
<spong@users.noreply.github.com>","sha":"973c8f30a69fa785a149cc50747d99c1db4ce7b6","branchLabelMapping":{"^v9.1.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","ci:cloud-deploy","ci:project-deploy-security","Team:Security
Generative
AI","backport:version","v9.1.0","v9.0.3","v8.18.3","v8.17.8"],"title":"[Security
solution][AI assistant] bug: encode security labs content to prevent
antivirus false
positive","number":221656,"url":"https://github.com/elastic/kibana/pull/221656","mergeCommit":{"message":"[Security
solution][AI assistant] bug: encode security labs content to prevent
antivirus false positive (#221656)\n\n## Summary\n\nFixes:
https://github.com/elastic/kibana/issues/202114\n\nSummarize your PR. If
it involves visual changes include a screenshot or\ngif.\n\nThis PR
addresses the
following\n[issue](https://github.com/elastic/kibana/issues/202114).\n\n####
Problem \nThe security labs' content is triggering false positive
anti-virus\nalerts from [ESET
cyber\nsecurity](https://www.eset.com/uk/home/cyber-security/?srsltid=AfmBOorLSTn6FfQXm9h4rm2nDpma91Q9-IfHmeUg4TuRL4TvMF9xB-Cc).\nThis
is because the content contains specific words and YARA rules that\nthe
antivirus picks up as malware. The content is not dangerous.\n\n####
Solution \nEncrypt the content so that the Yara rules do not trigger
alerts. The\ncontent is encrypted with AES-256-ECB and the key
`ELASTIC`. The\nencryption is not secure and does not need to be secure
(we just want to\nobfuscate the content).\n\n#### Considerations\n- An
alternative approach to fixing this issue that was considered
was\nbuilding an integration so that the content is only imported
after\nKibana has been started. As a team, we decided against this
because it\nis convenient for airgapped systems to have the secure labs
content\nshipped with the installation.\n- It would be great to test
this fix against many antivirus providers,\nhowever, I haven't found a
tool that lets me do this quickly.\n\n## How to test\n#### Verify the
security labs content is encoded\n- Check out the PR\n- Run the
following\n```bash\nyarn kbn bootstrap\nyarn build\n```\n- The build may
not fully complete locally; however, you should still be\nable to see
the build files at `/build`. Inspect the content of the\nfollowing
folder:\n`build/kibana/node_modules/@kbn/elastic-assistant-plugin/server/knowledge_base/security_labs`\nand
verify that only files ending with `.encoded.md` exist. The content\nof
these files should not be
human-readable.\n\n\n![image](https://github.com/user-attachments/assets/f8b4977d-2962-4cc5-a737-53cedd7b1dad)\n\n####
Verify the security labs content is encoded in the
cloud/serverless\nbuild (optional)\n- Log into docker.elastic.co in your
terminal. Do this by heading to\nhttps://docker-auth.elastic.co/ (more
info\n[here](https://elasticprod.service-now.com/esc?id=kb_article&table=kb_knowledge&sys_id=e8d361c747abb910ffad4438946d439a&recordUrl=kb_view.do%3Fsysparm_article%3DKB0012946))\n-
Use the latest CI run to get the serverless docker image and start
an\ninteractive shell with it:\n```bash\ndocker run -it --rm
docker.elastic.co/kibana-ci/kibana-serverless:pr-221656-ae41a481bbfc
sh\n```\n- Inspect the contents of the following directory and verify
that only\n`.encoded.md` files exist.\n```bash\ncd
node_modules/@kbn/elastic-assistant-plugin/server/knowledge_base/security_labs/\nls\n```\n-
Repeat the last 2 steps for the cloud deployment image (also found
in\nthe CI)\n\n<img width=\"1233\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/5d308537-b0a9-4bd0-a449-6ddae1ca1bfb\"\n/>\n\n####
Verify security labs content can be installed\n- Start Kibana locally\n-
Head over
to\n`http://localhost:5601/app/management/kibana/securityAiAssistantManagement?tab=knowledge_base`\nand
install the knowledge base.\n\n<img width=\"1454\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/e8c4b557-ea45-4c58-96ff-aacce47e9982\"\n/>\n\n-
Check that the security labs content is being installed\n\n<img
width=\"1456\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/e7c2b128-ee94-436e-b4e0-1d48cb9d19cb\"\n/>\n\n-
Once the security labs content is installed, go to the security
AI\nassistant and ask the following: `Which malware is mentioned in
the\nsecurity labs content. Include citations`\n- Verify the assistant
gives a proper response and the citation links to\nthe Elastic Security
Labs page.\n\n<img width=\"859\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/e89e523c-3054-4b8b-a3c3-2857cb1ed8cd\"\n/>\n\n####
Verify antivirus does not trigger (optional)\n- Download and install
ESET Cyber security trial
from\n[here](https://www.eset.com/uk/home/cyber-security/?srsltid=AfmBOorLSTn6FfQXm9h4rm2nDpma91Q9-IfHmeUg4TuRL4TvMF9xB-Cc)\n-
Open up the ESET\n- Click on `custom scan` and run the scan on the
folder
`build/kibana`\n\n\n![image](https://github.com/user-attachments/assets/4404f7cd-cc42-4476-bc05-24a1b67e80de)\n\n-
Expect no alerts to be
triggered\n\n\n![image](https://github.com/user-attachments/assets/45631134-512a-4a4d-ad4b-0de8074aa5aa)\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [X] Any text
added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n-
[X]\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\nwas
added for features that require explanation or tutorials\n- [X] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [X] If a plugin
configuration key changed, check if it needs to be\nallowlisted in the
cloud and added to the
[docker\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\n-
[X] This was checked for breaking HTTP API changes, and any
breaking\nchanges have been approved by the breaking-change committee.
The\n`release_note:breaking` label should be applied in these
situations.\n- [X] [Flaky
Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\nused on any tests changed\n- [X] The PR description includes the
appropriate Release Notes section,\nand the correct `release_note:*`
label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n\n###
Identify risks\n\nDoes this PR introduce any risks? For example,
consider risks like hard\nto test bugs, performance regression,
potential of data loss.\n\nDescribe the risk, its severity, and
mitigation for each identified\nrisk. Invite stakeholders and evaluate
how to proceed before merging.\n\n- [ ] [See some
risk\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)\n-
[ ] ...\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\nCo-authored-by: Garrett Spong
<garrett.spong@elastic.co>\nCo-authored-by: Garrett Spong
<spong@users.noreply.github.com>","sha":"973c8f30a69fa785a149cc50747d99c1db4ce7b6"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.18","8.17"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/221656","number":221656,"mergeCommit":{"message":"[Security
solution][AI assistant] bug: encode security labs content to prevent
antivirus false positive (#221656)\n\n## Summary\n\nFixes:
https://github.com/elastic/kibana/issues/202114\n\nSummarize your PR. If
it involves visual changes include a screenshot or\ngif.\n\nThis PR
addresses the
following\n[issue](https://github.com/elastic/kibana/issues/202114).\n\n####
Problem \nThe security labs' content is triggering false positive
anti-virus\nalerts from [ESET
cyber\nsecurity](https://www.eset.com/uk/home/cyber-security/?srsltid=AfmBOorLSTn6FfQXm9h4rm2nDpma91Q9-IfHmeUg4TuRL4TvMF9xB-Cc).\nThis
is because the content contains specific words and YARA rules that\nthe
antivirus picks up as malware. The content is not dangerous.\n\n####
Solution \nEncrypt the content so that the Yara rules do not trigger
alerts. The\ncontent is encrypted with AES-256-ECB and the key
`ELASTIC`. The\nencryption is not secure and does not need to be secure
(we just want to\nobfuscate the content).\n\n#### Considerations\n- An
alternative approach to fixing this issue that was considered
was\nbuilding an integration so that the content is only imported
after\nKibana has been started. As a team, we decided against this
because it\nis convenient for airgapped systems to have the secure labs
content\nshipped with the installation.\n- It would be great to test
this fix against many antivirus providers,\nhowever, I haven't found a
tool that lets me do this quickly.\n\n## How to test\n#### Verify the
security labs content is encoded\n- Check out the PR\n- Run the
following\n```bash\nyarn kbn bootstrap\nyarn build\n```\n- The build may
not fully complete locally; however, you should still be\nable to see
the build files at `/build`. Inspect the content of the\nfollowing
folder:\n`build/kibana/node_modules/@kbn/elastic-assistant-plugin/server/knowledge_base/security_labs`\nand
verify that only files ending with `.encoded.md` exist. The content\nof
these files should not be
human-readable.\n\n\n![image](https://github.com/user-attachments/assets/f8b4977d-2962-4cc5-a737-53cedd7b1dad)\n\n####
Verify the security labs content is encoded in the
cloud/serverless\nbuild (optional)\n- Log into docker.elastic.co in your
terminal. Do this by heading to\nhttps://docker-auth.elastic.co/ (more
info\n[here](https://elasticprod.service-now.com/esc?id=kb_article&table=kb_knowledge&sys_id=e8d361c747abb910ffad4438946d439a&recordUrl=kb_view.do%3Fsysparm_article%3DKB0012946))\n-
Use the latest CI run to get the serverless docker image and start
an\ninteractive shell with it:\n```bash\ndocker run -it --rm
docker.elastic.co/kibana-ci/kibana-serverless:pr-221656-ae41a481bbfc
sh\n```\n- Inspect the contents of the following directory and verify
that only\n`.encoded.md` files exist.\n```bash\ncd
node_modules/@kbn/elastic-assistant-plugin/server/knowledge_base/security_labs/\nls\n```\n-
Repeat the last 2 steps for the cloud deployment image (also found
in\nthe CI)\n\n<img width=\"1233\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/5d308537-b0a9-4bd0-a449-6ddae1ca1bfb\"\n/>\n\n####
Verify security labs content can be installed\n- Start Kibana locally\n-
Head over
to\n`http://localhost:5601/app/management/kibana/securityAiAssistantManagement?tab=knowledge_base`\nand
install the knowledge base.\n\n<img width=\"1454\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/e8c4b557-ea45-4c58-96ff-aacce47e9982\"\n/>\n\n-
Check that the security labs content is being installed\n\n<img
width=\"1456\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/e7c2b128-ee94-436e-b4e0-1d48cb9d19cb\"\n/>\n\n-
Once the security labs content is installed, go to the security
AI\nassistant and ask the following: `Which malware is mentioned in
the\nsecurity labs content. Include citations`\n- Verify the assistant
gives a proper response and the citation links to\nthe Elastic Security
Labs page.\n\n<img width=\"859\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/e89e523c-3054-4b8b-a3c3-2857cb1ed8cd\"\n/>\n\n####
Verify antivirus does not trigger (optional)\n- Download and install
ESET Cyber security trial
from\n[here](https://www.eset.com/uk/home/cyber-security/?srsltid=AfmBOorLSTn6FfQXm9h4rm2nDpma91Q9-IfHmeUg4TuRL4TvMF9xB-Cc)\n-
Open up the ESET\n- Click on `custom scan` and run the scan on the
folder
`build/kibana`\n\n\n![image](https://github.com/user-attachments/assets/4404f7cd-cc42-4476-bc05-24a1b67e80de)\n\n-
Expect no alerts to be
triggered\n\n\n![image](https://github.com/user-attachments/assets/45631134-512a-4a4d-ad4b-0de8074aa5aa)\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [X] Any text
added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n-
[X]\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\nwas
added for features that require explanation or tutorials\n- [X] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [X] If a plugin
configuration key changed, check if it needs to be\nallowlisted in the
cloud and added to the
[docker\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\n-
[X] This was checked for breaking HTTP API changes, and any
breaking\nchanges have been approved by the breaking-change committee.
The\n`release_note:breaking` label should be applied in these
situations.\n- [X] [Flaky
Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\nused on any tests changed\n- [X] The PR description includes the
appropriate Release Notes section,\nand the correct `release_note:*`
label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n\n###
Identify risks\n\nDoes this PR introduce any risks? For example,
consider risks like hard\nto test bugs, performance regression,
potential of data loss.\n\nDescribe the risk, its severity, and
mitigation for each identified\nrisk. Invite stakeholders and evaluate
how to proceed before merging.\n\n- [ ] [See some
risk\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)\n-
[ ] ...\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\nCo-authored-by: Garrett Spong
<garrett.spong@elastic.co>\nCo-authored-by: Garrett Spong
<spong@users.noreply.github.com>","sha":"973c8f30a69fa785a149cc50747d99c1db4ce7b6"}},{"branch":"9.0","label":"v9.0.3","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.3","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.17","label":"v8.17.8","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Garrett Spong <garrett.spong@elastic.co>
Co-authored-by: Garrett Spong <spong@users.noreply.github.com>
This commit is contained in:
Kenneth Kreindler 2025-06-06 17:54:49 +01:00 committed by GitHub
parent 865fa67b21
commit d5fd19058f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
191 changed files with 670 additions and 7 deletions

1
.github/CODEOWNERS vendored
View file

@ -974,6 +974,7 @@ x-pack/solutions/search/plugins/search_playground @elastic/search-kibana
x-pack/solutions/search/plugins/search_solution/search_navigation @elastic/search-kibana
x-pack/solutions/search/plugins/search_synonyms @elastic/search-kibana
x-pack/solutions/search/plugins/serverless_search @elastic/search-kibana
x-pack/solutions/security/packages/ai-security-labs-content @elastic/security-generative-ai
x-pack/solutions/security/packages/connectors @elastic/security-threat-hunting-explore
x-pack/solutions/security/packages/data-stream-adapter @elastic/security-threat-hunting
x-pack/solutions/security/packages/data-table @elastic/security-threat-hunting-investigations

View file

@ -170,6 +170,7 @@
"@kbn/ai-assistant-cta": "link:x-pack/platform/packages/shared/ai-assistant/ai-assistant-cta",
"@kbn/ai-assistant-icon": "link:x-pack/platform/packages/shared/ai-assistant/icon",
"@kbn/ai-assistant-management-plugin": "link:src/platform/plugins/shared/ai_assistant_management/selection",
"@kbn/ai-security-labs-content": "link:x-pack/solutions/security/packages/ai-security-labs-content",
"@kbn/aiops-change-point-detection": "link:x-pack/platform/packages/private/ml/aiops_change_point_detection",
"@kbn/aiops-common": "link:x-pack/platform/packages/shared/ml/aiops_common",
"@kbn/aiops-components": "link:x-pack/platform/packages/private/ml/aiops_components",

View file

@ -24,6 +24,8 @@
"@kbn/ai-assistant-icon/*": ["x-pack/platform/packages/shared/ai-assistant/icon/*"],
"@kbn/ai-assistant-management-plugin": ["src/platform/plugins/shared/ai_assistant_management/selection"],
"@kbn/ai-assistant-management-plugin/*": ["src/platform/plugins/shared/ai_assistant_management/selection/*"],
"@kbn/ai-security-labs-content": ["x-pack/solutions/security/packages/ai-security-labs-content"],
"@kbn/ai-security-labs-content/*": ["x-pack/solutions/security/packages/ai-security-labs-content/*"],
"@kbn/aiops-change-point-detection": ["x-pack/platform/packages/private/ml/aiops_change_point_detection"],
"@kbn/aiops-change-point-detection/*": ["x-pack/platform/packages/private/ml/aiops_change_point_detection/*"],
"@kbn/aiops-common": ["x-pack/platform/packages/shared/ml/aiops_common"],

View file

@ -0,0 +1,11 @@
# @kbn/ai-security-labs-content
Utility library for AI Security Labs Content.
### Usages
#### Encoding Security Labs Content
This package provides utilities to encode security labs content. Security labs content needs to be encoded in order to prevent the content from triggering false positive security alerts (see this [issue](https://github.com/elastic/kibana/issues/202114)).
For more information visit the [knowledge base readme](../../../../../x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/README.md)

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { encryptSecurityLabsContent, decryptSecurityLabsContent } from './src/utils';
/**
* Micromatch pattern for plain text markdown files in the security labs content.
*/
export const PLAIN_TEXT_FILE_MICROMATCH_PATTERN = ['*.md', '!*.encoded.md'];
/**
* Micromatch pattern for encoded markdown files in the security labs content.
*/
export const ENCODED_FILE_MICROMATCH_PATTERN = ['*.encoded.md'];

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../../..',
roots: ['<rootDir>/x-pack/solutions/security/packages/ai-security-labs-content'],
};

View file

@ -0,0 +1,7 @@
{
"type": "shared-server",
"id": "@kbn/ai-security-labs-content",
"owner": "@elastic/security-generative-ai",
"group": "security",
"visibility": "private"
}

View file

@ -0,0 +1,7 @@
{
"name": "@kbn/ai-security-labs-content",
"version": "1.0.0",
"description": "Utility library for AI security labs content",
"license": "Elastic License 2.0",
"private": true
}

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
// eslint-disable-next-line import/no-nodejs-modules
import crypto from 'crypto';
const key = crypto.createHash('sha256').update('ELASTIC').digest();
const keyUint8 = key as unknown as Uint8Array;
/**
* Unsafe encryption function for security labs content.
* @param text security labs content to encrypt
* @returns Encrypted content as a hex string.
*/
export function encryptSecurityLabsContent(text: string): string {
const cipher = crypto.createCipheriv('aes-256-ecb', keyUint8, null);
return cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
}
/**
* Decryption function for security labs content.
* @param encrypted Encrypted security labs content as a hex string.
* @returns Decrypted content as a string.
*/
export function decryptSecurityLabsContent(encrypted: string): string {
const decipher = crypto.createDecipheriv('aes-256-ecb', keyUint8, null);
return decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
}

View file

@ -0,0 +1,17 @@
{
"extends": "../../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
]
},
"include": ["**/*.ts"],
"kbn_references": [
],
"exclude": [
"target/**/*"
],
}

View file

@ -27,5 +27,12 @@
"spaces",
"security"
]
},
"build": {
"extraExcludes": [
// Only include the encoded versions in the build due to https://github.com/elastic/kibana/issues/202114
"**/knowledge_base/security_labs/*.md",
"!**/knowledge_base/security_labs/*.encoded.md"
]
}
}

View file

@ -6,6 +6,7 @@
"license": "Elastic License 2.0",
"scripts": {
"evaluate-model": "node ./scripts/model_evaluator",
"draw-graph": "node ./scripts/draw_graph"
"draw-graph": "node ./scripts/draw_graph",
"encode-security-labs-content": "node ./scripts/encode_security_labs_content"
}
}

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
require('../../../../../../src/setup_node_env');
require('./encode_security_labs_content_script').encodeSecurityLabsContent();

View file

@ -0,0 +1,81 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/* eslint-disable no-console */
import * as fs from 'fs/promises';
import * as path from 'path';
import globby from 'globby';
import { existsSync } from 'fs';
import {
ENCODED_FILE_MICROMATCH_PATTERN,
PLAIN_TEXT_FILE_MICROMATCH_PATTERN,
encryptSecurityLabsContent,
} from '@kbn/ai-security-labs-content';
// Path to the security labs markdown files
export const SECURITY_LABS_DIR = path.resolve(__dirname, '../server/knowledge_base/security_labs');
export const deleteFilesByPattern = async ({
directoryPath,
pattern,
}: {
directoryPath: string;
pattern: string | string[];
}): Promise<void> => {
try {
console.log(`Deleting files matching pattern "${pattern}" in directory "${directoryPath}"`);
const files = await globby(pattern, { cwd: directoryPath });
if (files.length === 0) {
console.log(`No files found matching pattern "${pattern}" in directory "${directoryPath}".`);
return;
}
for (const file of files) {
const filePath = path.join(directoryPath, file);
await fs.unlink(filePath);
console.log(`Deleted file: "${filePath}"`);
}
} catch (error) {
console.error(`Error deleting files: ${error.message}`);
throw error;
}
};
const encodedFile = async (fileName: string) => {
const filePath = path.join(SECURITY_LABS_DIR, fileName);
const content = await fs.readFile(filePath, 'utf-8');
const encodedContent = encryptSecurityLabsContent(content);
const fileNameWithoutExtension = path.basename(fileName, path.extname(fileName));
const outputFilePath = path.join(SECURITY_LABS_DIR, `${fileNameWithoutExtension}.encoded.md`);
await fs.writeFile(outputFilePath, encodedContent, 'utf-8');
console.log(`Encoded file: ${fileName} -> ${outputFilePath}`);
return outputFilePath;
};
/**
* Main function to encode all markdown files in the security labs directory
*/
export const encodeSecurityLabsContent = async () => {
console.log('Encoding files from:', SECURITY_LABS_DIR);
if (!existsSync(SECURITY_LABS_DIR)) {
throw new Error(`Input directory does not exist: ${SECURITY_LABS_DIR}`);
}
await deleteFilesByPattern({
directoryPath: SECURITY_LABS_DIR,
pattern: ENCODED_FILE_MICROMATCH_PATTERN,
});
const files = await globby(PLAIN_TEXT_FILE_MICROMATCH_PATTERN, {
cwd: SECURITY_LABS_DIR,
});
files.forEach((file) => {
encodedFile(file);
});
};

View file

@ -12,6 +12,35 @@ The assets are stored in their original source format, so `.asciidoc` for docume
NOTE: When adding knowledge base assets, please ensure that the source files and directories are not excluded as part of the Kibana build process, otherwise things will work fine locally, but will fail once a distribution has been built (i.e. cloud deployments). See `src/dev/build/tasks/copy_legacy_source_task.ts` for details on exclusion patterns.
#### Adding new security labs content
When adding new security labs content ensure that the content follows the same structure as the existing documents. The header of the files must be valid yaml format as described bellow and must be fenced by `---`.
```
---
title: <string>
slug: <string>
date: <ISO 8601 date format>
description: <string>
author: <list of slugs>
image: <string>
category: <list of slugs>
---
<content string>
```
The file name should be the article title in snakecase e.g. vulnerability_summary_follina.md
After adding new articles run the following to create the encoded article:
```bash
cd x-pack/solutions/security/plugins/elastic_assistant
yarn encode-security-labs-content
```
Files are encoded due to this [issue](https://github.com/elastic/kibana/issues/202114).
### Future
Once asset format and chunking strategies are finalized, we may want to either move the assets to a shared package so they can be consumed by other plugins, or potentially ship the pre-packaged ELSER embeddings as part of a Fleet Integration. For now though, the assets will be included in their source format within the plugin, and can then be processed and embedded at runtime.

View file

@ -0,0 +1,144 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import fs from 'fs';
import path from 'path';
import yaml from 'js-yaml';
import globby from 'globby';
import {
decryptSecurityLabsContent,
ENCODED_FILE_MICROMATCH_PATTERN,
PLAIN_TEXT_FILE_MICROMATCH_PATTERN,
} from '@kbn/ai-security-labs-content';
const isoDateRegex = /^\d{4}-\d{2}-\d{2}$/;
describe('Security labs content', () => {
const directoryPath = path.join(__dirname, 'security_labs');
const plainTextFiles: string[] = globby.sync(PLAIN_TEXT_FILE_MICROMATCH_PATTERN, {
cwd: directoryPath,
});
const encodedFiles: string[] = globby.sync(ENCODED_FILE_MICROMATCH_PATTERN, {
cwd: directoryPath,
});
describe('plaintext security labs content format', () => {
plainTextFiles.forEach((file) => {
it(`should have non-empty string content in file: ${file}`, () => {
const filePath = path.join(directoryPath, file);
const content = fs.readFileSync(filePath, 'utf-8');
expect(typeof content).toBe('string');
expect(content.trim().length).toBeGreaterThan(0);
});
});
plainTextFiles.forEach((file) => {
it(`should have title and slug defined in yaml header: ${file}`, () => {
const filePath = path.join(directoryPath, file);
const content = fs.readFileSync(filePath, 'utf-8');
const split = content.split('---');
const yamlString = split[1];
const article = split[2];
const parsed = yaml.load(yamlString) as {
slug: string;
title: string;
date: string;
description: string;
author: Array<{ slug: string }>;
image: string;
category: Array<{ slug: string }>;
};
const slug = parsed.slug;
const title = parsed.title;
const date = parsed.date;
const description = parsed.description;
const author = parsed.author;
const image = parsed.image;
const category = parsed.category;
expect(slug).toBeDefined();
expect(title).toBeDefined();
expect(typeof slug).toBe('string');
expect(typeof title).toBe('string');
expect(slug.trim().length).toBeGreaterThan(0);
expect(title.trim().length).toBeGreaterThan(0);
expect(date).toBeDefined();
expect(typeof date).toBe('string');
expect(date).toMatch(isoDateRegex);
expect(description).toBeDefined();
expect(author).toBeDefined();
expect(Array.isArray(author)).toBe(true);
expect(image).toBeDefined();
expect(typeof image).toBe('string');
expect(image.trim().length).toBeGreaterThan(0);
expect(category).toBeDefined();
expect(article).toBeDefined();
expect(typeof article).toBe('string');
});
});
plainTextFiles.forEach((file) => {
it(`article should come after yaml definition: ${file}`, () => {
const filePath = path.join(directoryPath, file);
const content = fs.readFileSync(filePath, 'utf-8');
const split = content.split('---');
const article = split[2];
expect(article).toBeDefined();
expect(typeof article).toBe('string');
expect(article.trim().length).toBeGreaterThan(0);
});
});
});
describe('encoded security labs content', () => {
plainTextFiles.forEach((file) => {
it(`corresponding encoded file exists for ${file}`, () => {
/**
* If this test fails, you probably forgot to run `yarn encode-security-labs-content` in x-pack/solutions/security/plugins/elastic_assistant/package.json
*/
const encodedFileName = `${path.basename(file, path.extname(file))}.encoded.md`;
const encodedFilePath = path.join(directoryPath, encodedFileName);
// check if encoded file exists
expect(encodedFiles).toContain(encodedFileName);
expect(fs.existsSync(encodedFilePath)).toBe(true);
});
});
encodedFiles.forEach((file) => {
it(`corresponding plaintext file exists for ${file}`, () => {
const plaintextFileName = `${path.basename(file, path.extname(file))}.md`;
const plaintextFilePath = path.join(directoryPath, plaintextFileName);
// check if plaintext file exists
expect(encodedFiles).toContain(plaintextFileName);
expect(fs.existsSync(plaintextFilePath)).toBe(true);
});
});
plainTextFiles.forEach((file) => {
it(`file ${file} was encoded correctly`, () => {
const plainTextFilePath = path.join(directoryPath, file);
const plainTextContent = fs.readFileSync(plainTextFilePath, 'utf-8');
const encodedFileName = `${path.basename(file, path.extname(file))}.encoded.md`;
const encodedFilePath = path.join(directoryPath, encodedFileName);
// read encoded content
const encodedContent = fs.readFileSync(encodedFilePath, 'utf-8');
const decodedContent = decryptSecurityLabsContent(encodedContent);
expect(decodedContent).toBe(plainTextContent);
});
});
});
});

View file

@ -0,0 +1 @@
59ffcbaaef80ffbe0e1c9c481b585ca55631d35a955880b5231e7c3bc07e02ae0873d267646a38903e854f9c80c187e0585e46ecd3912c29cfbc6d7925af371cee99ad5ab57bada92802903be5a89ac7109e1d4772260ce06f2a6548a951832c610bcbb26f2084c281f745d15a72b68f3344b357a71493c580ce449f744d4ac56837d65fb24155b2d89c39243ad2d888ff181e0294aebc6e29f0d2f8068d30320c1f1ba56eba545775de13b129b0c375657301249abe7072a22eb407b668bfd9faae339add53612c0e1ba714bac28ee41fed2259273a62ed4265088c9615f4b3bc28c00afd68e7b3d20e2510f6be7b185a82b267d5d818f83b2dc1fcdd81e4b50dd90bd1410d187773b8d614f52596814d73fe08552935423135238a4ffda2eff6cc0c6b6155a604083c0286b8a2d1ff452f98ef4b031c15b44ad66576a0f4f8f781105b3d240227a19b67428410badfdffb530fdf0d1c560a17b70961f037d5c7d1b882ad50897d9078ffdf59321b69174dd5548097a29c84f23a9808fb71b12391a547fd69b5fbfc7badd51be41ba11bfe9f39e911673fd0c6d1eb82d77cb093b5877b41259c6980b1cdf42eabb81452df43a18565a073d69c979ab537c8c5cfb5cc059a239b1f1814982241d2c369603677580c29871d2ff39cfc6f93a3ab43f47667477f325dac18f6cedb3af00944d4304b08802f876b02f0c894abbfcb20f9888e967b8c904bd191ae0f288d490d21830d84ec010bf322e63e7f37036e48aada8f7f15bcc7941cd8bc3ac39bf129cc9634b711822fee50d3cc4fd4355ec59ba21405912685243a657be48366ca1997665bfd21e411f4f04c98be855550b94955e7e3b44df5da856310fbdb2cbf4e6e844cc8e3c6dd5a5b7cbd82a222227b66de87f17003f85e454a47a269a537f420b67cb0c46da3df00b9c7fbd7418da5a9ccfb9b2cf6eb4b6666375211fa17cf67fef251686448c6152e7d9842e622749dd107303a8b6b4b256a73fbf06d8f079f37cbea3dcbb5566c9264fddb3a016bbd2b044a6c29984efc9bcfc8555670174cc8a18dbef11a9e3dc1f9596c6ff878cd391be183d5a184e2ccafcbe7c22f7e07fd43327d7a778e911ce316094dad2de6ac2153cf14efab7475912f3031af5df943ff3397942f7a68f71c8805fec9cbe71bfe8635476b965f4c172aae916e4f1464c7638979e40968e0b9090dce646d2cd85bfb38c6f405dde4c17f4c0a57dc6e50052f4379b7f9a3eeb5000592ab8a6eaf5cc83aa654f403fa2d59ef6057cefcf54e653658f931b8a52dc1b47c3ad2f57dc9825d7dce3e8dae2c4fb371915e189589bf355acd34f3f36941469c21ad99974eddc1ed22d7fe85bd99fff84c2700321825b4d54cbc57da7dbd3a5ffb

View file

@ -0,0 +1 @@
358211cdc77501b1a001254d456886d788535930d0cdc730bb42ced23eaef7270d4c1da15a72c44dd91e7c82fed57b144ba427ff2b6f9b8a854870d678fbf51a5ec5f6d1bcfc36a4f2db501037caacd2276a7de086a86f9691eec6966bb0d04f85cbf3b0fb83475e2ee52adc5c2caab46e1a817a1e40ef7b67bb50819e9aff602dfcdecc86545addae01cd7f4103334ce29f417df00a408b1de9a0f87c3f106471a0c765133799dd86be49ac1bc145c367738384ffeb95583331e6e8891df33bd325aa23f36afd81feb5e3db780223c5bebd10fa36eeae18badd6301eb50cb07f4122496ee452ecb817183fdfe3ab4ff4cad08981dad320ff240d6f6e49466359ef39261c555b03e87d80b3832cf97fca1edb2c87f358b4a53ca9c2abb0d9fb0d7f5264b8c4b6e39cb301c8d9e8c2284b9358022b3508080c08597d46294297a0b722cf7ce05c163feb11a0971f524a9608dfc8759a0a5a1bfea439fafc838fd5943132c02fe98fa1a7085ced8b50e80ea41486459cd53e5ce42a3d2616f5371628d0d034d9e126bfec654e7bef78a3480698b82981e33a3108eebcf7ff33b92d8966fb640bae5002d98938b30361eb3f970e3b1780ab4d11a515943150833f0547f5905697756cbe1fc71c05ae8db880e27dce9afd70be0799fe86bea687bcece2f6e90fa9fb5c90c75c58a52430fe6a09129af71f45be974954547c15abde4bf607ef5d9ee623fa5ef5cc2a082b08358c5d590c21b9aa2161ff1a852153a55dd7f8756ba225419eb9da0b3275b8edafd404d37e7eb3094dd6d80e4583e61844d45dab0236e66a43cc3697d72d2854b11b85b0b5e77297a3d6b8221b569a3875e3fc40de245de5d99e1c8acc363e11b6e74944e58042d3bb69662193b37b5630e9ca7dae5fd812d6e1e1ea0078249f1c24422ed5a4ac285e5e2313bcde2f4324471140e3f01c7fbbb501850d0a27b13109c9e441fa3e4e62c7fd7d32a954df2d627ec96a8703ab49a2cdfa3278c8b81d505010f17a6b68309bca30d9199e27ce3eba84f9a67c5367adabf938a7bb1fb0b731e93adddb12c81c94e2b834f75b23954450e64100b379f543755cf9c634249f07c5c1287cfe991c22a66db577430baa2eed82a2541bebed5d5e9d8d9a32a5229e1f067b4822e4c002d7e2dabcf4ea61d4538dad459f924eacfef8f1ff2dd2fd7a38e95fcb01aff292f71454af38d7de8e19d388bda85e70ce51d994c1ca3b95611b9b391bcf18f74e816704bbb653c9181388c87604430859714e7f0058df37cfa24e2c1827677e0938af9c23a155b8f6b08884738ad95803cc0d29c8413e375749008bc6b65db8becd55b772cf39794d13684a2802a07620bbcd6ecdf602e2729d364fcb351b8167726748c3f76c374527d3a6a6904fdeb18fa0236dfee8e43997ac40e162732866b045ba35443d6778e0c0e65bb95a280989b3e8adf1bc909ea0eca0886e4274bfc3d07475048e6f10aa305ad093122a5dcaefde3e4d02c5b3e2a7f6a0be1dfaea66b8f1ee36910827078a150c8c1598aa54af0493776725a4aee541291671e537ea0982be8a61f933073748a6f98887c97d5994795a1b1e05e7ec5b98267590ec9cd458f515c8e1f4698cc0c40a264c682ae74b75b41afdef9c25849db3df4b9695ff06f7af17a2838b09597bfe28acaed443b54dd34acc9d0bcdae0c582894e3b38034f407e951b7e6afed2d9a06c8402d886e4e5fa8e5778094882fc119e2693c64855ddd8699b052c61a1fe16c0a69799b2f64b8ebb5530bd79b454f8dc466a0fd7561ef4ebce3094d32c3f5b074c698f8f7a0dcc1d3a1fcbbb57108c060fc8ef623c4f3bd49884d5793ada57c92fb5c6ffe4105ff113176711f8ae7fffe1387b0a9e78f338a92e3beca806ed978fd68f0de51f91e92e5de69fd99f70d8b06f3185fcc67cacdf96a4afb8fb8280223d93677553170c8aef23170ee663da7e9e8fe67998a656e99e18b258dd55dbb5d4d10724fe51a9a983866318ee3a36d7348ce1a7faf8f35499bb781c58875c8f2ce9dd764cba67fe757a873f625fc3e265b12e7753c35f7c507df0cc458c7ed51bb9d54314535ca57b55b5c8b2cdf465f7145e6d08d48950b29cb4faf166515b9e285779eb03bee1ee75bcc9cabc7e283c080c12835127f574987e9d860c818c5ddd4e032683c2d7948658fdb91f6c46cfdc977f2b2364a2542ca0a845571455dd64b4fff9283709ee968219e99151f614270270a4583834c029d85dce308a2bfa10ab5544a74ec5553c32712f9fbceab890094ecf833834c029d85dce308a2bfa10ab5544a75101ccf9715e524331ad3f4df401c6193c3cae667804e4c4eb4e793d276f3fa6bb976f70e1478db768802f1d9459b486b1dc15a77a7d09b6329548cd0f150180cb56dece31eac9f2eea0c0cd3d4b634e6f3d9a628718571a232e6186bdc9181dac710f8c6570befef677d6bf326f7ed1d6c21e491e15a63753d501c62fdf4d46555f90fc6bb442cdf8682961f3c17da992d16fb1acb15723059ac87f054e5f6aceb5315230f28fbffa2d24792dfadcd1110e8294bc61f0a526a058cf3c2a8c66aacd3cf4b51f5cd847fd5e1f65c40eb2be826d6431ef9931da3e9aa4f09c1a320f782d48eea75274b0e4ee4995e9d20489dd87d3c34192d5039f174f8d206bec968a151a8c96d7ff23667f3f6f9cbdccb7433038ff9a3c07266d6238974521a0d0e4b498e180ff126ce564a2dd60d15768c86c87a67ac8ab599fcff079c9862acd34c9a2d1836327a5400f30de40b77add582b7a23652739a7898a056c35eba463479cd1078dc07c3e2aa670d727757c102ab290d372bec01520cfe6d5125d5a595dff30fdd833a85def525170c9ab6e1000cf6172451c50ee871aa9da21ee94762667ffb14af384a675c9b0012290b4aa891f20c22a248a46245c4e6981b07541473c7c897d1a8cd668ad1391104e4a3443c3f46d257d882d9967691316d2285135b10d94db8a1f2fcc5319e050d4111202573809988887b46112c2370e8a6ff76972f8e36de07aa19203f8ed813f8ff86b8257b050c68e5ab2bb0680d50f98a2d16250368b9868e9741b654d3192a409d08b719b7d0b02d0d1ccb3f72fba01271888ef9d91a4ea140e1d2ab7b2c1f35e9bdb81e3ed00e051d7de07db4507e8982ccf1a52bb18be9cf7faf54199a138733f65a3cc2863d2284c60b315dc980943a1ee53a36e3b61f23a1c6618e325b91bbebe22e873f802520871ec7ad3f8696c89e20048e22e66701e36861b03ab2995a5bc5e54e3014f3cc2c5cc57e0b45e8886d3b16bd8a95b80c1d6abd41d2c666609457875bc8356d8ef40c91e66d8ea6ae7cc9214c6ff6f2b2fe0234bd36c834ed16b516ea6267399e9977b41ecf0df58e50a0cf0c18bdc6ab8d5f33baa79f33403b2b65633d5b2e778d9223b2116e3d93fa56b50b739f6c2c6d013fddf4d2915236b4abd5ca6deaeb0ae5ea17233bc

View file

@ -0,0 +1 @@
858a029c9dc0acf038d4deab632b5fd5354a3119a8d3d6c22e0e34a9c611a80349f2a0ede136c8e502fbfba65eef66bda23a060b6254886705f3a50091145bf67042131a519012ee6dfeedf3e9d2e0c7c92d0a3cbbfbecb3598669738e0f6445d6b4a6d5555850689fbc94eaf18ef17adf1bebeb74ab64670d31c2c4ced69e77a1f6777f897cf22c2b0b92cf1872c3865b1a1919c74d8eebcbcef73fcd4666914f4cc763f892aceb9702cff3c5b80bd8fe9cbe8753f99e45c7e37093b793b9929a998c0e218f554f4a8c7ebf7579b08ec7d1b882ad50897d9078ffdf59321b697290844ebe59b84a05d4af22cd755572d7f2d87c60b3331e39317102807961fae4ce1c4e1115aa43f1c206224b1d5c1092c270fea56038e181e5df66d06a6568c732750d50616fe06bbcd8129d0e67d0977436497f97a371d3f726cd86e474ea070bfddbec4d874930810509160f50c4114659ffda8b38117af8ce49c6ff8d9c125b042952bda8be4e9d173b7aab24398aee7280f0532914cb4e61d9a790df29eb18b5b93ae79c20af0f1a36d7190e16bfe2493ade035d73d18232e83a645a3b3bac0d60223b0243cfcf185ee2859da1662ff24b1e15f4d2ee85696d21b2dfa3b3a9860508eda4860f736f5f735be42300dfea60d3fd43b089dc5526f0de29c846871c6e17dfe51b6ec1075712bb37cb8d7dbd6031a6a9388de4b98d5540340138f6b986f7ea1a9d16dd890084522042481d82577f7d4ee776a11547c73d9c8bfaf5a014015188b01a5fc4d56745a2c76b67dc254e91e820ec45b3c713efea85d46490625399a80e663e04bc6dfcc5a2ab78c3ed2facbe6c565e4fe6e78981cb20c83c21b50ec2cad8eea9568162e8adf37cfa24e2c1827677e0938af9c23a155143edad8ff021d68a8b3b100b3d2c0f6f47dc75aff189b33ab940bcbd3a1e097521ff03ad7ed61a61bbcd387424a1f5314b3b44be0307f35e29ebbd716c82b28c77f94a362a1b288e8d30cc237ae38fd809556d0d61fb0492b29d9fdf68dbae11184c95101c1354e6c47272e38fd30b1916a4ef9b2ac0187978675fdd12561f0154188fa7dd2f0ed010f41bfb57ac62ea785e09582448399ebe8feb6a3d216a0469a9983bad52fc964330c9da0c60046594025ae70fcb6527fd464b008389123d2e3c2b875c860a6f93ffb3d8fa9109084e24df6e9ce8227f9554921e313bc02daf7126beaf450b9f955d131a0acfb591411841421f704c8c8b38f4f686c056dc5f1adbe5267694c955320b4971d4119ac819229edd277f18ca8b300ff3bc566023fe7234234f1be883d64c1ef0c5852c0ba23d91a4d0932ac10ff20f5655f67ad8dd5d5dbe6811bda0f4e54668a74f11e09dc9c16ab86b50d16ed06af7ade3012930820c1b6f935b3fb11d3ceff2e7564ef017a975ac0f279b1e1d91327efa322038c4579bc1cb0d06a6110788d99eb7253db56c264cfd8e363fb85e5aa677871c524cdff9873ea02793f78f214614339309ef6af3c44505cd01d75a5c614bf0250d2c1df327618be081a1641bd533

View file

@ -1,7 +1,7 @@
---
title: "Callout example"
slug: "callout-example"
date: "1883-1-1"
date: "1883-01-01"
description: "This is an article with callout examples."
author:
- slug: andrew-pease

View file

@ -0,0 +1 @@
b811602cb36308a94550965e45dcaa8f2aa733c23e2fe479753e388db1882feafd3efba6fff1497934ce4f5bdcd508f9da891b22e1444773698ba08c1a0a7075f351e4d419e9845ae2de974de2063b3ca8cacec0b8310b92bd66fc339d3eaa2e2c4fda426ee25b50e8eae645cafd775bd8f6b6c367c28340463f0d159a024eeb7a616e1fb44af5c0cafb836d2e8ac26512ee67d746f950b170f6473d25df6767543c94d871b8b1a626cf50751b18168f77b82b9d6b3ca1cb416929ee5b4e453814c85dd8ee1cf71b3ed6ec11adb84e18d917e2f244122febae3ea88937dde94d85b6ff9bf87ef6390742bd6e34b4edc92539b88f435d41d2f3991e47dc667a318f83fe6017bf27524394efb36b697e63a3e2106df523f4e4abeb32affd56d83559b4ef8fc1fff0ae7241c7bea08701f3e7f65c4b3b42643b9d5904bc14cfc47caa40ec77c0b64e90fb95bc79271b361424dd89ee79e50f30e210a2d4e367280dfd1de7af2ee03bb3f393cead7ca59a80cfdac4fe6e4fcea3ea5b62fa869a06a9072db8211790aa45532e78e0ea66fee1cf54438101ce131f5c60b34e550103f979ec9dc945ba225addae34e2f659adaa2b6ff6b5bc62c09de92f5c5e39b90affa6d5c0bc68d9063ab316224cf0f2e3c61e56c70d0846268b7f84f1b2ee12d6347a1a668513dc5636d0a203e213414a49da483ccfb91c31e72da56d6c12f21654aa99e7178260a53d5326c274d5455a9a8ede419387e050f0c512e3f21c0d4740c65a555f6f64ff3b4a23edca3a7c4504056bcca0750b32a256a28c91a323ff1f672c331d377b5923afbb1eb198a24328e1d3a897ebb1ec25962ca3bde3ab73745879eaeb7856ce8f2e212404d2b68147afee2e6259b93c8f120960c6fca69431fd1d4c45f7265ee2647eadf9f70d6db85526e2f6c2cd4e55804cfdd61bcf98ff61e066bec2ae372ce2b322de584755f78e8fefa1618a285be2f928b602a8985065bede601846f61c67bd6ab71ac81d9f5333e496b2a6781209087946fb5389c3d598942e3e4914205c52d062a332a52c7ad1b26bb6431ec115e936b9daa7830111a1876aeab555b581c7511ffa397feb10dcc4289923a74f5f74efa07e886c09036daa3d37ab15c0bcf5128e04898f8ea88640b2aadd43591096163cd8f980f129ef765b100ae6d9b81a4d7f3e61b91def30da7a1bc51cf8194133215f1f41528d8e2df238219ffc66660519058bdc2464a660c1139945ceca8eb8aaa777ed3d505b9451778b601d02991e4ee4ff506f6d8fabb9573cfc1ed4bd964cffdf07d348b8b64fc4a06a34f0238285f65949ba3ee8fd60791fe4df2f97c8bb5b9d6455b594cb5044f7d95d81998dcb00969929b6c3f1c9c5e9d808b78f2cd93ed081e3d9d1d7470c05d92f445f39b0892175a678abfa3d39899e7ee7112940a77d28b7ef30da7a1bc51cf8194133215f1f41528d8e2df238219ffc66660519058bdc2464a660c1139945ceca8eb8aaa777ed3dbc05f5633b92b5b43ec6a641e6b61d17e5226cac72f3d7fcf7846b3a1152be879d69258d44fe8f38ab5a35af1c7c7ae0396e51670de40b38ef2b07b8253f469241ff58aa2cd9dd2a5814cccdb23a7bfdde34f1af21a7868a1e64b3c2da06d3796a423719dd73320453012bf11cd798148a210b91fd5053151e8f5897d10f844d6aeb2d84211db04ce711dbf6a5578689b48b8c3b8bea3c2283febd41fea89c68dc1f76a4bb771fcaca4e0a05a1ea1699a011a67551dfbf805ca48ee1009c3fc678f9d1433d025a4c48ac533301c7c7721dee28fb06aa8a00ed76ed2f9675fc92e695d4b3af40ec33989bbb0c91437cc1a0344bd8b62a125fa3d95f50387afeb7a21e277280acf0a8c97ed9c8e9bdc5c12e3c30b09d7933ddcef5ce1ce2faa523780f348a661372ff5fba6a898fe073ea506cbedb5b391413a72cf07e2649ccc20ab029a3a51364f56d64aa50e07460652a229c1bcf523477c93b1252313651b731908f89c7cc4964e6157786e33812b675855ed645eac685d204f56d639fa98e59de6820cfaa127187cc87a6349098fb24816d329af1ab8fabf312dc31c7174700367892e3da7795904dfb9100666b810149c756f8754d735fde7fb80a101c5ac4f77349e2def5b883a4dfb7c40b38e7a776adff5a6fa636b1dfcbdfd73c0dd1007e69f1d8e4dfd3100ec45196419af2cb64cfb18c80dc140ddc826af70900b4754f0d413d6e738743775be0b0b4ccd80286f3cc402b3071dfac6c5ac9a03fd33e8f23b4416657aabbce6b195883be44b5645fe1fb2887a25807f9743c806cae24015552e1794de7e25f7c8a655abbe7f005a3666e3b3554627b5e2513e703f556f81c84280593543c79fa889a01cda3badbfce7168a17b503a32cb2244af9470fa8d8bfdebafcb82e3732b362ed441928a122bad7b347395ebe59f8067b1c2740b26f58b13c2f4448a86a5ab9d69e8708939bc14a8d12fd1a3ee5aec6b96bef7d07112502e4b4d3ed4536303d9ebcfa083d18d9da6b794bc6b46ca10d9c50ec34ae21def4d995640f621586f22fc3847ed54f7df25cc0b0f13c7ef71a96bcded0c911d83ecb113a162c6d42dfb4ccc43dae790c8f272ef4da4b18b03a901e44d0478f405986aa7df56685e25c79afd590df142f1e6e23616724de7e778c712bb9c0f59e5da1ea01ac7b20065cc9f4f2f62f07a8fa71024f9a1695664004f03f87c318076c20a3994f84a957a7415baaba4d7bd9f62417293a768d43c92b2ef923757be40a2f95952443a1f44877dab45be5ceb6faf341ada61f0238025fc4d1e740dc8a230efb897f78fb44f2917880

View file

@ -0,0 +1 @@
a7984d3c7e678f96b0761270e0392e63eb53206cf721e39d5c58537b52c89f93efba56fb08502cfca942d00352c3fd143ca3ea8ff9f67924becac878957d9d993a0bcc622e7ad0d645545d49f33f0dfe9e7dc02dbd99240e1eab7daad6ffae66b5293fcd51489c474b0c2958cf8b977c49c1a71c83a8b9f8afc1a42beccc1902211ff23ef054f61d6a73198a0729219dd8a85a0385b8d9ce8fe49cbf637d47ceb813e8b316ca39d12e7a3b99ad22ba2ed01fb409c3d9056cdeb1e808bb0f2f071d9da468b2bfb2550e5e5130777c661e38d71398bbb8957e54f2527ad39643cbd5259be4f701d023c5577ffe087eee5b3f3c0986112af3c412c5a47af41a639318ccb38d2e4b9fd55abc60ca68491495ea3f97bdc43a6cef2fc1f997178f89b5d03fa79ddee91973f28591b7d86a9aa1a7dea39ed5973827c9d7c75aa2f1ecbbeddfe3f35c0ea337c07d4a8c132b392f82a20225680dd91196d8cc699bd90980243693630302bb4ff7942cf57d495a3673b68d2ecd8602db28b290492169edb12950aff4e605c063f0d204e25f4085190b059b1a7efbd985af03e77824fb02ff7cb9a627689a8045413eeb8e457509c0890d5a76a5607c498913f32bc163b1e59ecc400f5f06a9f36f2c9ebc965aa7e1e24233323f2fb248c079104106647acdc8fbc8b31fe3e38ed290abaf2680e954bdedd7402276144fc32441a8e42cd36f06eacd613c6afb09b8be9f5130e8a35208d0e3d86cfaec99054fd29d524ff67a7101ec6fff26c5448d3e7b6cefd34bf0e0c062e7f025b6c8506dd993a5ab336a9c047ec137e3b77d903ffaa886698591a06c653d7ea3df5d4e1b91cd71f19f00dc55e2146116a5f4206ccfa4a2f39d7f83c2e3e75215f363efd8a3b7294ec88c437543b570d918aaa00c59e1addc00ce43bf6a6ba82a92a7bb6514f35013cee97b13fa40a9dd540c6291ef135ed5d3e1b6897132b2688e2a690b18c2d32b3af0d88ca138cb1ed85877da71a6bbe5a348564dde05a26724e8a1e2f10132df2b969a3fc01e825e602aba42f2bf2d858cf4f1cdef475ead44d48b3b8db2d98119651aa05c938c4dfd241656312befd01d35cd18208c0ae088f7b4cd017bb44a7e8686d5047e5932ff59ad3de2cf59de6a92737c665b5416d6f07514fe3cd1dc12ed30b0391b706873657dcc1118171ef0a7639410b20292ebbe6b08cb3d53c4a2d64a090281cd0d83b29ccada57e282a04e5235824c41aa61ccc674248e4c377cb21ce6be0f5e6090b97449968b4388414537553b2a4d30100df718a1ad3085d2f60d892a57998d63f682f59a5dd2832c2e4d13aa89ef9b0ef315cb308b32b82d095f45bbddd0afa06de916fa1a79a4be52059a3f67bd68343f5b6a37bfa9d37f60c30873455973b96890343a1be22531dc10942a38f9eb340711a453de5b07260633021cf51eec3c37c94330fbc529cc9af37fa3ff5407e88d6d5b9166742cc5c01c310aa26a24ac2e72a0e6204aa710f6dbefac752ebc7f61d022d17b43c2780b259aff535a0588fb5e9f4050a22698ab7d6e70223f4b38cc57d1b9fc979a6bab9efe763f523c4ec12cdb0971451461b5c418481443d3bcd72e4bb102129ffaa35d89a26e8418dd4ece03707a63bf48f873ac574825b76fa24eb76a99caa16be187abfd225f04c744e2ed641a255b0ac47ba796986d632416263b2bb54f45461b62a568c95f7927f4da498e7f0de32eab22a895f4bc9887b8d7ec02f0384236c69ed7a11af3c7923ff9df136cae8e7ea87d1d7fc93259d30a64a2516427ba8bc4c9ee6f511947a4e8f3cd36ef9d4b3ff2bb03748d0a7d6fa9df94001ec7e9909d00518dec42841257535055174fcbaa7354809ea30f0991a05d4afb4060feff4e43eeed7c09eb0e53fb01d5897b94b6eb90775b2f08143b8934cf53ada09b35fb26ff3bc2ac3e2ce724457221fbff122d9e6ac560f3852c49787c0381f862931b8513dbb09c833de448ac9812e7d535d6dba34f5668d3461bd0a2571c26a8ae3becc38e29f2465576b50f7a2512b24fd5

View file

@ -0,0 +1 @@
592a3b67a8145248b50877c08173a6632b72a432548d24f72036b4a71753d2e6d430eaa09b937d8c38aff68068342260b87d5a94a4c1dbf582c2912437eb33040d7ddd230cfe257774b8fea330954ea22e0e39f9ff032087cd524bc7d2b2323f55abdc31c3c279860e695c494f96ca7a1c60c74592ffdd6858d9d579cba8513014c85dd8ee1cf71b3ed6ec11adb84e1880b3e86e3e10d5b63eb3c45640205b691914e70147073d6571968d5f2cde0bf9d313cfa33c69fced0436cf75f6daa8be7b518a2ac4fdacf4c09afc57d594a2c0c5c67237d35d9de2c8b699c8ff5723ef0a12ccf5cfa0588cb31c4b3e9e762e3eb2d5baea85b8c20d1f424a06b2c07045340454432ce4ead6b76f1013974d37e3eddfe3f35c0ea337c07d4a8c132b392f82a20225680dd91196d8cc699bd90980e8970d57ed1df37f9cf1c209ade1429207973bb98c9c5f401eafd3c5b21fc8faee0a1992e65fb793c17921de2f17bf3253d330d8d38bf2f2cb178c4ad67e8ab37964241c08fcd0519c067dd7839adcaa4468e76f60db15f40693a84e904692bc57b92db0772f5a25dfb76d7701cfead0d313cfa33c69fced0436cf75f6daa8beaa29cea90960153f89063363da07a8a056e093cb0427eda552c68e78a8b99ba28bd10305e5bafcd8d5105df45e53b7efd482c7727834adcab0c800f7cc79d08fc909ea0eca0886e4274bfc3d074750481809f442c7fc5f8d2e0fc08f2e5d1752dba1b6ce3093f7a089b2d74600e4241513c6076fe7a236348cd4a457fee947f8f517cd21e6f24ec6d502a60180a5b75bbb59842010adc91863bd8367d7cd6f556da215dd7d6f18219d85e8263e3803b9ee8dfc0984ab87f643980a5aae4020f02cb3ea2c836d93de0c583faaf02d6124b88783357fc01c9648f2b83afb161019b3c20f68d93811fb7549c838bde5fcb8e8e5b8b69b3f09ed172a591b00dee40f5b8f521f9dbd7e8c949dcc474844cbaf23f7df8339a0e51bdde8d26f97aa175fd6ff70d21e4cdf1d4b23be49f3388421e82c67110d73176dcf1fcb19f10ef72bc6363bd3e626eb36550a5d5928b58df777a39481fc2d211470dab6c57e92aa86f27d11ee5f45123cdae48c5d763d88c89d56599b7f6beef1d22abe53c7af1ed1c7bca8f59a830b3ad465aa82e6c42c2859ddbb7bd57ea94e7607a74273ea9ad21b2b804c476a8dc0c83310e59b86aff1b66db45ab93a2e53e5e08708c3c61cd3c8702537772e523d1f6dd13d9e6ce3dc38223240e54abdc0339a073c3e0f67a810dae6c521a5fea857d7adc011f3c3fd01612e1ab20079f85b80a370f29632ad82f3011dac796e6d8a7eec8946a3d475ae6f802964946ccf840b0b53ce7c7cb02a07831bdb4053675c342b6c4599e9d0c39a30580411166558a70ea2c852f0fb47e7b62df254de96fa3190cf1aade765242bbd7ea016102993fa30f39e1eff4e63bd9e4156ea84512145f90d233aa7fc3dd839d5f1b6af7072222dc1657eaa3ec54138c2054da9221b95b1904bcd4fe8bdcf538a56f410dbba287a2ed9a8aadf519d877181a61225fb8c404bfc59e7a2c808ee85f84e4ad2b10976ce1d36e1441b3b0e34076d31b253919c18b08826666f94a1b6f8c6e5b43b2dcf85d4df69ee9f4ca5ce43e32b74ee371f7d1d06b088beb8b7192972f49dd095519dd5cb83c541515f75ae116b88622beef02c30f1db6cf15c87e5062946c723dc7e23dad512b4954e6a8d2ada650c7fbfb18c86a20a4b5dea6a28af38d0d3dd1c79814114ea3fcc69c78eeecd3aa85c37e625ad33521f5a1588902808955af3dad111457173d834edf908d9f8976817e252549be8f4c3c92210cf14ac856ba4d63c08d22a11cad843bf135d4d5a642cff0f2152bbf6c36efe33e9b63572081523c66e52712b90ff31df0c92020897ac29cd2b262ffdf87d8c8a267801bbfc529a6e9673f42ff8ff819d855140c84833103b9daf7a3884a55a7afe08ea22d8d86bc71e67a1e8bb4797a6cefb520303801b6a7f59191da464f4b06f0e6af0196e8e75ebaa96d8841adb3f1096278966a7e880402445ae8fafb0dd6dc8df192cfbde1af6d941c814c491fe5483a3013c222195a8d92e8ead0e1d8f59eb6afd4ce9858f119e0dda4139e21a6606bd4488a2d80e0c94694211cf078e146b5c31206b29b337b0c18d6737e97e04ed670b63d930eb815c4d1676caae6d98f9b26d886e2fca456d1130c0046cdc7dd6d300b5f9c3d53df5fffac4604ed96d54a113efa34d1c11bea1e9437be5877261e3a6ee25de2b3495a6bbbefb3ec45159626146e92a42ee247c55a805ca1cb186fdaf55e22339a3cf165e9e23f4182df88dfd41dfe09e12f8806f440681247909e5906d30ad90a86e7cc7188827bc6925707bc990c73b648fec1e73988ff7aa8ce5f3e46990b5d547a4a91509254e5c874cf5574c115fb6b0068dc909ea0eca0886e4274bfc3d07475048ef857e1a67a4c9f53db169627b94e65c69f579ce8e2abe2e82839bd8e04926664ffde253236b80c43eb357017c39c7fa5dfe639faac5d194411c9a607fb6bc926570b432bce3da8822a9b64a7324dacee930578c28d856723ed9c2354e1e99a3

Some files were not shown because too many files have changed in this diff Show more