From 7155d6e3e1e91da5bba272aa6f2ce9f3a422765a Mon Sep 17 00:00:00 2001 From: Andrew Lilley Brinker Date: Sun, 27 Oct 2024 16:10:02 -0700 Subject: [PATCH] feat(site): Substantial site redesign This redesign is still a draft with pieces unfinished, but it's at the point where we can probably start to have others contribute to it. Signed-off-by: Andrew Lilley Brinker --- .zed/settings.json | 35 ++ site/config.toml | 35 +- site/content/contribute/_index.md | 62 --- site/content/docs/_index.md | 33 +- site/content/docs/contributing/_index.md | 33 ++ .../docs/contributing/coordinating-changes.md | 25 ++ .../docs/contributing/describing-changes.md | 16 + .../contributing/intellectual-property.md | 15 + site/content/docs/contributing/testing.md | 21 + site/content/docs/getting-started/_index.md | 35 ++ .../first-run.md} | 20 +- .../getting-started/install.md} | 115 ++--- .../docs/{guide => getting-started}/why.md | 3 +- site/content/docs/guide/_index.md | 42 +- site/content/docs/guide/cli/_index.md | 50 +++ site/content/docs/guide/cli/general-flags.md | 81 ++++ site/content/docs/guide/cli/hc-cache.md | 128 ++++++ site/content/docs/guide/cli/hc-check.md | 63 +++ site/content/docs/guide/cli/hc-ready.md | 28 ++ site/content/docs/guide/cli/hc-schema.md | 17 + site/content/docs/guide/cli/hc-scoring.md | 54 +++ site/content/docs/guide/cli/hc-setup.md | 33 ++ site/content/docs/guide/cli/hc-update.md | 36 ++ site/content/docs/guide/concepts/_index.md | 37 ++ site/content/docs/guide/concepts/analyses.md | 97 ++++ site/content/docs/guide/concepts/concerns.md | 33 ++ .../concepts/{ => data}/concepts-diagram.svg | 0 .../content/docs/guide/concepts/data/index.md | 36 ++ site/content/docs/guide/concepts/index.md | 415 ------------------ .../docs/guide/concepts/scoring/index.md | 109 +++++ .../{ => scoring}/score-tree-final.svg | 0 .../concepts/{ => scoring}/score-tree-pct.svg | 0 .../concepts/{ => scoring}/score-tree.svg | 0 site/content/docs/guide/concepts/targets.md | 142 ++++++ site/content/docs/guide/config/_index.md | 23 + .../guide/{plugin => config}/policy-expr.md | 1 + .../for-users.md => config/policy-file.md} | 5 +- site/content/docs/guide/configuration.md | 62 --- site/content/docs/guide/debugging/_index.md | 28 ++ site/content/docs/guide/debugging/debugger.md | 21 + .../{debugging.md => debugging/logging.md} | 65 +-- site/content/docs/guide/debugging/starting.md | 27 ++ site/content/docs/guide/how-to-use.md | 262 ----------- site/content/docs/guide/introduction.md | 68 --- .../docs/guide/making-plugins/_index.md | 26 ++ .../guide/making-plugins/creating-a-plugin.md | 34 ++ .../rust-sdk.md} | 36 +- site/content/docs/guide/plugin/index.md | 30 -- .../guide/{analyses.md => plugins/_index.md} | 10 +- .../docs/guide/plugins/mitre-activity.md | 9 + .../docs/guide/plugins/mitre-affiliation.md | 9 + .../docs/guide/plugins/mitre-binary.md | 9 + .../content/docs/guide/plugins/mitre-churn.md | 9 + .../docs/guide/plugins/mitre-entropy.md | 9 + site/content/docs/guide/plugins/mitre-fuzz.md | 9 + site/content/docs/guide/plugins/mitre-git.md | 9 + .../docs/guide/plugins/mitre-github.md | 9 + .../docs/guide/plugins/mitre-identity.md | 9 + site/content/docs/guide/plugins/mitre-npm.md | 9 + .../docs/guide/plugins/mitre-review.md | 9 + site/content/docs/guide/plugins/mitre-typo.md | 9 + site/content/{ => docs}/rfds/0000-rfds.md | 4 + .../rfds/0001-release-engineering.md | 4 + .../{ => docs}/rfds/0002-hipchecks-values.md | 4 + .../rfds/0003-plugin-architecture-vision.md | 4 + .../{ => docs}/rfds/0004-plugin-api.md | 4 + .../rfds/0005-target-resolution-refactor.md | 4 + .../{ => docs}/rfds/0006-rust-plugin-sdk.md | 6 + .../0007-simplified-release-procedures.md | 6 + site/content/{ => docs}/rfds/_index.md | 8 +- site/scripts/deno.json | 13 + site/scripts/deno.lock | 140 ++++++ site/scripts/src/footer/installer.ts | 138 ++++++ site/scripts/src/footer/main.ts | 31 ++ site/scripts/src/footer/scroll.ts | 27 ++ site/scripts/src/footer/search.ts | 52 +++ site/scripts/src/footer/theme.ts | 18 + site/scripts/src/header/main.ts | 19 + site/scripts/src/header/theme.ts | 8 + site/scripts/src/util/theme.ts | 100 +++++ site/scripts/src/util/web.ts | 44 ++ site/scripts/tasks/bundle.ts | 40 ++ site/static/fonts/plex/IBMPlexMono-Bold.woff2 | Bin 0 -> 46684 bytes site/static/fonts/plex/IBMPlexMono-Text.woff2 | Bin 0 -> 46228 bytes site/static/js/elasticlunr.min.js | 10 + site/static/js/footer.mjs | 2 + site/static/js/footer.mjs.map | 7 + site/static/js/header.mjs | 2 + site/static/js/header.mjs.map | 7 + site/static/js/load-theme.mjs | 62 --- site/static/js/setup-theme-toggle.mjs | 15 - site/static/js/theme.mjs | 14 - site/styles/main.css | 14 + site/tailwind.config.js | 12 +- site/templates/bases/base.tera.html | 44 +- site/templates/bases/docs.tera.html | 135 ++++++ site/templates/blog.html | 4 - site/templates/docs.html | 17 + site/templates/docs_page.html | 17 + site/templates/index.html | 158 ++++--- site/templates/macros/breadcrumbs.tera.html | 29 +- site/templates/macros/toc.tera.html | 32 +- site/templates/page.html | 3 - site/templates/partials/end.tera.html | 3 + site/templates/partials/footer.tera.html | 22 +- site/templates/partials/head.tera.html | 6 + site/templates/partials/install.tera.html | 17 + site/templates/partials/nav.tera.html | 150 ++++--- .../templates/partials/prose-config.tera.html | 33 ++ site/templates/partials/search.tera.html | 40 ++ site/templates/rfds.html | 53 ++- site/templates/section.html | 4 - site/templates/shortcodes/button.html | 4 +- site/templates/shortcodes/info.html | 10 +- site/templates/shortcodes/install.html | 18 + site/templates/shortcodes/warn.html | 9 + site/templates/shortcodes/waypoint.html | 24 + 117 files changed, 2897 insertions(+), 1409 deletions(-) create mode 100644 .zed/settings.json delete mode 100644 site/content/contribute/_index.md create mode 100644 site/content/docs/contributing/_index.md create mode 100644 site/content/docs/contributing/coordinating-changes.md create mode 100644 site/content/docs/contributing/describing-changes.md create mode 100644 site/content/docs/contributing/intellectual-property.md create mode 100644 site/content/docs/contributing/testing.md create mode 100644 site/content/docs/getting-started/_index.md rename site/content/docs/{quickstart/_index.md => getting-started/first-run.md} (94%) rename site/content/{install/_index.md => docs/getting-started/install.md} (85%) rename site/content/docs/{guide => getting-started}/why.md (98%) create mode 100644 site/content/docs/guide/cli/_index.md create mode 100644 site/content/docs/guide/cli/general-flags.md create mode 100644 site/content/docs/guide/cli/hc-cache.md create mode 100644 site/content/docs/guide/cli/hc-check.md create mode 100644 site/content/docs/guide/cli/hc-ready.md create mode 100644 site/content/docs/guide/cli/hc-schema.md create mode 100644 site/content/docs/guide/cli/hc-scoring.md create mode 100644 site/content/docs/guide/cli/hc-setup.md create mode 100644 site/content/docs/guide/cli/hc-update.md create mode 100644 site/content/docs/guide/concepts/_index.md create mode 100644 site/content/docs/guide/concepts/analyses.md create mode 100644 site/content/docs/guide/concepts/concerns.md rename site/content/docs/guide/concepts/{ => data}/concepts-diagram.svg (100%) create mode 100644 site/content/docs/guide/concepts/data/index.md delete mode 100644 site/content/docs/guide/concepts/index.md create mode 100644 site/content/docs/guide/concepts/scoring/index.md rename site/content/docs/guide/concepts/{ => scoring}/score-tree-final.svg (100%) rename site/content/docs/guide/concepts/{ => scoring}/score-tree-pct.svg (100%) rename site/content/docs/guide/concepts/{ => scoring}/score-tree.svg (100%) create mode 100644 site/content/docs/guide/concepts/targets.md create mode 100644 site/content/docs/guide/config/_index.md rename site/content/docs/guide/{plugin => config}/policy-expr.md (99%) rename site/content/docs/guide/{plugin/for-users.md => config/policy-file.md} (99%) delete mode 100644 site/content/docs/guide/configuration.md create mode 100644 site/content/docs/guide/debugging/_index.md create mode 100644 site/content/docs/guide/debugging/debugger.md rename site/content/docs/guide/{debugging.md => debugging/logging.md} (54%) create mode 100644 site/content/docs/guide/debugging/starting.md delete mode 100644 site/content/docs/guide/how-to-use.md delete mode 100644 site/content/docs/guide/introduction.md create mode 100644 site/content/docs/guide/making-plugins/_index.md create mode 100644 site/content/docs/guide/making-plugins/creating-a-plugin.md rename site/content/docs/guide/{plugin/for-developers.md => making-plugins/rust-sdk.md} (86%) delete mode 100644 site/content/docs/guide/plugin/index.md rename site/content/docs/guide/{analyses.md => plugins/_index.md} (99%) create mode 100644 site/content/docs/guide/plugins/mitre-activity.md create mode 100644 site/content/docs/guide/plugins/mitre-affiliation.md create mode 100644 site/content/docs/guide/plugins/mitre-binary.md create mode 100644 site/content/docs/guide/plugins/mitre-churn.md create mode 100644 site/content/docs/guide/plugins/mitre-entropy.md create mode 100644 site/content/docs/guide/plugins/mitre-fuzz.md create mode 100644 site/content/docs/guide/plugins/mitre-git.md create mode 100644 site/content/docs/guide/plugins/mitre-github.md create mode 100644 site/content/docs/guide/plugins/mitre-identity.md create mode 100644 site/content/docs/guide/plugins/mitre-npm.md create mode 100644 site/content/docs/guide/plugins/mitre-review.md create mode 100644 site/content/docs/guide/plugins/mitre-typo.md rename site/content/{ => docs}/rfds/0000-rfds.md (97%) rename site/content/{ => docs}/rfds/0001-release-engineering.md (96%) rename site/content/{ => docs}/rfds/0002-hipchecks-values.md (97%) rename site/content/{ => docs}/rfds/0003-plugin-architecture-vision.md (98%) rename site/content/{ => docs}/rfds/0004-plugin-api.md (99%) rename site/content/{ => docs}/rfds/0005-target-resolution-refactor.md (99%) rename site/content/{ => docs}/rfds/0006-rust-plugin-sdk.md (98%) rename site/content/{ => docs}/rfds/0007-simplified-release-procedures.md (97%) rename site/content/{ => docs}/rfds/_index.md (60%) create mode 100644 site/scripts/deno.json create mode 100644 site/scripts/deno.lock create mode 100644 site/scripts/src/footer/installer.ts create mode 100644 site/scripts/src/footer/main.ts create mode 100644 site/scripts/src/footer/scroll.ts create mode 100644 site/scripts/src/footer/search.ts create mode 100644 site/scripts/src/footer/theme.ts create mode 100644 site/scripts/src/header/main.ts create mode 100644 site/scripts/src/header/theme.ts create mode 100644 site/scripts/src/util/theme.ts create mode 100644 site/scripts/src/util/web.ts create mode 100644 site/scripts/tasks/bundle.ts create mode 100644 site/static/fonts/plex/IBMPlexMono-Bold.woff2 create mode 100644 site/static/fonts/plex/IBMPlexMono-Text.woff2 create mode 100644 site/static/js/elasticlunr.min.js create mode 100644 site/static/js/footer.mjs create mode 100644 site/static/js/footer.mjs.map create mode 100644 site/static/js/header.mjs create mode 100644 site/static/js/header.mjs.map delete mode 100644 site/static/js/load-theme.mjs delete mode 100644 site/static/js/setup-theme-toggle.mjs delete mode 100644 site/static/js/theme.mjs create mode 100644 site/templates/bases/docs.tera.html create mode 100644 site/templates/docs.html create mode 100644 site/templates/docs_page.html create mode 100644 site/templates/partials/end.tera.html create mode 100644 site/templates/partials/head.tera.html create mode 100644 site/templates/partials/install.tera.html create mode 100644 site/templates/partials/prose-config.tera.html create mode 100644 site/templates/partials/search.tera.html create mode 100644 site/templates/shortcodes/install.html create mode 100644 site/templates/shortcodes/warn.html create mode 100644 site/templates/shortcodes/waypoint.html diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 00000000..18c48cc4 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,35 @@ +// Folder-specific settings +// +// For a full list of overridable settings, and general information on folder-specific settings, +// see the documentation: https://zed.dev/docs/configuring-zed#settings-files +{ + "lsp": { + "deno": { + "settings": { + "deno": { + "enable": true + } + } + } + }, + "languages": { + "TypeScript": { + "language_servers": [ + "deno", + "!typescript-language-server", + "!vtsls", + "!eslint" + ], + "formatter": "language_server" + }, + "TSX": { + "language_servers": [ + "deno", + "!typescript-language-server", + "!vtsls", + "!eslint" + ], + "formatter": "language_server" + } + } +} diff --git a/site/config.toml b/site/config.toml index c6a90bfc..5f0d1bf1 100644 --- a/site/config.toml +++ b/site/config.toml @@ -4,53 +4,54 @@ base_url = "https://mitre.github.io/hipcheck" # Don't use Sass compile_sass = false -# Build the search engine. +# Build the search index. build_search_index = true +[search] + +# Use the elasticlunr format for the search index. +index_format = "elasticlunr_javascript" + [markdown] # Use syntax highlighting. highlight_code = true +highlight_theme = "ayu-light" [extra] # Define the site navigation. nav = [ - { name = "Documentation", url = "@/docs/_index.md" }, - { name = "Contribute", url = "@/contribute/_index.md" }, - # TODO: Uncomment this when the blog is ready. - # { name = "Blog", url = "@/blog/_index.md" }, - { sep = true }, + { name = "Docs", url = "@/docs/_index.md" }, + { name = "Contribute", url = "@/docs/contributing/_index.md" }, + { name = "Blog", url = "@/blog/_index.md" }, { name = "Get Help", url = "https://github.com/mitre/hipcheck/discussions", icon = "life-buoy", external = true }, - { name = "Install", url = "@/install/_index.md", highlight = true, icon = "arrow-down" }, - { sep = true }, + { name = "Install", url = "@/docs/getting-started/install.md", highlight = true, icon = "arrow-down" }, { url = "https://github.com/mitre/hipcheck", icon = "github", icononly = true }, - { url = "#", icon = "sun", icononly = true, id = "toggle-darkmode" }, ] # Define the site footer. footer = [ [ { name = "Documentation", title = true }, - { name = "Quickstart", url = "@/docs/quickstart/_index.md" }, + { name = "Getting Started", url = "@/docs/getting-started/_index.md" }, { name = "Complete Guide", url = "@/docs/guide/_index.md" }, - { name = "Requests for Discussion", url = "@/rfds/_index.md" }, - # TODO: Uncomment this when the blog is ready. - # { name = "Development Blog", url = "@/blog/_index.md" }, + { name = "Requests for Discussion", url = "@/docs/rfds/_index.md" }, + { name = "Blog", url = "@/blog/_index.md" }, { name = "Project", title = true }, - { name = "Hipcheck Values", url = "@/rfds/0002-hipchecks-values.md" }, + { name = "Hipcheck Values", url = "@/docs/rfds/0002-hipchecks-values.md" }, { name = "Open Source License", url = "https://github.com/mitre/hipcheck/blob/main/LICENSE", external = true }, { name = "Code of Conduct", url = "https://github.com/mitre/hipcheck/blob/main/CODE_OF_CONDUCT.md", external = true }, ], [ { name = "Install", title = true }, - { name = "Installer", url = "@/install/_index.md" }, + { name = "Installer", url = "@/docs/getting-started/install.md" }, { name = "Container Image", url = "https://hub.docker.com/r/mitre/hipcheck", external = true }, { name = "Release Notes", url = "https://github.com/mitre/hipcheck/releases", external = true }, { name = "Changelog", url = "https://github.com/mitre/hipcheck/blob/main/CHANGELOG.md", external = true }, { name = "Packages", title = true }, - { name = "GitHub", url = "https://github.com/mitre/hipcheck", external = true }, - { name = "Crates.io", url = "https://crates.io/crates/hipcheck", external = true }, + { name = "Hipcheck", url = "https://github.com/mitre/hipcheck/releases/tag/hipcheck-v3.7.0", external = true }, + { name = "Rust Plugin SDK", url = "https://crates.io/crates/hipcheck-sdk", external = true }, ], [ { name = "Contribute", title = true }, diff --git a/site/content/contribute/_index.md b/site/content/contribute/_index.md deleted file mode 100644 index 38f74479..00000000 --- a/site/content/contribute/_index.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Contribute ---- - -# Contribute to Hipcheck - -The Hipcheck project is happy to accept contributions! - -## Coordinating Changes - -For small changes, including improvements to documentation, correction of -bugs, fixing typos, and general code quality improvements, submitting -without coordinating with the Hipcheck team is generally fine and -appreciated! - -For larger changes, including the addition of new data sources, new analyses, -refactoring modules, changing the CLI or configuration, or similar, we -highly suggest discussing your proposed changes before submission. Often -this will begin with opening up a GitHub Issue or a Discussion, and for -larger changes may also involve writing a Request for Discussion (RFD) -document. - -RFD's are how the Hipcheck project manages large scale changes to the tool, -and are documented more on the RFD's page. - -The Hipcheck product roadmap is public, and we always recommend checking -there to see how your proposed changes may fit into the currently-planned -work. - -## Commit Messages - -All commits to Hipcheck are required to follow the Conventional Commits -specfication. We use this requirement to help us auto-generate material -for our `CHANGELOG.md` and GitHub Release notes with each new version, -though we do still double-check and write them by hand. - -We also generally try to make sure commits serve a reasonably clear -purpose, and include comments whenever appropriate to explain the -reasoning behind what is being changed, or at least link to a GitHub -Issue or Discussion for further explanation. - -## Testing - -All changes to Hipcheck must pass continuous integration (CI) tests prior -to being merged. You can simulate this test suite, at least on your own -operating system and architecture, using the following command: - -```sh -$ cargo xtask ci -``` - -Passing this command is not a _guarantee_ of passing the official CI suite -on GitHub, but is a good way to approximate things locally. - -If you want faster tests locally, we also recommend installing `cargo-nextest`. -The `cargo xtask ci` command will use it instead of `cargo test` if it's -installed. - -## Intellectual Property - -When you make contributions to Hipcheck, they're done under the terms -of the Apache 2.0 license. diff --git a/site/content/docs/_index.md b/site/content/docs/_index.md index abe63105..a2bd8f09 100644 --- a/site/content/docs/_index.md +++ b/site/content/docs/_index.md @@ -1,5 +1,7 @@ --- title: Documentation +template: docs.html +page_template: docs_page.html sort_by: weight --- @@ -7,25 +9,22 @@ sort_by: weight Welcome to the official Hipcheck documentation! -## Quickstart +
-This is a guide to installing and running Hipcheck for the first time, and -is our recommended starting point for beginners! +{% waypoint(title="Getting Started", path="@/docs/getting-started/_index.md", icon="map-pin") %} +A guide to installing and running Hipcheck for the first time. +{% end %} -{{ button(link="@/docs/quickstart/_index.md", text="Check out the Quickstart Guide") }} +{% waypoint(title="Complete Guide", path="@/docs/guide/_index.md", icon="map") %} +A complete guide to all of Hipcheck's functionality. +{% end %} -## Complete Guide +{% waypoint(title="Contribute", path="@/docs/contributing/_index.md", icon="award") %} +Learn how to make contributions to Hipcheck itself. +{% end %} -This is a complete guide to all of Hipcheck's functionality, including both -how to use Hipcheck and how to develop plugins for Hipcheck. +{% waypoint(title="RFDs", path="@/docs/rfds/_index.md", icon="pen-tool") %} +Design documents proposing important changes to Hipcheck. +{% end %} -{{ button(link="@/docs/guide/_index.md", text="Check out the Complete Guide") }} - -## RFDs - -Hipcheck's evolution is managed by Requests for Discussion (RFDs), documents -which describe in detail any proposals for improvement or modification of -Hipcheck's behavior. This list shows all completed RFDs; draft or proposed -RFDs can be found on the [Hipcheck GitHub repository](https://github.com/mitre/hipcheck). - -{{ button(link="@/rfds/_index.md", text="Check out the RFDs") }} +
diff --git a/site/content/docs/contributing/_index.md b/site/content/docs/contributing/_index.md new file mode 100644 index 00000000..d76135bd --- /dev/null +++ b/site/content/docs/contributing/_index.md @@ -0,0 +1,33 @@ +--- +title: Contribute +template: docs.html +sort_by: weight +page_template: docs_page.html +aliases: + - "/contribute/_index.md" +weight: 3 +--- + +# Contribute to Hipcheck + +The Hipcheck project is happy to accept contributions! + +
+ +{% waypoint(title="Coordinating Changes", path="@/docs/contributing/coordinating-changes.md", icon="user") %} +A guide to all methods for installing Hipcheck. +{% end %} + +{% waypoint(title="Testing Changes", path="@/docs/contributing/testing.md", icon="cloud-lightning") %} +Some history on the creation and purpose of Hipcheck. +{% end %} + +{% waypoint(title="Intellectual Property", path="@/docs/contributing/intellectual-property.md", icon="shield") %} +A walkthrough of running Hipcheck for the first time. +{% end %} + +{% waypoint(title="Describing Changes", path="@/docs/contributing/describing-changes.md", icon="message-circle") %} +A walkthrough of running Hipcheck for the first time. +{% end %} + +
diff --git a/site/content/docs/contributing/coordinating-changes.md b/site/content/docs/contributing/coordinating-changes.md new file mode 100644 index 00000000..dd90128f --- /dev/null +++ b/site/content/docs/contributing/coordinating-changes.md @@ -0,0 +1,25 @@ +--- +title: Coordinating Changes +weight: 1 +--- + +# Coordinating Changes + +For small changes, including improvements to documentation, correction of +bugs, fixing typos, and general code quality improvements, submitting +without coordinating with the Hipcheck team is generally fine and +appreciated! + +For larger changes, including the addition of new data sources, new analyses, +refactoring modules, changing the CLI or configuration, or similar, we +highly suggest discussing your proposed changes before submission. Often +this will begin with opening up a GitHub Issue or a Discussion, and for +larger changes may also involve writing a Request for Discussion (RFD) +document. + +RFD's are how the Hipcheck project manages large scale changes to the tool, +and are documented more on the RFD's page. + +The Hipcheck product roadmap is public, and we always recommend checking +there to see how your proposed changes may fit into the currently-planned +work. diff --git a/site/content/docs/contributing/describing-changes.md b/site/content/docs/contributing/describing-changes.md new file mode 100644 index 00000000..e0b38939 --- /dev/null +++ b/site/content/docs/contributing/describing-changes.md @@ -0,0 +1,16 @@ +--- +title: Describing Changes +weight: 4 +--- + +# Describing Changes + +All commits to Hipcheck are required to follow the Conventional Commits +specfication. We use this requirement to help us auto-generate material +for our `CHANGELOG.md` and GitHub Release notes with each new version, +though we do still double-check and write them by hand. + +We also generally try to make sure commits serve a reasonably clear +purpose, and include comments whenever appropriate to explain the +reasoning behind what is being changed, or at least link to a GitHub +Issue or Discussion for further explanation. diff --git a/site/content/docs/contributing/intellectual-property.md b/site/content/docs/contributing/intellectual-property.md new file mode 100644 index 00000000..a586e97b --- /dev/null +++ b/site/content/docs/contributing/intellectual-property.md @@ -0,0 +1,15 @@ +--- +title: Intellectual Property +weight: 3 +--- + +# Intellectual Property + +When you make contributions to Hipcheck, they're done under the terms +of the Apache 2.0 license. + +Apache 2.0 is approved by the Open Source Initiative (OSI) as an +open source license, meeting the Open Source Definition. + +ChooseALicense.com, a project by GitHub, has a layperson's [breakdown of the +terms of the Apache 2.0 license](https://choosealicense.com/licenses/apache-2.0/). diff --git a/site/content/docs/contributing/testing.md b/site/content/docs/contributing/testing.md new file mode 100644 index 00000000..9b37076c --- /dev/null +++ b/site/content/docs/contributing/testing.md @@ -0,0 +1,21 @@ +--- +title: Testing Changes +weight: 2 +--- + +# Testing Changes + +All changes to Hipcheck must pass continuous integration (CI) tests prior +to being merged. You can simulate this test suite, at least on your own +operating system and architecture, using the following command: + +```sh +$ cargo xtask ci +``` + +Passing this command is not a _guarantee_ of passing the official CI suite +on GitHub, but is a good way to approximate things locally. + +If you want faster tests locally, we also recommend installing `cargo-nextest`. +The `cargo xtask ci` command will use it instead of `cargo test` if it's +installed. diff --git a/site/content/docs/getting-started/_index.md b/site/content/docs/getting-started/_index.md new file mode 100644 index 00000000..d4d770ed --- /dev/null +++ b/site/content/docs/getting-started/_index.md @@ -0,0 +1,35 @@ +--- +title: Getting Started +template: docs.html +page_template: docs_page.html +weight: 1 +sort_by: weight +aliases: + - "/quickstart" +--- + +# Getting Started + +Hello, and welcome! This is a "Quickstart" guide to Hipcheck, which means our +goal with this guide is to get you up and running as a user of Hipcheck as +quickly as possible. If you'd like a more thorough guide to Hipcheck which +explains its core concepts, how it works under the hood, and how to configure +it to your heart's content, we recommend the [Complete Guide](@/docs/guide/_index.md). + + +
+ +{% waypoint(title="Install Hipcheck", path="@/docs/getting-started/install.md", icon="download") %} +A guide to all methods for installing Hipcheck. +{% end %} + +{% waypoint(title="Why Hipcheck?", path="@/docs/getting-started/why.md", icon="book-open") %} +Some history on the creation and purpose of Hipcheck. +{% end %} + + +{% waypoint(title="Quickstart: Your First Analysis", path="@/docs/getting-started/first-run.md", icon="watch") %} +A walkthrough of running Hipcheck for the first time. +{% end %} + +
diff --git a/site/content/docs/quickstart/_index.md b/site/content/docs/getting-started/first-run.md similarity index 94% rename from site/content/docs/quickstart/_index.md rename to site/content/docs/getting-started/first-run.md index 9488d4e0..67a56c06 100644 --- a/site/content/docs/quickstart/_index.md +++ b/site/content/docs/getting-started/first-run.md @@ -1,23 +1,9 @@ --- -title: Quickstart +title: "Quickstart: Your First Analysis" +weight: 3 --- -# Quickstart - -Hello, and welcome! This is a "Quickstart" guide to Hipcheck, which means our -goal with this guide is to get you up and running as a user of Hipcheck as -quickly as possible. If you'd like a more thorough guide to Hipcheck which -explains its core concepts, how it works under the hood, and how to configure -it to your heart's content, we recommend the [Complete Guide](/docs/guide). - -## Installing Hipcheck - -First, you'll need to install Hipcheck. We __strongly__ recommend using the -install script if you're on a platform for which we provide prebuilt binaries. - -{{ button(link="@/install/_index.md", text="Install Hipcheck") }} - -## Your First Hipcheck Run +# Quickstart: Your First Analysis With Hipcheck installed, let's use it to analyze something! To do that, we'll use the `hc check` subcommand. This command takes a "target" (like a package diff --git a/site/content/install/_index.md b/site/content/docs/getting-started/install.md similarity index 85% rename from site/content/install/_index.md rename to site/content/docs/getting-started/install.md index 72faf0e6..9f5e7dc8 100644 --- a/site/content/install/_index.md +++ b/site/content/docs/getting-started/install.md @@ -1,5 +1,8 @@ --- title: Install Hipcheck +aliases: + - "/install/_index.md" +weight: 1 --- # Install Hipcheck @@ -14,13 +17,12 @@ Use the following instructions if you want to install Hipcheck onto your local system _outside_ of a container. If you want to install Hipcheck _inside_ of a container, see the [container installation instructions](#using-in-a-container). -### Install from a pre-built script - +### Install from Script The easiest way to install Hipcheck is to use an install script included with each release. -{{ button(link="https://github.com/mitre/hipcheck/releases/latest", text="Install with the latest install script") }} +{{ install() }} {% info(title="Setting up post-install") %} After running the install script, run `hc setup` to set up your local @@ -29,20 +31,18 @@ configuration and script files so Hipcheck can run. We currently provide prebuilt binaries for the following targets: -- x64 Linux (`x86_64-unknown-linux-gnu`) -- x64 Windows (`x86_64-pc-windows-msvc`) -- Apple Silicon macOS (`aarch64-apple-darwin`) -- Intel macOS (`x86_64-apple-darwin`) +- x64 Linux: `x86_64-unknown-linux-gnu` +- x64 Windows: `x86_64-pc-windows-msvc` +- Apple Silicon macOS: `aarch64-apple-darwin` +- Intel macOS: `x86_64-apple-darwin` We provide installation shell scripts for: -- __POSIX-compliant shells__: recommended on Linux, macOS, and in the Windows +- POSIX-compliant shells: recommended on Linux, macOS, and in the Windows Subsystem for Linux (WSL) on Windows. -- __PowerShell__: recommended on Windows. +- PowerShell: recommended on Windows. -These scripts install the Hipcheck binary (`hc`), the Hipcheck self-updater -(`hc-update` or `hipcheck-update`, depending on the version of Hipcheck), and -Hipcheck's required configuration and script files. +These scripts install the Hipcheck binary and the Hipcheck self-updater. {% info(title="Install Script Security") %} Some users may be uncertain about using an install script piped into a shell @@ -55,43 +55,7 @@ the artifacts are checked against SHA-256 hashes also included with each release to ensure artifact integrity. {% end %} -### Install with `cargo-binstall` - -Hipcheck is written in Rust, and releases of Hipcheck are published to -[Crates.io](https://crates.io), the official Rust open source package host. -The Rust ecosystem has a popular tool, called [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall) -that can search for and install prebuilt binaries for packages published -to Crates.io. - -To install Hipcheck with `cargo-binstall`, you'll need: - -- A Rust toolchain: see the [official Rust installation instructions](https://www.rust-lang.org/tools/install) -- `cargo-binstall`: see their [installation instructions](https://github.com/cargo-bins/cargo-binstall?tab=readme-ov-file#installation) - -Then you can run: - -```sh -$ cargo binstall hipcheck -``` - -{% info(title="Setting up post-install") %} -After running the install script, run `hc setup` to set up your local -configuration and script files so Hipcheck can run. -{% end %} - -This will install the latest version of Hipcheck. To install a specific older -version instead, replace `hipcheck` with `hipcheck@`, replacing -`` with the version you need. - -{% info(title="Disadvantages of this Approach") %} -Installing Hipcheck with `cargo-binstall` _does_ work, but _does not_ install -the Hipcheck self-updater, which the recommended install scripts _do_ install. -If you want Hipcheck to be able to update itself to newer versions, you'll need -to install it with an [install script](#install-from-a-pre-built-script) or -install the self-updater yourself. -{% end %} - -### Install from source +### Install from Source If you're on a platform for which Hipcheck does not provide pre-built binaries, or want to modify Hipcheck's default release build in some way, you @@ -168,7 +132,7 @@ Hipcheck image, you might run: $ docker run mitre/hipcheck:latest ``` -### Using the `Containerfile` Directly +### Using the `Containerfile` You can also run Hipcheck from the local `Containerfile`, first by building the image, and then by running that image. For example, with Docker: @@ -183,3 +147,54 @@ $ docker build -f dist/Containerfile . ``` This will build the image, which you can then use normally. + +## Deprecated Methods + +The following section lists any methods which were supported at one point +for prior versions of Hipcheck. They're recorded here in case you need to use +one of these older versions, but they are not recommended regardless, and +can't be used at all with newer versions. + +### Install with `cargo-binstall` + +{% warn(title="Deprecated in 3.8.0") %} +This method is deprecated as of version 3.8.0. You can use this method to +install old versions of Hipcheck, but it will not work for newer versions. + +Read [RFD #7: Simplified Release Procedures](@/docs/rfds/0007-simplified-release-procedures.md) +to learn more. +{% end %} + +Hipcheck is written in Rust, and releases of Hipcheck are published to +[Crates.io](https://crates.io), the official Rust open source package host. +The Rust ecosystem has a popular tool, called [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall) +that can search for and install prebuilt binaries for packages published +to Crates.io. + +To install Hipcheck with `cargo-binstall`, you'll need: + +- A Rust toolchain: see the [official Rust installation instructions](https://www.rust-lang.org/tools/install) +- `cargo-binstall`: see their [installation instructions](https://github.com/cargo-bins/cargo-binstall?tab=readme-ov-file#installation) + +Then you can run: + +```sh +$ cargo binstall hipcheck +``` + +{% info(title="Setting up post-install") %} +After running the install script, run `hc setup` to set up your local +configuration and script files so Hipcheck can run. +{% end %} + +This will install the latest version of Hipcheck. To install a specific older +version instead, replace `hipcheck` with `hipcheck@`, replacing +`` with the version you need. + +{% info(title="Disadvantages of this Approach") %} +Installing Hipcheck with `cargo-binstall` _does_ work, but _does not_ install +the Hipcheck self-updater, which the recommended install scripts _do_ install. +If you want Hipcheck to be able to update itself to newer versions, you'll need +to install it with an [install script](#install-from-a-pre-built-script) or +install the self-updater yourself. +{% end %} diff --git a/site/content/docs/guide/why.md b/site/content/docs/getting-started/why.md similarity index 98% rename from site/content/docs/guide/why.md rename to site/content/docs/getting-started/why.md index 409207c5..6ccfbeba 100644 --- a/site/content/docs/guide/why.md +++ b/site/content/docs/getting-started/why.md @@ -1,5 +1,6 @@ --- title: Why Hipcheck? +weight: 2 --- # Why Hipcheck? @@ -110,5 +111,3 @@ is a wonderful set of techniques for finding real bugs and vulnerabilities, as shown by the track record of groups like [Fish in a Barrel](https://fishinabarrel.github.io), a security research team who run fuzzers against open source code written in C and C++. - -{{ button(link="@/docs/guide/concepts/index.md", text="Key Concepts") }} diff --git a/site/content/docs/guide/_index.md b/site/content/docs/guide/_index.md index bc8fe37b..c2bc6802 100644 --- a/site/content/docs/guide/_index.md +++ b/site/content/docs/guide/_index.md @@ -1,8 +1,12 @@ --- -title: Complete Guide to Hipcheck +title: Complete Guide +weight: 2 +template: docs.html +page_template: docs_page.html +sort_by: weight --- -# Complete Guide to Hipcheck +# Complete Guide Welcome to the Complete Guide to Hipcheck! This guide is intended to explain: @@ -17,12 +21,30 @@ Since we intend this to be a __complete__ guide, if you encounter questions you don't feel are adequately answered by this guide, please let us know by opening an issue on our [issue tracker](https://github.com/mitre/hipcheck/issues)! -## Table of Contents +
-- [Why Hipcheck?](@/docs/guide/why.md) -- [Key Concepts](@/docs/guide/concepts/index.md) -- [How to use Hipcheck](@/docs/guide/how-to-use.md) -- [Plugins](@/docs/guide/plugin/index.md) -- [Analyses](@/docs/guide/analyses.md) -- [Configuration](@/docs/guide/configuration.md) -- [Debugging](@/docs/guide/debugging.md) +{% waypoint(title="Key Concepts", path="@/docs/guide/concepts/_index.md", icon="key") %} +An explanation of the ideas underpinning Hipcheck's design. +{% end %} + +{% waypoint(title="Configuration", path="@/docs/guide/config/_index.md", icon="settings") %} +How to configure Hipcheck and describe your policies to apply. +{% end %} + +{% waypoint(title="CLI Reference", path="@/docs/guide/cli/_index.md", icon="terminal") %} +Reference for all CLI commands and arguments. +{% end %} + +{% waypoint(title="Debugging", path="@/docs/guide/debugging/_index.md", icon="target") %} +How to identify errors during Hipcheck execution. +{% end %} + +{% waypoint(title="Plugins", path="@/docs/guide/plugins/_index.md", icon="box") %} +Index of existing plugins for Hipcheck, both for data and analyses. +{% end %} + +{% waypoint(title="Making Plugins", path="@/docs/guide/making-plugins/_index.md", icon="tool") %} +A guide for making new Hipcheck plugins. +{% end %} + +
diff --git a/site/content/docs/guide/cli/_index.md b/site/content/docs/guide/cli/_index.md new file mode 100644 index 00000000..656711a9 --- /dev/null +++ b/site/content/docs/guide/cli/_index.md @@ -0,0 +1,50 @@ +--- +title: CLI Reference +template: docs.html +page_template: docs_page.html +sort_by: slug +weight: 3 +--- + +# CLI Reference + +If you are interested in a quick guide to getting started with Hipcheck, +we recommend checking out the [Getting Started guide](@/docs/getting-started/_index.md) +first! For a more thorough explanation of Hipcheck's Command Line Interface +(CLI), please continue with this section! + +
+ +{% waypoint(title="General Flags", path="@/docs/guide/cli/general-flags.md", icon="flag") %} +Flags which apply to all Hipcheck subcommands. +{% end %} + +{% waypoint(title="hc cache", path="@/docs/guide/cli/hc-cache.md", icon="database", mono=true) %} +Inspect and control Hipcheck's local data cache. +{% end %} + +{% waypoint(title="hc check", path="@/docs/guide/cli/hc-check.md", icon="check", mono=true) %} +Run analyses against specified targets. +{% end %} + +{% waypoint(title="hc ready", path="@/docs/guide/cli/hc-ready.md", icon="loader", mono=true) %} +Check if Hipcheck is ready to run. +{% end %} + +{% waypoint(title="hc schema", path="@/docs/guide/cli/hc-schema.md", icon="hash", mono=true) %} +Get a JSON schema for Hipcheck's JSON output. +{% end %} + +{% waypoint(title="hc scoring", path="@/docs/guide/cli/hc-scoring.md", icon="star", mono=true) %} +Get a visualization of Hipcheck's scoring tree based on your policy. +{% end %} + +{% waypoint(title="hc setup", path="@/docs/guide/cli/hc-setup.md", icon="briefcase", mono=true) %} +Complete post-installation setup of Hipcheck. +{% end %} + +{% waypoint(title="hc update", path="@/docs/guide/cli/hc-update.md", icon="refresh-ccw", mono=true) %} +Have Hipcheck update itself. +{% end %} + +
diff --git a/site/content/docs/guide/cli/general-flags.md b/site/content/docs/guide/cli/general-flags.md new file mode 100644 index 00000000..08d6b889 --- /dev/null +++ b/site/content/docs/guide/cli/general-flags.md @@ -0,0 +1,81 @@ +--- +title: General Flags +--- + +# General Flags + +There are three categories of flags which Hipcheck supports on all subcommands, +output flags, path flags, and the help and version flags (which actually +operate like subcommands themselves). + +## Output Flags + +"Output flags" are flags which modify the output that Hipcheck produces. +Currently, there are three output flags: + +- `-v `/`--verbosity `: Specifies how noisy Hipcheck + should be when running. Options are: + - `quiet`: Produce as little output as possible. + - `normal`: Produce a normal amount of output. (default) +- `-k `/`--color `: Specifies whether the Hipcheck output should + include color or not. Options are: + - `always`: Try to produce color regardless of the output stream's support + for color. + - `never`: Do not produce color. + - `auto`: Try to infer whether the output stream supports ANSI color codes. + (default) +- `-f `/`--format `: Specifies what format to use for the + output. Options are: + - `json`: Use JSON output. + - `human`: Use human-readable output. (default) + +Each of these can also be set by environment variable: + +- `HC_VERBOSITY` +- `HC_COLOR` +- `HC_FORMAT` + +The precedence is, in increasing order: + +- Environment variable +- CLI flag + +## Path Flags + +"Path flags" are flags which modify the paths Hipcheck uses for configuration, +data, and caching repositories locally. The current flags are: + +- `-c `/`--config `: the path to the configuration folder to + use. +- `-d `/`--data `: the path to the data folder to use. +- `-C `/`--cache `: the path to the cache folder to use. + +Each of these is inferred by default based on the user's platform. They can +also be set with environment variables: + +- `HC_CONFIG` +- `HC_DATA` +- `HC_CACHE` + +The priority (in increasing precedence), is: + +- System default +- Environment variable +- CLI flag + +## Help and Version + +All commands in Hipcheck also support help flags and the version flag. +These act more like subcommands, in that providing the flag stops Hipcheck +from executing the associated command, and instead prints the help or +version text as requested. + +For each command, the `-h` or `--help` flag can be used. The `-h` flag gives +the "short" form of the help text, which is easier to skim, while the `--help` +flag gives the "long" form of the help text, which is more complete. + +The `-V`/`--version` flag may also be used. Both the short and long variants +of the flag produce the same output. The version flag is valid on all +subcommands, but all subcommands are versioned together, to the output will +be the same when run as `hc --version` or when run as +`hc --version`. diff --git a/site/content/docs/guide/cli/hc-cache.md b/site/content/docs/guide/cli/hc-cache.md new file mode 100644 index 00000000..7630b380 --- /dev/null +++ b/site/content/docs/guide/cli/hc-cache.md @@ -0,0 +1,128 @@ +--- +title: hc cache +extra: + nav_title: "hc cache" +--- + +# `hc cache` + +`hc cache` is a command for users to manage Hipcheck's data cache. + +When Hipcheck runs with `hc check`, one of its first operations is to resolve +the target of analysis from the target specifier provided by the user. A +resolved target must include a Git repository, as that's the basis for most +kinds of analysis we want to run with Hipcheck, analyzing the behaviors +associated with the development of the software in question. + +After that Git repository is identified, it's cloned into Hipcheck's local +repository cache, so that any operations which need the Git metadata can run +on a local copy of that data instead of operating over the network in the case +of a remote repo. Note that Hipcheck creates a copy in the local repository +cache even if the target of analysis is a local repo. This is to ensure that +any analysis operations which may change the state of the repo, but example +by checking out a different commit, branch, or tag, don't modify the existing +repository on disk. + +Over time, this local cache of repositories can grow large, as Hipcheck does +not do any automation cleanup of prior repositories stored there. This is +intended to make it easier to re-analyze existing repositories, as Hipcheck +will merely pull the latest changes from a repository which has been +analyzed before and remains in the repository cache. + +The following is the CLI help text for `hc cache`: + +``` +Manage Hipcheck cache + +Usage: hc cache [OPTIONS] + +Commands: + list List existing caches + delete Delete existing caches + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help (see more with '--help') + +Output Flags: + -v, --verbosity How verbose to be [possible values: quiet, normal] + -k, --color When to use color [possible values: always, never, auto] + -f, --format What format to use [possible values: json, human] + +Path Flags: + -C, --cache Path to the cache folder + -p, --policy Path to the policy file +``` + +As shown, this allows the user to list the items currently found in the cache, +and to delete specific items. + +## `hc cache list` + +The following is the help text for `hc cache list`: + +``` +List existing caches + +Usage: hc cache list [OPTIONS] + +Options: + -s, --strategy Sorting strategy for the list, default is 'alpha' [default: alpha] [possible values: oldest, newest, largest, smallest, alpha, ralpha] + -m, --max Max number of entries to display + -P, --pattern Consider only entries matching this pattern + -h, --help Print help (see more with '--help') + +Output Flags: + -v, --verbosity How verbose to be [possible values: quiet, normal] + -k, --color When to use color [possible values: always, never, auto] + -f, --format What format to use [possible values: json, human] + +Path Flags: + -C, --cache Path to the cache folder + -p, --policy Path to the policy file +``` + +This by default lists all entries found in the repository cache. Those entries +can be filtered, sorted, and a maximum number to show can be set. The pattern +defines a prefix pattern to search for when filtering repositories. The +strategy defines how sorting should be done, and supports the following +options: + +| Strategy | What It Does | +|:-----------|:-------------------------------| +| `oldest` | Sort from oldest to newest. | +| `newest` | Sort from newest to oldest. | +| `largest` | Sort from largest to smallest. | +| `smallest` | Sort from smallest to largest. | +| `alpha` | Sort alphabetically. | +| `ralpha` | Sort reverse-alphabetically. | + +## `hc cache delete` + +`hc cache delete` is for deleting entries from the repository cache. The +help text for it is: + +``` +Delete existing caches + +Usage: hc cache delete [OPTIONS] + +Options: + -s, --strategy ... Sorting strategy for deletion. Args of the form 'all|{ [N]}'. Where is the same set of strategies for `hc cache list`. If [N], the max number of entries to delete is omitted, it will default to 1 + -P, --pattern Consider only entries matching this pattern + --force Do not prompt user to confirm the entries to delete + -h, --help Print help (see more with '--help') + +Output Flags: + -v, --verbosity How verbose to be [possible values: quiet, normal] + -k, --color When to use color [possible values: always, never, auto] + -f, --format What format to use [possible values: json, human] + +Path Flags: + -C, --cache Path to the cache folder + -p, --policy Path to the policy file +``` + +The same `pattern` and `strategy` flags apply to this command. By default it +will prompt the user to confirm before deleting; this can be overriden with the +`--force` flag. diff --git a/site/content/docs/guide/cli/hc-check.md b/site/content/docs/guide/cli/hc-check.md new file mode 100644 index 00000000..3dd44d28 --- /dev/null +++ b/site/content/docs/guide/cli/hc-check.md @@ -0,0 +1,63 @@ +--- +title: hc check +extra: + nav_title: "hc check" +--- + +# `hc check` + +`hc check` is the primary command that users of Hipcheck will run. It's the +command for running analyses of target packages (for more information on how +to specify "targets," see [the "Targets" documentation][target]). + +The short help text for `hc check` looks like this: + +``` +Analyze a package, source repository, SBOM, or pull request + +Usage: hc check [OPTIONS] + +Arguments: + The target package, URL, commit, etc. for Hipcheck to analyze. If ambiguous, the -t flag must be set + +Options: + -t, --target [possible values: maven, npm, pypi, repo, request, spdx] + -h, --help Print help (see more with '--help') + +Output Flags: + -v, --verbosity How verbose to be [possible values: quiet, normal] + -k, --color When to use color [possible values: always, never, auto] + -f, --format What format to use [possible values: json, human] + +Path Flags: + -c, --config Path to the configuration folder + -d, --data Path to the data folder + -C, --cache Path to the cache folder +``` + +The only positional argument is the ``, as explains in [the Targets +documentation][target]. This argument is _required_, and tells Hipcheck what to +analyze. + +It is possible for a target specifier to be ambiguous. For example, Hipcheck +accepts targets of the form `[@]`. In this case, +it's not clear from the target specifier what package host this package is +supposed to be hosted on. In these ambiguous cases, the user needs to specify +the __target type__ with the `-t`/`--type` flag. The full list of current types +is: + +- `maven`: A package on Maven Central +- `npm`: A package on NPM +- `pypi`: A package on PyPI +- `repo`: A Git repository +- `spdx`: An SPDX document + +If you attempt to run `hc check` with an ambiguous target specifier, Hipcheck +will produce an error telling you to use the `-t`/`--target` flag to manually +specify the target type. + +Besides this flag, all other flags are general flags which Hipcheck accepts +for every command. See [General Flags](@/docs/guide/cli/general-flags.md) +for more information. + +[target]: @/docs/guide/concepts/targets.md diff --git a/site/content/docs/guide/cli/hc-ready.md b/site/content/docs/guide/cli/hc-ready.md new file mode 100644 index 00000000..e53064ad --- /dev/null +++ b/site/content/docs/guide/cli/hc-ready.md @@ -0,0 +1,28 @@ +--- +title: hc ready +extra: + nav_title: "hc ready" +--- + +# `hc ready` + +`hc ready` is a command for checking that Hipcheck is ready to run analyses. +This is intended to help the user debug issues with a Hipcheck installation, +including problems like missing configuration files, inaccessible config, +data, or cache paths, missing authentication tokens, and more. + +`hc ready` has no special flags currently, and only accepts the +[General Flags](@/docs/guide/cli/general-flags.md) that _all_ Hipcheck +commands accept. + +The output of `hc ready` is a report containing key information about +Hipcheck, the third-party tools it relies on as external data sources, +the paths it's currently using for configuration files, data files, +and local repository clones, along with any API tokens it will use +for external API access. + +If all required information is found and passes requirements for Hipcheck +to run, it will report that Hipcheck is ready to run. + +We recommend running `hc ready` before running Hipcheck the first time, +and as a good first debugging step if Hipcheck begins reporting issues. diff --git a/site/content/docs/guide/cli/hc-schema.md b/site/content/docs/guide/cli/hc-schema.md new file mode 100644 index 00000000..a1d92658 --- /dev/null +++ b/site/content/docs/guide/cli/hc-schema.md @@ -0,0 +1,17 @@ +--- +title: hc schema +extra: + nav_title: "hc schema" +--- + +# `hc schema` + +The `hc schema` command is intended to help users of Hipcheck who are trying +to integrate Hipcheck into other tools and systems. Hipcheck supports a JSON +output format for analyses, and `hc schema` produces a JSON schema description +of that output. + +`hc schema` takes the name of the target type for which to print the schema. +For the list of target types, see [the documentation for the `hc check` command](@/docs/guide/cli/hc-check.md). + +`hc schema` also takes the usual [General Flags](@/docs/guide/cli/general-flags.md). diff --git a/site/content/docs/guide/cli/hc-scoring.md b/site/content/docs/guide/cli/hc-scoring.md new file mode 100644 index 00000000..8264a888 --- /dev/null +++ b/site/content/docs/guide/cli/hc-scoring.md @@ -0,0 +1,54 @@ +--- +title: hc scoring +extra: + nav_title: "hc scoring" +--- + +# `hc scoring` + +Hipcheck's scoring system works by calculating percentages for how much each +analysis in the user's configured analysis tree contributes to the overall +score, based on weights users set for each analysis and category. + +The `hc scoring` command takes that configured tree and weights, calculates +scoring percentages, and displays them to the user to make it clear how their +current policies will be converted to scores based on the results of a run +of analyses. + +The help text looks like: + +``` +Print the tree used to weight analyses during scoring + +Usage: hc scoring [OPTIONS] + +Options: + -h, --help Print help (see more with '--help') + +Output Flags: + -v, --verbosity How verbose to be [possible values: quiet, normal] + -k, --color When to use color [possible values: always, never, auto] + -f, --format What format to use [possible values: json, human] + +Path Flags: + -C, --cache Path to the cache folder + -p, --policy Path to the policy file +``` + +The following is an example output: + +``` +risk +|-- practices +| |-- mitre::activity: 10.00% +| |-- mitre::binary: 10.00% +| |-- mitre::fuzz: 10.00% +| |-- mitre::identity: 10.00% +| `-- mitre::review: 10.00% +`-- attacks + |-- mitre::typo: 25.00% + `-- commit + |-- mitre::affiliation: 8.33% + |-- mitre::churn: 8.33% + `-- mitre::entropy: 8.33% +``` diff --git a/site/content/docs/guide/cli/hc-setup.md b/site/content/docs/guide/cli/hc-setup.md new file mode 100644 index 00000000..8664b6a3 --- /dev/null +++ b/site/content/docs/guide/cli/hc-setup.md @@ -0,0 +1,33 @@ +--- +title: hc setup +extra: + nav_title: "hc setup" +--- + +# `hc setup` + +The `hc setup` command is intended to be run after first installing Hipcheck, +and again after updating Hipcheck, to ensure you have the required configuration +and data files needed for Hipcheck to run. + +When installing Hipcheck, regardless of method, you are only installing the +`hc` binary, not these additional files. `hc setup` gathers those files for you, +and installs them into the appropriate locations. + +If Hipcheck has been installed with the recommended install scripts included +with each release, then the correct configuration and data files for each +version are included with the bundle downloaded by that script. In that case, +`hc setup` will attempt to find those files locally and copy them into the +configuration and data directories. + +If Hipcheck was installed via another method, or the files can't be found, +then `hc setup` will attempt to download them from the appropriate Hipcheck +release. Users can pass the `-o`/`--offline` flag to ensure `hc setup` does +_not_ use the network to download materials, in which case `hc setup` will +fail if the files can't be found locally. + +The installation directories for the configuration and data files are +specified in the way they're normally specified. For more information, +see the documentation on Hipcheck's [Path Flags](@/docs/guide/cli/general-flags.md#path-flags). + +`hc setup` also supports Hipcheck's [General Flags](@/docs/guide/cli/general-flags.md). diff --git a/site/content/docs/guide/cli/hc-update.md b/site/content/docs/guide/cli/hc-update.md new file mode 100644 index 00000000..3b0d7f60 --- /dev/null +++ b/site/content/docs/guide/cli/hc-update.md @@ -0,0 +1,36 @@ +--- +title: hc update +extra: + nav_title: "hc update" +--- + +# `hc update` + +When Hipcheck is installed using the recommend install scripts provided with +each release, the install scripts also provide an "updater" program, built +by `cargo-dist` (which Hipcheck uses to handle creating prebuilt artifacts +with each release, and with announcing each release on GitHub Releases). +This updater program handles checking for a newer version of Hipcheck, +downloading it, and replacing the current version with the newer one. +The updater is provided as a separate binary (named either `hc-update` +or `hipcheck-update`, due to historic bugs). + +The `hc update` command simply delegates to this separate update program, +and provides the same interface that this separate update program does. +In general, you only need to run `hc update` with no arguments, followed +by `hc setup` to ensure you have the latest configuration and data files. + +If you want to specifically download a version besides the most recent +version of Hipcheck, you can use the following flags: + +- `--tag `: install the version from this specific Git tag. +- `--version `: install the version from this specific GitHub + release. +- `--prerelease`: permit installing a pre-release version when updating + to `latest`, + +The `--tag` and `--version` flags are mutually exclusive; the updater will +produce an error if both are provided. + +This command also supports Hipcheck's [General Flags](@/docs/guide/cli/general-flags.md), though +they are ignored. diff --git a/site/content/docs/guide/concepts/_index.md b/site/content/docs/guide/concepts/_index.md new file mode 100644 index 00000000..fb73191b --- /dev/null +++ b/site/content/docs/guide/concepts/_index.md @@ -0,0 +1,37 @@ +--- +title: Key Concepts +template: docs.html +page_template: docs_page.html +sort_by: weight +weight: 1 +--- + +# Key Concepts + +To understand Hipcheck, it's useful to understand some of the key concepts +underlying its design, which we'll explore here. + + +
+ +{% waypoint(title="Targets", path="@/docs/guide/concepts/targets.md", icon="target") %} +How Hipcheck identifies what package or project to analyze. +{% end %} + +{% waypoint(title="Data", path="@/docs/guide/concepts/data/index.md", icon="database") %} +How Hipcheck collects data from external sources. +{% end %} + +{% waypoint(title="Analyses", path="@/docs/guide/concepts/analyses.md", icon="alert-triangle") %} +What kinds of analyses Hipcheck is focused on. +{% end %} + +{% waypoint(title="Scoring", path="@/docs/guide/concepts/scoring/index.md", icon="activity") %} +How Hipcheck converts individual analysis results into a risk score. +{% end %} + +{% waypoint(title="Concerns", path="@/docs/guide/concepts/concerns.md", icon="list") %} +How plugins report extra information to support manual analysis. +{% end %} + +
diff --git a/site/content/docs/guide/concepts/analyses.md b/site/content/docs/guide/concepts/analyses.md new file mode 100644 index 00000000..a69e8c55 --- /dev/null +++ b/site/content/docs/guide/concepts/analyses.md @@ -0,0 +1,97 @@ +--- +title: Analyses +weight: 3 +--- + +# Analyses + +As suggested in the section on data, _analyses_ in Hipcheck are +about computations performed on the data Hipcheck collects, with the purpose +of producing _measurements_ about that data to which policies can be applied. + +In general, analyses can be grouped into two broad categories: + +- __Practice__: Analyses which assess the software development practices a + project follows. +- __Attack__: Analyses which try to detect active software supply chain + attacks. + +To understand these, it's useful to ask: what is software supply chain risk? +In general, we understand software supply chain risk to be the collection of +risks associated with adopting third-party software dependencies. This may +include: + +- __Intellectual property risk__: The risk that a project may re-license in + a manner that prohibits or raises the cost of its use, may introduce + trademarks which limit its use, may introduce patents which limit its use, + or may fall victim to any intellectual-property-related issues with its + own contributors or dependencies (for example, contributors revoking the + license of their own prior contributions, or an outside party asserting that + contributions made violate that party's own intellectual property rights; + see the [SCO-Linux disputes][sco] for an example of this kind of problem). +- __Vulnerability risk__: The risk that a project may introduce vulnerabilities + into its users. In general, we expect software of any kind to have defects, + and use _assurance_ techniques like code review, testing, code analysis, + and more to identify and remove defects, and thereby reduce code weaknesses + and vulnerabilities in shipped code. + +{% info(title="Weaknesses and Vulnerabilities") %} +It's worthwhile to be precise about "weaknesses" and "vulnerabilities" in +software. Both are important, but the distinction matters. To explain, we will +borrow definitions from the Common Weakness Enumeration (CWE) and Common +Vulnerabilities and Exposures (CVE) programs. CWE is a program for enumerating +a taxonomy of known software and hardware weakness types. CVE is a program for +tracking known software vulnerabilities. + +Definition of "weakness": + +> A 'weakness' is a condition in a software, firmware, hardware, or service +> component that, under certain circumstances, could contribute to the +> introduction of vulnerabilities. +> — [Common Weakess Enumeration](https://cwe.mitre.org/about/index.html) + +Definition of "vulnerability": + +> An instance of one or more weaknesses in a Product that can be exploited, +> causing a negative impact to confidentiality, integrity, or availability; +> a set of conditions or behaviors that allows the violation of an explicit +> or implicit security policy. +> — [Common Vulnerabilities & Exposures](https://www.cve.org/ResourcesSupport/Glossary?activeTerm=glossaryVulnerability) +{% end %} + + +- __Supply chain attack risk__: The risk that a project may become the victim + of a supply chain attack. These attacks exist on spectrums of targeting and + sophistication, from extremes like the generally unsophisticated and + untargeted [typosquatting attack](https://arxiv.org/pdf/2005.09535), to the + highly sophisticated and highly targeted + ["xz-utils" backdoor](https://en.wikipedia.org/wiki/XZ_Utils_backdoor). + +In general, Hipcheck is _not_ concerned with intellectual-property risks, +as there exist many tools today that effectively extract licensing information +for open source software, analyze those licenses for compatibility and +compliance requirements, and report back to users to ensure users avoid +violating the terms of licenses and meet their compliance obligations. We do +not believe there's significant value for Hipcheck to re-implement these +same analyses. + +However, Hipcheck _does_ care about vulnerability risk, which is what the +"practice" analyses are concerned with, and about supply chain attack risk, +which is the concern of the "attack" analyses. + +In general, we believe that _most_ open source software will not be the +victim of supply chain _attacks_, at least currently. This may change in the +future if open source software supply chain attacks continue to become +more common. To quote the paper ["Backstabber’s Knife Collection: A Review of +Open Source Software Supply Chain Attacks"](https://arxiv.org/pdf/2005.09535) +by Ohm, Plate, Sykosch, and Meier: + +> From an attacker’s point of view, package repositories represent a reliable +> and scalable malware distribution channel. + +However, in the current landscape, users of open source software dependencies +are rightfully more concerned with the risk that their dependencies will +include vulnerabilities which have to be managed and responded to in the +future. This is what "practice" analyses intend to assess. + +[sco]: https://en.wikipedia.org/wiki/SCO%E2%80%93Linux_disputes diff --git a/site/content/docs/guide/concepts/concerns.md b/site/content/docs/guide/concepts/concerns.md new file mode 100644 index 00000000..f96f7a4f --- /dev/null +++ b/site/content/docs/guide/concepts/concerns.md @@ -0,0 +1,33 @@ +--- +title: Concerns +weight: 6 +--- + +# Concerns + +Besides a risk score and a recommendation, Hipcheck's other major output +is a list of "concerns" from individual analyses. "Concerns" are Hipcheck's +mechanism for analyses to report specific information about what they found +that they think the user may be interested in knowing and possibly +investigating further. For example, some of Hipcheck's analyses that work +on individual commits will produce the hashes of commits they find concerning, +so users can audit those commits by hand if they want to do so. + +Concerns are the most flexible mechanism Hipcheck has, as they are essentially +a way for analyses to report freeform text out to the user. They do not have +a specific structured format, and they are not considered at all for the +purpose of scoring. The specific concerns which may be reported vary from +analysis to analysis. + +In general, we want analyses to report concerns wherever possible. For +some analyses, there may not be a reasonable type of concern to report; +for example, the "activity" analysis checks the date of the most recent +commit to a project to see if the project appears "active," and the +_only_ fact that it's assessing is also the fact which results in the +measure the analysis produces, so there's not anything sensible for +the analysis to report as a concern. + +However, many analysis _do_ have meaningful concerns they can report, and if an +analysis _could_ report a type of concern but _doesn't_, we consider that +something worth changing. Contributions to make Hipcheck report more concerns, +or to make existing concerns more meaningful, are [always appreciated](@/docs/contributing/_index.md)! diff --git a/site/content/docs/guide/concepts/concepts-diagram.svg b/site/content/docs/guide/concepts/data/concepts-diagram.svg similarity index 100% rename from site/content/docs/guide/concepts/concepts-diagram.svg rename to site/content/docs/guide/concepts/data/concepts-diagram.svg diff --git a/site/content/docs/guide/concepts/data/index.md b/site/content/docs/guide/concepts/data/index.md new file mode 100644 index 00000000..6120dbf9 --- /dev/null +++ b/site/content/docs/guide/concepts/data/index.md @@ -0,0 +1,36 @@ +--- +title: Data +weight: 2 +template: docs_page.html +--- + +# Data + +To analyze packages, Hipcheck needs to gather data about those packages. +That data can come from a variety of sources, including: + +- Git commit histories +- The GitHub API or, in the future, similar source repository platform + APIs +- Package host APIs like the NPM API + +Each of these sources store information about the history of a project, +which may be relevant for understanding the _practices_ associated with +the code's development, or for detecting possible activate supply chain +_attacks_. + +Hipcheck tries to cleanly distinguish between _data_, _analyses_, and +_configuration_. _Data_ is the raw pieces of information pulled from +exterior sources. It is solely factual, recording prior events. +_Analyses_ are computations performed on data which produce _measures_, +and which may also produce _concerns_. Finally, _configuration_ is an +expression of the user's policy, which turns the _measures_ produced +by analyses into a _score_ and a _recommendation_. This is perhaps +easier to see in a diagram. + +![Concepts diagram](concepts-diagram.svg) + +With this structure, Hipcheck tries to cleanly separate the parts +that are _factual_, from the parts that are _measuring_ facts, and +from the parts that are applying subjective policies on those +measurements. diff --git a/site/content/docs/guide/concepts/index.md b/site/content/docs/guide/concepts/index.md deleted file mode 100644 index 4bd8aa8c..00000000 --- a/site/content/docs/guide/concepts/index.md +++ /dev/null @@ -1,415 +0,0 @@ ---- -title: Key Concepts ---- - -# Key Concepts - -To understand Hipcheck, it's useful to understand some of the key concepts -underlying its design, which we'll explore here. - -In this section we'll discuss: - -- [Targets](#targets) -- [Data](#data) -- [Analyses](#analyses) -- [Scoring](#scoring) -- [Concerns](#concerns) - -## Targets - -__Targets__ are Hipcheck's term for "things that Hipcheck analyzes," and -they are what you specify with the positional argument in the `hc check` -command. Generally, targets are intended to specify _something that leads -to a source repository_, which can seem like a vague concept. - -More concretely, targets can be: - -- A Git source repository URL or local path -- A package name and optional version (perhaps requiring you to specify the - package host) -- An SPDX software bill of materials (SBOM) file with a source repository - reference for the main package in it - -Let's break each of those down in turn. - -### Git Source Repository URL or Local Path - -Hipcheck's central focus for analysis is a project's _source repository_, -because Hipcheck cares about analyzing the metadata associated with a project's -development (see [Why Hipcheck?](@/docs/guide/why.md) for more information). -Giving Hipcheck the source repository URL or local path is the most _direct_ -way of telling Hipcheck what you want it to analyze. When you specify other -types of targets, Hipcheck will see if it can find a reference to a source -repository from those targets, and will produce an error if it can't. - -Specifying a Git source repository looks like: - -```sh -$ # For a remote Git repository. -$ hc check https://github.com/mitre/hipcheck -$ # For an existing local Git repository. -$ hc check ~/Projects/hipcheck -``` - -When Hipcheck is given a remote URL for a Git repository, it will clone -that repository into a local directory, as analyses on local data are _much_ -faster than trying to gather data across the network. - -{% info(title="Hipcheck's Storage Paths") %} -Hipcheck uses three directories to store important materials it needs to -run. Each can be specified by a command line flag, by an environment -variable, or inferred from the user's current platform (in decreasing -priority). Each directory serves a specific purpose: - -- The Config directory: stores Hipcheck's configuration files. -- The Data directory: stores Hipcheck's helper scripts needed for running - additional external tools Hipcheck relies on. -- The Cache directory: stores local clones of repositories Hipcheck is - analyzing. - -Of these, the Cache directory is one that has a tendency to grow as your -use of Hipcheck continues. Some Git repositories, especially those for -long-running and very active projects, can be quite large. In the future, -we [plan to augment Hipcheck with tooling for better managing this -cache directory](https://github.com/mitre/hipcheck/issues/182). -{% end %} - -In general, Hipcheck tries to ensure it ends up with both a _local path_ -for a repository (either because the user specified a local repository -in the CLI, or by cloning a remote repository to a local cache) and -a _remote URL_. If the user provided a remote URL directly, that's not -a problem. If the user provided a local path, then Hipcheck tries to -infer the upstream repository by seeing if the default branch has an -upstream remote branch configured. If it does, then Hipcheck records -that as the remote branch for the local repository. - -Hipcheck does this because some analyses rely on APIs provided by -specific source repository hosts. Today, only GitHub is supported, -but we'd like to add support for more source repository APIs in -the future. If the user provides a GitHub source repository URL, -or a local repository path from which a remote GitHub URL can be -inferred, then the GitHub-specific analyses will be able to run. - -### Package Name and Optional Version - -Users can also specify targets as a package name and version from some -popular open source package repositories. Today Hipcheck supports packages -on NPM (JavaScript), PyPI (Python), and Maven Central (Java). We'd like to -expand that support to more platforms in the future. - -Packages from these hosts may be specified as package names, with optional -versions. When specifying one of these targets, it is not sufficient to -specify just the package name, you'll need to use the `-t`/`--type` flag to -specify the package host. For example: - -```sh -$ hc check --type npm chalk@5.3.0 -$ hc check --type pypi numpy@2.0.0 -$ hc check --type maven commons-csv@1.11.0 -``` - -Without specifying the platform, Hipcheck will be unable to determine -what package is being specified, and will produce an error. - -For each of these types of targets, Hipcheck will then try to identify a -source repository associated with the package in the package's metadata. -The specific method of doing this differs depending on the platform. -Some provide a standard mechanism for specifying the source repository, -and some don't. For those that don't though, there are generally common -norms for how that information is provided, so Hipcheck can often still -identify the source repository in one of the common locations. - -When the source repository is discovered, it is handled in the same way -as if it had been provided as the target directly by the user. See -this page's [Git Source Repository URL or Local Path](#git-source-repository-url-or-local-path) -section for more information. - -### SPDX Software Bill of Materials - -Finally, Hipcheck can accept SPDX version 2 Software Bills of Material (SBOM) -files, in the JSON or key-value text formats. SPDX is a popular format for -specifying Software Bills of Materials, meaning it contains information about -a package and the package's dependencies. - -Running Hipcheck on an SPDX SBOM looks like: - -```sh -$ hc check my-package.spdx.json -``` - -Today, Hipcheck only supports the SPDX 2.3 SBOM format, though we'd like to -add support for more formats, and for SPDX 3.0, in the future. - -When provided with an SBOM, Hipcheck parses the file to identify the "root" -package being specified, and tries to infer any source repository information -for that package. If it is unable to identify a source repository for the -package being described, it produces an error. If it _can_ identify a source -repository, that repository is processed as if the user specified it directly -on the command line (see this page's [Git Source Repository URL or Local Path](#git-source-repository-url-or-local-path) -section for more information). - -When provided with an SBOM, Hipcheck today _does not_ separately analyze -each of the dependencies specified in the SBOM. Rather, it _only_ analyzes -the root package. If you'd like to analyze each of the dependencies in the -SBOM, you'll need to call Hipcheck separately for each of them. - -## Data - -To analyze packages, Hipcheck needs to gather data about those packages. -That data can come from a variety of sources, including: - -- Git commit histories -- The GitHub API or, in the future, similar source repository platform - APIs -- Package host APIs like the NPM API - -Each of these sources store information about the history of a project, -which may be relevant for understanding the _practices_ associated with -the code's development, or for detecting possible activate supply chain -_attacks_. - -Hipcheck tries to cleanly distinguish between _data_, _analyses_, and -_configuration_. _Data_ is the raw pieces of information pulled from -exterior sources. It is solely factual, recording prior events. -_Analyses_ are computations performed on data which produce _measures_, -and which may also produce _concerns_. Finally, _configuration_ is an -expression of the user's policy, which turns the _measures_ produced -by analyses into a _score_ and a _recommendation_. This is perhaps -easier to see in a diagram. - -![Concepts diagram](concepts-diagram.svg) - -With this structure, Hipcheck tries to cleanly separate the parts -that are _factual_, from the parts that are _measuring_ facts, and -from the parts that are applying subjective policies on those -measurements. - -## Analyses - -As suggested in the section on data, _analyses_ in Hipcheck are -about computations performed on the data Hipcheck collects, with the purpose -of producing _measurements_ about that data to which policies can be applied. - -Hipcheck currently includes a number of built-in analyses, which are described -more fully in the [Analyses](@/docs/guide/analyses.md) documentation. In -general, these analyses can be grouped into two broad categories: - -- __Practice__: Analyses which assess the software development practices a - project follows. -- __Attack__: Analyses which try to detect active software supply chain - attacks. - -To understand these, it's useful to ask: what is software supply chain risk? -In general, we understand software supply chain risk to be the collection of -risks associated with adopting third-party software dependencies. This may -include: - -- __Intellectual property risk__: The risk that a project may re-license in - a manner that prohibits or raises the cost of its use, may introduce - trademarks which limit its use, may introduce patents which limit its use, - or may fall victim to any intellectual-property-related issues with its - own contributors or dependencies (for example, contributors revoking the - license of their own prior contributions, or an outside party asserting that - contributions made violate that party's own intellectual property rights; - see the [SCO-Linux disputes][sco] for an example of this kind of problem). -- __Vulnerability risk__: The risk that a project may introduce vulnerabilities - into its users. In general, we expect software of any kind to have defects, - and use _assurance_ techniques like code review, testing, code analysis, - and more to identify and remove defects, and thereby reduce code weaknesses - and vulnerabilities in shipped code. - -{% info(title="Weaknesses and Vulnerabilities") %} -It's worthwhile to be precise about "weaknesses" and "vulnerabilities" in -software. Both are important, but the distinction matters. To explain, we will -borrow definitions from the Common Weakness Enumeration (CWE) and Common -Vulnerabilities and Exposures (CVE) programs. CWE is a program for enumerating -a taxonomy of known software and hardware weakness types. CVE is a program for -tracking known software vulnerabilities. - -Definition of "weakness": - -> A 'weakness' is a condition in a software, firmware, hardware, or service -> component that, under certain circumstances, could contribute to the -> introduction of vulnerabilities. -> — [Common Weakess Enumeration](https://cwe.mitre.org/about/index.html) - -Definition of "vulnerability": - -> An instance of one or more weaknesses in a Product that can be exploited, -> causing a negative impact to confidentiality, integrity, or availability; -> a set of conditions or behaviors that allows the violation of an explicit -> or implicit security policy. -> — [Common Vulnerabilities & Exposures](https://www.cve.org/ResourcesSupport/Glossary?activeTerm=glossaryVulnerability) -{% end %} - - -- __Supply chain attack risk__: The risk that a project may become the victim - of a supply chain attack. These attacks exist on spectrums of targeting and - sophistication, from extremes like the generally unsophisticated and - untargeted [typosquatting attack](https://arxiv.org/pdf/2005.09535), to the - highly sophisticated and highly targeted - ["xz-utils" backdoor](https://en.wikipedia.org/wiki/XZ_Utils_backdoor). - -In general, Hipcheck is _not_ concerned with intellectual-property risks, -as there exist many tools today that effectively extract licensing information -for open source software, analyze those licenses for compatibility and -compliance requirements, and report back to users to ensure users avoid -violating the terms of licenses and meet their compliance obligations. We do -not believe there's significant value for Hipcheck to re-implement these -same analyses. - -However, Hipcheck _does_ care about vulnerability risk, which is what the -"practice" analyses are concerned with, and about supply chain attack risk, -which is the concern of the "attack" analyses. - -In general, we believe that _most_ open source software will not be the -victim of supply chain _attacks_, at least currently. This may change in the -future if open source software supply chain attacks continue to become -more common. To quote the paper ["Backstabber’s Knife Collection: A Review of -Open Source Software Supply Chain Attacks"](https://arxiv.org/pdf/2005.09535) -by Ohm, Plate, Sykosch, and Meier: - -> From an attacker’s point of view, package repositories represent a reliable -> and scalable malware distribution channel. - -However, in the current landscape, users of open source software dependencies -are rightfully more concerned with the risk that their dependencies will -include vulnerabilities which have to be managed and responded to in the -future. This is what "practice" analyses intend to assess. - -[sco]: https://en.wikipedia.org/wiki/SCO%E2%80%93Linux_disputes - -## Scoring - -To go from individual analysis results to a final recommendation to a user, -Hipcheck combines the results from those analyses in a process called -"scoring." The basic idea of scoring is simple: each analysis produces -a measurement, and that measurement is compared against a policy defined by -the user's configuration. If the measurement passes the policy, the score -is a `0`. If it does _not_ pass the policy, the score is a `1`. Those -individual-analysis scores are then combined in a defined process to -produce an overall "risk score." This risk score is compared to the user's -configured "risk tolerance." If the risk score is less than or equal to -the tolerance, Hipcheck's recommendation is "PASS," meaning the target -being analyzed is considered sufficiently low risk, and can be used. If the -risk score is greater than the risk tolerance, Hipcheck's recommendation is -"INVESTIGATE," meaning the user should manually assess the target software -before using it. - -{% info(title="Hipcheck Never Recommends Not Using a Package") %} -It's important to note that Hipcheck's only two possible recommendations -are "PASS" and "INVESTIGATE". Hipcheck will _never_ recommend not using -something without further investigation. This is because Hipcheck can't -assume that it's own analyses are infallible, and the final decision about -whether _not_ to use something should therefore always be made by a human. - -There are benign reasons why individual analyses may fail. For example, -checks for whether a project is actively maintained may fail because the -last commit was too long ago. It may be that this is true because the project -does not need to be updated. Maybe it is feature complete and has not had -to respond to vulnerabilities or changes in its own dependencies, and so it -has remained stable and reliable. In this case, lack of updates isn't a -_negative_ signal about the software not being maintained, but is instead a -_positive_ signal about the software being high quality and complete. - -We believe that, in general, a confluence of multiple failed analyses resulting -in a high risk score is a good signal of concern about a target of analysis, -but we will never assume that a high risk score means the project is actually -high risk and must categorically be avoided. - -When Hipcheck recommends "INVESTIGATE," we do so to signal to users that the -targeted software is concerning, and we try to provide specific information -whenever possible about _what_ concerns Hipcheck has identified. Our goal is -to empower users to make informed decisions, not to substitute our own -perspective for theirs. -{% end %} - -The process for turning individual analyses scores into the final risk score -is worth breaking down in greater detail. To understand it, we'll need to -explain the "Score Tree" concept Hipcheck uses. - -Within Hipcheck, analyses are organized into a tree structure, with similar -analyses grouped into categories as children of a shared node. The full -state of the current Hipcheck score tree looks like this: - -![Score Tree](score-tree.svg) - -As you can see, each analysis has an associated _weight_, in this case each -weight defaults to 1. These weights are configurable, and allow users to -change how much influence the results of individual analyses have on the -overall risk score calculation. To perform the final risk score reducation, -these weights are converted into percentages by summing up the weights of -children of the same node, and dividing each child's weight by that sum. -In the case of the score tree shown above, that would result in the following -percentages: - -![Score Tree Percentages](score-tree-pct.svg) - -From this, we can calculate the percentages of the total risk score to apply -for each individual analysis (the leaf nodes of the tree) by multiplying -down from the root to each leaf node, resulting in the following final -percentages per-analysis: - -![Score Tree Final](score-tree-final.svg) - -So with this score tree, and the results of individual analyses, we can then -calculate a risk score. The following table summarizes the results of that -calculation in this example: - -| Analysis | Result | Score | Weight | Analysis Risk Score | -|:------------|:-------|:------|:-------|:--------------------| -| Activity | Pass | 0 | 0.10 | 0 | -| Binary | Fail | 1 | 0.10 | 0.1000 | -| Fuzz | Fail | 1 | 0.10 | 0.1000 | -| Identity | Pass | 0 | 0.10 | 0 | -| Review | Fail | 1 | 0.10 | 0.1000 | -| Typo | Pass | 0 | 0.25 | 0 | -| Affiliation | Pass | 0 | 0.1665 | 0 | -| Churn | Fail | 1 | 0.1665 | 0.1665 | -| Entropy | Pass | 0 | 0.1665 | 0 | -| __Total__ | | | | 0.4665 | - -So in this case, with that configured score tree and the specific analysis -results, the overall risk score would be __0.4665__. - -If the user's configured risk threshold is __0.5__ (which is currently -the default risk threshold), this would result in a "PASS" recommendaton. -if the risk threshold were lower than the risk score, for example if it -were __0.3__, then this would result in an "INVESTIGATE" recommendation. - -Similarly, if users wanted to prioritize or deprioritize specific analyses, -they could change the configured weights for those analyses to be lower or -higher. - -That's how scoring works in Hipcheck! - -## Concerns - -Besides a risk score and a recommendation, Hipcheck's other major output -is a list of "concerns" from individual analyses. "Concerns" are Hipcheck's -mechanism for analyses to report specific information about what they found -that they think the user may be interested in knowing and possibly -investigating further. For example, some of Hipcheck's analyses that work -on individual commits will produce the hashes of commits they find concerning, -so users can audit those commits by hand if they want to do so. - -Concerns are the most flexible mechanism Hipcheck has, as they are essentially -a way for analyses to report freeform text out to the user. They do not have -a specific structured format, and they are not considered at all for the -purpose of scoring. The specific concerns which may be reported vary from -analysis to analysis. - -In general, we want analyses to report concerns wherever possible. For -some analyses, there may not be a reasonable type of concern to report; -for example, the "activity" analysis checks the date of the most recent -commit to a project to see if the project appears "active," and the -_only_ fact that it's assessing is also the fact which results in the -measure the analysis produces, so there's not anything sensible for -the analysis to report as a concern. - -However, many analysis _do_ have meaningful concerns they can report, and if an -analysis _could_ report a type of concern but _doesn't_, we consider that -something worth changing. Contributions to make Hipcheck report more concerns, -or to make existing concerns more meaningful, are [always appreciated](@/contribute/_index.md)! - -{{ button(link="@/docs/guide/how-to-use.md", text="How to Use Hipcheck") }} diff --git a/site/content/docs/guide/concepts/scoring/index.md b/site/content/docs/guide/concepts/scoring/index.md new file mode 100644 index 00000000..9d3268ee --- /dev/null +++ b/site/content/docs/guide/concepts/scoring/index.md @@ -0,0 +1,109 @@ +--- +title: Scoring +weight: 4 +template: docs_page.html +--- + +# Scoring + +To go from individual analysis results to a final recommendation to a user, +Hipcheck combines the results from those analyses in a process called +"scoring." The basic idea of scoring is simple: each analysis produces +a measurement, and that measurement is compared against a policy defined by +the user's configuration. If the measurement passes the policy, the score +is a `0`. If it does _not_ pass the policy, the score is a `1`. Those +individual-analysis scores are then combined in a defined process to +produce an overall "risk score." This risk score is compared to the user's +configured "risk tolerance." If the risk score is less than or equal to +the tolerance, Hipcheck's recommendation is "PASS," meaning the target +being analyzed is considered sufficiently low risk, and can be used. If the +risk score is greater than the risk tolerance, Hipcheck's recommendation is +"INVESTIGATE," meaning the user should manually assess the target software +before using it. + +{% info(title="Hipcheck Never Recommends Not Using a Package") %} +It's important to note that Hipcheck's only two possible recommendations +are "PASS" and "INVESTIGATE". Hipcheck will _never_ recommend not using +something without further investigation. This is because Hipcheck can't +assume that it's own analyses are infallible, and the final decision about +whether _not_ to use something should therefore always be made by a human. + +There are benign reasons why individual analyses may fail. For example, +checks for whether a project is actively maintained may fail because the +last commit was too long ago. It may be that this is true because the project +does not need to be updated. Maybe it is feature complete and has not had +to respond to vulnerabilities or changes in its own dependencies, and so it +has remained stable and reliable. In this case, lack of updates isn't a +_negative_ signal about the software not being maintained, but is instead a +_positive_ signal about the software being high quality and complete. + +We believe that, in general, a confluence of multiple failed analyses resulting +in a high risk score is a good signal of concern about a target of analysis, +but we will never assume that a high risk score means the project is actually +high risk and must categorically be avoided. + +When Hipcheck recommends "INVESTIGATE," we do so to signal to users that the +targeted software is concerning, and we try to provide specific information +whenever possible about _what_ concerns Hipcheck has identified. Our goal is +to empower users to make informed decisions, not to substitute our own +perspective for theirs. +{% end %} + +The process for turning individual analyses scores into the final risk score +is worth breaking down in greater detail. To understand it, we'll need to +explain the "Score Tree" concept Hipcheck uses. + +Within Hipcheck, analyses are organized into a tree structure, with similar +analyses grouped into categories as children of a shared node. The full +state of the current Hipcheck score tree looks like this: + +![Score Tree](score-tree.svg) + +As you can see, each analysis has an associated _weight_, in this case each +weight defaults to 1. These weights are configurable, and allow users to +change how much influence the results of individual analyses have on the +overall risk score calculation. To perform the final risk score reducation, +these weights are converted into percentages by summing up the weights of +children of the same node, and dividing each child's weight by that sum. +In the case of the score tree shown above, that would result in the following +percentages: + +![Score Tree Percentages](score-tree-pct.svg) + +From this, we can calculate the percentages of the total risk score to apply +for each individual analysis (the leaf nodes of the tree) by multiplying +down from the root to each leaf node, resulting in the following final +percentages per-analysis: + +![Score Tree Final](score-tree-final.svg) + +So with this score tree, and the results of individual analyses, we can then +calculate a risk score. The following table summarizes the results of that +calculation in this example: + +| Analysis | Result | Score | Weight | Analysis Risk Score | +|:------------|:-------|:------|:-------|:--------------------| +| Activity | Pass | 0 | 0.10 | 0 | +| Binary | Fail | 1 | 0.10 | 0.1000 | +| Fuzz | Fail | 1 | 0.10 | 0.1000 | +| Identity | Pass | 0 | 0.10 | 0 | +| Review | Fail | 1 | 0.10 | 0.1000 | +| Typo | Pass | 0 | 0.25 | 0 | +| Affiliation | Pass | 0 | 0.1665 | 0 | +| Churn | Fail | 1 | 0.1665 | 0.1665 | +| Entropy | Pass | 0 | 0.1665 | 0 | +| __Total__ | | | | 0.4665 | + +So in this case, with that configured score tree and the specific analysis +results, the overall risk score would be __0.4665__. + +If the user's configured risk threshold is __0.5__ (which is currently +the default risk threshold), this would result in a "PASS" recommendaton. +if the risk threshold were lower than the risk score, for example if it +were __0.3__, then this would result in an "INVESTIGATE" recommendation. + +Similarly, if users wanted to prioritize or deprioritize specific analyses, +they could change the configured weights for those analyses to be lower or +higher. + +That's how scoring works in Hipcheck! diff --git a/site/content/docs/guide/concepts/score-tree-final.svg b/site/content/docs/guide/concepts/scoring/score-tree-final.svg similarity index 100% rename from site/content/docs/guide/concepts/score-tree-final.svg rename to site/content/docs/guide/concepts/scoring/score-tree-final.svg diff --git a/site/content/docs/guide/concepts/score-tree-pct.svg b/site/content/docs/guide/concepts/scoring/score-tree-pct.svg similarity index 100% rename from site/content/docs/guide/concepts/score-tree-pct.svg rename to site/content/docs/guide/concepts/scoring/score-tree-pct.svg diff --git a/site/content/docs/guide/concepts/score-tree.svg b/site/content/docs/guide/concepts/scoring/score-tree.svg similarity index 100% rename from site/content/docs/guide/concepts/score-tree.svg rename to site/content/docs/guide/concepts/scoring/score-tree.svg diff --git a/site/content/docs/guide/concepts/targets.md b/site/content/docs/guide/concepts/targets.md new file mode 100644 index 00000000..c2e50362 --- /dev/null +++ b/site/content/docs/guide/concepts/targets.md @@ -0,0 +1,142 @@ +--- +title: Targets +weight: 1 +--- + +# Targets + +__Targets__ are Hipcheck's term for "things that Hipcheck analyzes," and +they are what you specify with the positional argument in the `hc check` +command. Generally, targets are intended to specify _something that leads +to a source repository_, which can seem like a vague concept. + +More concretely, targets can be: + +- A Git source repository URL or local path +- A package name and optional version (perhaps requiring you to specify the + package host) +- An SPDX software bill of materials (SBOM) file with a source repository + reference for the main package in it + +Let's break each of those down in turn. + +## Git Source Repository URL or Local Path + +Hipcheck's central focus for analysis is a project's _source repository_, +because Hipcheck cares about analyzing the metadata associated with a project's +development (see [Why Hipcheck?](@/docs/getting-started/why.md) for more +information). Giving Hipcheck the source repository URL or local path is the +most _direct_ way of telling Hipcheck what you want it to analyze. When you +specify other types of targets, Hipcheck will see if it can find a reference to +a source repository from those targets, and will produce an error if it can't. + +Specifying a Git source repository looks like: + +```sh +$ # For a remote Git repository. +$ hc check https://github.com/mitre/hipcheck +$ # For an existing local Git repository. +$ hc check ~/Projects/hipcheck +``` + +When Hipcheck is given a remote URL for a Git repository, it will clone +that repository into a local directory, as analyses on local data are _much_ +faster than trying to gather data across the network. + +{% info(title="Hipcheck's Storage Paths") %} +Hipcheck uses three directories to store important materials it needs to +run. Each can be specified by a command line flag, by an environment +variable, or inferred from the user's current platform (in decreasing +priority). Each directory serves a specific purpose: + +- The Config directory: stores Hipcheck's configuration files. +- The Data directory: stores Hipcheck's helper scripts needed for running + additional external tools Hipcheck relies on. +- The Cache directory: stores local clones of repositories Hipcheck is + analyzing. + +Of these, the Cache directory is one that has a tendency to grow as your +use of Hipcheck continues. Some Git repositories, especially those for +long-running and very active projects, can be quite large. In the future, +we [plan to augment Hipcheck with tooling for better managing this +cache directory](https://github.com/mitre/hipcheck/issues/182). +{% end %} + +In general, Hipcheck tries to ensure it ends up with both a _local path_ +for a repository (either because the user specified a local repository +in the CLI, or by cloning a remote repository to a local cache) and +a _remote URL_. If the user provided a remote URL directly, that's not +a problem. If the user provided a local path, then Hipcheck tries to +infer the upstream repository by seeing if the default branch has an +upstream remote branch configured. If it does, then Hipcheck records +that as the remote branch for the local repository. + +Hipcheck does this because some analyses rely on APIs provided by +specific source repository hosts. Today, only GitHub is supported, +but we'd like to add support for more source repository APIs in +the future. If the user provides a GitHub source repository URL, +or a local repository path from which a remote GitHub URL can be +inferred, then the GitHub-specific analyses will be able to run. + +## Package Name and Optional Version + +Users can also specify targets as a package name and version from some +popular open source package repositories. Today Hipcheck supports packages +on NPM (JavaScript), PyPI (Python), and Maven Central (Java). We'd like to +expand that support to more platforms in the future. + +Packages from these hosts may be specified as package names, with optional +versions. When specifying one of these targets, it is not sufficient to +specify just the package name, you'll need to use the `-t`/`--type` flag to +specify the package host. For example: + +```sh +$ hc check --type npm chalk@5.3.0 +$ hc check --type pypi numpy@2.0.0 +$ hc check --type maven commons-csv@1.11.0 +``` + +Without specifying the platform, Hipcheck will be unable to determine +what package is being specified, and will produce an error. + +For each of these types of targets, Hipcheck will then try to identify a +source repository associated with the package in the package's metadata. +The specific method of doing this differs depending on the platform. +Some provide a standard mechanism for specifying the source repository, +and some don't. For those that don't though, there are generally common +norms for how that information is provided, so Hipcheck can often still +identify the source repository in one of the common locations. + +When the source repository is discovered, it is handled in the same way +as if it had been provided as the target directly by the user. See +this page's [Git Source Repository URL or Local Path](#git-source-repository-url-or-local-path) +section for more information. + +## SPDX Software Bill of Materials + +Finally, Hipcheck can accept SPDX version 2 Software Bills of Material (SBOM) +files, in the JSON or key-value text formats. SPDX is a popular format for +specifying Software Bills of Materials, meaning it contains information about +a package and the package's dependencies. + +Running Hipcheck on an SPDX SBOM looks like: + +```sh +$ hc check my-package.spdx.json +``` + +Today, Hipcheck only supports the SPDX 2.3 SBOM format, though we'd like to +add support for more formats, and for SPDX 3.0, in the future. + +When provided with an SBOM, Hipcheck parses the file to identify the "root" +package being specified, and tries to infer any source repository information +for that package. If it is unable to identify a source repository for the +package being described, it produces an error. If it _can_ identify a source +repository, that repository is processed as if the user specified it directly +on the command line (see this page's [Git Source Repository URL or Local Path](#git-source-repository-url-or-local-path) +section for more information). + +When provided with an SBOM, Hipcheck today _does not_ separately analyze +each of the dependencies specified in the SBOM. Rather, it _only_ analyzes +the root package. If you'd like to analyze each of the dependencies in the +SBOM, you'll need to call Hipcheck separately for each of them. diff --git a/site/content/docs/guide/config/_index.md b/site/content/docs/guide/config/_index.md new file mode 100644 index 00000000..844c2587 --- /dev/null +++ b/site/content/docs/guide/config/_index.md @@ -0,0 +1,23 @@ +--- +title: Configuration +template: docs.html +page_template: docs_page.html +weight: 2 +sort_by: weight +--- + +# Configuration + +This section covers how to configure Hipcheck through policy files. + +
+ +{% waypoint(title="Policy Files", path="@/docs/guide/config/policy-file.md", icon="file") %} +How to configure what plugins to use and how to score their results. +{% end %} + +{% waypoint(title="Policy Expressions", path="@/docs/guide/config/policy-expr.md", icon="message-square") %} +How to convert individual analysis results into a pass/fail determination. +{% end %} + +
diff --git a/site/content/docs/guide/plugin/policy-expr.md b/site/content/docs/guide/config/policy-expr.md similarity index 99% rename from site/content/docs/guide/plugin/policy-expr.md rename to site/content/docs/guide/config/policy-expr.md index b7c91cd3..38f75773 100644 --- a/site/content/docs/guide/plugin/policy-expr.md +++ b/site/content/docs/guide/config/policy-expr.md @@ -1,5 +1,6 @@ --- title: Policy Expressions +weight: 2 --- # Policy Expressions diff --git a/site/content/docs/guide/plugin/for-users.md b/site/content/docs/guide/config/policy-file.md similarity index 99% rename from site/content/docs/guide/plugin/for-users.md rename to site/content/docs/guide/config/policy-file.md index b99ecb0b..fd83cb83 100644 --- a/site/content/docs/guide/plugin/for-users.md +++ b/site/content/docs/guide/config/policy-file.md @@ -1,8 +1,9 @@ --- -title: Using Plugins +title: Policy Files +weight: 1 --- -# Using Plugins +# Policy Files When running Hipcheck, users provide a "policy file", which is a [KDL](https://kdl.dev/)-language configuration file that describes everything diff --git a/site/content/docs/guide/configuration.md b/site/content/docs/guide/configuration.md deleted file mode 100644 index b61cc355..00000000 --- a/site/content/docs/guide/configuration.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Configuration ---- - -# Configuration - -Hipcheck's configuration is used to describe: - -1. What analyses to run. -2. How to run those analyses. -3. How to weight the analyses when scoring. -4. How to turn the overall risk score into a recommendation. - -This section describes the overall structure and some of the repeated -configuration keys found in the Hipcheck configuration files. For guidance -on how to configure individual analyses, we recommend reading the -[Analyses](@/docs/guide/analyses.md) documentation. - -## What Analyses to Run - -All analyses in Hipcheck can be turned off. Every grouping of analyses, -and every individual analyses, has an `active` key which can be `true` -or `false`. If `true`, the group or analysis is active and will be run, -and its results will be part of scoring. If `false`, the group or analysis -will _not_ be run, and it will have no results. - -Analyses can also be set to run with `active = true`, but have the weight -of their results set to `0`. In this case, the analysis will be run, and -any [Concerns](@/docs/guide/concepts/index.md#concerns) it identifies will -be reported, but it will be ignored for the purpose of scoring. - -## Configuring Individual Analyses - -Individual analyses each have their own configuration which is specific to -them. For full details on this, see the [Analyses](@/docs/guide/analyses.md) -documentation. Several analyses define their own additional TOML files which -contain more complex configuration. - -## Weighting Analyses for Scoring - -All analyses have an associated weight which can be modified to change how -the results of the analysis are considered for scoring. The full details -of the scoring algorithm can be found in the [Scoring](@/docs/guide/concepts/index.md#scoring) -documentation. To configure the weight of an analysis or analysis group, -use the `weight` key. This is expected to be a non-negative integer. - -By default, all weights are equal to `1`. - -## Setting a Risk Tolerance - -The overall risk tolerance determines whether the risk scores calculated -from the results of individual analyses result in a final recommendation of -"PASS" or "INVESTIGATE." The risk tolerance is set with the key -`risk.tolerance`, and must be a floating-point value between 0 and 1, -inclusive. - -Note that risks less than or equal to the tolerance will result in a "PASS" -recommendation. If you want a risk score of `0.5`, for example, to result -in an "INVESTIGATE" recommendation, the risk tolerance must be set to less -than `0.5`. - -{{ button(link="@/docs/guide/debugging.md", text="Debugging") }} diff --git a/site/content/docs/guide/debugging/_index.md b/site/content/docs/guide/debugging/_index.md new file mode 100644 index 00000000..f6baa7cc --- /dev/null +++ b/site/content/docs/guide/debugging/_index.md @@ -0,0 +1,28 @@ +--- +title: Debugging +template: docs.html +page_template: docs_page.html +sort_by: weight +weight: 4 +--- + +# Debugging + +The following is a guide for debugging Hipcheck's execution. + + +
+ +{% waypoint(title="Starting Debugging", path="@/docs/guide/debugging/starting.md", icon="thumbs-up") %} +The basics for how to check Hipcheck's execution setup before anything else. +{% end %} + +{% waypoint(title="Logging", path="@/docs/guide/debugging/logging.md", icon="list") %} +How to configure Hipcheck's logging, including a variety of filtering options. +{% end %} + +{% waypoint(title="Using a Debugger", path="@/docs/guide/debugging/debugger.md", icon="paperclip") %} +How to debug Hipcheck using a separate debugger. +{% end %} + +
diff --git a/site/content/docs/guide/debugging/debugger.md b/site/content/docs/guide/debugging/debugger.md new file mode 100644 index 00000000..6e28be3e --- /dev/null +++ b/site/content/docs/guide/debugging/debugger.md @@ -0,0 +1,21 @@ +--- +title: Using a Debugger +weight: 3 +--- + +# Debugger + +Hipcheck can be run under a debugger like `gdb` or `lldb`. Because Hipcheck is +written in Rust, we recommend using the Rust-patched versions of `gdb` or `lldb` +which ship with the Rust standard tooling. These versions of the tools include +specific logic to demangle Rust symbols to improve the experience of debugging +Rust code. + +You can install these tools by following the standard [Rust installation +instructions](https://www.rust-lang.org/tools/install). + +With one of these debuggers installed, you can then use them to set breakpoints +during Hipcheck's execution, and do all the normal program debugging processes +you're familiar with if you've used a debugger before. Explaining the use of +these tools is outside of the scope of the Hipcheck documentation, so we defer +to their respective documentation sources. diff --git a/site/content/docs/guide/debugging.md b/site/content/docs/guide/debugging/logging.md similarity index 54% rename from site/content/docs/guide/debugging.md rename to site/content/docs/guide/debugging/logging.md index 5523ca4b..fbef1d10 100644 --- a/site/content/docs/guide/debugging.md +++ b/site/content/docs/guide/debugging/logging.md @@ -1,53 +1,23 @@ --- -title: Debugging +title: Logging +weight: 2 --- -# Debugging - -- [Using `hc ready`](#using-hc-ready) -- [Logging](#logging) - - [Filtering Log Messages](#filtering-log-messages) - - [Filtering by Level](#filtering-by-level) - - [Filtering by Target](#filtering-by-target) - - [Filtering by Content](#filtering-by-content) - - [Controlling Log Style](#controlling-log-style) - - [Where do Logs Write?](#where-do-logs-write) -- [Using a Debugger](#using-a-debugger) - -## Using `hc ready` - -The `hc ready` command prints a variety of information about how Hipcheck is -currently configured, including Hipcheck's own version, the versions of tools -Hipcheck may need to run its analyses, the configuration of paths Hipcheck will -use during execution, and the presence of API tokens Hipcheck may need. - -This is a very useful starting point when debugging Hipcheck. While Hipcheck -can only automatically check basic information like whether configured paths -are present and accessible, you should also review whether the paths `hc ready` -reports are the ones you intend for Hipcheck to use. - -Similarly, for any API tokens, it's good to make sure those tokens are valid -to use, and have tha appropriate permissions required to access the -repositories or packages you are trying to analyze. - -See the [`hc ready`](@/docs/guide/how-to-use.md#hc-ready) documentation for more -information on its specific CLI. - -## Logging +# Logging Hipcheck logging is controlled with two environment variables: * `HC_LOG` configures what should be logged. * `HC_LOG_STYLE` configures the format of the log output. -### Filtering Log Messages +## Filtering Log Messages Every log entry in Hipcheck is accompanied by a "target" and a "level": * __Target__: The module in which the log message originates. * __Level__: One of `error`, `warn`, `info`, `debug`, or `trace`. -#### Filtering By Level +### Filtering By Level You may use a "level filter" to control what log messages to show: @@ -74,7 +44,7 @@ $ # See all log messages. $ HC_LOG="trace" hc check -t npm express ``` -#### Filtering by Target +### Filtering by Target You can also filter by the target module which produced the error. In general, you'll start with printing messages from _all_ targets, then observe in the log @@ -95,7 +65,7 @@ $ # See all log messages from the `analysis` module of Hipcheck. $ HC_LOG="hc::analysis=trace" hc check -t npm express ``` -#### Filtering by Content +### Filtering by Content Log messages may also be filtered based on their contents, by appending `/` followed by a regular expression to the end of the `HC_LOG` to match specific messages. If the @@ -109,30 +79,13 @@ $ # The "/message" indicates to search for the "message" string $ HC_LOG=hc::shell=trace,salsa=off/message hc check -t npm express ``` -### Controlling Log Style +## Controlling Log Style Log style is controlled with the `HC_LOG_STYLE` environment variable. The acceptable values are `always`, `auto`, or `never`, and they control whether to try outputting color codes with the log messages. -### Where do Logs Write? +## Where do Logs Write? Log messages output to `stderr`. They can be redirected using standard shell redirection techniques. - -## Using a Debugger - -Hipcheck can be run under a debugger like `gdb` or `lldb`. Because Hipcheck is -written in Rust, we recommend using the Rust-patched versions of `gdb` or `lldb` -which ship with the Rust standard tooling. These versions of the tools include -specific logic to demangle Rust symbols to improve the experience of debugging -Rust code. - -You can install these tools by following the standard [Rust installation -instructions](https://www.rust-lang.org/tools/install). - -With one of these debuggers installed, you can then use them to set breakpoints -during Hipcheck's execution, and do all the normal program debugging processes -you're familiar with if you've used a debugger before. Explaining the use of -these tools is outside of the scope of the Hipcheck documentation, so we defer -to their respective documentation sources. diff --git a/site/content/docs/guide/debugging/starting.md b/site/content/docs/guide/debugging/starting.md new file mode 100644 index 00000000..a2de81a8 --- /dev/null +++ b/site/content/docs/guide/debugging/starting.md @@ -0,0 +1,27 @@ +--- +title: Starting Debugging +weight: 1 +--- + +# Starting Debugging + +## Using `hc ready` + +The `hc ready` command prints a variety of information about how Hipcheck is +currently configured, including Hipcheck's own version, the versions of tools +Hipcheck may need to run its analyses, the configuration of paths Hipcheck will +use during execution, and the presence of API tokens Hipcheck may need. + +This is a very useful starting point when debugging Hipcheck. While Hipcheck +can only automatically check basic information like whether configured paths +are present and accessible, you should also review whether the paths `hc ready` +reports are the ones you intend for Hipcheck to use. + +See the [`hc ready`](@/docs/guide/cli/hc-ready.md) documentation for more +information on its specific CLI. + +## Checking API Tokens + +Similarly, for any API tokens, it's good to make sure those tokens are valid +to use, and have tha appropriate permissions required to access the +repositories or packages you are trying to analyze. diff --git a/site/content/docs/guide/how-to-use.md b/site/content/docs/guide/how-to-use.md deleted file mode 100644 index b107d489..00000000 --- a/site/content/docs/guide/how-to-use.md +++ /dev/null @@ -1,262 +0,0 @@ ---- -title: How to Use Hipcheck ---- - -# How to Use Hipcheck - -If you are interested in a quick guide to getting started with Hipcheck, -we recommend checking out the [Quickstart guide](@/docs/quickstart/_index.md) -first! For a more thorough explanation of Hipcheck's Command Line Interface -(CLI), please continue with this section! - ---- - -Hipcheck's Command Line Interface features a number of different commands -for analyzing software packages (`hc check`), understanding the functioning -of Hipcheck (`hc schema`, `hc ready`), and managing Hipcheck itself -(`hc setup`, `hc update`). In this section, we'll walk through each of the -commands, describe their interface, what they're used for, and how to make -the most of them. - -- [Subcommands](#subcommands) - - [`hc check`](#hc-check) - - [`hc ready`](#hc-ready) - - [`hc schema`](#hc-schema) - - [`hc setup`](#hc-setup) - - [`hc update`](#hc-update) -- [General Flags](#general-flags) - - [Output Flags](#output-flags) - - [Path Flags](#path-flags) - - [Help and Version](#help-and-version) - -## Subcommands - -### `hc check` - -`hc check` is the primary command that user's of Hipcheck will run. It's the -command for running analyses of target packages (for more information on how -to specify "targets," see [the "Targets" documentation][target]). - -The short help text for `hc check` looks like this: - -``` -Analyze a package, source repository, SBOM, or pull request - -Usage: hc check [OPTIONS] - -Arguments: - The target package, URL, commit, etc. for Hipcheck to analyze. If ambiguous, the -t flag must be set - -Options: - -t, --target [possible values: maven, npm, pypi, repo, request, spdx] - -h, --help Print help (see more with '--help') - -Output Flags: - -v, --verbosity How verbose to be [possible values: quiet, normal] - -k, --color When to use color [possible values: always, never, auto] - -f, --format What format to use [possible values: json, human] - -Path Flags: - -c, --config Path to the configuration folder - -d, --data Path to the data folder - -C, --cache Path to the cache folder -``` - -The only positional argument is the ``, as explains in [the Targets -documentation][target]. This argument is _required_, and tells Hipcheck what to -analyze. - -It is possible for a target specifier to be ambiguous. For example, Hipcheck -accepts targets of the form `[@]`. In this case, -it's not clear from the target specifier what package host this package is -supposed to be hosted on. In these ambiguous cases, the user needs to specify -the __target type__ with the `-t`/`--type` flag. The full list of current types -is: - -- `maven`: A package on Maven Central -- `npm`: A package on NPM -- `pypi`: A package on PyPI -- `repo`: A Git repository -- `spdx`: An SPDX document - -If you attempt to run `hc check` with an ambiguous target specifier, Hipcheck -will produce an error telling you to use the `-t`/`--target` flag to manually -specify the target type. - -Besides this flag, all other flags are general flags which Hipcheck accepts -for every command. See [General Flags](#general-flags) for more information. - -[target]: @/docs/guide/concepts/index.md#targets - -### `hc ready` - -`hc ready` is a command for checking that Hipcheck is ready to run analyses. -This is intended to help the user debug issues with a Hipcheck installation, -including problems like missing configuration files, inaccessible config, -data, or cache paths, missing authentication tokens, and more. - -`hc ready` has no special flags currently, and only accepts the -[General Flags](#general-flags) that _all_ Hipcheck commands accept. - -The output of `hc ready` is a report containing key information about -Hipcheck, the third-party tools it relies on as external data sources, -the paths it's currently using for configuration files, data files, -and local repository clones, along with any API tokens it will use -for external API access. - -If all required information is found and passes requirements for Hipcheck -to run, it will report that Hipcheck is ready to run. - -We recommend running `hc ready` before running Hipcheck the first time, -and as a good first debugging step if Hipcheck begins reporting issues. - -### `hc schema` - -The `hc schema` command is intended to help users of Hipcheck who are trying -to integrate Hipcheck into other tools and systems. Hipcheck supports a JSON -output format for analyses, and `hc schema` produces a JSON schema description -of that output. - -`hc schema` takes the name of the target type for which to print the schema. -For the list of target types, see [the documentation for the `hc check` command](#hc-check). - -`hc schema` also takes the usual [General Flags](#general-flags). - -### `hc setup` - -The `hc setup` command is intended to be run after first installing Hipcheck, -and again after updating Hipcheck, to ensure you have the required configuration -and data files needed for Hipcheck to run. - -When installing Hipcheck, regardless of method, you are only installing the -`hc` binary, not these additional files. `hc setup` gathers those files for you, -and installs them into the appropriate locations. - -If Hipcheck has been installed with the recommended install scripts included -with each release, then the correct configuration and data files for each -version are included with the bundle downloaded by that script. In that case, -`hc setup` will attempt to find those files locally and copy them into the -configuration and data directories. - -If Hipcheck was installed via another method, or the files can't be found, -then `hc setup` will attempt to download them from the appropriate Hipcheck -release. Users can pass the `-o`/`--offline` flag to ensure `hc setup` does -_not_ use the network to download materials, in which case `hc setup` will -fail if the files can't be found locally. - -The installation directories for the configuration and data files are -specified in the way they're normally specified. For more information, -see the documentation on Hipcheck's [Path Flags](#path-flags). - -`hc setup` also supports Hipcheck's [General Flags](#general-flags). - -### `hc update` - -When Hipcheck is installed using the recommend install scripts provided with -each release, the install scripts also provide an "updater" program, built -by `cargo-dist` (which Hipcheck uses to handle creating prebuilt artifacts -with each release, and with announcing each release on GitHub Releases). -This updater program handles checking for a newer version of Hipcheck, -downloading it, and replacing the current version with the newer one. -The updater is provided as a separate binary (named either `hc-update` -or `hipcheck-update`, due to historic bugs). - -The `hc update` command simply delegates to this separate update program, -and provides the same interface that this separate update program does. -In general, you only need to run `hc update` with no arguments, followed -by `hc setup` to ensure you have the latest configuration and data files. - -If you want to specifically download a version besides the most recent -version of Hipcheck, you can use the following flags: - -- `--tag `: install the version from this specific Git tag. -- `--version `: install the version from this specific GitHub - release. -- `--prerelease`: permit installing a pre-release version when updating - to `latest`, - -The `--tag` and `--version` flags are mutually exclusive; the updater will -produce an error if both are provided. - -This command also supports Hipcheck's [General Flags](#general-flags), though -they are ignored. - -## General Flags - -There are three categories of flags which Hipcheck supports on all subcommands, -output flags, path flags, and the help and version flags (which actually -operate like subcommands themselves). - -### Output Flags - -"Output flags" are flags which modify the output that Hipcheck produces. -Currently, there are three output flags: - -- `-v `/`--verbosity `: Specifies how noisy Hipcheck - should be when running. Options are: - - `quiet`: Produce as little output as possible. - - `normal`: Produce a normal amount of output. (default) -- `-k `/`--color `: Specifies whether the Hipcheck output should - include color or not. Options are: - - `always`: Try to produce color regardless of the output stream's support - for color. - - `never`: Do not produce color. - - `auto`: Try to infer whether the output stream supports ANSI color codes. - (default) -- `-f `/`--format `: Specifies what format to use for the - output. Options are: - - `json`: Use JSON output. - - `human`: Use human-readable output. (default) - -Each of these can also be set by environment variable: - -- `HC_VERBOSITY` -- `HC_COLOR` -- `HC_FORMAT` - -The precedence is, in increasing order: - -- Environment variable -- CLI flag - -### Path Flags - -"Path flags" are flags which modify the paths Hipcheck uses for configuration, -data, and caching repositories locally. The current flags are: - -- `-c `/`--config `: the path to the configuration folder to - use. -- `-d `/`--data `: the path to the data folder to use. -- `-C `/`--cache `: the path to the cache folder to use. - -Each of these is inferred by default based on the user's platform. They can -also be set with environment variables: - -- `HC_CONFIG` -- `HC_DATA` -- `HC_CACHE` - -The priority (in increasing precedence), is: - -- System default -- Environment variable -- CLI flag - -### Help and Version - -All commands in Hipcheck also support help flags and the version flag. -These act more like subcommands, in that providing the flag stops Hipcheck -from executing the associated command, and instead prints the help or -version text as requested. - -For each command, the `-h` or `--help` flag can be used. The `-h` flag gives -the "short" form of the help text, which is easier to skim, while the `--help` -flag gives the "long" form of the help text, which is more complete. - -The `-V`/`--version` flag may also be used. Both the short and long variants -of the flag produce the same output. The version flag is valid on all -subcommands, but all subcommands are versioned together, to the output will -be the same when run as `hc --version` or when run as -`hc --version`. - -{{ button(link="@/docs/guide/analyses.md", text="Analyses") }} diff --git a/site/content/docs/guide/introduction.md b/site/content/docs/guide/introduction.md deleted file mode 100644 index c5d3c7d7..00000000 --- a/site/content/docs/guide/introduction.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Introduction ---- - -# Introduction - -Hipcheck is a tool for automatically assessing risks associated with software -repositories. It exists to make it possible for maintainers to identify their -riskiest dependencies and do their own reviews. - -Hipcheck works by collecting metrics from Git logs, the APIs of common Git hosts -like GitHub, and language-specific tools like NPM and using them to detect risky -development practices and possible supply chain attacks. - -## Why Use Hipcheck? - -It's common to hear you need to audit your dependencies, less common to do it, -even less common to do it consistently. For projects that have said "we'll -do it when we have time," Hipcheck makes the problem easier to manage by -helping you target your review. Instead of reviewing 100 dependencies, -direct and transitive, you might review 5 that Hipcheck flags for further -investigation. When new versions come out, you run Hipcheck again and let it -tell you if anything has changed in the risk profile. - -## How Does Hipcheck Compare to Alternatives? - -Hipcheck fills a unique role in this space. The other major categories of tools -in this area are vulnerable version detectors, static code analyzers, and practices -analyzers. - -### Vulnerable Version Detectors - -These are things like GitHub's Dependabot or Snyk Open Source. These are -extremely useful tools for identifying vulnerable versions of your -dependencies, and you should use them or a similar alternative. What they -don't do is detect risks in a project's development practices, or possible -supply chain attacks like malicious contributions or typosquatted dependencies. - -### Static Code Analyzers - -Often you'll see projects try running static code analyzers like Fortify -Static Code Analyzer, Checkmarx SAST, or SonarQube against open source -they're considering incorporating. Static code analyzers are great tools -for identifying code weaknesses that may be true vulnerabilities; but -static code analysis techniques produce false positives, especially on code -not written with them in the process throughout development. - -Applying static code analysis to open source dependencies _can_ find real -risks, but it requires a lot of work to filter through results, and often -requires building expertise in the internals of a library to assess -findings. - -### Practices Analyzers - -There is one other similar tool in this space, Scorecard, by the Open Source -Security Foundation. Scorecard tackles the same problem, and is a worthwhile tool -to try. There are definite differences to highlight between Scorecard and Hipcheck: - -#### Configuration - -Hipcheck is more configurable. You can override any thresholds and weights to -change when individual analyses will flag a repository, and how failing analyses -will contribute to the overall risk score. - -#### Attack Detection - -Hipcheck includes analyses to detect possible attacks like malicious -contributions, using statistical analysis of commit-level data to do the job. diff --git a/site/content/docs/guide/making-plugins/_index.md b/site/content/docs/guide/making-plugins/_index.md new file mode 100644 index 00000000..78889410 --- /dev/null +++ b/site/content/docs/guide/making-plugins/_index.md @@ -0,0 +1,26 @@ +--- +title: Making Plugins +weight: 6 +template: docs.html +page_template: docs_page.html +sort_by: weight +--- + +# Making Plugins + +The following is a guide for making plugins for Hipcheck. Plugins can add new +data source and new analyses, and can be written using an SDK or by hand. The +rest of this section details the protocols plugins are expected to follow. + +
+ +{% waypoint(title="Creating a Plugin", path="@/docs/guide/making-plugins/creating-a-plugin.md", icon="box") %} +How to start making a new Hipcheck plugin. +{% end %} + + +{% waypoint(title="The Rust Plugin SDK", path="@/docs/guide/making-plugins/rust-sdk.md", icon="tool") %} +How to use the Rust SDK to create a plugin. +{% end %} + +
diff --git a/site/content/docs/guide/making-plugins/creating-a-plugin.md b/site/content/docs/guide/making-plugins/creating-a-plugin.md new file mode 100644 index 00000000..2675f95e --- /dev/null +++ b/site/content/docs/guide/making-plugins/creating-a-plugin.md @@ -0,0 +1,34 @@ +--- +title: Creating a Plugin +weight: 1 +--- + +# Creating a Plugin + +A Hipcheck plugin is a separate executable artifact that Hipcheck downloads, +starts, and communicates with over a gRPC protocol to request data. A plugin's +executable artifact is the binary, set of executable program files, Docker +container, or other artifact which can be run as a command line interface +program through a singular "start command" defined in the plugin's +manifest file. + +The benefit of the executable-and-gRPC plugin design is that plugins can be +written in any of the many languages that have a gRPC library. One drawback is +that plugin authors have to at least be aware of the target platform(s) they +compile their plugin for, and more likely will need to support a handful of +target platforms. This can be simplified through the optional use of container +files as the plugin executable artifact. + +Once a plugin author writes their plugin, compiles, packages, and +distribute it, Hipcheck users can specify the plugin in their policy file for +Hipcheck to fetch and use in analysis. + +## Plugin CLI + +Hipcheck requires that plugins provide a CLI which accepts a `--port ` +argument, enabling Hipcheck to centrally manage the ports plugins are listening +on. The port provided via this CLI argument must be the port the running plugin +process listens on for gRPC requests, and on which it returns responses. + +Once started, the plugin should continue running, listening for gRPC requests +from Hipcheck, until shut down by the Hipcheck process. diff --git a/site/content/docs/guide/plugin/for-developers.md b/site/content/docs/guide/making-plugins/rust-sdk.md similarity index 86% rename from site/content/docs/guide/plugin/for-developers.md rename to site/content/docs/guide/making-plugins/rust-sdk.md index 215100fb..bd0fe9b8 100644 --- a/site/content/docs/guide/plugin/for-developers.md +++ b/site/content/docs/guide/making-plugins/rust-sdk.md @@ -1,40 +1,10 @@ --- -title: Developing Plugins +title: The Rust Plugin SDK +weight: 2 --- -# Developing Plugins -## Creating a New Plugin - -A Hipcheck plugin is a separate executable artifact that Hipcheck downloads, -starts, and communicates with over a gRPC protocol to request data. A plugin's -executable artifact is the binary, set of executable program files, Docker -container, or other artifact which can be run as a command line interface -program through a singular "start command" defined in the plugin's -manifest file. - -The benefit of the executable-and-gRPC plugin design is that plugins can be -written in any of the many languages that have a gRPC library. One drawback is -that plugin authors have to at least be aware of the target platform(s) they -compile their plugin for, and more likely will need to support a handful of -target platforms. This can be simplified through the optional use of container -files as the plugin executable artifact. - -Once a plugin author writes their plugin, compiles, packages, and -distribute it, Hipcheck users can specify the plugin in their policy file for -Hipcheck to fetch and use in analysis. - -## Plugin CLI - -Hipcheck requires that plugins provide a CLI which accepts a `--port ` -argument, enabling Hipcheck to centrally manage the ports plugins are listening -on. The port provided via this CLI argument must be the port the running plugin -process listens on for gRPC requests, and on which it returns responses. - -Once started, the plugin should continue running, listening for gRPC requests -from Hipcheck, until shut down by the Hipcheck process. - -## The Rust SDK +# The Rust Plugin SDK The Hipcheck team maintains a library crate `hipcheck-sdk` which provides developers with tools for greatly simplifying plugin development in Rust. This diff --git a/site/content/docs/guide/plugin/index.md b/site/content/docs/guide/plugin/index.md deleted file mode 100644 index 0091a5ab..00000000 --- a/site/content/docs/guide/plugin/index.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: Plugins ---- - -# Introduction - -After Hipcheck resolves a user's desired analysis target, it moves to the main -analysis phase. This involves Hipcheck passing the target description to a set of -user-specified, top-level analyses which measure some aspect of the target and -produce a pass/fail result. These tertiary data sources often rely on -lower-level measurements about the target to produce their results. - -To facilitate the integration of third-party data sources and analysis -techniques into Hipcheck's analysis phase, data sources are split out into -plugins that Hipcheck can query. In order to produce their result, plugins can -in turn query information from other plugins, which Hipcheck performs on their -behalf. - -The remainder of this section of the documentation is split in two. The [first -section](for-users) is aimed at users. It covers how they can specify analysis -plugins and control the use of their data in producing a pass/fail determination -for a given target. The [second section](for-developers) is aimed at plugin -developers, and explains how to create and distribute your own plugin. - - -## Table of Contents - -- [Using Plugins](@/docs/guide/plugin/for-users.md) -- [Developing Plugins](@/docs/guide/plugin/for-developers.md) -- [Policy Expressions](@/docs/guide/plugin/policy-expr.md) diff --git a/site/content/docs/guide/analyses.md b/site/content/docs/guide/plugins/_index.md similarity index 99% rename from site/content/docs/guide/analyses.md rename to site/content/docs/guide/plugins/_index.md index 96bf1a06..b6caba17 100644 --- a/site/content/docs/guide/analyses.md +++ b/site/content/docs/guide/plugins/_index.md @@ -1,8 +1,12 @@ --- -title: Analyses +title: Plugins +template: docs.html +page_template: docs_page.html +weight: 5 +sort_by: title --- -# Analyses +# Plugins This page lists Hipcheck's analyses with the names they are given in the configuration file, what their data source is, the details of the analysis @@ -328,5 +332,3 @@ deletion, substitution, swapping, and more. dependencies, and requires legwork to produce the list of popular package names, which are not currently pulled from any external API or authoritative source. - -{{ button(link="@/docs/guide/configuration.md", text="Configuration") }} diff --git a/site/content/docs/guide/plugins/mitre-activity.md b/site/content/docs/guide/plugins/mitre-activity.md new file mode 100644 index 00000000..4e62728f --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-activity.md @@ -0,0 +1,9 @@ +--- +title: mitre/activity +extra: + nav_title: "mitre/activity" +--- + +# mitre/activity + +TODO diff --git a/site/content/docs/guide/plugins/mitre-affiliation.md b/site/content/docs/guide/plugins/mitre-affiliation.md new file mode 100644 index 00000000..965d26b7 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-affiliation.md @@ -0,0 +1,9 @@ +--- +title: mitre/affiliation +extra: + nav_title: "mitre/affiliation" +--- + +# mitre/affiliation + +TODO diff --git a/site/content/docs/guide/plugins/mitre-binary.md b/site/content/docs/guide/plugins/mitre-binary.md new file mode 100644 index 00000000..f3df87f3 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-binary.md @@ -0,0 +1,9 @@ +--- +title: mitre/binary +extra: + nav_title: "mitre/binary" +--- + +# mitre/binary + +TODO diff --git a/site/content/docs/guide/plugins/mitre-churn.md b/site/content/docs/guide/plugins/mitre-churn.md new file mode 100644 index 00000000..05fa769c --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-churn.md @@ -0,0 +1,9 @@ +--- +title: mitre/churn +extra: + nav_title: "mitre/churn" +--- + +# mitre/churn + +TODO diff --git a/site/content/docs/guide/plugins/mitre-entropy.md b/site/content/docs/guide/plugins/mitre-entropy.md new file mode 100644 index 00000000..4dfb07e5 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-entropy.md @@ -0,0 +1,9 @@ +--- +title: mitre/entropy +extra: + nav_title: "mitre/entropy" +--- + +# mitre/entropy + +TODO diff --git a/site/content/docs/guide/plugins/mitre-fuzz.md b/site/content/docs/guide/plugins/mitre-fuzz.md new file mode 100644 index 00000000..be774988 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-fuzz.md @@ -0,0 +1,9 @@ +--- +title: mitre/fuzz +extra: + nav_title: "mitre/fuzz" +--- + +# mitre/fuzz + +TODO diff --git a/site/content/docs/guide/plugins/mitre-git.md b/site/content/docs/guide/plugins/mitre-git.md new file mode 100644 index 00000000..51b0d5c4 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-git.md @@ -0,0 +1,9 @@ +--- +title: "mitre/git" +extra: + nav_title: "mitre/git" +--- + +# `mitre/git` + +TODO diff --git a/site/content/docs/guide/plugins/mitre-github.md b/site/content/docs/guide/plugins/mitre-github.md new file mode 100644 index 00000000..b55fbabd --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-github.md @@ -0,0 +1,9 @@ +--- +title: "mitre/github" +extra: + nav_title: "mitre/github" +--- + +# `mitre/github` + +TODO diff --git a/site/content/docs/guide/plugins/mitre-identity.md b/site/content/docs/guide/plugins/mitre-identity.md new file mode 100644 index 00000000..cee6a841 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-identity.md @@ -0,0 +1,9 @@ +--- +title: mitre/identity +extra: + nav_title: "mitre/identity" +--- + +# mitre/identity + +TODO diff --git a/site/content/docs/guide/plugins/mitre-npm.md b/site/content/docs/guide/plugins/mitre-npm.md new file mode 100644 index 00000000..61961b07 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-npm.md @@ -0,0 +1,9 @@ +--- +title: "mitre/npm" +extra: + nav_title: "mitre/npm" +--- + +# `mitre/npm` + +TODO diff --git a/site/content/docs/guide/plugins/mitre-review.md b/site/content/docs/guide/plugins/mitre-review.md new file mode 100644 index 00000000..f1359fae --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-review.md @@ -0,0 +1,9 @@ +--- +title: mitre/review +extra: + nav_title: "mitre/review" +--- + +# mitre/review + +TODO diff --git a/site/content/docs/guide/plugins/mitre-typo.md b/site/content/docs/guide/plugins/mitre-typo.md new file mode 100644 index 00000000..17a4bb6c --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-typo.md @@ -0,0 +1,9 @@ +--- +title: mitre/typo +extra: + nav_title: "mitre/typo" +--- + +# mitre/typo + +TODO diff --git a/site/content/rfds/0000-rfds.md b/site/content/docs/rfds/0000-rfds.md similarity index 97% rename from site/content/rfds/0000-rfds.md rename to site/content/docs/rfds/0000-rfds.md index 20af4f3f..b2d8bf15 100644 --- a/site/content/rfds/0000-rfds.md +++ b/site/content/docs/rfds/0000-rfds.md @@ -4,6 +4,10 @@ weight: 0 slug: "0000" extra: rfd: "0" + primary_author: Andrew Lilley Brinker + primary_author_link: https://github.com/alilleybrinker + status: Accepted + pr: 24 --- # The RFD Process diff --git a/site/content/rfds/0001-release-engineering.md b/site/content/docs/rfds/0001-release-engineering.md similarity index 96% rename from site/content/rfds/0001-release-engineering.md rename to site/content/docs/rfds/0001-release-engineering.md index f0b17e0b..8ed354aa 100644 --- a/site/content/rfds/0001-release-engineering.md +++ b/site/content/docs/rfds/0001-release-engineering.md @@ -4,6 +4,10 @@ weight: 1 slug: 0001 extra: rfd: 1 + primary_author: Andrew Lilley Brinker + primary_author_link: https://github.com/alilleybrinker + status: Accepted + pr: 48 --- # Hipcheck Release Engineering diff --git a/site/content/rfds/0002-hipchecks-values.md b/site/content/docs/rfds/0002-hipchecks-values.md similarity index 97% rename from site/content/rfds/0002-hipchecks-values.md rename to site/content/docs/rfds/0002-hipchecks-values.md index f2f8ef84..22001322 100644 --- a/site/content/rfds/0002-hipchecks-values.md +++ b/site/content/docs/rfds/0002-hipchecks-values.md @@ -4,6 +4,10 @@ weight: 2 slug: 0002 extra: rfd: 2 + primary_author: Andrew Lilley Brinker + primary_author_link: https://github.com/alilleybrinker + status: Accepted + pr: 70 --- diff --git a/site/content/rfds/0003-plugin-architecture-vision.md b/site/content/docs/rfds/0003-plugin-architecture-vision.md similarity index 98% rename from site/content/rfds/0003-plugin-architecture-vision.md rename to site/content/docs/rfds/0003-plugin-architecture-vision.md index 554292e6..c49a44d1 100644 --- a/site/content/rfds/0003-plugin-architecture-vision.md +++ b/site/content/docs/rfds/0003-plugin-architecture-vision.md @@ -4,6 +4,10 @@ weight: 3 slug: 0003 extra: rfd: 3 + primary_author: Andrew Lilley Brinker + primary_author_link: https://github.com/alilleybrinker + status: Accepted + pr: 71 --- # Plugin Architecture Vision diff --git a/site/content/rfds/0004-plugin-api.md b/site/content/docs/rfds/0004-plugin-api.md similarity index 99% rename from site/content/rfds/0004-plugin-api.md rename to site/content/docs/rfds/0004-plugin-api.md index 49e31c34..1ae3267d 100644 --- a/site/content/rfds/0004-plugin-api.md +++ b/site/content/docs/rfds/0004-plugin-api.md @@ -4,6 +4,10 @@ weight: 4 slug: 0004 extra: rfd: 4 + primary_author: Andrew Lilley Brinker + primary_author_link: https://github.com/alilleybrinker + status: Accepted + pr: 149 --- # Plugin API diff --git a/site/content/rfds/0005-target-resolution-refactor.md b/site/content/docs/rfds/0005-target-resolution-refactor.md similarity index 99% rename from site/content/rfds/0005-target-resolution-refactor.md rename to site/content/docs/rfds/0005-target-resolution-refactor.md index b9ac7fbb..4ea9475a 100644 --- a/site/content/rfds/0005-target-resolution-refactor.md +++ b/site/content/docs/rfds/0005-target-resolution-refactor.md @@ -4,6 +4,10 @@ weight: 5 slug: 0005 extra: rfd: 5 + primary_author: Julian Lanson + primary_author_link: https://github.com/j-lanson + status: Accepted + pr: 266 --- # Target Resolution Refactor diff --git a/site/content/rfds/0006-rust-plugin-sdk.md b/site/content/docs/rfds/0006-rust-plugin-sdk.md similarity index 98% rename from site/content/rfds/0006-rust-plugin-sdk.md rename to site/content/docs/rfds/0006-rust-plugin-sdk.md index 66a638b8..cf9ec8ec 100644 --- a/site/content/rfds/0006-rust-plugin-sdk.md +++ b/site/content/docs/rfds/0006-rust-plugin-sdk.md @@ -4,8 +4,14 @@ weight: 6 slug: 0006 extra: rfd: 6 + primary_author: Andrew Lilley Brinker + primary_author_link: https://github.com/alilleybrinker + status: Accepted + pr: 402 --- +# Rust Plugin SDK + Now that we've landed the initial implementation of the plugin system as described in [RFD 4], the next step is to split out our own existing analyses and data providers into separate plugins. We have already developed diff --git a/site/content/rfds/0007-simplified-release-procedures.md b/site/content/docs/rfds/0007-simplified-release-procedures.md similarity index 97% rename from site/content/rfds/0007-simplified-release-procedures.md rename to site/content/docs/rfds/0007-simplified-release-procedures.md index 5859ea6f..315bfb13 100644 --- a/site/content/rfds/0007-simplified-release-procedures.md +++ b/site/content/docs/rfds/0007-simplified-release-procedures.md @@ -4,8 +4,14 @@ weight: 7 slug: 0007 extra: rfd: 7 + primary_author: Andrew Lilley Brinker + primary_author_link: https://github.com/alilleybrinker + status: Accepted + pr: 430 --- +# Simplified Release Procedures + ## The Current Situation Currently, the Hipcheck _project_ has the following artifacts we release diff --git a/site/content/rfds/_index.md b/site/content/docs/rfds/_index.md similarity index 60% rename from site/content/rfds/_index.md rename to site/content/docs/rfds/_index.md index 48985106..7500ab17 100644 --- a/site/content/rfds/_index.md +++ b/site/content/docs/rfds/_index.md @@ -2,13 +2,17 @@ title: Requests for Discussion sort_by: weight template: rfds.html +page_template: docs_page.html +weight: 5 +aliases: + - "/rfds/_index.md" --- # RFDs RFDs are Hipcheck's mechanism for managing major changes to the tool. The motivation and details of the process are described in [RFD #0, -"The RFD Process."](/rfds/0000) +"The RFD Process."](@/docs/rfds/0000-rfds.md) Anyone can submit an RFD! If you're interested in contributing to Hipcheck's -design, take a look at the [Contribution Guide](/contribute). +design, take a look at the [Contribution Guide](@/docs/contributing/_index.md). diff --git a/site/scripts/deno.json b/site/scripts/deno.json new file mode 100644 index 00000000..1c4a5359 --- /dev/null +++ b/site/scripts/deno.json @@ -0,0 +1,13 @@ +{ + "tasks": { + "bundle": "deno run -A tasks/bundle.ts", + "dev": "deno run -A --watch tasks/bundle.ts" + }, + "imports": { + "@luca/esbuild-deno-loader": "jsr:@luca/esbuild-deno-loader@^0.11.0", + "esbuild": "npm:esbuild@^0.24.0" + }, + "compilerOptions": { + "lib": ["deno.window", "dom"] + } +} diff --git a/site/scripts/deno.lock b/site/scripts/deno.lock new file mode 100644 index 00000000..bf6be76d --- /dev/null +++ b/site/scripts/deno.lock @@ -0,0 +1,140 @@ +{ + "version": "4", + "specifiers": { + "jsr:@luca/esbuild-deno-loader@*": "0.11.0", + "jsr:@luca/esbuild-deno-loader@0.11": "0.11.0", + "jsr:@std/bytes@^1.0.2": "1.0.2", + "jsr:@std/encoding@^1.0.5": "1.0.5", + "jsr:@std/path@^1.0.6": "1.0.7", + "npm:esbuild@*": "0.24.0", + "npm:esbuild@0.24": "0.24.0" + }, + "jsr": { + "@luca/esbuild-deno-loader@0.11.0": { + "integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c", + "dependencies": [ + "jsr:@std/bytes", + "jsr:@std/encoding", + "jsr:@std/path" + ] + }, + "@std/bytes@1.0.2": { + "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57" + }, + "@std/encoding@1.0.5": { + "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" + }, + "@std/path@1.0.7": { + "integrity": "76a689e07f0e15dcc6002ec39d0866797e7156629212b28f27179b8a5c3b33a1" + } + }, + "npm": { + "@esbuild/aix-ppc64@0.24.0": { + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==" + }, + "@esbuild/android-arm64@0.24.0": { + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==" + }, + "@esbuild/android-arm@0.24.0": { + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==" + }, + "@esbuild/android-x64@0.24.0": { + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==" + }, + "@esbuild/darwin-arm64@0.24.0": { + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==" + }, + "@esbuild/darwin-x64@0.24.0": { + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==" + }, + "@esbuild/freebsd-arm64@0.24.0": { + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==" + }, + "@esbuild/freebsd-x64@0.24.0": { + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==" + }, + "@esbuild/linux-arm64@0.24.0": { + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==" + }, + "@esbuild/linux-arm@0.24.0": { + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==" + }, + "@esbuild/linux-ia32@0.24.0": { + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==" + }, + "@esbuild/linux-loong64@0.24.0": { + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==" + }, + "@esbuild/linux-mips64el@0.24.0": { + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==" + }, + "@esbuild/linux-ppc64@0.24.0": { + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==" + }, + "@esbuild/linux-riscv64@0.24.0": { + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==" + }, + "@esbuild/linux-s390x@0.24.0": { + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==" + }, + "@esbuild/linux-x64@0.24.0": { + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==" + }, + "@esbuild/netbsd-x64@0.24.0": { + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==" + }, + "@esbuild/openbsd-arm64@0.24.0": { + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==" + }, + "@esbuild/openbsd-x64@0.24.0": { + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==" + }, + "@esbuild/sunos-x64@0.24.0": { + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==" + }, + "@esbuild/win32-arm64@0.24.0": { + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==" + }, + "@esbuild/win32-ia32@0.24.0": { + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==" + }, + "@esbuild/win32-x64@0.24.0": { + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==" + }, + "esbuild@0.24.0": { + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "dependencies": [ + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" + ] + } + }, + "workspace": { + "dependencies": [ + "jsr:@luca/esbuild-deno-loader@0.11", + "npm:esbuild@0.24" + ] + } +} diff --git a/site/scripts/src/footer/installer.ts b/site/scripts/src/footer/installer.ts new file mode 100644 index 00000000..fe40bb63 --- /dev/null +++ b/site/scripts/src/footer/installer.ts @@ -0,0 +1,138 @@ +import { + copyToClipboard, + querySelector, + querySelectorAll, +} from "../util/web.ts"; + +/** + * Setup behavior for the install picker. + * + * This makes sure the platform buttons are clickable and update the install + * command appropriately, and that the copy-to-clipboard button works. + */ +export function setupInstallerPicker() { + // The buttons used to select the platform. + let $buttons: Array; + + // The block containing the install command. + let $cmd: HTMLElement; + + // The copy-to-clipboard button. + let $copy: HTMLElement; + + // The icon inside the copy-to-clipboard button. + let $copyIcon: HTMLElement; + + try { + $buttons = querySelectorAll(".installer-button"); + $cmd = querySelector("#installer-cmd"); + $copy = querySelector("#installer-copy"); + $copyIcon = querySelector("#installer-copy > svg > use"); + } catch (_e) { + // Swallow these errors. + return; + } + + $buttons.forEach(($button) => { + $button.addEventListener("click", (e) => { + e.preventDefault(); + $buttons.forEach(($button) => delete $button.dataset.active); + $button.dataset.active = "true"; + $cmd.innerText = installerForPlatform($button.dataset.platform); + }); + + if ($button.dataset.active && $button.dataset.active === "true") { + $cmd.innerText = installerForPlatform($button.dataset.platform); + } + }); + + $copy.addEventListener("click", (e) => { + e.preventDefault(); + copyToClipboard($cmd.innerText); + + const iconUrl = getIconUrl($copyIcon); + + // Update the icon on click. + const newIconUrl = iconUrl.replace("#icon-clipboard", "#icon-check"); + console.log(newIconUrl); + setIconUrl($copyIcon, newIconUrl); + + // Set the icon back on a timer. + setTimeout(() => setIconUrl($copyIcon, iconUrl), 1_500); + }); +} + +/** + * Get the URL out of an icon `use` element. + */ +function getIconUrl($node: HTMLElement): string { + const iconUrl = $node.getAttributeNS(XLINK_NS, "href"); + if (iconUrl === null) throw new IconError(); + return iconUrl; +} + +/** + * Get the URL on an icon `use` element. + */ +function setIconUrl($node: HTMLElement, url: string) { + $node.setAttributeNS(XLINK_NS, "href", url); +} + +/** + * The namespace URL for the Xlink namespace + */ +const XLINK_NS: string = "http://www.w3.org/1999/xlink"; + +/** + * Get the install script based on the chosen platform. + */ +function installerForPlatform(platform: string | undefined): string { + if (platform === undefined) throw new UnknownPlatformError(platform); + + switch (platform) { + case "macos": + return UNIX_INSTALLER; + case "linux": + return UNIX_INSTALLER; + case "windows": + return WINDOWS_INSTALLER; + default: + throw new UnknownPlatformError(platform); + } +} + +/** + * The current host of the site. + */ +const HOST: string = + `${globalThis.window.location.protocol}//${globalThis.window.location.host}`; + +/** + * The install script to use for Unix (macOS and Linux) platforms. + */ +const UNIX_INSTALLER: string = `curl -LsSf ${HOST}/dl/install.sh | sh`; + +/** + * The install script to use for Windows. + */ +const WINDOWS_INSTALLER: string = `irm ${HOST}/dl/install.ps1 | iex`; + +/** + * Indicates an error while trying to detect the user's install platform. + */ +class UnknownPlatformError extends Error { + constructor(platform: string | undefined) { + super( + `could not determine platform: '${platform || "undefined"}'`, + ); + } +} + +/** + * Error arising when trying to update the copy-to-clipboard icon. + */ +class IconError extends Error { + constructor() { + super(`could not find copy icon`) + } +} diff --git a/site/scripts/src/footer/main.ts b/site/scripts/src/footer/main.ts new file mode 100644 index 00000000..ed78795e --- /dev/null +++ b/site/scripts/src/footer/main.ts @@ -0,0 +1,31 @@ +import { setupInstallerPicker } from "./installer.ts"; +import { setupSmoothScrolling } from "./scroll.ts"; +import { setupSearch } from "./search.ts"; +import { setupThemeController } from "./theme.ts"; + +/** + * Run all page setup operations, initializing all interactive widgets. + * + * There are currently three widgets: + * + * - Theme Controller in the navigation bar. + * - Installer Picker on the homepage. + * - Search button in the navigation bar. + */ +function setup() { + setupThemeController(); + setupInstallerPicker(); + setupSearch(); + setupSmoothScrolling(); +} + +/** + * Do setup, logging errors to the console. + */ +(function () { + try { + setup(); + } catch (e) { + console.error(e); + } +})(); diff --git a/site/scripts/src/footer/scroll.ts b/site/scripts/src/footer/scroll.ts new file mode 100644 index 00000000..bb6aff75 --- /dev/null +++ b/site/scripts/src/footer/scroll.ts @@ -0,0 +1,27 @@ +import { querySelector, querySelectorAll } from "../util/web.ts"; + +export function setupSmoothScrolling() { + /* + * This code from: https://stackoverflow.com/a/7717572 + * Used under the CC BY-SA 3.0 license with modifications. + */ + querySelectorAll('a[href^="#"]').forEach((anchor) => { + anchor.addEventListener("click", (e) => { + e.preventDefault(); + if (e.currentTarget === null) return; + + const targetHeader = (e.currentTarget as HTMLElement).getAttribute("href"); + if (targetHeader === null) return; + + const $header = querySelector(targetHeader); + const headerPosition = $header.getBoundingClientRect().top; + const scrollAmount = globalThis.window.scrollY; + const offsetPosition = headerPosition + scrollAmount; + + globalThis.window.scrollTo({ + top: offsetPosition, + behavior: "smooth", + }); + }); + }); +} diff --git a/site/scripts/src/footer/search.ts b/site/scripts/src/footer/search.ts new file mode 100644 index 00000000..7ce920ca --- /dev/null +++ b/site/scripts/src/footer/search.ts @@ -0,0 +1,52 @@ +import { querySelector } from "../util/web.ts"; + +/** + * Sets up functionality for opening and closing the search modal. + */ +export function setupSearch() { + const $button = querySelector("#search-button"); + const $modal = querySelector("#search-modal"); + const $modalClose = querySelector("#search-modal-close"); + const $modalShroud = querySelector("#search-modal-shroud"); + const $modalBox = querySelector("#search-modal-box"); + + // Need all of these together to make sure clicking the *background* closes + // the search modal, but clicking inside the box (anywhere other than the + // close button) does *not* close the modal. + $button.addEventListener("click", (e) => toggleModal(e, $modal)); + $modalShroud.addEventListener("click", (e) => toggleModal(e, $modal)); + $modalClose.addEventListener("click", (e) => toggleModal(e, $modal)); + $modalBox.addEventListener("click", (e) => e.stopPropagation()); + + // Keyboard shortcuts. + document.addEventListener("keydown", (e) => { + // 'Meta+K' to open or close the modal. + if (e.metaKey === true && e.shiftKey === false && e.key === "k") { + e.preventDefault(); + $button.click(); + return; + } + + // 'Escape' when the modal is open to close it. + if (modalIsOpen($modal) && e.key === "Escape") { + e.preventDefault(); + $modalShroud.click(); + return; + } + }); +} + +/** + * Toggle whether the modal is open or not. + */ +function toggleModal(e: MouseEvent, $modal: HTMLElement) { + e.preventDefault(); + $modal.classList.toggle("hidden"); +} + +/** + * Check if the modal is open. + */ +function modalIsOpen($modal: HTMLElement): boolean { + return !$modal.classList.contains("hidden"); +} diff --git a/site/scripts/src/footer/theme.ts b/site/scripts/src/footer/theme.ts new file mode 100644 index 00000000..fbc2144c --- /dev/null +++ b/site/scripts/src/footer/theme.ts @@ -0,0 +1,18 @@ +import { querySelectorAll } from "../util/web.ts"; +import { setPageTheme } from "../util/theme.ts"; + +/** + * Sets up the logic for updating theme post-load. + * + * Note that this does _not_ handle setting the theme initially on page load. + * That's done in a separate file since it needs to happen in the head, whereas + * this code runs at the end of the body. + */ +export function setupThemeController() { + querySelectorAll(".theme-option").forEach(($option) => { + $option.addEventListener("click", (e) => { + e.preventDefault(); + setPageTheme($option.dataset.theme); + }); + }); +} diff --git a/site/scripts/src/header/main.ts b/site/scripts/src/header/main.ts new file mode 100644 index 00000000..de54bbd4 --- /dev/null +++ b/site/scripts/src/header/main.ts @@ -0,0 +1,19 @@ +import { setupTheme } from "./theme.ts"; + +/** + * Run all page setup operations, initializing all interactive widgets. + */ +function setup() { + setupTheme(); +} + +/** + * Do setup, logging errors to the console. + */ +(function () { + try { + setup(); + } catch (e) { + console.error(e); + } +})(); diff --git a/site/scripts/src/header/theme.ts b/site/scripts/src/header/theme.ts new file mode 100644 index 00000000..3c4b8f5d --- /dev/null +++ b/site/scripts/src/header/theme.ts @@ -0,0 +1,8 @@ +import { getConfiguredTheme, setPageTheme } from "../util/theme.ts"; + +/** + * Sets up the logic for updating theme pre-load. + */ +export function setupTheme() { + setPageTheme(getConfiguredTheme()); +} diff --git a/site/scripts/src/util/theme.ts b/site/scripts/src/util/theme.ts new file mode 100644 index 00000000..8de2e24a --- /dev/null +++ b/site/scripts/src/util/theme.ts @@ -0,0 +1,100 @@ +import { querySelectorAll } from "./web.ts"; + +/** + * Get the theme that's currently set by the user. + */ +export function getConfiguredTheme(): KnownTheme { + const theme = localStorage.getItem("theme") ?? "system"; + + switch (theme) { + case "dark": + return theme; + case "light": + return theme; + case "system": + return theme; + default: + throw new ThemeError(theme); + } +} + +/** + * Update the theme on the page and in local storage. + */ +export function setPageTheme(theme: string | undefined) { + if (theme === undefined) throw new ThemeError(theme); + + switch (theme) { + case "system": + localStorage.removeItem("theme"); + switch (preferredTheme()) { + case "dark": + document.documentElement.classList.add("dark"); + break; + case "light": + document.documentElement.classList.remove("dark"); + break; + } + + break; + + case "light": + localStorage.setItem("theme", theme); + document.documentElement.classList.remove("dark"); + break; + + case "dark": + localStorage.setItem("theme", theme); + document.documentElement.classList.add("dark"); + break; + + default: + throw new ThemeError(theme); + } + + setButtons(theme); +} + +/** + * The known theme selector options. + */ +type KnownTheme = "dark" | "light" | "system"; + +/** + * A theme that can be pulled explicitly from local storage. + */ +type StoredTheme = "dark" | "light"; + +/** + * Set as active the button that matches the theme. + * + * Make sure to set all another buttons as inactive. + */ +function setButtons(theme: KnownTheme) { + querySelectorAll(".theme-option").forEach(($option) => { + if ($option.dataset.theme === theme) $option.dataset.active = "true"; + else delete $option.dataset.active; + }); +} + +/** + * Get the user's preferred theme based on a media query. + */ +function preferredTheme(): StoredTheme { + const prefersDark = + globalThis.window.matchMedia("(prefers-color-scheme: dark)").matches; + + if (prefersDark) return "dark"; + return "light"; +} + +/** + * Indicates an error during theme selection. + */ +class ThemeError extends Error { + constructor(theme: string | undefined) { + super( + `could not determine theme: '${theme || "undefined"}'`, + ); + } +} diff --git a/site/scripts/src/util/web.ts b/site/scripts/src/util/web.ts new file mode 100644 index 00000000..dc9116b9 --- /dev/null +++ b/site/scripts/src/util/web.ts @@ -0,0 +1,44 @@ +/** + * document.querySelector with type conversion and error handling. + */ +export function querySelector(selector: string): HTMLElement { + const $elem = document.querySelector(selector); + if ($elem === null) throw new QueryError(`could not find ${selector}`); + return $elem as HTMLElement; +} + +/** + * document.querySelectorAll with type conversions and error handling. + */ +export function querySelectorAll(selector: string): Array { + const $elems = document.querySelectorAll(selector); + if ($elems === null) throw new QueryError(`could not find all '${selector}'`); + return Array.from($elems) as Array; +} + +/** + * navigator.clipboard.writeText with error handling. + */ +export function copyToClipboard(text: string) { + navigator.clipboard.writeText(text).then(null, (reason) => { + throw new ClipboardError(reason); + }); +} + +/** + * Indicates an error while attempting to put data into the clipboard. + */ +class ClipboardError extends Error { + constructor(source: unknown) { + super(`clipboard copy rejected: '${source}'`); + } +} + +/** + * Indicates an error while trying to select one or more elements on the page. + */ +class QueryError extends Error { + constructor(msg: string) { + super(msg); + } +} diff --git a/site/scripts/tasks/bundle.ts b/site/scripts/tasks/bundle.ts new file mode 100644 index 00000000..9b1681bf --- /dev/null +++ b/site/scripts/tasks/bundle.ts @@ -0,0 +1,40 @@ +import * as esbuild from "npm:esbuild"; +import { denoPlugins } from "jsr:@luca/esbuild-deno-loader"; + +const footerResult = await esbuild.build({ + plugins: [...denoPlugins()], + entryPoints: ["src/footer/main.ts"], + outfile: "../static/js/footer.mjs", + bundle: true, + minify: true, + sourcemap: true, + format: "esm", +}); + +for (const warning in footerResult.warnings) { + console.warn(`footer: ${warning}`); +} + +for (const error in footerResult.errors) { + console.error(`footer: ${error}`); +} + +const headerResult = await esbuild.build({ + plugins: [...denoPlugins()], + entryPoints: ["src/header/main.ts"], + outfile: "../static/js/header.mjs", + bundle: true, + minify: true, + sourcemap: true, + format: "esm", +}); + +for (const warning in headerResult.warnings) { + console.warn(`header: ${warning}`); +} + +for (const error in headerResult.errors) { + console.error(`header: ${error}`); +} + +await esbuild.stop(); diff --git a/site/static/fonts/plex/IBMPlexMono-Bold.woff2 b/site/static/fonts/plex/IBMPlexMono-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..1a30540c9f28857d039763d440b7df509000e55f GIT binary patch literal 46684 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

tNl+nU9!rT4-0sw@p5p|y8T0k5q zG$Q($^q#Z;$)wbbY-^^sqP7&^Qmj(F=C#x7WwHQ$MojWqmo3c(?6A)fr@Ws_%>mqS z*T)`vnQuA?@Y?VD-xaU~Fw2@75G4T|7CFr31@MT-X{E){{D5o)N>%fGxBe%cDxk}N zG1F$Z4O_i!b~)gf#q<2z&k7)=D{i^(Q%|oiR+|5T_hbCs^Uo_foiy)Qfjh>##d zwqn(qbQv-gKnwkP%e@$EVxo;CTQO9sSNm6ujCbvVGju&?&CL(re37eEu|j98l+Q7< z-?92O!#LEWg_&WgZBF1)dmqGv56GO0D}QGpX%20ut9qS0F&!HHZ3mp7<2rLaoa0G24@F`%(&tnf8ux~#Rzpjtp>5Ex!l)s(-q5ivx}~1P zlXwzO;z>M-Cy|nzch6zX>ar`^u_uv|E2ZBUgbFMQwzfu(CpSbV`1X_tBZm$6vo+c-5RYFoqT1Hl`h$47BhB9@e`JkI7u>UdL|ZjP8qW0$ycIUtyWmm0gdoiJ}*IA4#u(6 zxz%()S0TF4gx)8%nooRUOcH7#PsmZ}TIL8&%F7CpBT@q>m6Z`ZM`mPJaM>h=Dq}AT zxCWPbX`ab|MN-N|=x=FQfs}CTMy^dX0yd&-yyY}r?>vBc$xxaSf;c^18q;Q5?6uGS z2A97y9cT!Jrh^Ts*mS6&tTi2OXr-nj4Wrz2v|&}6jy0UXIo|Mni=JqNeXzKGAartk zq7Gkk@UzO7>MEGSFzyeHkI*a1?$5pfA0m%1qpqM?q@sPa*4E&)AQKx~=B``#vx^ft%r; z4-qtrO%WJ~<^gfx6Rh7BK0n&_-sP}Ds=zppIo#DqOCjN}H|Sx2>O>M?B^Yc6Uk(Dy zJO+m1|Gbei=72A~#c&3g-*Ovlr{j+2UZ*=#ag1joQXg)0b-JZzn$`c3n1h9!7L$_q znMscx*si%(wWeKM&AOLhe^IJo68Io|6Z|h^C^8laM#7P3BpyjZGLal)GE#}mN7f_T zkfX@0$nD4-$OFhD$m7UGT8N13fg)jz8@ew7~IS%_1k`-zyOqn^f{! zA8U`&Re$8%pBLhCA~FVKKE#74;Fj3AboPhNSf@HSydyP7ec$>|BQXcs z+CmVE%qFs%+w{@TkO_=led__bqT^1&048gkh$jj_#Z3?}=j9TMoggQYRobp4Hd6%? z^Ho-(ssH`bn7F}4xR*}4?!E^sIk@VyOP<=~ifg`d_02t}JoL;Xb}n`xkQkU)*m%$$ zd+iDPegtE|!UT!%#@mkJy-y5z^=o|S2+TM&4(25(LXCIaIzOhtlu_fVYa^w?ai%Od zvSjZkPhtE836?44w;Z2pWNKjx7io(p5J3_YArq8m+oo2XTX*cz0|j#sB9J0X1-`E} z>Rf@CXfbVN3Kc8SsX&*>?MXJ%?7+SouRVG1#bb9qd-L5#1SSgD-@qsm3k8CFm8D^| z+=*82)mSqnWy=*EOAXtLLk*1b3?^k!S8dY|b2s4We68Gjo0B%{tGO7Pb-IVM+yDAq zJ_`>On)8L^9DX^)vYaP4X9MM2iQ2#{hX|v;l))SW4*L4Guw2l{ve(TX7v-N*#b^|( zNxT+unwwck`^MRu{oU^WR_*lFaQdcY{163`rj+F5%_y5zL?O`1*&9lasP~nd#}!Wf zM(5%BS%WbPt}0Z-t@vxVsMKgNDn$d9Ta`>X1trz@a-?HMVP>H^k!6a=sT^pEy8dEoF>yc1oHD3HquNRbbu^P%^@^UxztArY%BXgWMYrnJbIp7?>ngyUAt+}F z&xIm2h~g2=D?&Y_o`!Wp>5XaOc?>F(#$t0#OwBATd3^TIO}FT}?G8Owthx6Kyb>!; zyaXWggL8TDM_SnJU$4)tF(XI`tYgXwq()j*@WnlXp9zMXTnKR)di# zVAdoIU^16Bo>N~pTWz!54%&9wWw$*nyU8W1Vwo9)W+aeTBrBJysQhgM=EWZS4Xe{o z3NqzCt8Q4T*p!7(Y&u;!gyNYCUyx8QJVQB-6*CR+L*H`uE;@Xu{;^XehA;V0AJCDLk@Vc;|-F1wuf?PIp7&LuD$^Cja%t#*$*QI=%{pWwjzO3NvKUapI|>-Dv`08?b0WCmzoh( zYd7y}*0NeADltHQF_M!KRk2EliE1$v(pE3}RARWVbl-b_U#WoeW-op9i84FJ;eK=m z;!wJr*FTI^nurRa!VY2zwK^e3FR8U%(<`1Vl?$~`7?%R zCtIOqw$*dZHLI`nYrvfx57<%U5zY>>Nz`oI7*K8;L{0z+!B}_*V2-0ZKtCN|=H2Hb zVVBbY4t)qLz5>7kI{|Kn2f`zflx zZQHlBe*&$GCV)e=m$#_G4JX_M)G;(X9a577c!!uO)zHaqIg+l)Re5^+^t5?iTi3(Fn zD(N~CXQgkD>6f!H6W?_7&N;T>b-E*Bi*j z?8wdRK8hz^${>r#4h_A<2Rv)|+JQEl<2~eXD2Jc`P#}tE2U@epge!#qar>Je2iz}( zYWk^gJO-m0kZx+-+W>t5T{PFS4J@(c~qs+L(BBE2LC-r3` zBa%>(9!Enz^rM(G?3$qN_zrs;gxl7+npp>}6DOvMPp5ujV-~F2`RMMCG;h;(DD@z) zrZMmJP-|`baUMj^4nhgf+OXj8R8}vpE*EJxQ?1n?ZP?Z4xqE*u)3C z^)X~%j1RT*Wy3b2b;Lx|%G3PW=Dcm|dVNi;xy8&PwRX(6H8e-4(dKj(5$9l$5|h+1 z`G^&NA+^lBu==jBNr1Rjy5;H@O%D9P(aPhDqg9u+7MU%kuNeW(m|jpx5#NXA6WMS^Xq?xH5nX(*CnHeS_m2R9E|x2#1sZKV7BN=FnZD#` z>UcZnV32^D*7voGQ!5oVS#1g8jl42Hi12*DBmqlY5dx`XqW1KNTzfE{oaS@aS#rY; zI@Rx(a3%Ru)X~uqcRgndWVWvly@-#ePR$wMPNY6fnw1NeCM?4uM*EA^#Q~jTr1S%7 zE~Z|Nf=W>pqyW`k2!eA}=Sm@u%d!eG!2QAlaph2I`|FJc$vn~?{2L>~SlhB1 zS>Y}fBINr_M*z&)!Oo#n#g~Yy1eKYPkd;hGZV0g{IbcT?ur73_E2U0EqHqxIj#o#B zAq)*|{LF!V`3mad#$wBghWrpAIZ)$=Xu<>t3E6f^BFe|uAzi~0y8|f-P&I}jWITKh6*@Kd;?-AfuNMEX(V%GX{%k=HXSZ867my$h zmO+Umpzbub(nltvaU?%M%6;4`Q9-#|ctf9ynN8DU5WH6m(?b!E}oa9M#r+P_-|gd*!6s@;0h>Yj^U(?92A zBd{@+;&U|Y*m5sM2(&E;{%gIm8@st`@z*zKkzblay*-mEesSs_DHd>Vvn_9u4-1)s zlj9czcC_&}_K@GUsD56C>8(Vx7OHtDuR7sBZbk|K?_uYPh?ia%q4N3y%VX_a&P5N$ zTd(3Q+H`-{mtEAmZJyR&lIc)AHtAy7J*9gohghzz6G=;?xoQb+#nO=v+>?)`MRA)A zgX-n<7TO~@K;JYQs?iMMVzUL;%e;+R<>zpB*w+%}Vd6s@QI5@NTA`vqHLBz3{V*Qy zAFoN$3S?y$hsOLNB?tBUgs2G@fhk}SHv(Z{8Y9&J;!5DoS_X_m3mHXfn-7_av;&D` zP3&7wqN76qxMN(_$1`^HCBd9iut+;CDl?YY`Jx#=8Hr>f{pYITKqdw1M9e^}QKZ$o z$Qpc5jT#91WR*ahu3D*{Rml6xph!N_@)@TOhFz-TX+Nx?;2kBB#u$GrZnYzm9Gd5c z3Y4Q!B?U->R>1ehLKunhhEK=@^MEN*B>KV?@<$yZ6&j9MmJHk>DfN~S?LB3S4*-VX zj8fn{^S`~T>P?s+ufK~YXSe9JoQ$0BB1Z@f?U+*eJE*#|z@qQW2S;Qq`p?n-+FDou z)w_9u;ckKM@-Dx=peqIsL@IEY$^Y6jvfrV|m)ABHiKf6q^UdA4Yl}>`lIy}&c_-P| zOL_PKh{D+zP>AA{_+V&iwQ%lhqqs|wLp6i74t@4??LCI0L#E?#$ zL``E*Na;=Rq(sIsX^xjIhVi|)kAb`-^uAo*yQbi{TSC4sOA_{Ggt^D2@)9J zfE7L>P(L{aR7PYj#wL>R&6;kV<#AZDFB)r7_1th%|SCSmzULK{aP^qGrNc@BJ)4+4P0pUzfYV5mj z)9Xa*$hb(8)g$QYaJ1GdceF(t!>1#>om>@PsJD1y5#oWoGCtM~BmTG8Z6y-U9ZNbQd zg%TJasKEw6#UQsElssKYcpqw2E5^z{d+rvmGvm>x){SpP| zmZz3HjYsn$%17%C;&FwETsDBfps+BFKzc%?4|o*_q~-Cx2QJvnt4?HXf7nY?0g+f` zt$6{x7Z<9wGV((la-qMdeIaCv_P2iTpuYu zv`B+j%?%b>-Kt-wh9qyPC=z)#dD~642>?nfbk75#dOHT2;t`}4mEb9aTsyy^9lG4D zDc?Pg$A9Vm^L5QPrV^=B{~yb2N`?kr@Y81Fbz5Da#?lia~CbP{=;)B&14(=}?Rcrk!5< zsEssVsa9I$xTdZdKO-xDTA))>v2^rqJttS{tinc$7nlY!=jpOR#}jh z(uK0mN?E2OPq+1tuft1Mk=iGRY-ut3pcSzceG5|Oj!{6CZ7ho}PX(AvQMiK1$8Z4; z?|#SyPiliddqEyH&q6?jNw5zV8aY>J*bbR6Ba*_`Wnbh57a>LfDbhwx(USZ+jY~)8 zud~yZYp&RlEvaxSq3t_G;5)>L4nFi%$Hv@5E<`ReRMmy?VNg;wl>Dm+Ibjo3xC_p^ z0LCE!7dqDv?iMsPsDu@=Dx0D7id*zpf9)+r4Ad1vXpT-(tRDS3N2OTIzVU>KAUfW} zdN^NwRKqA6q{9bH9)Q$!4FFq0mtc{H#?C|a1~;F#97{yuFt$!Vv|RLZguK)owG<&cQa*mC z_cf}A11GFW!$W$AMcXJRKveki<8^}X^7@2QUw~sxRNAAA`(Pj3)hW`Cto%m zJu>HbNtN8Z2)rWK1*$W%v^YlY6yA0Lcq`8wf=#w~9zy^7X-4dgzlY9$eGks7S;FK8 z4;Yt_8V7&cdJTVX1oT3bLZkJ-Tx|I|-UoO`+@NMg{Z_-k0FtNDZL8^nEk>)wAmxck z2}z4nuZUjOc|G(E-x6xETzcwS-+p2RLv4+wn%V~5x8SB2IiZ!PZXeI=FdWr&;dV$IhrhZo)a*JCKn&@O14e~}~td#6IamV~^`Tw`# zQFUs_l^)OATd90WGw5*ZHpEk%Q7+D&H(~T9%b-50J#IEauJ*-L%>#D+YpmTsuhac8 zhawBBSVffrY6gUb*V6`Db+H~MBfci+JSu2;52$BK3w5FDr#QhV2^)2Cg8i-mMj$KD%7waCNt%r;N(~qT0m`@dq^8DgAu)J|j6z|O zN6wHF`4Wv((nK#4oTC$y2N@XdU76lBX+zFpz-#|lF4TL{)P!nIONxS>J)|H-xzoV_Eq5Y` z^F9nsBX5hDFXc|EmAd7*eG78_wzz7V*~r|&P4{h^{T^Mqd6B|EJYA|r9l9VclZpvl zvLakKnKNr!Ku%)v9ynDRJM3)NQ_#s>?QtPO0RK%yFyN;9Ju&P9PyqnCwiFgq&QdA@ z9}MY`afflbgFOUmj^qOuWFmvYr$}YbJ*?IeV({%lmEbW~lfoeTtumx?s#N>6wo-E?M&P)_ zx@sB_glYr_&!p-5K*~V#9ex4|d@8N>;aGM7sei!_{L@I>d{W=lJ$4o~>fqOx*Oss+ zK4Rk0R*DhSTi}gdO(|~idF|XrKcOCBQ!bg2$7WY%s#ti5%t?C7dI#ImV!Q#5ko~&c z=%VYd_%IC8=8MG$+a_70*aPHmw>RNG|S8sQTS~eA*A8kuEkFiZ7Ukb@TUX5CxVUKjREBHtB0!k;_Pm{nu z0Hl#haF7#w*30YckmTSSN+rMyzX=)K)FF@l{0t#j#?{TZI-3@LE)NU{npb!s9_nuf z_^TD~Jl$4OMW?_bNJx?;kN-^~U?mG&*)_L1f7(|3c@pOxPVMJ^JMUsAI*cJJT!Sxc ztF#Qy=SAtOq6w=%opv2*R7JXv|McrW&)WuxYCV_G*c=8vBW!VL7 z-Ow-)zgMl<^?U1|Al{Jt6Zk8LwiEl4D>2yBNTo3Qu(P^6E2K*0ciOdky1RCQ#7q%x zhB!HX=cT_H{f840NeLb+5J#4OF#hQsZ`p|&Glo_L{bu@|LMoux5(9w#X1Lw)nV3iu zzWIH^8%YbF*>@@sx1=c)ks6H%@w%gMqYrtgXomYs|NBy$rS52t6TtZ|Pe7J0y>H5M zZk#^9MpK2y*k1l$mXEd^=p%&Dm*Io)e|*129N(mDWk70rOC;s&4D4`oG(z7+q@2y* ziP2hI<+SwAZDz6$Uds%11h@klOaeMy-ozTOaG6$GWV!Y#`gb(+(Y)r>w>E~c_|s;R zI{MQpmq|)9+fCu`P#J2;ZXhJ~734Tff%NH-jZAmvWG@}$$io_KbY3`h=92HRWUo{C z6k2kc+~1zCJ;L2Y%~EOU?|{^U(L){efQW%nvmqGwQW|*}Hj^$yV*o=ZnivCqf6;Cv zONUQTOd;q(-+TFoP&9#*(h($?oM99ka!N(21q8MxxUDT|P{&4y4`a|Tt`(tLXlvSd zAOw*jS+kR(J!iz4DpP{j4wl_Yt2H%+q_)P@q}?UCH4OLu-k^WV88_HX{$Xc3{EZRI z3|I1VrKKMonVGQ!znK1c)#PSE{{x>UoMGwrSay4%on6A-4!AO$P7KzYE0$})<4txj zAw)C}>YSr_+IoU=v&33-G0D^7k)^D7G(X1sb+>P8vJW#$?d%`u`)hJvfiS|@ikBxI z#!Q(`a)Mv9^*5kpeja}VvN0cV_s6^T8Lc z@Zq8o(MOlaC7UdAo*47FR@w$F1y&zRm2+rG#vH;xf0tG<;mF={k{}epamMEDO}=gN(q}eN}{@uMPDG zA?FIiQn;d*$t{W+f}(*nM}6i((T{LVxg60jB^%RHdf`4B>qftGx**AkJe2k0vN-48 z#1{JDgBe|6X{gKOMdbg#%QL17HLrKkV%lB&L;lT<@Az7}xW%mox+U8?OFSCrdakW; zzwRZ-uOZr=o?0k~E!r9Sx9)t%ch98Jj2`$c-A#pc;)}B>ks4%OqPt6&?-s5mgt#vv zQm=F)@yuOZ5%+_Pdo9+bHE7|H7JM;kdtjvgw%K$nx^K;`{-odgeliWDFlOrIbGX&L z<&pGO`K&QUxU$!09NAC-SA)u+scPa)EWtlH&9~gye{KQX^iq~5^_J#xG+P!{v>ke~dVzt>i+Ty70PvyMMdW*#K z_bJO}Pg?a){^G`>v?PB`?=l43w{b*Up?Wb)X-^aC8mDx*cSzE+S?*<4WcQdmWD z5S-XlbB;I$vVLSFFECY|oJvt6*fMB|T3uhohR2V^6vVrOt=T}A58%r$B@B|*kOm3t zT%LuO+nfB#ml{aZq{uHy)g;qh`){RE23x0UuM{LMz{*t^w?v}Y@HmdhS*qOCAbxU{ z%&Fu!=oOqx2@J23oT^kRX#C2lzAqm0LQvn}RHp=8ELG=Xlaq6>5(Tp|~A%>tD4F>uGD=GaC}K7D?~R27&i z`W_fujK*Wu0zYD#g*1e6zcrr2S_g&C;c}`wgICjy{F!>i`?jZxcprBDMcA^l>cip9 zd{b<^a|I>lPugK-V!|XVgY(FjPFsOaK(C-tJJ1Q}j+;=px3{}Lt(g2%S{~LYTEQy| zOjUE(%Yf#~WL(NfdfRV(eoEJVb9s5g#0R0Fk6+)}?YU*_6&H)_=X~*0gq+YmQMOUj z>^&OWoEHW8%yU!v%9jcpTy0z;ulniX1Pvq2mt33?8MN~IXuGe6vw(4Qi7 zo)&nE7ql|4YM{IY&l;)a^QDZnd%VXzI8b^Q=U^+j~!iwvv%av z8!L%_=ogKOBCSLd*p?6QByy*a1_Oxwtb>)z{dy{E-}c|ed>oXSRR z-#^uT0AIz!Q~n#|kC}Uej(8Yn_LRR8=-+|e8Y0@-m*TQKb&}V9l(O?2dFPrl3lH({5IiwR5jb6JT==7`+MO;+R@VWg;cBxcv$m{w z*^_c!G!w>*<{2*wh}>W4aQZJ?;klX=(D=}qefx&ajPq>Jqj=r^6{yU=CZu37+vR9? zuwCM$pSG?*?Z@km*&qtZ!i$OFqq}zxADwvdag##qv~^p>PKDx*!Ls;%2XPC$1HPNs zkzrZgz1`FwVPLGpa zBI7_9oR-S}nua|6m_yjP#W~0!2!6ymL*4A-*}995X~X{uZx~49Whazd)mRyxJ^}5# zO2cpA)O^(;iARgmZ_O^wVuK9c_Jh0b;y+)x+nu{OC!uz55AXsn2KSa8^v`U#SXPaZsq@m8yUR7aS4C9#L z6%Z?j!+J%0P&h6Bw^+HubU1yOYjuXcKWSB%t9#d)L!h0lHE zVfB)VEhBN;8~s#HZ~2s;exn`<%Ip<3q~~X46BxX18g8DT1?7d?D@`T+wJp{bmfqod z-+*S8*s&iFwaXk~IfZr>%BBz%q1?@>l3B_14q}J+*1{r6jdP$-4xAdbIJ-j|RmIih zV~j?{hR5e3?GE+DQwuw-MZ{}Cq-(aK&8HTWJn}+s^wxm>;FO8sl!@Sg?&$eW^1}P< zo)C6Vi+QhRj9C-Rs?m*Y{Gj}VWv`%?T^GcuOR3#>!q)QQP5tkkx~<%iAnu56>(<8? z-{=tD^ea!Nbi6w6z-&)(YLg-<&(Y^N1{9~E@{^doF>x__G1V{|)nBp#H9tS29q9k% z-|5DmuK!+aakZU!7(B6%uix?LE19n?hw|s>qZ2(W4Kai&qO!Ag_OXtVq*8_xDiCHN zK0z31RH&ArOsN?twb&!(lMTo~Je5JwljmF+!oN3ZBk01ML@5)aAfud`pku(lPdT&< zyg|(^hNf~Yu@BD&*E%beyky=CHIO1Br^uG~*oTsPVvim>pWT=}WcllYYHV-LUA_>oRXXkzKc*0#WE8+ z=)B3Mq@wi-ZXrC?lT=5FA-lL-7deJh$KkNWgm8k9#WE7Y3F1#!hxD{wIzRKgQ4PO6 zXyDJ_g?9)yfU}Nu z_;;M5+Qf{sMjmvaVmyz%AQ@mo{56Ce5BeIQ?mT40#FX?k=R-r>=y`@NdgXz?%B@Fd-Yy4o z4D4WLCxh_%S$hPeZ$m3R;&ISmamXDw)d(F%T9de_Ou4iGlg$977T{%CbcsYq=~LHc zWZXXYq`;=KQ`2Yq`Uj1XD2fiA0k^Pt*etm;ACuC^Ddj{dg@;`K*0tY??ssu!&`(B(l7iOBc=>QqJ;y?h zAzL{dE2>!7Y^H<|ftN6M>*6B_5-$Nm>xwmmE@@R()`oNYiX0L%(Kekf4MaTwW@fj{ z{s(;K4v3*a0u!_(NlM<#zep7x2bHF&!DQU;OF1niyn_$na0M(NtTpwUu2Sf_U zL6Jvl)#hmVue}>LnF|7oOu~fv`1+4~-dpo4%#VpYakjp~?&WC85F08y=yh^eAyFc8 z;ly%SR#Ib?IB@%Km=tMgeZ90a1@qhXKnPi=U?6aFqcLtnZ*ok|Qw=hyLU0`VA!Ho# zArx{|AQxQiS@G?pbTU5Wx3=eP@xQlawuPpgn-8Uc1WQO|^b*$`6TJn*GverR3_Mn{ zYmjWiO~DykeWqX^sN$FV-yc!d#(uj|J)nkDUeyRx@5A4`N*h(x!2Nq0aaVT^JhQ3! zy`#NjvBw7l2RtF()Y#9D-xU1Ae5Twy7Q(%S^fS-$cxRD|Zn2|(onc-JR{Z~-eu&## z?rp^FaiB=Lz)gpJgfh^`P^F&~!PSnh)%#`8gkfZVEGcz$8+(G(X#x}DxQ~%YJ@Ik6QRM~(Eph%o z-kATb2yhNByEvPd1CF@mKg$1aK9R=QpgiJUVB&{g0iGerGbe&h%#e}K0I!De%mue5 z^i9T}i9h=D9OL!Vj>bgy4QglXs5$7Tg|dB=GGAdU%f8)euoh)xkZG6<)u+QMFjPzh z%o>y%(J)JCI98~3;V>v!wacrfv<8P-W3X$v%dSz6{WNI_s}hh(Ybny<+Y`}pEZT=q z8C&i(lU<1n{GBx_VXpwqQ2x|$DPR3C+Sk<=`@?iuSJ`s#>r)80|NS(PvQ5?mR#G|Q zq&8!%6pE)NAs`1HnPT>C;r$awNm36*ti@``abB{VJK$iLF{bwqPqZL@UakH2KY#`G z2yJ7rRDTp75C+HG@<{clC5aL#sis++^fr@`-fnSL(h$yrI^VI$ghbSDh;gllS4?UIsw!{p8Wv~kAdl6u?T4Byo22Dt76trvhZ>U z0wO06AaX=x_-kD_v>qxCe~B_-ek?OQ25x5g!KyEe1%icKd^CiFz)Qs+#kGQC0%O1l zj9;y5zEFvui&bA_qjLC_Kz^kYr7aB11*FZS%>V*(3&;1_kp*nvZn*wbgUw{09Qb$zX)Fv`m-co&l2YkjSGK2rA*nddXEtA*&#aIb69Y| z6a0o}#;o>5{&2P6Q1zkbUC;YaL3RR@V2p>(#&5h(UE33uRZ+Vfb)l(!p-J>CL%G?; z#o0=`qAIFN^%@d+qx@>-e}t%typ;V{U$2obqxZ#H?nU3m3qJVpKGN}d@l0f5f?`EQ zbaLa8+P`6R zw;|OLj>r#06t+7;&;v`OP$@Tc&r7ywJ^4NPI~fxCPP3vx^U2U0cX*AAJE1 zqz76v%{^~#zQv3GsA<^Jc~I~H=i?y$$JK3}w-;;>$HMk6Jns|RWhT4jpk&K(ttrpx zwB;E%+LSmYAW*Iplik;bRq%gpUpr^^~x`h;8GAAUpG)D_hUK z-`e>8IfZiZU}9PXa9Mihv}()B_{%s0_DX!h39P^D6B&n0k4=Z@k$P;ELghjUKf672 zl$fGa0VIbb)nVuY1G}>yYTh{7srdX+W}2#;)aSQEeX zU<;eAdRN2=&UR)aD`+UQlJbTl;Jl%XtE^tyP@NZ1Hd<4o*q$$|J(ZuCBz2WS-)|M1 zHOdp^$#r(gUzNLcgrHF7zX?RI8KkOW>T~de=}!ERKBtPQ;^<-;DuLC8^jWe%E;{iv z`wQ(C*-uZ%MFR3{dbI&q*`SM=f;&zr=H83)Y>#dYk)>&^3sIhh`00I-o&%C@*FPwy z@>L`Cx5+{NRkfPaR4g51kaBZi0%Hx=R+L*)N@h5i&?R?<*Nz8*KdCu>C0N(v{x|qQ zKs}F|?)pWG8$9LCkI1PsIm;>vmQ{Glzs_Xbcjj;WZ>KvyGN;r?0&56*{M%|zN)>X+ z+;yse?=0v@?L42llV88TdVfRt=Bf?jRUs2qbDOHl8cI*S-!zxhdm4@{9&7M;vH?ZK zX0HbU{C|&Como@nURpMNL2zNU|B0h06zb@Syf3c^uB={iaxDoO2Q^yIgB3u8Y_HlIS71WOnC{P&c z`;x?7E(xUg2D;;`gmmF)(ibB*U2aw>^KgAy7L}F~v1i7KPCcHrvhFadGizi=YLJZ8 z7yD!J>)NkrE*euu`TC5^o|Pe1{1GzI9gm0Okq>Z-vzfWB3@$Nym+;te9fnSf{5$gX zyIGK%wUR_*9xnZ4c^Doa<@28lG1X?u*1A&TjKcGwGSoK20UQ~30D(TTO%W=}5iT|G zbCUvgbshTcP)ES7q+I?|gD^)F6nYGPIstzASW9k#i=FSE&rWciJ{gt2;3Qv2`K|1C zHbfVxiYHy3UA#(C#xI5F*rt4A%3*SPK~wv`zz0Ra=U~_?=MuE+;@mtP>|C(h103zD z=;DlX*cI$;FX3%$3~$NBVOr}6rMytc z#d9n8h^%wz*!oa+W6i|@W_D(WFv_6Hf!S#?I?J?~1LE0WjSv+vl&DwQntXO(h|QWC zO_;}H#@qCn0mW1a+XQjFj}8+h8*L4>ClJtc?UmINR7=JG6-N(k zncZ@{Vu{|Gf|Gx=*R-+aaKW#+zpCU2R$C3Q=6-;}wSPRfUn|t(?nU%gUr`~-pF&DqEEzQ1<|3;VQGnXfM= z^b>J?Qr7Dr@x&Jsc|otUfYpd{@&CYRs!A=oKX8ej^kJkyW*<2hYH5f ziY~N^1`mEho|JcBB1;k=1FZw_-3?UcqM>XzNa5Ku(L<_vGQdUXXVT)M>F)Yk|8W3C#3$v$5Gs#kVjHSo}JWC~T~lZklm)p@er4FyUYTV92S2H<7k z`z1ze%FNH8sC*4BK4vc$>VaV~+pqOtB#T#+&Fbbd+KWq5U^@>o?OViLq4sWy5t^RB zo*f{93|<6~G?1`o55|@1ba5qQ=~+B=H3mS|8F3f($7S%vUxz27)hO_u>H=_ezY`ak z4|&CzndlKZbVv}@{~s6M zDH540&wHEd_BB{4$1YjIatfSP6V13*;QZXOaGdyoh!hR#RER4n^mn!r}DBwC$f zS+MZG6yf7!(DYpNIm~qtNoGZ+19jcYj=74t!s{c=JHEeRI<3FjDv`yu9^0{FrBhP% z#>2kK@%L`+{#%ULb|2|+hLbmI4)~(_GQ6GL7i6)bbb=v~&L}o}-{^?jS&rPfa?`@r zy7fn9>Sj)_ufxS!Q<7~d;{u}?c9)ONsHt=VR(=CpXG!C-W$IDV~(b#~AIW zJid32Qn%EW*IqeoUO;I@?T>a>I}W*i>`@SLne5!RXdzE^rI#6>*HuzJiBz(boC2r+ z&-nBp@&&cA{^2+9)8?FqoO5aM9{n@7*?q`;P+u#q6?KS?wy<*9!W#y{c-#r)@odl| z71gwriJm4s%ar6>RNepfWWpt2yg5y0VOQM&E_+=mYBcBAHFfZ-Kcd z4mvz-Z3eFYB6wW&(H8#i?brZ3JRgeC@@NTWqYa&mH%CKL%e;)({cl`RSfr4g-qf}2 zk$P2qb0T;aJmr}>3h6@EkH#Gp3a>PqC<)s}LbY-CG+p{Ht=MS8w^4wa&=5efR%|u$}wFnh?Vb0zwB0`!HP_lOJwO?v4nd1eG27 zRF|^we9P^tcj5>7{lB@)(;_d$D}sx>Y?=OHxnjnbO9eFzubyZ| z?bg?|^3(m-0f$xqQ1XPr2xDyEQe_g4rbFaHvqet~|L0qkhZWY)@;GT64uftI3WTOw zvp+#=(1@s{bpa3MLXJAannnsMr;-!m8e_U^sxQT)$HvB{$JEs9AcMSJ%ROi=D*aYE zlE+DhK0s^8Qls^Y0Ly3R;^agQ&Y=+HqKxOLDL5SI4v}=0+?;P^%9w1jfmu*5`Mjph zf5`jmj88}e)itCU#F@I1xIh0sBY|Vj$<{f{=2g`-CZjUfT$}i3D8O(IkF(|T5KPAdpqX_?Pl{$ zTGEX+^7g*iYk>mt9BFqC>mKWV^QQ@a^}Wq|U-Wk(LDmiq$dPtegW%KON%zwQZAI0{ zJ@LT!59u^1^tRQoqMGU8*<D2B^y<9QAVNMCgMRS=E_Nfy2ydWW-Wv%u-J& z;SM>L@RWcFdas#X+oZL!?9lsYH7V8L{5l|Nd^{J+z?tOyJVc(te|iUK4!9^@0hoSd zA zPE0sK5XU917qkl2OOb&;oP)z|J_{=~7{W@Q-Gsr<{SfHi>fc@x+`5^R#z@Se`h%Zo z#7@KURWNsIs$#=qFMH|3=7%#UnsGdgaac>wG%ry(;76q^C34BDliJ!`?5AISVSA%% zW_%aR#7|ssxq#0N0w$`eC{{(W_>vh8B=R)c5;5rqO&OHTet-ywpWN;?a= z-~;%|VE!#@a@Gtk834%{1^q5mspY4lA?v@t2WVbnT4Sb2-d0w9uU?$xJvR}#uRSY# zm=<5QxuN;P8M%?+BTA?XkHqVw>c8V#a^%j1dfS_T*f7O$Tx z%xkN&KwLZQ6vE8DRO727cr@Wq1g-Q%-RC)9v$sX>5zqo(Rwv)Al`12tsfGW@`wSC1 zE96%yML!AXygkv|hDSh7TRE^%dcPCUc~ctH;3&4z6)>_irbMTUEul!|FiLr7!YMtv z8m&K-5IQfJ%k&%eMd#knh+e8kv|=;~C=IYGWQ0wRr15wpz_%b3*R*%j%(`{8kP9*K zF?S%f?H!wryTQOKW`#7)y6W;%I~?Gi@-t$+WZ`fJc=Z^|1TTe|7z~)H!h~R<08Ms1 zS#Pg>KfiKez9URBCk$z4`#p}y1$8;jwMZ0^W3$BBv9eFyE`5+EJiYFtt;3=1qL$Q; zn^Hx=lP--woX7tVNcsVJKJPmsnNJ+1zwK?v0KPSvqxr>|~rW_%g8x#SJD#UIg<@n+3u zsTW`8f)K@-FPMd(vgyh3@|6W3yU%V*4VIXLnunTSX3u1-g%_yOWkqXOA+IN{%jskV z=lO67)sTuieLb_R_JtLTbCOLGy-}vf$@cy~0(5U-Q=H(+#C+AWxmVhcc<>;NWo|jM z7qZZ!v*;*m5t;Mi@?xuQPwA!$18Y~crZ#qf{?#vPXfEf#td%A>Yv~tFc73Yeo_4?d zv+Dm+X&N{gY~^skHd1^G)Ap^dcj~ozXWU~%{bJAJfMQ|p-~QG#@PChqvisR^kiY@C zk5{c-flB&gZvjloaXA1dJ5DY+X5>m>1?&G%u0XBDtL{4>f~ny%`}Xl?jC}+|lfkI! zxMsghKB;H12gzURi1UzDau|cDgi|dsD{87@SJ13V1g$=lbfWa~Qh~qkOwda0(oFz|@XVDXLxTJMn7a-M5Q*>!P<6ZHr#Vrwe|n6kVy15C5GC z=$m^VD?9{$RJ^Tx!QZ;`_4dHLxw6Ev+_}K*uXmER_OPzAZZzLuv9EXM66Cc&0Fdt% zH$MTcNvE$w2u~t}KVBFSGi8>^WcACCGvBy;3dNH_BEShYsZNDrzF*RFdfcmg46 zYZ>GfJ`w*4g4w@@=kL3`=CW^w|F45vYi{`_VE!%F%CEJoqNHDY)7|pBE&YY5-Wk&O zpN9DlUiDt3)$Lk6c*lEZaByYrTewJYHf(ThOypZa;K?vpf#j^f$zOzAoDPs1H945y zrPIG_UXsuAKVNY`8g`~mVvQbLppphlhs6e|m7CXaL-mmh;mjt=M1=lhS+4m)(M*O{ z?#+O=&-iB4?@VvLyyCw>eu=p=h`CdKY4(c$HDBI~^1h0`Yf82{-{)OGFQ2WvVR7x{ z)~>U>Gqt$j`Xr!6iKlKqO>BI4!hWdk_|H0T1+*va%q!6IwQ;wj)QsBT)3h^V1!Jo6 zSn89q*11Ywua6VCFkU~NGJu!GhCrlb3PcK)$5MltWN_@lvn1Lntv#^=++ zV+BKpTA1t$+1)*$imEnXoz&_s^fVxYtIHd5ODYC?sFA^JBLi7jV@`i)_*y0$RG+8-`D1(h!!IG^+>Yc;P!!@3hIIj6+U?JbB+ z_oTDu*x8=!*0dN>+T-=;QZ3KX7PKk4^pUdkC?=!@e~THK2^_CHP5u``l*9f4+mg6o z>tAvhas605)Q;EU?NHF;{)Kex#)aI{M!*p9tG{bxIov8)$ew@UnPrWYmD(?J(?mU{ zfBoA>KPK$)QOLp=ybF$R+@KyKb`i%WSN&e&Y85}H05tS{R7|ig@bk!s_RoV0_&Lfq zbK7LOwtKVWl9N%BG#2+dCnWIk=-mPA4U??f}x6h>>!1#Ej_9d zRoS}ACK5JHLRmvWxc|8HtmXZ^0Y#`quHV*7uF}TnRt5~3>2Jbw6q@9OCSR4MxTuOO z%y9Oxs$*q=Tcm+K(vYY@pc`?Bpu?|<1z8PgM@H?wBA0FDXd!tBx)+Up{FJjyvw3r< zCaG5Wabn_^@~Kk?7IqqnFxP@m*O(i_2Iid<_>VWKt=0Sg)=MJ-UN)B*}pvj8A&KrziJ znip~2h@ePsT%0#DD8d^%7w3)$iLl4V+jnPmAGtzy)~h=vS&SArHEFO|*XI`ZgDf^Z zgjzxgNO^@b#Rx7IKUJxluV3t-ms9*j{FzvsFK5qW1VGuBFb@IcI({+S?4MpAUU>m2 z#d=k?-KI=$wKjW}P68{%sIjGV+mvF}m{O?apZJA?&Wi^8UnNPd5QFM%&Yt5HW^!Ge zUwC13?6oE>#FF~gQ~4APpH4XV#Iaok%pesD-voDW!$X+ppy((tM1p{N!-7H!V(4#K z?HidA4U2@F`v8DljiMX5dHmgT`o7EM52#|Q#L?LmU|(KAEEatFcel4D0o)N25HlPT zeMn^pTz38&)xN;;fHf8ZU7Ey6(kWYM#d3{Kgw9L)e6mwR&oGT!`4`QLF_fLmB)$9z zSQir#qXYjwb}}+r+zQ=bfUYVaUqUICYI$VT>aPKFN(4d5Nlle<2ngjI;Oo^WvX&>g z5!)CU6yc6L_793|j2VwFp3W$Vk1ttX+;pPS&$JuSK^cW@L5EzPp81pjo?B7QlYagM z%prAt1kA61W0w741nw^ajIvZ^9G)R7&5S2ZBK5Es^LfW>2M;5a%aKXdsS3&KQ6?KdV zeA4tsDkj^5UbTNQK{CbS32g8EVNmIJ#9Q(|@N@?HrPae}EIpF6bt^`59)6ubd|WNAnM!TOdFYOWmpDC}t@_Kj z6r}clrGd;))P|0mFuY2So1vfj$GF3Jy@5x#8IL`j!q|N=*z7pyXsTn zfL%?f*P2Vn5^)JhYauilxKS3Q^x@7QqSsk&Rqjry3?Xc%UWonm?|ONML>csVT)$(Y z4EvMv4(`6@i{4t&yTh-*J*-``3h)P4TlDsQH{t^R;nhq1iodwgBQ@x!dFVat@icPz zJ*<6b`S1bN92L>e_dSk%{J<(IqMvHVmejx%3|QVrAuepzSO!28&h!E#Vpb}(0mYj< zT?c_{3k@m)jIKHgK%98V27Rg*9N~lGia-GMf*@{h;tGsTZ~=9lHW7s;SV;ud19J$$ zumvm)TR=lU#oSmj<2)>5^d(#j+djxLQE&5@aUQZU`Vua}_6%Ktw+h=kT#+CIh)sQz zhzp2Hj5aBCWi)JZ#HP-PxPXX7t_j}bkvqK(hVa(b>=STfa-K@g*u*7X84-e;gIW_& zsE8df`fY;1CX8uI5rA`>ARveiggLK}9Ea&j18^K=d|tp+sS1fm)Cfir2n#9=zEUMOMpckwly_WXflyYt*&O&I^bW+iCpPS;DD%n zP?~upRt1C_+^`MJj>Bi{TexPHP&!uK-V?q}i+N?&{p5#|H?1sz9^N7e%?-&=E`rfa zRqQ&2566j``J;(d)@l`IokPT`T~`(qZdS;2QX=ZwxlCuGK}Cb9%ju133o$U-Gcwv4 z2DB0_F2pI7xI(c@2?@?~i9;ux;Eai9bwlkwk&(e@H@G0CjHG)I^Xfz?3lnW4XfQDW zuS9Id5e*??^u_`xfTx1B5%83t$yr!96l?SN6BXa5j19Ko9u^$M+&rGA;`>HogKgH= zCPkP7Zf}9Q>j5j71ZKe{fr?G);}A|{j%9+*WNyn0P0Un9e9*rUPW5(um`MX(t%l4G zTQQlQZp(fNr5X_04vjr=kuIWzT0kW#UeF87(ku)whS;bNB7VXAPlPMO`?aoy*uNCNzqTchH&u|X&f1g{2>UvbF-iSIzTt?Sd`v0S734sjsu0Hcf7lK3>O}nO z_Z;7{l?l{!X^ zrBFtz6ING9@6E)3zfNjoW3xaKMghD7(87#j)w>+_769Oyv51ISOzi z@#Ho27Mf!o1rmRVOl+iq0MWBP$hyf67_@V;4he53bOVHnPYX#n z26(*@?43I5Zjua$^uC?#A?wGXptvIryJfU-fju ztC8;25r{`u3}g1%SN~{(!CTVs*uw?^x90Z7E0zWUUQ;YY4|a^ud4&vvV`Y9z(x2qoh`8I1#m3(^oHGP z-NF6?q3^VNM#kLdH0j?xy;5Zt$*Scrz8z~!k#^gb+j<byXHnG(SILqzv8Ulmp9l4{=KUK$IP-qf5+o}xx|SR^-UBgXsbc9=&(@k z_c+V$iVCN6P*zcP(nD<-S}uP-1j^IM#qhfzh3&Y$7zSfPgK!_C+%m*OLPjJg(wUIp z90p`m`T9y?zgF@m{~!d-Iy6o8AL)3!YnE-Rm24 zee&wxE$gSPiNW}ifMsw=J^8t1e(#Id{Nmx<6d-c-(5Y!*3&m_^F>?a{W7?HMlVpaL z6=YzVIA}A7+-8w>_>72&l;)-D++H5VKxX;Xyz4S$QdmMGnrS2Jw9wQxhIL6 zN$De{0GQ1zW+wa`e#6q@Y)Zy3)JdQgtt))pustN^Qv?f_m19mKw4KXU3czz?Zt$VS zz_@by%OwKJ{%=J@xFI?RmO+#vqNS->L<11BnZ-LN;?%Q(0m-QFIJ)RW50bfq;40Q#e_y2a=<>@u&j{? z1f!SK&1l}E&KU2RInw;qP(^wHONDD8B*3>h5k-brJwA!T7_tDtUCm`1miN~0(M#$k zOdq|ey#95~x#OC-;<{GEGw$L+{6VcP>Fi1O``z(rZ47$SSR7qSLs!g=hDzHW5cr|# zNzwNJvCAO`?8C0iVE|AO95?EMd|KB{8+?(n;PRe?=WskU;5+qJfCd^UFiQ_TATTo} z5aWzH0SEAnEwq?}099wiiY_?hfPL8BpeNbW&FB!DTu=H;jlp$!CwLA=G%91!BHhum z=nkEl9@uAFJn`6t?|RL7q4r$6-$mc^1G^kF*4qJs9|t1C2tzH#8q`NNy<}K62Git) zI0ndpQ-OdKBgbKi2pJxLDhin95>ubjyC7LVkvf{}+X4Euj%epIlc%NcpRZE(S=p%o z1<6Ji#RsLE{(5ceD5Wm0`m(;)*OL4x_+hDar;iASl%1(H(|y0k%|d&{M>AfXeLfG+ z1ei`0oRs<5-+0G>#?tYM#iG8pqN+rSbeSEYA~pD$i&fE5399pQe}g0pl|C((VWJmOEE5 z^^zOp5ik;kfr*8U2aWf^$GvL!$&f5SKTn{8cJJpQ5U?G24WRwM7{;HxF}@zJ{H=a| ztQ((iy1JhW%m*W5&;mBM)0z9(ve>?x;U&F^gkM$oPeY?v` zm)G65?Kn1J6mYv;{oesy-Lm5tv869ztta_;>VN2FV5oW5yFSTYZxd%}C@wJMeK&y3L5=APfx(dX<#|fiProR1i^KiNl1%S`>Gy z4Jbo{)H{KFJiP-F7a|W}Us2#fD>MM?d$n(V4TAY$C#s4R2!tVlOGZ?|b(ma2SFNq- zDhLpk-re!DMMh%U%KJy1&9Psiyt83jS{(J3lx$;o?iQ9%1=QLO2!tVtBcePmFfTqM zfp+VN+R}1%1zw{YYcOteO1soS%~8Fi)H}X?JiRkNLYI0$5dkg*J!2f^J==Y zMhb>CTLWAwn3?f zxX>OQdDh#-Sz7Ij^`@=Rd^CCn!9Z<9nq=%!Crb^a)zz_T4gmuNE}}%dUxFB+WSe_0 z;06+@Q@R#xN7unWwfdY@2!$6a@$-a1yb+10iK#5?j8=1kfPn%RQ6fI8$P&Z|EjvPH zH=AR}Ez^;{s+t|mZAjY*Mq_iNfiz(5hV9R2Tb5##q} zIAvGPoMd(l4AxtXH4sY{j1U=T_I#32K-}>txkpsj5GL{S0TK=)l{CQ!<=2iS1`2GN zYznDqwvt1H_7E^o#0Bp|##P2&gl-JU>}D9yTa7gkOI9V$ZxzhF8bXjIbEqCq5!_o3 zp%TkXkci?r9d?Z|VN`JJtkGx%s#cJaNcd=0Z)Uc&qfBOTbri`_wX)Mq={9*)Sm9J| zz~`AZsg=;C1!UZ8!qQgSvulFmdeU&%YNNV>te~#x z0p9dh(_a|@ts<7Hi+><0jX{uk_G>f-HwBcf*IteqrKw3`7E*mA|7Mt&ZCw*TcJFl2 zqc;&vw&bj8JT8yMSMXMgZLx|t{CEZ!YA|TaS3Q#1&(w<9`*TP~U^_7r1(r{Q^lYQt zi&?Y9XO{FtxwE+sdlQ;_zTr%QiDs$B-g5gp5nwl}V1S|Agedp!s|;$&>?~*C%@IHf zn?5(SJoxaEVjOtM#aJqPN%rIX!NKJn4QJ^8xJYAh>`TG^lYHcSl@uk^|8s8B_GVBW zaQa!=2K#@oXKQ}|jzO?JLH_}M{oijb&&8AWoJ`aoN>7$I8G*(7wnl_|stRn`dFkiEtQ&TJ!#_#$EL+6w-^|-#ECjuAL z48g$%dYbVx&UsN;v5G2B44NaY;fvfPltgqHz-cUq8bM;KS;{|+w5E1y^cUBvWJ8CT zz=x?#`H?QQ?>~cQ4X=;}hLCw{xa}w(7`-$e zXHUiwqrRZh#+6qlZ*#{4pq`weV0jk(nj0a81aJ}O?UeYXUvV?O2MIq%YP!YFY<4#f zhP;;un+^N*ql*VfVIC$&HrK&iHub@}2|tvNcwx1^_|T>oMj!qs%b`2oafjLT-Y<#g z=k_%5fS*@rx~PARmSXF=9c*)AR$a-a<1z6AMare#IYHDdqq7K;A;ksz%Vo2e7BW)+ zyx~oK@8rF|39hYDX30Pc9f^k8Vgn!HJk+A1TYE4;;iKPS*v zUZ>vIBc!(Eb$=um@dC-@K;q#C73%xmbPi%z~;jFv|WF@92#A37i-{y0;t3Y z90)5MZ_dIt{}V_umV}1@AoH97AH9CMWbA(1=%lD##C&PXduYR@1okQrLQI(ogqJX< zDPho(kga^-RiJ?!f;AWP%~(;}`fF>^wuL^4c`_0k^MwuFujyxlgSlVqd~-JXRU)L` z?)8vB0r=jW6RTJ;>#g?;I#VTfB%w&XX$>>ay1sCHD!QAfR=*6zs^rR;v3VMfz7@|V zW~;(2z~{BuLc!@2N65EwrS1`(ezPROWBpfboh05 zMCEw|HtJ_r4@y_ci48e@xkhZfI_HE%;((6SvSXkH22J>N9mBc@M(Qdt9Q^nsfBvzz zwR?v&r=3AjL1b*U0@bI&K#$)(UkR-!YOQ|UsmEzWyN<*df{&$y-^J^N`EO|_t-nQPcVQ7KV@v|}d( z=IO6XIR=2#J`MpUTb^+?6G!Z3QlpY0_Uj&_&QfW1#W}3uAk>h;3sMQ7TpLIjk@H_bF8m*s$+<&$!6XlSMMQfu{_*g;w?b?6~g zMZ;t?4GuDwyV5(~)e27IMRaTdI ziCM%^LcnH;3N%bJT{p7U@>9IE7}$SXD+Ijt&P<=nBjqHpQ{|?6VhuSHIg2i6n8kOY zRj9^CE#QKd?3dT{JQuV~v~&pMBAFuMrt^y-WWS0I2d6y%o^+T-o=-vq*~U{2qG69X z(9*YhsAXyH`7yJC2-V2EzC0Z;1}oN+H{CJ`C2^?ID4z`bAg*5druf-weSm36C9)}| zG}X8Mo!l_x&{?<|hil27sem6wGkAzGhHd<_fm02ZcXIo-BVBXaJu#EEDZ|O5Y8G38 zQM;{MJ8aMopBq{j2I(o&p&7+&ONTiJqtByf2Mh<4tGtNTByYw95GA9auYiN$G1gqd zZ&Co}j0>y9^r4wu^G6fU6A|-wT2D`I>AQ~oxhWuxufNT_(=J2vDe&zxbY%h~jLI%u zu`$MrN!e}p6Hdwjj#|@Ci_vp??3_lLAE}+TzY?v#uk*mC5rRu_Qn1!cv9Oc|7M(r3 z-kaEt)Sfoo?`78Pzxdw3!{_&=1gHV?WfpIN?rmaMW%y3N8u!iKPLN)7;9qlNKP{{i z8ejY>CJ}ZzW1_C&Zl#zU9Y#Y1BaRVCJ!Tt$a!0d^hsIyHQrn_*S8{mG^}tX4Ar6_B zkHq)ZsPc`~Ew{7$1n#P5lTBEs5>BstVISkQ4Y?Z%nxu^y zierkKFkqnM7t-w|TT_jza49*NVvzzYKD{go0avR!xXlhI%Pm-{ynI&Xa7PJh`^`_B zS6Yn3CuUL)7HyJP>u1eAfeJH&pxTH{896fxu;4)4jY+HL+log@|LJ2nof>62(H=H3 zwE;ZJvG?0s9Sn>CH<+@;#;*VLr)$wC0Nkx^KP=4?h8-l7gt;s8#va~SJ@UBuqsJ%s zXiC%@A)9CWT-DFzb?v*QRn#;en*w=Vk-ztT&PUyz#=ftOErC6vy-XRX*im8XfssR^bRjHdTxdoK3)V9%=#Vp z3q-sba`m|{Xmh#r9;1POv0Nr3%u=XMEvj1?c# zM@8zcKGG1RC~dy?MhbSw6apeR1RqEbu$I4%tedno?oca$BNrsB`CJOLVZUp?1Gda zYlSYp-|fD*(ALA|(n|fQ*Nn!vo6`f@Oi<%*Ck56mu0iJ)Yo033ae0BOo%dvH=9-ef zHWAq+H-^bWllJ-xaxIX7>x%rRUlgBtVEpv6a{62G?4g^R3uDTz4W)+~WzGz-WSBqy zf}AWXo!1ub*6Rb4v#u~#qX$3&E|YonQGSwaq)Rh@;whi&?-_cB?n!@ja5?Ry)JY5o z^eUP?kH~26N%&z?#h z5KO(zr{TtwhKkmu75W-A*xMU#%ppT@a5#aG3j$z1bJqy@8_^H}Qkk=PqkUAp>CW4o zRV*u@Ywj@C9Ac`ue6<;;$Dv>dPqhl|@P7#oe!GCbr|_YN3P7`3vnc2{NW^;H1WiiyM!&a;V8EqdRP32A22(eh;k+TfG5fWKaBvmjA>QlYa6S8<-SDRU z?0YsEx!Iug4`+YRqcxlDjr;mWYi1prEhd@oLm z@$>seQH}wN|J#j35gyXaF~?~$r=uRk2D3lBKb4R*?gWxwmOI;4}#zZ%f3})-&jQdjJ4?+30YEfxlF797q6q(cJT$q^*A(@(wAAF#ah<-z~;f zJ|7rQwcOkG$o+O? z$PP)91698BvN`uG8^o-EKc*?UWttf(5k7&AuQrWqEf%qB_J*y0;5_VP5O%sf{ld?q z`drw!In3b9ICu;AbS^Au1L4B$9copug)8G}g*Qhpk72!rXZ&2p)93npuESdoxM)WN zeTtgkRQBnSWl%0ntg|1>pVsXKn|EwCV*}G{Dzw5j1%*CB%-#YJYq$Np{FDak!z723 zA&>17ZYvZ>9hG4W%sL?ruX@oyp-?{Qz|g?z_!%a2n1cfDgY-f|zVKV*T=mOb+KZGb zK4t)XT;t_>Z{dc^Mpo1e_fTWv#6un9Ig=jF9c(|c4bNl@Xg3{yAo_^19`0&L}VAN(U}iU1V<5jnyh4_V>kclRTS4NqmabZ#8zu@uj5{} zr~zHfP_H)@^4hA%_0qGNN!YO01vDiTtK`7vJkYBRleUk8=1Bh-x+WNJ?$?e9OW?^R zs(^_#{>Us60n9Zs%BJyF9cC?tct_Ot^?-p1gOu7D5Kwt>5D*Z@YF^Y5hzm7w%e z^*NrIAZCJEnwrgKY9}$9V_1^Fgb-{pGx$@J1O<=4d{%n=>;X zJr_kV>w%RWR!A5E&z+eeeG{CAGn-@poH-0Mf6+^q@7wSD24w8rJ_Czj9>xzo)3wx8 zSCdy}BQu>EK(2lDI>h<(Uz~>r+BS2hLv5Gmr2lZMdE?2{ozK=ezTwsRCEs&DMc2H9 zmq@t^i)vn?^=ygw7d8FIrb#)i=$xkWmZ!ToU4E#WGBsbZx~~LHW%JtCMwO&1(F`Gy z38QwV^FIR$9pUmQBnA|?&sqj7W!-UE#so)fea zPgcduyWiNop|Yemi6;g3y{4YY;WmA;DD=5cixTWL_J-){5eVg#-SrslKD8686Fn~3 zqOj)?isL@1F(5po7bFa37sAM8XPovL_no=J#;S*9>uQ6(Ujb@({^tWjJ4j=PD)5aN zQDihUaM`IKYaG}k6*t!9cG|_0clkhVl|p9>9ENSQoDxt_P~~KSa%I4bP_?X4q~SEw zLL|e(v`TXwnZC6_9%U8QoB*u*EL4{$j*=MNbnipMMB1Jj$H>D2DgWHBa@3xR+QttrP45V zu5!)9#c#P{P4CR(rPX7p8vOSwVc&q_#FBbg46%rYCHW2@0U{i8^Qwur@1MpH4y8MA z&0u3Y!*WgleW-{ECE_vla7lX;Rfo_mC2HAG_k6s$P2B%NhSJu6}~iFc#(%rlFc7^thdgvU&`F<9Qa$S9XPo#3u=t| z=%Ii)%_+`d37?vRv^HZF`qqzrse;;CD-~uAiwE;b`$^O7mu4FFut$3c5FSFv`pM>t zgWj~^$(H*ih6_y^pHO{T(0Z@Oi?w5!9)lh1xQq>3i4gz#93`k=XOf3X2so2oYJx-; z>q}-c(ZmjCL#pMFCX_9}-^wYO)^4~F=;~WyyNc5OFWV|rS+SxwrIVy)g8Qlnya5xm{6?zULQrAsD@QGM zVW}zg6Qn`C^wKiB@vc2jWo%&MtPQ(Eb;E?VBO{bfx`x;5`kUAOwV3TWB8-x*AiGG)!Cp5}quDHFZI12?yq zY&IWS*;n>`8mLPnZnB=s2YxHfC@<;DwVY-a7-o^^xH>lAJ{~PaR!SLbPJ=kHc-xZS z!H8cO!c<+vZV?xF1froM8TKe`s?)e(vG9--Wg@F(ug*aoS4&GmH7L4Wtv)@Gy zq*ZF5#RD3!L)>NZjpmJjQ89o}$=5A7$xgE-`<8&WdEP!Oz)o92$N7?&Cy50A*siCr zlp`FIGDijt%{e5m^62_zC9b@@=?-{eyGl~_NiLmH!6i**{XJ8TN(fve0;WX+KZ+`C zoSST%%_{b@stz8d+o}r?=of}gcHt-%Sp|ZXQRPtOdGF8j_5<~lFLV)+-GVeLgU> zg*<9sdV^786#`Z{B_yjR9|Wcgmql8_A$&eCdgW;08cx0Cp7d~8U^K-_2N(4w$o&G zAbknG8ki+4LS_LpfhbzSNbKj{aPvOQ>v6BbUv$JoQsyQE;iiosqbKA@PpK_UIsjXslq$`XH>o@1bpP2GTaLy4!ajg~P90ey^*OoY}J!jl= zBEvGqvPjuPr75wol%k$c9g;-*&AgB1Aijdb zYDt8HLPm-pkshozyDWKw4`JVOf<=}rlijLV!@#NK(`z;{Mwd$a)A=`52T@*gK!W9j zx>t=NjXoJg7|Z!CN(GHa#zwMYb4tF}5J|73=|(ocq+`F(W_C9ScK~A+b-RG+?4t!E4IBk_-%U7KZ9jJU%TleXUE)Ge z>fK1)oN&M!>^zZLBh4p$e%A&DBnu)oUIVNC4xl1zBwuueg#W282y4h8A{$fg>hnP0 zvlKB!m(9!t&xVQ_@D~qqpX%z(kYsGqq3e=C&>$RI9!c&2ZR#%XemCRL4IXC(ui~tV zv`6c_Vzg;1f-m|^780|7wqc=V5^^io76L-_>87w!0|KGydQVm?4+H!Xf$0fOOc+?9 z6_k$eHCb7(6i#;}E=bG3^^`ywk~&%G>gm4o6-`DayuWmLTCmJ%kIz zlptEKi8rN-m-DqEPSY-H5n=Mu3Q6RZ}2y;N(tUedv)@X?Xy>DYTI z*WSpsK?Ze}(f$duwst=$EM|V{YH9z@Rb(Vskx~>PAEoXECo=jYL+MS#1ytMCZQbje zywEd=HYmA=qiOH%xd?H-GjJ_fbU-Y6WEv0eK?4NQOi3#Ro*5*=5Xj!NKC7yWGXPtq zgMUl2!|20$aFYoY29;wNcwzf!arS$ulLS@<&!dELifiFp5N$sC2C@_f@u(af{AW4^#y}O|+n{GXQMCFa~4*0DQhU z(vz+v13hZ!oVd7;D4pI))=UjI(>0CkyuJOh-t%+F=IC>l{d=2N-5LG3Ap?>aj!{4XQgd*R3^*jWB z^`6>fiTuz7-yt6?KUoFREOS&OWU%ykTRru0ZG5$TSgfaU2nKrD*Mz7viQ;4QR=hbn zcqf+XrTTGe0VH}oQ3YOn5q+)vf4tY3^v~kQNL7<$noiQPA}{=UaL`-%Hol_6Wm(fH zW|%*-?gLckXYY`%pJ_4O@+CMpa84#b-@9_xm+C`W3yV$D^p2M0(AlJi18; z!?_cSJtb)PZ3YmQZGtNEXQ9Sw^T`=BmgWh{ETpB8-5+o%(dg!>I$`PPOwFp|*J$X` z!tQ@$`cDu-pj@;JE##Lb(yAcGl6n=Vb)k|Wp(09EbcR=WqB$Uq=BV^7nLi*c8L$>U zd5tDt;nY0cRzhOAQb&USPwFUX*l*&+*}Ecdua(M;QMW@wX|{X7H9}VezB7ns5^Whk*rj@@K99CSEXBb+8nd2VoM!T{7IlbrAC_Pt7R{J+ zd1t}ow5nb&RKz_O)`=(I_S7J7YIl9WRlD#x7gyOAb^f7JSlD7n;>n2rvIU^)^xl12 ztNZpm=y6;((V$-7f4t39kq+pL+HUas4xO9!JfO^2E6D_zf)R~p`UbHU8|Bx)BRchQ zqKGo$ljD7730otD@K#@iFMc2t#M;7~+QzaOG2-aIP&lTgQwwU0&=+4f@|~pbyoPN-ZZYlmb7XIMCx^obbMl+9I)N56rB0Zq6oi}s{nTdn!$N{2} zZFKk|Dh5PK136mp)Np7Qmf*$eWlcqmmM`V(-Tw+sYzIK|WR$oZMWRXX)s`$eGmHPL z>A$+&0Z{elsE1-*y!W?$3*dhm03PpzoE422XcGpm<0;l#r2-z83pk8k`k2mM?M)Ho zDf}ho3MgL`*YIQ=u4t;%KrDMSvtYrh2`5do{T6U{cuGX+&5I!i2t`&L@yZ7!e2X@K>yO^Ce*P6J zl)itFmFa~Su!robX6`<&u1uofE>{K?1IRQPigOI-Y|VU**?s$1O%-P)WgslSR1=Ej z)6ndjMZC{?m;C_O*KFpU9eJr+X_jM_Z()H2ur5e(e<+^8twIXmz*$f5E}ihE8Y;;f z&OwCRdZBm!*yAc(HUp65{LKUe3Bth;JiRF;#84}bp0L3j#a2)X5_QAH780svrqwoz zsOAW%&KWGT1}-}h9K7sa5Rlc;PRA+Y$3R0SR<8UO3_RT$mbq{j-CbG*m>_x*!b{ry zxPj}qWbNSc`qNn}LvZo)QFVF70k3%*-jE$R+cDIaw%uXjh7Kn+#4l)5rmfynJg4-a zr2d{BBw3!L@P@FO1w{08MGXxe*8YUVoyaB_K&MEqk-!&hJgm zpimxTh-ZBL1S7JqJ%xyu>FqW5^UYiR7b;5Irnh{V0BHq12S~#7?ME=*DWOVaSvl#V zzXQ!rNXBLg2Hsnknf=b*=`8J}ZCPdpB}yvM6W#c864<}*DyVtaxsKi*pJumdzG}@b z(cDO_Wpy=GZWzNHCooQIRY8^d)efP^_eX(kdff?9B}I(4Q`HTrB7=!)Ov57mnxscwBclAqMfAbExwFK#T_f!XKABz?= z=n!%l(y}v9+7`l~5X5i*=N#<3Iu{QLtNLYK7;UwKmb5W%ycs)L;G0>%6LzIIYY|+% zLGp;HaA8Xkf z4yCgv9v=)#v&W?u!?VQ03hVCz4SymW_5;yfUC8ZrzS)5@d*=}# z7jkUx=n}M9svn7dg0g8@&P8kWfN@C7ER6e^wUtrhL=4$DYpy~SE~*$E_l13y>c^#f z|G*8`{WB2Q$pz=2$^Z-wzX^Xyhgy6!5U40?tD(rMEX`7+loC~4!@72rNQ8b0_UK9J z>tS^)1;%+OEvH*;+Jcoo+W?H14y$rnVZKID2!#dQvCFN*DwyS53#%f%D4_XRD9Xj2 z1F{JRg;R%b^aWudEg8~`WGF*Ggzn1}MZsjdLb0SHMpp^CsrKxazb}Z))%5zJMLIW2 zSV$Ae3$0|gy4Q)>?uFEMJTU;cE6OX`0EL8KAQtgQQ?Jjl8svFUK-4Kkkez@}{~s&TpK^H}kO=s((0O~@o*fmj#)1Q4 z{=|Pjk(lM86g=Z%t1(q9a9AmS6pZ1D+DuC$(UQ__n?zjvCiKZVWLJPdgnc?QXR6_NJ6;EZ^~qhmk=I6ZUR6*Nj&OoKD|Oac!?-!zF$GM~)W2mK z*iNeb?42yuv>lB}K|^>#V069?d*8K6!4gjbnZzvKc9YoN@W_{#B;}c5^~Fs2KyA+t z^e&5huHk@~mt99hoB_blYMd?i2xmIL*YYS$E?}k_XEWXZT0)Fu$^$zI>JZb14w?-yiEfL z=*Go6u>xlt=bU&>oG$M%>OFY&Ua30M=mylp*E=ULSFbVA=ccH-_T zs7bC{L9-)QXS#4p$BLo05JlOkx_GzI2U$;Oy;3%+55XP;gs(^qE0CqdIn;*(fb>Ck z_h4!9_|>w4QDS8^ve1_muRPVpg9c7;ddAbz^KBaS$Be!#^!F3qQ|C%QQT-gRRnbx|L)buAy6A!s=RUcz0GEG-^Neb{Sc zsUD{}J`S5B77b}5WOO+Y{(Qom1rxXdCJ|!eGTosfGLO_HRV*NyDm}!Bgul09EB3*& z|7JL5pa6v9@ZD-w#z_KW@YNOF423PbAw)^Q<;0M~geKgQXxWM;9xgBs0t12sA*Vu8 z6WUt86f|9JOBKYVoi!JVPa1TDj~toQcw~jVEqO^E?H^wo6A~}gznQZ2tY6WIq1Rea zHeq(y?#E2ND%ke->fN%(OXaZiW-j*2YdR?7#D24Wfnnn$^m*hB< zc1D(#+7AmrxHN-${FY{qUoFiZzlymqMHBYU#>+A2xotWH0zO14{0LB9I$i-E<{o=# z_JA7cR;u!|GWm6Ff0G1AMb-&EH~<|FeffAEowv`_m%U^eu2)Isrc%j~{{;R*LM+Fb zs#CEkobK9ow^|rfR##w$MF?TZ4n762=7NDcgTq*F0Rvf(bw0YqWz_=q086!gbOJ*; z3UKR?5n5V$gz|A?`;;a1^`8!doN{<`1M(ec z8*YtKXhrjw{{QC|Sp;B`0wZi$ zjH|^#DIO?gc=o*o06L6}Bjo7jw&)Q8OHd}^FV>bBlH@%Ve>wv2zSm9G;*l>-0UjKT z4c=gXDWW`X&=KO_`;O|iSsk8GL12$YgpP}x{W&I8=Ruv^V6q8^(n%E16Dm$#E;>an zg+)VoKTo1X!^If<&t>VA+T1OIjW!7SKn_oHuI0B|1<(@vZ2`gkZqxlyLy(V0%W@&N zFKDdKd56O4TVuSC!03V%yy3)j_Y*LFBtcRSORy34FOp_X6>w28nIytfIx@>pknPnG{z)GsmA|?0;nlx zff1;JQ{JWFyaffA3K*0Kgp~`}0-5`P`2ho9V*p>6U`BA zmXX;YSc4y@n3)*?L{klt8_QDVEEq59eAq|;2R_vtaKP4avXB66L$0B>gsXQc<^+~X ziBv*KVYWuvL%O6VFsv;DGx|6Ymr*hh9SY8fl$aRgA>zKHPC5%PfQ2 zT2oyRI5qoiDx@zrB-qERut*V6gy1xa0&_P&+|e_#gH8h=ti9mi!7bhIkqMA4v5MN| zi3L1k%&kS>6g{zBt}gC7n`EWR@83$D_O}2t3?9HQi-q|ROxkR2D4LGnGURrZ0pX5S z*L-((%8f22?jnN07bT1-f(yccLBMY&VSNf|f^d*?cl#1k^3RGBXW<{Tv|;I6H2q_z zVp)FkKGj6YQL&b`eRSwC^qM7mULwWFRH|7|hPu>Gm>48fsq`$0G#awf8hadd#vPw~ z9t&LmwU}&yC#oK+RnG={Zd>zGGx65{X8(sa-KMvT(jqVRW?WH6S~9_IDlQi~Sn0=^ z^hfsg!<>%4eqL3EH<}gS(o!em9#fus#|A*6=snZoz}~W*_p?b;q;8cZu_3nE?Vz)c z$ECPIkBJu^#k(+L!->1E;Typs#rq>Qa-vAZ!{!>PZZLgyo@-POxy1?e1&*>)!S^9v zvY>1k&C@j?HrZjXlMcuExZ)NAPu%m$llZ`bo$p-2GyH{$mM+mh>5=#Uq2#Y%9w$44 zaV#whbt(3L6UASI6N6voe8qZ0ZqC-DRhoh_A@{3V&MB7UZa?R8;4v zuKG>0I-{D0=}C^p9R$CJk|dib+wne4$#TVBw?-ZIfdPwA_xQ6DI?6?!XeFZ(4XQF; z|4Efr|NljFOWH)Lx{|Nte>FCxvFFjyfv(zCxw-e&Oz1ca>IyB;m0X}fpuv<5HLpWY zt~7Hkbb|5x8C|WhB-+ls{gU~ot&)3`2LFat_YsNEf0IdmFV$VQ$DSShnDJ=fZ?5;Q znH)qX&V;kEG|Hq7k>f+yM(X`^{DqnBdLJydp4(bwI8jt&f|3cEN@2or?+uF|P$gdw zI!LKcSsdHwyLqdLyL~#N?0aK+006KqnnQP>d51N*?o0h8CyT^=5l0&CEV^7;nOjPW z92e}#=_WI+bhvKYv1Zw8BoEm)*^)Z`9>2J4Y+kC`*p(N-XYcHu1S*%w0|f=1FPV6s38OqTKWw$iFW4xUosFPJ=%d7(d zUN3F$g?Hvl1TmQoO$mQug6vsLqvU?TNSw*NXSo@)KzuoYkWuX0s%XL4IsiRMigPxn zV-avS>wqI5aOdcW2fE2$WKHs@t{9%pACw$=MhRmad;QRf(kh01kmu>FmdhjLQUwDB z4EOU^jQZq-7N1q;11VCZW#uatF0k}e$|p6eyU#FFJe{8%X7g=#AnC@p)df4bA+skV z5qUvC#awX&8}E0Y6qH$5{V%!GpA%U9e5T8TSZu`KiyQ)-ooZ>S_;C$pnyj|B!+X4v9xx5$PQ`Fu|a}E zUh3_!+%m=o*Z~@$V@gXIs)3~Z@Djb2irI0idd7GW z>V9U-G~|^(n1mc!0F# zk#Kro01D?Bc;wCNzkq^DCRA@Q-!5__yLyoeEu>?nejz^783O~0tNYl5k2V|b#XzaB z?+S9!oqW&?ej$(I5tA=xrGjSXB67+>UHPZR^;mD62R`1VKEQ zm)%hN>Hv`Lddr@N2pNhs89L-_c$*HPmcw=^0_?z7B{(r%OSG`&n2pa(q(6jJ+fI6xo73>c3c~C#SnJ9p) zw!OrxI};};40I6?=e6u?E5uVtD8-qlC6>bsK;^V$EDP)9}th{Y1MVKKN`dHuHF!bF9%=2A|%ACewSV`3+ zfg*g7Kl;ZiLE}Q8K`{oAx;xinR2Y{;!Kqpr&lSE&WN!I#l88gT3@aFB;R?%x&p=06 z1Hu4|xYg=6@)Yd2$ajq*T>5djT2|4`J??GCIW%1(PlJY zHK%P$li7xLO~P`b$JSDFy)2wxJ(L}W$(+A+=ttz_f-nV(*l0Tlv&acd9-c8WJ`Y?(x zWn*{v?7P_ZzF5z9Iy2X2)_l0KWW#}TzW%rDIezfw#Usy{Pa)yA+~}{-2Jm3b7BaN9 zpicsrTMn(ou!3?Fz5tDn#ozI+VY9Rzx>`j@4BfQh$>Bdetv9{et`hVq8y&(`i}k zoi?s->Te8v;p{&PY0GCI74~sS;k+pWbx!n)D zznuHv^;BE;Cw$AZ2{L3bpJa*3w`GbCX-qNCr0RhYd3VsqCPpn(F5O890T20Pcr7hv9=;hM+>f<yIR< zG&w0!ZA~&Lv?G$qt63$BSeKLSc2^{AWxqQ)RQgBB8-*<=pVb4tChXy0Uxxrz8(xOu znju1s=-I;SFU0;DMqX*R7~v z=iOG|=U|(MJHl(D2sWNe=n_3|y%iskL8I?J(m}mn4kQTKyGfl(OC)OW0)o8BFy`y^ z9lVJr(no+D@VR@V)Ju1Wb@4zVf}aUmzxnz(w!}xxp+x8#JyWG~%^xM9GvOh?oz&aP zfL{qE7!vh9R^EemP}$Seiw0tu{2r=3>7a_)pFoNSQ}HXqoi0)&hwcy~Hz5FSWMcWe zYfG~_KKH5+E{eAMx7El|M5qLuJ9Xv;Fw`Og z7qDEIpe*B^79|pZEs8tChA`Xt1&C0_w8jS=+XG}A!?kb;NPIR(of;TbaY!>SCVjEvVRKU3 z@nFP90y)XV^a&-fi(zCy9QGKnW0}fwd~Rx_m?%`p-$0UDAd>0`gB&$%b73A4kwj)# zMKGJ4%qzG^t@`dy1OLsDQ0t zJ4<1{Q}z#s7j__T{=U-$4jfYHD~@#Z&b@r-DOt^*(N0tuw=TTiz#zfELr=>&4);)1h4mgV?>%A zuHiGt$&dsJ{MnJG$eBxI_`@u+B=X_whwbcWgsb+Y)IM7B>r+gL6@|uNad-kzjYOsZ zR2rSZETdl+7CVCyPWoIPKVv~g!m>x0Pyi7olyM=IrW>YZJFe#kVH786Hs_d!Tnhj= zmwW}5I(Ao~qVD&ATXFZ4C{?Chg-TVb)u?TniFyq@yc#uW)>2guwQB2ePqg<)hfd3N z>DB`Tfgw;B9D%Hr9F6Iz30%)i5qi-}Gd;JGOv#hlE3%)pFF- z%j&w@g?edecf-BH{Pgq<42_IUOwF?QZ$2*M3B*v{-SWb+>~gKF%d@eyvv+WGa&~cT z%AmVpBSy>jHeNx6&6udj&y;C1X3d$mfUDyHk*aE1AZt>C+X5BCs#ztwBSaZ=%~$ht zh);O@wU9|ua`L7jPzA+BBn4A8v$oBWgR3HtDD?VQjw*H)(?wV%8U&&miA({gG&%#C zIdg=2@<6Yb&7vi@RYQ|(9cb_KI;-s!h8NotEp;t;aj>eee;=F@R}4m<3B#tl6-|lFJhXjTaH|L@)amlq*#ekWy)2kyhN(i$feb(_g{lXO`5exq)oe^&L7;ZN3TBp3dSBX zY{aNB<0dX_3fQkXYtFm{#JjK8R^L!i+F0iyQ5S?j?FUdzfk3-3R2MAS6!^EhLj`~v zbYvneZ5`cIQvK9vtWn@%?~}+v#ee(%wKSM4Hiv6sYG!U>X=QB#^7s%eK!m7BERkZk ztxT>^s_fME8V8S_JbUr#4-f=~Kw)qM5{1TKad-leM5a(_bOw{f=5Tp@fvTFihNhOb zj;@}*fuWJHiK&^ng{76Xjjf%%gQJtPi;S$Cyn>>VvWlvjx`w8fwvMizzJZ~Uv5Bdf zxdp@!(bCG=#@5c>!O_Xt#nsJSu28Df8m&%mFq+I3U2a*VVZv;&+UyRe%kA-2BWYTF zwi>gsQ>wW-v4vDlKCDRrfhs5}!IaIYz*P}Q6dHrY;R!@F5}5)}X>bN_SD#heXBv4c+CX7qI9eOrMt=SI{?cJ_2nKk`ZX@ zI7W}Zar@S-*A;9ALY2a9Bhwkb{hr-cdcAg5l{XpN6H0p)kWcJ}f|o9?o+Qo>DupEk zBS?y7$qtPWf)XUf`gj0(SL(FgptCQ~WTm&9z|?8w$rjwr3XgUbBuOE?KYiR6$5*)& zT0`L?_lk(dKK2c~XCJT27frV2uaqrn8|UABB7`)bRp}CUHGiH_Z&)F$=h1nU&_sX{y5|=D(VeQHCOT`rK8r`Qf={%?;+MWsQn1~ zQ2FR|FSIN&+yg}S?BgTgm`y{salgW+EWO;$o_d0lUa*yk)@2x`njOeApNW=x4IwkF zm-`iYZ7Ow&EP>VU8}F~NBsq6wU)*&QdXsA%wueA-cD7&i z@ttn1fUAdyFLH~A@qeqy^>Nrw9(TeIx-O?R_bqlWv)}5I)AbnFSFOwDryhTJj)G=} zg(IJ6$SW^-oS#9_NpTSueLa5g^LF+1ntwgcr~6yajmlFfbL-pu4Ew=zVB>TS!Rr@3o9GS7v%{UDep1$9lM`d(QZqO{N&orI{d zXFZnCytThw|9EWSkkz0%DC>r3npxm&MnptaB%weU0zp7w-Z&z5oB4O!szmLo(+tVY zCdsO6T%@aZF}EG*j&}(p6bP%t-89Z~f2_aXKlvG0hIkMU!yMdgLxv1xcxN~w!_f-9 z59?9E8)H4Fq(r@n#L~%+WCTPJC02HRci9Tms)dDw?bK2w-c`kq)mX*B2qX~1z!0|- z7-EbWV#a1{_Q_vmy0P?p%%Udf&V~__s%2wiSGL%(jRA245{lEe65QC`cg5AG69T5F zTLxy;Y-$zX3Wir(qwe1=BTJw+ix2-_fT#ZNKMU3v(Zp5UiZpW<%?T|?Eh(*Nt$pyN z*nVu4Qv?75y;)}iyA)}HVw&NE7M@vJQd)V#Xv1P_+ZKf;BkVK?!9m`vb3rkj@Wjhi zD_fNsf*^Q?M*;x|!PZudAP9m+)Ire}$rz05WO z=xBAgn6m02&!S-MMzlO3J1dpNSdx!hF*qOR&lj)Qh>+-ta?-Rv5=IN zz;u%`l9(yuQ$z+U-&qo6gRO(gb@&3xn*hZCviBgnWDpD@5G>;Z!Ul3388!eR7(oaD z0002M9#90sCM*C`J4{U!!wJa}pg{;mPz)zzc!t{ii4la-ih$T#hyz#@y#+#?e}C2l z6alff5C^a*dJBZOL9|?QD{Dd-7g7OjEPLUpkwPLFg$HsK_>>(F~l zMUEVmG2*RE9liRJ01#n985dG%W7#=HfCv-HxR6R4%g!kQM3_*d2Ss}5dNmBo!sVKbs!Be(&wZj@-VtZq6~Ibz+CU6b6xm>WL# z43|xJiu5Vs#usszL5 z$iy0XWQ6FjKugJ{e($FqTNRbVF+cITn>MsJ32thf#1bA%KolNKNBT=YabE>9M5uZf z)RKpYR-WqvLM3T@;2%!rkc9QJ2Z*%-MsvnK9B)ud!0)*D^CDP5KY<_0-pKb&(tpj9 zcs}?1$Me6MMEdXfJotqvrRZ11^B>QDJ^%Ckzv17q`oAlrf9T)f%M5t_i~da!0~_aa I1H=~!00>3;K>z>% literal 0 HcmV?d00001 diff --git a/site/static/fonts/plex/IBMPlexMono-Text.woff2 b/site/static/fonts/plex/IBMPlexMono-Text.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..26d726427bd5415d9e63022efd9ea3ca47e5d380 GIT binary patch literal 46228 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

U|KcPD}1}75g2?D|d0xIox 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 U&7#>pmg4({a)vp+wZ{(u0I<@zcK`qY literal 0 HcmV?d00001 diff --git a/site/static/js/elasticlunr.min.js b/site/static/js/elasticlunr.min.js new file mode 100644 index 00000000..06cc9b32 --- /dev/null +++ b/site/static/js/elasticlunr.min.js @@ -0,0 +1,10 @@ +/** + * elasticlunr - http://weixsong.github.io + * Lightweight full-text search engine in Javascript for browser search and offline search. - 0.9.5 + * + * Copyright (C) 2017 Oliver Nightingale + * Copyright (C) 2017 Wei Song + * MIT Licensed + * @license + */ +!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o{throw new m(t)})}var m=class extends Error{constructor(t){super(`clipboard copy rejected: '${t}'`)}},i=class extends Error{constructor(t){super(t)}};function k(){let e,t,l,c;try{e=s(".installer-button"),t=o("#installer-cmd"),l=o("#installer-copy"),c=o("#installer-copy > svg > use")}catch{return}e.forEach(n=>{n.addEventListener("click",r=>{r.preventDefault(),e.forEach(a=>delete a.dataset.active),n.dataset.active="true",t.innerText=w(n.dataset.platform)}),n.dataset.active&&n.dataset.active==="true"&&(t.innerText=w(n.dataset.platform))}),l.addEventListener("click",n=>{n.preventDefault(),p(t.innerText);let r=b(c),a=r.replace("#icon-clipboard","#icon-check");console.log(a),g(c,a),setTimeout(()=>g(c,r),1500)})}function b(e){let t=e.getAttributeNS(y,"href");if(t===null)throw new f;return t}function g(e,t){e.setAttributeNS(y,"href",t)}var y="http://www.w3.org/1999/xlink";function w(e){if(e===void 0)throw new d(e);switch(e){case"macos":return T;case"linux":return T;case"windows":return A;default:throw new d(e)}}var E=`${globalThis.window.location.protocol}//${globalThis.window.location.host}`,T=`curl -LsSf ${E}/dl/install.sh | sh`,A=`irm ${E}/dl/install.ps1 | iex`,d=class extends Error{constructor(t){super(`could not determine platform: '${t||"undefined"}'`)}},f=class extends Error{constructor(){super("could not find copy icon")}};function L(){s('a[href^="#"]').forEach(e=>{e.addEventListener("click",t=>{if(t.preventDefault(),t.currentTarget===null)return;let l=t.currentTarget.getAttribute("href");if(l===null)return;let n=o(l).getBoundingClientRect().top,r=globalThis.window.scrollY,a=n+r;globalThis.window.scrollTo({top:a,behavior:"smooth"})})})}function S(){let e=o("#search-button"),t=o("#search-modal"),l=o("#search-modal-close"),c=o("#search-modal-shroud"),n=o("#search-modal-box");e.addEventListener("click",r=>h(r,t)),c.addEventListener("click",r=>h(r,t)),l.addEventListener("click",r=>h(r,t)),n.addEventListener("click",r=>r.stopPropagation()),document.addEventListener("keydown",r=>{if(r.metaKey===!0&&r.shiftKey===!1&&r.key==="k"){r.preventDefault(),e.click();return}if(M(t)&&r.key==="Escape"){r.preventDefault(),c.click();return}})}function h(e,t){e.preventDefault(),t.classList.toggle("hidden")}function M(e){return!e.classList.contains("hidden")}function v(e){if(e===void 0)throw new u(e);switch(e){case"system":switch(localStorage.removeItem("theme"),I()){case"dark":document.documentElement.classList.add("dark");break;case"light":document.documentElement.classList.remove("dark");break}break;case"light":localStorage.setItem("theme",e),document.documentElement.classList.remove("dark");break;case"dark":localStorage.setItem("theme",e),document.documentElement.classList.add("dark");break;default:throw new u(e)}H(e)}function H(e){s(".theme-option").forEach(t=>{t.dataset.theme===e?t.dataset.active="true":delete t.dataset.active})}function I(){return globalThis.window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}var u=class extends Error{constructor(t){super(`could not determine theme: '${t||"undefined"}'`)}};function x(){s(".theme-option").forEach(e=>{e.addEventListener("click",t=>{t.preventDefault(),v(e.dataset.theme)})})}function $(){x(),k(),S(),L()}(function(){try{$()}catch(e){console.error(e)}})(); +//# sourceMappingURL=footer.mjs.map diff --git a/site/static/js/footer.mjs.map b/site/static/js/footer.mjs.map new file mode 100644 index 00000000..c0cedf04 --- /dev/null +++ b/site/static/js/footer.mjs.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../scripts/src/util/web.ts", "../../scripts/src/footer/installer.ts", "../../scripts/src/footer/scroll.ts", "../../scripts/src/footer/search.ts", "../../scripts/src/util/theme.ts", "../../scripts/src/footer/theme.ts", "../../scripts/src/footer/main.ts"], + "sourcesContent": ["/**\n * document.querySelector with type conversion and error handling.\n */\nexport function querySelector(selector: string): HTMLElement {\n const $elem = document.querySelector(selector);\n if ($elem === null) throw new QueryError(`could not find ${selector}`);\n return $elem as HTMLElement;\n}\n\n/**\n * document.querySelectorAll with type conversions and error handling.\n */\nexport function querySelectorAll(selector: string): Array {\n const $elems = document.querySelectorAll(selector);\n if ($elems === null) throw new QueryError(`could not find all '${selector}'`);\n return Array.from($elems) as Array;\n}\n\n/**\n * navigator.clipboard.writeText with error handling.\n */\nexport function copyToClipboard(text: string) {\n navigator.clipboard.writeText(text).then(null, (reason) => {\n throw new ClipboardError(reason);\n });\n}\n\n/**\n * Indicates an error while attempting to put data into the clipboard.\n */\nclass ClipboardError extends Error {\n constructor(source: unknown) {\n super(`clipboard copy rejected: '${source}'`);\n }\n}\n\n/**\n * Indicates an error while trying to select one or more elements on the page.\n */\nclass QueryError extends Error {\n constructor(msg: string) {\n super(msg);\n }\n}\n", "import {\n copyToClipboard,\n querySelector,\n querySelectorAll,\n} from \"../util/web.ts\";\n\n/**\n * Setup behavior for the install picker.\n *\n * This makes sure the platform buttons are clickable and update the install\n * command appropriately, and that the copy-to-clipboard button works.\n */\nexport function setupInstallerPicker() {\n // The buttons used to select the platform.\n let $buttons: Array;\n\n // The block containing the install command.\n let $cmd: HTMLElement;\n\n // The copy-to-clipboard button.\n let $copy: HTMLElement;\n\n // The icon inside the copy-to-clipboard button.\n let $copyIcon: HTMLElement;\n\n try {\n $buttons = querySelectorAll(\".installer-button\");\n $cmd = querySelector(\"#installer-cmd\");\n $copy = querySelector(\"#installer-copy\");\n $copyIcon = querySelector(\"#installer-copy > svg > use\");\n } catch (_e) {\n // Swallow these errors.\n return;\n }\n\n $buttons.forEach(($button) => {\n $button.addEventListener(\"click\", (e) => {\n e.preventDefault();\n $buttons.forEach(($button) => delete $button.dataset.active);\n $button.dataset.active = \"true\";\n $cmd.innerText = installerForPlatform($button.dataset.platform);\n });\n\n if ($button.dataset.active && $button.dataset.active === \"true\") {\n $cmd.innerText = installerForPlatform($button.dataset.platform);\n }\n });\n\n $copy.addEventListener(\"click\", (e) => {\n e.preventDefault();\n copyToClipboard($cmd.innerText);\n\n const iconUrl = getIconUrl($copyIcon);\n\n // Update the icon on click.\n const newIconUrl = iconUrl.replace(\"#icon-clipboard\", \"#icon-check\");\n console.log(newIconUrl);\n setIconUrl($copyIcon, newIconUrl);\n\n // Set the icon back on a timer.\n setTimeout(() => setIconUrl($copyIcon, iconUrl), 1_500);\n });\n}\n\n/**\n * Get the URL out of an icon `use` element.\n */\nfunction getIconUrl($node: HTMLElement): string {\n const iconUrl = $node.getAttributeNS(XLINK_NS, \"href\");\n if (iconUrl === null) throw new IconError();\n return iconUrl;\n}\n\n/**\n * Get the URL on an icon `use` element.\n */\nfunction setIconUrl($node: HTMLElement, url: string) {\n $node.setAttributeNS(XLINK_NS, \"href\", url);\n}\n\n/**\n * The namespace URL for the Xlink namespace\n */\nconst XLINK_NS: string = \"http://www.w3.org/1999/xlink\";\n\n/**\n * Get the install script based on the chosen platform.\n */\nfunction installerForPlatform(platform: string | undefined): string {\n if (platform === undefined) throw new UnknownPlatformError(platform);\n\n switch (platform) {\n case \"macos\":\n return UNIX_INSTALLER;\n case \"linux\":\n return UNIX_INSTALLER;\n case \"windows\":\n return WINDOWS_INSTALLER;\n default:\n throw new UnknownPlatformError(platform);\n }\n}\n\n/**\n * The current host of the site.\n */\nconst HOST: string =\n `${globalThis.window.location.protocol}//${globalThis.window.location.host}`;\n\n/**\n * The install script to use for Unix (macOS and Linux) platforms.\n */\nconst UNIX_INSTALLER: string = `curl -LsSf ${HOST}/dl/install.sh | sh`;\n\n/**\n * The install script to use for Windows.\n */\nconst WINDOWS_INSTALLER: string = `irm ${HOST}/dl/install.ps1 | iex`;\n\n/**\n * Indicates an error while trying to detect the user's install platform.\n */\nclass UnknownPlatformError extends Error {\n constructor(platform: string | undefined) {\n super(\n `could not determine platform: '${platform || \"undefined\"}'`,\n );\n }\n}\n\n/**\n * Error arising when trying to update the copy-to-clipboard icon.\n */\nclass IconError extends Error {\n constructor() {\n super(`could not find copy icon`)\n }\n}\n", "import { querySelector, querySelectorAll } from \"../util/web.ts\";\n\nexport function setupSmoothScrolling() {\n /*\n * This code from: https://stackoverflow.com/a/7717572\n * Used under the CC BY-SA 3.0 license with modifications.\n */\n querySelectorAll('a[href^=\"#\"]').forEach((anchor) => {\n anchor.addEventListener(\"click\", (e) => {\n e.preventDefault();\n if (e.currentTarget === null) return;\n\n const targetHeader = (e.currentTarget as HTMLElement).getAttribute(\"href\");\n if (targetHeader === null) return;\n\n const $header = querySelector(targetHeader);\n const headerPosition = $header.getBoundingClientRect().top;\n const scrollAmount = globalThis.window.scrollY;\n const offsetPosition = headerPosition + scrollAmount;\n\n globalThis.window.scrollTo({\n top: offsetPosition,\n behavior: \"smooth\",\n });\n });\n });\n}\n", "import { querySelector } from \"../util/web.ts\";\n\n/**\n * Sets up functionality for opening and closing the search modal.\n */\nexport function setupSearch() {\n const $button = querySelector(\"#search-button\");\n const $modal = querySelector(\"#search-modal\");\n const $modalClose = querySelector(\"#search-modal-close\");\n const $modalShroud = querySelector(\"#search-modal-shroud\");\n const $modalBox = querySelector(\"#search-modal-box\");\n\n // Need all of these together to make sure clicking the *background* closes\n // the search modal, but clicking inside the box (anywhere other than the\n // close button) does *not* close the modal.\n $button.addEventListener(\"click\", (e) => toggleModal(e, $modal));\n $modalShroud.addEventListener(\"click\", (e) => toggleModal(e, $modal));\n $modalClose.addEventListener(\"click\", (e) => toggleModal(e, $modal));\n $modalBox.addEventListener(\"click\", (e) => e.stopPropagation());\n\n // Keyboard shortcuts.\n document.addEventListener(\"keydown\", (e) => {\n // 'Meta+K' to open or close the modal.\n if (e.metaKey === true && e.shiftKey === false && e.key === \"k\") {\n e.preventDefault();\n $button.click();\n return;\n }\n\n // 'Escape' when the modal is open to close it.\n if (modalIsOpen($modal) && e.key === \"Escape\") {\n e.preventDefault();\n $modalShroud.click();\n return;\n }\n });\n}\n\n/**\n * Toggle whether the modal is open or not.\n */\nfunction toggleModal(e: MouseEvent, $modal: HTMLElement) {\n e.preventDefault();\n $modal.classList.toggle(\"hidden\");\n}\n\n/**\n * Check if the modal is open.\n */\nfunction modalIsOpen($modal: HTMLElement): boolean {\n return !$modal.classList.contains(\"hidden\");\n}\n", "import { querySelectorAll } from \"./web.ts\";\n\n/**\n * Get the theme that's currently set by the user.\n */\nexport function getConfiguredTheme(): KnownTheme {\n const theme = localStorage.getItem(\"theme\") ?? \"system\";\n\n switch (theme) {\n case \"dark\":\n return theme;\n case \"light\":\n return theme;\n case \"system\":\n return theme;\n default:\n throw new ThemeError(theme);\n }\n}\n\n/**\n * Update the theme on the page and in local storage.\n */\nexport function setPageTheme(theme: string | undefined) {\n if (theme === undefined) throw new ThemeError(theme);\n\n switch (theme) {\n case \"system\":\n localStorage.removeItem(\"theme\");\n switch (preferredTheme()) {\n case \"dark\":\n document.documentElement.classList.add(\"dark\");\n break;\n case \"light\":\n document.documentElement.classList.remove(\"dark\");\n break;\n }\n\n break;\n\n case \"light\":\n localStorage.setItem(\"theme\", theme);\n document.documentElement.classList.remove(\"dark\");\n break;\n\n case \"dark\":\n localStorage.setItem(\"theme\", theme);\n document.documentElement.classList.add(\"dark\");\n break;\n\n default:\n throw new ThemeError(theme);\n }\n\n setButtons(theme);\n}\n\n/**\n * The known theme selector options.\n */\ntype KnownTheme = \"dark\" | \"light\" | \"system\";\n\n/**\n * A theme that can be pulled explicitly from local storage.\n */\ntype StoredTheme = \"dark\" | \"light\";\n\n/**\n * Set as active the button that matches the theme.\n *\n * Make sure to set all another buttons as inactive.\n */\nfunction setButtons(theme: KnownTheme) {\n querySelectorAll(\".theme-option\").forEach(($option) => {\n if ($option.dataset.theme === theme) $option.dataset.active = \"true\";\n else delete $option.dataset.active;\n });\n}\n\n/**\n * Get the user's preferred theme based on a media query.\n */\nfunction preferredTheme(): StoredTheme {\n const prefersDark =\n globalThis.window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n if (prefersDark) return \"dark\";\n return \"light\";\n}\n\n/**\n * Indicates an error during theme selection.\n */\nclass ThemeError extends Error {\n constructor(theme: string | undefined) {\n super(\n `could not determine theme: '${theme || \"undefined\"}'`,\n );\n }\n}\n", "import { querySelectorAll } from \"../util/web.ts\";\nimport { setPageTheme } from \"../util/theme.ts\";\n\n/**\n * Sets up the logic for updating theme post-load.\n *\n * Note that this does _not_ handle setting the theme initially on page load.\n * That's done in a separate file since it needs to happen in the head, whereas\n * this code runs at the end of the body.\n */\nexport function setupThemeController() {\n querySelectorAll(\".theme-option\").forEach(($option) => {\n $option.addEventListener(\"click\", (e) => {\n e.preventDefault();\n setPageTheme($option.dataset.theme);\n });\n });\n}\n", "import { setupInstallerPicker } from \"./installer.ts\";\nimport { setupSmoothScrolling } from \"./scroll.ts\";\nimport { setupSearch } from \"./search.ts\";\nimport { setupThemeController } from \"./theme.ts\";\n\n/**\n * Run all page setup operations, initializing all interactive widgets.\n *\n * There are currently three widgets:\n *\n * - Theme Controller in the navigation bar.\n * - Installer Picker on the homepage.\n * - Search button in the navigation bar.\n */\nfunction setup() {\n setupThemeController();\n setupInstallerPicker();\n setupSearch();\n setupSmoothScrolling();\n}\n\n/**\n * Do setup, logging errors to the console.\n */\n(function () {\n try {\n setup();\n } catch (e) {\n console.error(e);\n }\n})();\n"], + "mappings": "AAGO,SAASA,EAAcC,EAA+B,CAC3D,IAAMC,EAAQ,SAAS,cAAcD,CAAQ,EAC7C,GAAIC,IAAU,KAAM,MAAM,IAAIC,EAAW,kBAAkBF,CAAQ,EAAE,EACrE,OAAOC,CACT,CAKO,SAASE,EAAiBH,EAAsC,CACrE,IAAMI,EAAS,SAAS,iBAAiBJ,CAAQ,EACjD,GAAII,IAAW,KAAM,MAAM,IAAIF,EAAW,uBAAuBF,CAAQ,GAAG,EAC5E,OAAO,MAAM,KAAKI,CAAM,CAC1B,CAKO,SAASC,EAAgBC,EAAc,CAC5C,UAAU,UAAU,UAAUA,CAAI,EAAE,KAAK,KAAOC,GAAW,CACzD,MAAM,IAAIC,EAAeD,CAAM,CACjC,CAAC,CACH,CAKA,IAAMC,EAAN,cAA6B,KAAM,CACjC,YAAYC,EAAiB,CAC3B,MAAM,6BAA6BA,CAAM,GAAG,CAC9C,CACF,EAKMP,EAAN,cAAyB,KAAM,CAC7B,YAAYQ,EAAa,CACvB,MAAMA,CAAG,CACX,CACF,EC/BO,SAASC,GAAuB,CAErC,IAAIC,EAGAC,EAGAC,EAGAC,EAEJ,GAAI,CACFH,EAAWI,EAAiB,mBAAmB,EAC/CH,EAAOI,EAAc,gBAAgB,EACrCH,EAAQG,EAAc,iBAAiB,EACvCF,EAAYE,EAAc,6BAA6B,CACzD,MAAa,CAEX,MACF,CAEAL,EAAS,QAASM,GAAY,CAC5BA,EAAQ,iBAAiB,QAAUC,GAAM,CACvCA,EAAE,eAAe,EACjBP,EAAS,QAASM,GAAY,OAAOA,EAAQ,QAAQ,MAAM,EAC3DA,EAAQ,QAAQ,OAAS,OACzBL,EAAK,UAAYO,EAAqBF,EAAQ,QAAQ,QAAQ,CAChE,CAAC,EAEGA,EAAQ,QAAQ,QAAUA,EAAQ,QAAQ,SAAW,SACvDL,EAAK,UAAYO,EAAqBF,EAAQ,QAAQ,QAAQ,EAElE,CAAC,EAEDJ,EAAM,iBAAiB,QAAUK,GAAM,CACrCA,EAAE,eAAe,EACjBE,EAAgBR,EAAK,SAAS,EAE9B,IAAMS,EAAUC,EAAWR,CAAS,EAG9BS,EAAaF,EAAQ,QAAQ,kBAAmB,aAAa,EACnE,QAAQ,IAAIE,CAAU,EACtBC,EAAWV,EAAWS,CAAU,EAGhC,WAAW,IAAMC,EAAWV,EAAWO,CAAO,EAAG,IAAK,CACxD,CAAC,CACH,CAKA,SAASC,EAAWG,EAA4B,CAC9C,IAAMJ,EAAUI,EAAM,eAAeC,EAAU,MAAM,EACrD,GAAIL,IAAY,KAAM,MAAM,IAAIM,EAChC,OAAON,CACT,CAKA,SAASG,EAAWC,EAAoBG,EAAa,CACnDH,EAAM,eAAeC,EAAU,OAAQE,CAAG,CAC5C,CAKA,IAAMF,EAAmB,+BAKzB,SAASP,EAAqBU,EAAsC,CAClE,GAAIA,IAAa,OAAW,MAAM,IAAIC,EAAqBD,CAAQ,EAEnE,OAAQA,EAAU,CAChB,IAAK,QACH,OAAOE,EACT,IAAK,QACH,OAAOA,EACT,IAAK,UACH,OAAOC,EACT,QACE,MAAM,IAAIF,EAAqBD,CAAQ,CAC3C,CACF,CAKA,IAAMI,EACJ,GAAG,WAAW,OAAO,SAAS,QAAQ,KAAK,WAAW,OAAO,SAAS,IAAI,GAKtEF,EAAyB,cAAcE,CAAI,sBAK3CD,EAA4B,OAAOC,CAAI,wBAKvCH,EAAN,cAAmC,KAAM,CACvC,YAAYD,EAA8B,CACxC,MACE,kCAAkCA,GAAY,WAAW,GAC3D,CACF,CACF,EAKMF,EAAN,cAAwB,KAAM,CAC5B,aAAc,CACZ,MAAM,0BAA0B,CAClC,CACF,ECvIO,SAASO,GAAuB,CAKrCC,EAAiB,cAAc,EAAE,QAASC,GAAW,CACnDA,EAAO,iBAAiB,QAAUC,GAAM,CAEtC,GADAA,EAAE,eAAe,EACbA,EAAE,gBAAkB,KAAM,OAE9B,IAAMC,EAAgBD,EAAE,cAA8B,aAAa,MAAM,EACzE,GAAIC,IAAiB,KAAM,OAG3B,IAAMC,EADUC,EAAcF,CAAY,EACX,sBAAsB,EAAE,IACjDG,EAAe,WAAW,OAAO,QACjCC,EAAiBH,EAAiBE,EAExC,WAAW,OAAO,SAAS,CACzB,IAAKC,EACL,SAAU,QACZ,CAAC,CACH,CAAC,CACH,CAAC,CACH,CCrBO,SAASC,GAAc,CAC5B,IAAMC,EAAUC,EAAc,gBAAgB,EACxCC,EAASD,EAAc,eAAe,EACtCE,EAAcF,EAAc,qBAAqB,EACjDG,EAAeH,EAAc,sBAAsB,EACnDI,EAAYJ,EAAc,mBAAmB,EAKnDD,EAAQ,iBAAiB,QAAUM,GAAMC,EAAYD,EAAGJ,CAAM,CAAC,EAC/DE,EAAa,iBAAiB,QAAUE,GAAMC,EAAYD,EAAGJ,CAAM,CAAC,EACpEC,EAAY,iBAAiB,QAAUG,GAAMC,EAAYD,EAAGJ,CAAM,CAAC,EACnEG,EAAU,iBAAiB,QAAUC,GAAMA,EAAE,gBAAgB,CAAC,EAG9D,SAAS,iBAAiB,UAAYA,GAAM,CAE1C,GAAIA,EAAE,UAAY,IAAQA,EAAE,WAAa,IAASA,EAAE,MAAQ,IAAK,CAC/DA,EAAE,eAAe,EACjBN,EAAQ,MAAM,EACd,MACF,CAGA,GAAIQ,EAAYN,CAAM,GAAKI,EAAE,MAAQ,SAAU,CAC7CA,EAAE,eAAe,EACjBF,EAAa,MAAM,EACnB,MACF,CACF,CAAC,CACH,CAKA,SAASG,EAAY,EAAeL,EAAqB,CACvD,EAAE,eAAe,EACjBA,EAAO,UAAU,OAAO,QAAQ,CAClC,CAKA,SAASM,EAAYN,EAA8B,CACjD,MAAO,CAACA,EAAO,UAAU,SAAS,QAAQ,CAC5C,CC5BO,SAASO,EAAaC,EAA2B,CACtD,GAAIA,IAAU,OAAW,MAAM,IAAIC,EAAWD,CAAK,EAEnD,OAAQA,EAAO,CACb,IAAK,SAEH,OADA,aAAa,WAAW,OAAO,EACvBE,EAAe,EAAG,CACxB,IAAK,OACH,SAAS,gBAAgB,UAAU,IAAI,MAAM,EAC7C,MACF,IAAK,QACH,SAAS,gBAAgB,UAAU,OAAO,MAAM,EAChD,KACJ,CAEA,MAEF,IAAK,QACH,aAAa,QAAQ,QAASF,CAAK,EACnC,SAAS,gBAAgB,UAAU,OAAO,MAAM,EAChD,MAEF,IAAK,OACH,aAAa,QAAQ,QAASA,CAAK,EACnC,SAAS,gBAAgB,UAAU,IAAI,MAAM,EAC7C,MAEF,QACE,MAAM,IAAIC,EAAWD,CAAK,CAC9B,CAEAG,EAAWH,CAAK,CAClB,CAiBA,SAASG,EAAWH,EAAmB,CACrCI,EAAiB,eAAe,EAAE,QAASC,GAAY,CACjDA,EAAQ,QAAQ,QAAUL,EAAOK,EAAQ,QAAQ,OAAS,OACzD,OAAOA,EAAQ,QAAQ,MAC9B,CAAC,CACH,CAKA,SAASH,GAA8B,CAIrC,OAFE,WAAW,OAAO,WAAW,8BAA8B,EAAE,QAEvC,OACjB,OACT,CAKA,IAAMD,EAAN,cAAyB,KAAM,CAC7B,YAAYD,EAA2B,CACrC,MACE,+BAA+BA,GAAS,WAAW,GACrD,CACF,CACF,ECzFO,SAASM,GAAuB,CACrCC,EAAiB,eAAe,EAAE,QAASC,GAAY,CACrDA,EAAQ,iBAAiB,QAAUC,GAAM,CACvCA,EAAE,eAAe,EACjBC,EAAaF,EAAQ,QAAQ,KAAK,CACpC,CAAC,CACH,CAAC,CACH,CCHA,SAASG,GAAQ,CACfC,EAAqB,EACrBC,EAAqB,EACrBC,EAAY,EACZC,EAAqB,CACvB,EAKC,UAAY,CACX,GAAI,CACFJ,EAAM,CACR,OAAS,EAAG,CACV,QAAQ,MAAM,CAAC,CACjB,CACF,GAAG", + "names": ["querySelector", "selector", "$elem", "QueryError", "querySelectorAll", "$elems", "copyToClipboard", "text", "reason", "ClipboardError", "source", "msg", "setupInstallerPicker", "$buttons", "$cmd", "$copy", "$copyIcon", "querySelectorAll", "querySelector", "$button", "e", "installerForPlatform", "copyToClipboard", "iconUrl", "getIconUrl", "newIconUrl", "setIconUrl", "$node", "XLINK_NS", "IconError", "url", "platform", "UnknownPlatformError", "UNIX_INSTALLER", "WINDOWS_INSTALLER", "HOST", "setupSmoothScrolling", "querySelectorAll", "anchor", "e", "targetHeader", "headerPosition", "querySelector", "scrollAmount", "offsetPosition", "setupSearch", "$button", "querySelector", "$modal", "$modalClose", "$modalShroud", "$modalBox", "e", "toggleModal", "modalIsOpen", "setPageTheme", "theme", "ThemeError", "preferredTheme", "setButtons", "querySelectorAll", "$option", "setupThemeController", "querySelectorAll", "$option", "e", "setPageTheme", "setup", "setupThemeController", "setupInstallerPicker", "setupSearch", "setupSmoothScrolling"] +} diff --git a/site/static/js/header.mjs b/site/static/js/header.mjs new file mode 100644 index 00000000..6a0a5eca --- /dev/null +++ b/site/static/js/header.mjs @@ -0,0 +1,2 @@ +function o(e){let t=document.querySelectorAll(e);if(t===null)throw new n(`could not find all '${e}'`);return Array.from(t)}var n=class extends Error{constructor(t){super(t)}};function s(){let e=localStorage.getItem("theme")??"system";switch(e){case"dark":return e;case"light":return e;case"system":return e;default:throw new r(e)}}function c(e){if(e===void 0)throw new r(e);switch(e){case"system":switch(localStorage.removeItem("theme"),d()){case"dark":document.documentElement.classList.add("dark");break;case"light":document.documentElement.classList.remove("dark");break}break;case"light":localStorage.setItem("theme",e),document.documentElement.classList.remove("dark");break;case"dark":localStorage.setItem("theme",e),document.documentElement.classList.add("dark");break;default:throw new r(e)}l(e)}function l(e){o(".theme-option").forEach(t=>{t.dataset.theme===e?t.dataset.active="true":delete t.dataset.active})}function d(){return globalThis.window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}var r=class extends Error{constructor(t){super(`could not determine theme: '${t||"undefined"}'`)}};function a(){c(s())}function u(){a()}(function(){try{u()}catch(e){console.error(e)}})(); +//# sourceMappingURL=header.mjs.map diff --git a/site/static/js/header.mjs.map b/site/static/js/header.mjs.map new file mode 100644 index 00000000..8936e12d --- /dev/null +++ b/site/static/js/header.mjs.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../scripts/src/util/web.ts", "../../scripts/src/util/theme.ts", "../../scripts/src/header/theme.ts", "../../scripts/src/header/main.ts"], + "sourcesContent": ["/**\n * document.querySelector with type conversion and error handling.\n */\nexport function querySelector(selector: string): HTMLElement {\n const $elem = document.querySelector(selector);\n if ($elem === null) throw new QueryError(`could not find ${selector}`);\n return $elem as HTMLElement;\n}\n\n/**\n * document.querySelectorAll with type conversions and error handling.\n */\nexport function querySelectorAll(selector: string): Array {\n const $elems = document.querySelectorAll(selector);\n if ($elems === null) throw new QueryError(`could not find all '${selector}'`);\n return Array.from($elems) as Array;\n}\n\n/**\n * navigator.clipboard.writeText with error handling.\n */\nexport function copyToClipboard(text: string) {\n navigator.clipboard.writeText(text).then(null, (reason) => {\n throw new ClipboardError(reason);\n });\n}\n\n/**\n * Indicates an error while attempting to put data into the clipboard.\n */\nclass ClipboardError extends Error {\n constructor(source: unknown) {\n super(`clipboard copy rejected: '${source}'`);\n }\n}\n\n/**\n * Indicates an error while trying to select one or more elements on the page.\n */\nclass QueryError extends Error {\n constructor(msg: string) {\n super(msg);\n }\n}\n", "import { querySelectorAll } from \"./web.ts\";\n\n/**\n * Get the theme that's currently set by the user.\n */\nexport function getConfiguredTheme(): KnownTheme {\n const theme = localStorage.getItem(\"theme\") ?? \"system\";\n\n switch (theme) {\n case \"dark\":\n return theme;\n case \"light\":\n return theme;\n case \"system\":\n return theme;\n default:\n throw new ThemeError(theme);\n }\n}\n\n/**\n * Update the theme on the page and in local storage.\n */\nexport function setPageTheme(theme: string | undefined) {\n if (theme === undefined) throw new ThemeError(theme);\n\n switch (theme) {\n case \"system\":\n localStorage.removeItem(\"theme\");\n switch (preferredTheme()) {\n case \"dark\":\n document.documentElement.classList.add(\"dark\");\n break;\n case \"light\":\n document.documentElement.classList.remove(\"dark\");\n break;\n }\n\n break;\n\n case \"light\":\n localStorage.setItem(\"theme\", theme);\n document.documentElement.classList.remove(\"dark\");\n break;\n\n case \"dark\":\n localStorage.setItem(\"theme\", theme);\n document.documentElement.classList.add(\"dark\");\n break;\n\n default:\n throw new ThemeError(theme);\n }\n\n setButtons(theme);\n}\n\n/**\n * The known theme selector options.\n */\ntype KnownTheme = \"dark\" | \"light\" | \"system\";\n\n/**\n * A theme that can be pulled explicitly from local storage.\n */\ntype StoredTheme = \"dark\" | \"light\";\n\n/**\n * Set as active the button that matches the theme.\n *\n * Make sure to set all another buttons as inactive.\n */\nfunction setButtons(theme: KnownTheme) {\n querySelectorAll(\".theme-option\").forEach(($option) => {\n if ($option.dataset.theme === theme) $option.dataset.active = \"true\";\n else delete $option.dataset.active;\n });\n}\n\n/**\n * Get the user's preferred theme based on a media query.\n */\nfunction preferredTheme(): StoredTheme {\n const prefersDark =\n globalThis.window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n if (prefersDark) return \"dark\";\n return \"light\";\n}\n\n/**\n * Indicates an error during theme selection.\n */\nclass ThemeError extends Error {\n constructor(theme: string | undefined) {\n super(\n `could not determine theme: '${theme || \"undefined\"}'`,\n );\n }\n}\n", "import { getConfiguredTheme, setPageTheme } from \"../util/theme.ts\";\n\n/**\n * Sets up the logic for updating theme pre-load.\n */\nexport function setupTheme() {\n setPageTheme(getConfiguredTheme());\n}\n", "import { setupTheme } from \"./theme.ts\";\n\n/**\n * Run all page setup operations, initializing all interactive widgets.\n */\nfunction setup() {\n setupTheme();\n}\n\n/**\n * Do setup, logging errors to the console.\n */\n(function () {\n try {\n setup();\n } catch (e) {\n console.error(e);\n }\n})();\n"], + "mappings": "AAYO,SAASA,EAAiBC,EAAsC,CACrE,IAAMC,EAAS,SAAS,iBAAiBD,CAAQ,EACjD,GAAIC,IAAW,KAAM,MAAM,IAAIC,EAAW,uBAAuBF,CAAQ,GAAG,EAC5E,OAAO,MAAM,KAAKC,CAAM,CAC1B,CAuBA,IAAME,EAAN,cAAyB,KAAM,CAC7B,YAAYC,EAAa,CACvB,MAAMA,CAAG,CACX,CACF,ECtCO,SAASC,GAAiC,CAC/C,IAAMC,EAAQ,aAAa,QAAQ,OAAO,GAAK,SAE/C,OAAQA,EAAO,CACb,IAAK,OACH,OAAOA,EACT,IAAK,QACH,OAAOA,EACT,IAAK,SACH,OAAOA,EACT,QACE,MAAM,IAAIC,EAAWD,CAAK,CAC9B,CACF,CAKO,SAASE,EAAaF,EAA2B,CACtD,GAAIA,IAAU,OAAW,MAAM,IAAIC,EAAWD,CAAK,EAEnD,OAAQA,EAAO,CACb,IAAK,SAEH,OADA,aAAa,WAAW,OAAO,EACvBG,EAAe,EAAG,CACxB,IAAK,OACH,SAAS,gBAAgB,UAAU,IAAI,MAAM,EAC7C,MACF,IAAK,QACH,SAAS,gBAAgB,UAAU,OAAO,MAAM,EAChD,KACJ,CAEA,MAEF,IAAK,QACH,aAAa,QAAQ,QAASH,CAAK,EACnC,SAAS,gBAAgB,UAAU,OAAO,MAAM,EAChD,MAEF,IAAK,OACH,aAAa,QAAQ,QAASA,CAAK,EACnC,SAAS,gBAAgB,UAAU,IAAI,MAAM,EAC7C,MAEF,QACE,MAAM,IAAIC,EAAWD,CAAK,CAC9B,CAEAI,EAAWJ,CAAK,CAClB,CAiBA,SAASI,EAAWJ,EAAmB,CACrCK,EAAiB,eAAe,EAAE,QAASC,GAAY,CACjDA,EAAQ,QAAQ,QAAUN,EAAOM,EAAQ,QAAQ,OAAS,OACzD,OAAOA,EAAQ,QAAQ,MAC9B,CAAC,CACH,CAKA,SAASH,GAA8B,CAIrC,OAFE,WAAW,OAAO,WAAW,8BAA8B,EAAE,QAEvC,OACjB,OACT,CAKA,IAAMF,EAAN,cAAyB,KAAM,CAC7B,YAAYD,EAA2B,CACrC,MACE,+BAA+BA,GAAS,WAAW,GACrD,CACF,CACF,EC9FO,SAASO,GAAa,CAC3BC,EAAaC,EAAmB,CAAC,CACnC,CCFA,SAASC,GAAQ,CACfC,EAAW,CACb,EAKC,UAAY,CACX,GAAI,CACFD,EAAM,CACR,OAAS,EAAG,CACV,QAAQ,MAAM,CAAC,CACjB,CACF,GAAG", + "names": ["querySelectorAll", "selector", "$elems", "QueryError", "QueryError", "msg", "getConfiguredTheme", "theme", "ThemeError", "setPageTheme", "preferredTheme", "setButtons", "querySelectorAll", "$option", "setupTheme", "setPageTheme", "getConfiguredTheme", "setup", "setupTheme"] +} diff --git a/site/static/js/load-theme.mjs b/site/static/js/load-theme.mjs deleted file mode 100644 index 13619294..00000000 --- a/site/static/js/load-theme.mjs +++ /dev/null @@ -1,62 +0,0 @@ -import { themes, themeSet, themeKey } from "theme"; - -/** - * Get any theme setting from localStorage. - * - * @return the theme string from localStorage. - */ -const getStoredTheme = function () { - return localStorage.getItem(themeKey); -}; - -/** - * Get the user's theme preference with a media query. - * - * @return the theme preference. - */ -const getUserPreferredTheme = function () { - let prefersDark = window.matchMedia("(prefers-color-scheme: dark)"); - - if (prefersDark) { - return themes.DARK; - } - - return themes.LIGHT; -}; - -/** - * Set the theme by updating the site styles. - * - * @param theme The theme enum indicating what theme to use. - * @return if setting the theme succeeded. - */ -const setTheme = function (theme) { - if (theme === themes.DARK) { - document.documentElement.classList.add("dark"); - return themeSet.YES; - } else if (theme === themes.LIGHT) { - document.documentElement.classList.remove("dark"); - return themeSet.YES; - } else { - console.error(`unexpected theme ${theme}, should be "light" or "dark"`); - return themeSet.NO; - } -}; - -(function () { - let storedTheme = getStoredTheme(); - - if (storedTheme) { - console.debug(`Found stored theme '${storedTheme}'`); - let result = setTheme(storedTheme); - if (result === themeSet.YES) return; - } - - let userPreferredTheme = getUserPreferredTheme(); - console.debug(`Found preferred theme '${userPreferredTheme}'`); - let result = setTheme(userPreferredTheme); - if (result === themeSet.YES) return; - - console.error("unable to set the theme, defaulting to 'light'"); - setTheme(themes.LIGHT); -})(); diff --git a/site/static/js/setup-theme-toggle.mjs b/site/static/js/setup-theme-toggle.mjs deleted file mode 100644 index fa940149..00000000 --- a/site/static/js/setup-theme-toggle.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import { themes, themeKey } from "theme"; - -(function () { - document.getElementById("toggle-darkmode").addEventListener("click", (e) => { - let containsDark = document.documentElement.classList.toggle("dark"); - - if (containsDark) { - localStorage.setItem(themeKey, themes.DARK); - } else { - localStorage.setItem(themeKey, themes.LIGHT); - } - - e.preventDefault(); - }); -})(); diff --git a/site/static/js/theme.mjs b/site/static/js/theme.mjs deleted file mode 100644 index dc328729..00000000 --- a/site/static/js/theme.mjs +++ /dev/null @@ -1,14 +0,0 @@ -// An enum representing the theme options. -export const themes = Object.freeze({ - DARK: "dark", - LIGHT: "light", -}); - -// Has the theme been set successfully? -export const themeSet = Object.freeze({ - YES: "yes", - NO: "no", -}); - -// The key to use with localStorage. -export const themeKey = "theme"; diff --git a/site/styles/main.css b/site/styles/main.css index b33f278f..e8310852 100644 --- a/site/styles/main.css +++ b/site/styles/main.css @@ -2,6 +2,20 @@ @tailwind components; @tailwind utilities; +@font-face { + font-family: "IBM Plex Mono"; + src: url("/fonts/plex/IBMPlexMono-Text.woff2") format("woff2"); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: "IBM Plex Mono"; + src: url("/fonts/plex/IBMPlexMono-Bold.woff2") format("woff2"); + font-weight: 700; + font-style: normal; +} + .icon { @apply inline-block; @apply w-4; diff --git a/site/tailwind.config.js b/site/tailwind.config.js index 90ce8a55..a9344498 100644 --- a/site/tailwind.config.js +++ b/site/tailwind.config.js @@ -11,10 +11,14 @@ module.exports = { fontFamily: { // Use Inter as the default font, but otherwise use // the default sans-serif font. - sans: ['"Inter"', ...defaultTheme.fontFamily.sans], - }, - backgroundImage: { - homepage: "url('/images/homepage-bg.png')", + sans: [ + "'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", + { + fontFeatureSettings: + '"calt", "dlig", "case", "ss03", "cv01", "cv10"', + }, + ], + mono: ["IBM Plex Mono", ...defaultTheme.fontFamily.mono], }, }, }, diff --git a/site/templates/bases/base.tera.html b/site/templates/bases/base.tera.html index f9a5ccfd..64b24bd5 100644 --- a/site/templates/bases/base.tera.html +++ b/site/templates/bases/base.tera.html @@ -1,27 +1,16 @@ {% import "macros/breadcrumbs.tera.html" as bc %} +{% import "macros/icon.tera.html" as ic %} +{% import "macros/toc.tera.html" as toc %} - - - - - - - - {% block title %}Hipcheck{% endblock %} + {% include "partials/head.tera.html" %} - +

-
+
{% include "partials/nav.tera.html" %} {% block breadcrumbs %} @@ -30,28 +19,31 @@ {% endblock %}
-
-
-
-
+
+
+
+
{% block content %}{% endblock %}
-
-
+
+
{% block sidebar %}{% endblock %}
- - - {% block extra %}{% endblock %}
+ {% block extra %}{% endblock %} + {% include "partials/footer.tera.html" %}
- + {% include "partials/search.tera.html" %} + {% include "partials/end.tera.html" %} diff --git a/site/templates/bases/docs.tera.html b/site/templates/bases/docs.tera.html new file mode 100644 index 00000000..36a39029 --- /dev/null +++ b/site/templates/bases/docs.tera.html @@ -0,0 +1,135 @@ +{% import "macros/breadcrumbs.tera.html" as bc %} +{% import "macros/icon.tera.html" as ic %} +{% import "macros/toc.tera.html" as toc %} + + + + + {% block title %}Hipcheck{% endblock %} + {% include "partials/head.tera.html" %} + + +
+
+ {% include "partials/nav.tera.html" %} + + {% block breadcrumbs %} + {% set current = section | default(value=page) %} + {{ bc::breadcrumbs(current=current) }} + {% endblock %} +
+ +
+
+
+ {% set docs_section = get_section(path="docs/_index.md") %} +
    +
  1. Documentation
  2. + {% for section_path in docs_section.subsections %} + {% set section = get_section(path=section_path) %} + +
  3. + + +
      + {% if section.title == "Requests for Discussion" %} +
    1. + The RFD Process +
    2. +
    3. + All RFDs +
    4. + {% else %} + {% for nav_page in section.pages %} +
    5. + {{ nav_page.title }} +
    6. + {% endfor %} + {% endif %} + + {% for subsection_path in section.subsections %} + {% set subsection = get_section(path=subsection_path) %} + +
    7. + + {{ subsection.title }} + + +
        + {% for nav_page in subsection.pages %} +
      1. + {% if nav_page.extra.nav_title %} + {% set subnav_title = nav_page.extra.nav_title %} + {% else %} + {% set subnav_title = nav_page.title %} + {% endif %} + {{ subnav_title | safe }} +
      2. + {% endfor %} +
      +
    8. + {% endfor %} +
    +
  4. + {% endfor %} +
+
+
+
+
+ {% block content %}{% endblock %} +
+ +
+
+ {% if current.lower %} + {{ ic::icon(name="arrow-left") }} {{ current.lower.title }} + {% endif %} +
+
+ {% if current.higher %} + {{ current.higher.title }} {{ ic::icon(name="arrow-right") }} + {% endif %} +
+
+
+
+
+ {% block sidebar %}{% endblock %} +
+
+
+ + {% include "partials/footer.tera.html" %} +
+ + {% include "partials/search.tera.html" %} + {% include "partials/end.tera.html" %} + + diff --git a/site/templates/blog.html b/site/templates/blog.html index 7db943f1..eb29c0a5 100644 --- a/site/templates/blog.html +++ b/site/templates/blog.html @@ -1,7 +1,3 @@ - -{% import "macros/icon.tera.html" as ic %} -{% import "macros/toc.tera.html" as toc %} - {% extends "bases/base.tera.html" %} {% block title %} diff --git a/site/templates/docs.html b/site/templates/docs.html new file mode 100644 index 00000000..10612f3a --- /dev/null +++ b/site/templates/docs.html @@ -0,0 +1,17 @@ +{% extends "bases/docs.tera.html" %} + +{% block title %} + {% if section.title %} + {{ section.title }} + {% else %} + Hipcheck + {% endif %} +{% endblock %} + +{% block content %} + {{ section.content | safe }} +{% endblock %} + +{% block sidebar %} + {{ toc::toc(content=section.toc, is_doc=true) }} +{% endblock %} diff --git a/site/templates/docs_page.html b/site/templates/docs_page.html new file mode 100644 index 00000000..dc7df5dc --- /dev/null +++ b/site/templates/docs_page.html @@ -0,0 +1,17 @@ +{% extends "bases/docs.tera.html" %} + +{% block title %} + {% if page.title %} + {{ page.title }} + {% else %} + Hipcheck + {% endif %} +{% endblock %} + +{% block content %} + {{ page.content | safe }} +{% endblock %} + +{% block sidebar %} + {{ toc::toc(content=page.toc, is_doc=true) }} +{% endblock %} diff --git a/site/templates/index.html b/site/templates/index.html index 497d5e47..9b2c190c 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -1,6 +1,3 @@ - -{% import "macros/icon.tera.html" as ic %} - {% extends "bases/base.tera.html" %} {% block title %} @@ -11,96 +8,141 @@ {% endif %} {% endblock %} +{% block body_classes %} +{% endblock %} + {# Turn off breadcrumbs for the index page. #} {% block breadcrumbs %}{% endblock %} {% block content %} -

- Automatically assess - software packages + + Helping maintainers assess + software packages for supply chain risk -

+ -

Hipcheck is a tool for analyzing software packages from hosts like NPM, PyPI, and Maven, and source repositories like GitHub, GitLab, Sourcehut, and more. It assesses the practices a project follows for building their software, and tries to detect active supply chain attacks as well.

-

Use Hipcheck to filter hundreds of dependencies to just a few you can manually review!

+
    +
  • Filter hundreds of dependencies to a few for review
  • +
  • Use plugins to run only the analyses you choose
  • +
  • Configure scoring to decide when to investigate further
  • +
+ + {% include "partials/install.tera.html" %} - {{ ic::icon(name="book-open", classes="mt-[-2px] ml-[-4px] mr-1") }} Read the Docs - - - - Try Hipcheck! {{ ic::icon(name="arrow-down", classes="mt-[-2px] mr-[-4px] mr-1") }} + Read the Docs {{ ic::icon(name="book-open", classes="mt-[-2px] ml-2") }} {% endblock %} {% block sidebar %} - -{% endblock %} +
+
+
+
+
-{% block extra %} -
-

Simplify using Open Source Software

+
+
+
+ Analyzing +
+
+ {{ ic::icon(name="link") }} +
+ pkg:github/example/project +
+
+
-
-
-
-
- {{ ic::icon(name="info", classes="-mt-1 text-blue-500") }} +
+
+
+ {{ ic::icon(name="box") }} mitre/activity
-
-

Identify High Risk Dependencies

-

Audit a project’s development practices, like code review, fuzz testing, and active maintenance, automatically!

-

Learn about Hipcheck’s analyses →

+ Is the project maintained? + Active. Last commit 7 days ago. +
+ {{ ic::icon(name="check", classes="fill-green-800") }} Pass + 50% weight
-
-
-
- {{ ic::icon(name="settings", classes="-mt-1 text-blue-500") }} + +
+
+
+ {{ ic::icon(name="box") }} mitre/review
-
-

Configure Analyses You Care About

-

Keep only the analyses that matter to you, change how they contribute to scoring, and how much risk you’re willing to tolerate.

-

Learn about configuring Hipcheck →

+ Are there code reviews? + Code reviews common on PRs +
+ {{ ic::icon(name="check", classes="fill-green-800") }} Pass + 30% weight
-
-
-
- {{ ic::icon(name="package", classes="-mt-1 text-blue-500") }} + +
+
+
+ {{ ic::icon(name="box") }} mitre/binary
-
-

Make Dependencies Manageable

-

Many software projects use hundreds of open source dependencies! Filter that list to something manageable with Hipcheck

-

Learn about using Hipcheck →

+ Are there binaries in the repo? + Warning: found prebuilt, prebuilt.exe +
+ {{ ic::icon(name="alert-circle", classes="fill-yellow-800") }} Investigate + 20% weight
+ +
+
+ + 0.2 + Risk Score + + is + + ≤ 0.5 + Risk Policy + + so + + {{ ic::icon(name="check", classes="") }} Pass + Result + +
+
+
+
+{% endblock %} + +{% block extra %} +
+
+

You Decide What to Analyze

+

With Hipcheck, you decide what analyses to run. Analyses in Hipcheck are provided + by plugins, and you decide what plugins to run with your policy file, and how to score + their output with policy expressions.

+

Empowering Human Review

+

Hipcheck never "fails" a package; it's only recommendations are either "Pass" or "Investigate." + Hipcheck also tries to give plenty of information to guide human review if you need it!

+

Dependency Management that Works

+

Get the power of code reuse from open source dependencies while still managing supply chain risk.

{% endblock %} diff --git a/site/templates/macros/breadcrumbs.tera.html b/site/templates/macros/breadcrumbs.tera.html index 5d9f8815..f8474f2e 100644 --- a/site/templates/macros/breadcrumbs.tera.html +++ b/site/templates/macros/breadcrumbs.tera.html @@ -2,32 +2,43 @@ {# TODO: Don't hide this on mobile #} {% endmacro breadcrumbs %} diff --git a/site/templates/macros/toc.tera.html b/site/templates/macros/toc.tera.html index ec699288..7a251da8 100644 --- a/site/templates/macros/toc.tera.html +++ b/site/templates/macros/toc.tera.html @@ -1,23 +1,39 @@ -{% macro toc_level(content) %} +{% macro toc_level(content, level) %}
    {% for item in content %} -
  1. - {{ item.title }} +
  2. + {% set prefix = config.base_url ~ current_path %} + {% set link = item.permalink | replace(from=prefix, to="") %} + + {{ item.title }} {% if item.children %} {% set children = item.children %} - {{ toc::toc_level(content=children) }} + {{ toc::toc_level(content=children, level = level + 1) }} {% endif %}
  3. + {% else %} + {% if level == 1 %} +
    +

    No table of contents.

    +
    + {% endif %} {% endfor %}
{% endmacro toc_level %} -{% macro toc(content) %} -
- Table of Contents +{% macro toc(content, is_doc=false) %} +
+ + On This Page + To Top {{ ic::icon(name="arrow-up") }} + - {{ toc::toc_level(content=content) }} +
+ {% if content | length > 0 %} + {{ toc::toc_level(content=content[0].children, level=1) }} + {% endif %} +
{% endmacro toc %} diff --git a/site/templates/page.html b/site/templates/page.html index 761c336b..271cd171 100644 --- a/site/templates/page.html +++ b/site/templates/page.html @@ -1,6 +1,3 @@ - -{% import "macros/icon.tera.html" as ic %} - {% extends "bases/base.tera.html" %} {% block title %} diff --git a/site/templates/partials/end.tera.html b/site/templates/partials/end.tera.html new file mode 100644 index 00000000..5f7c8451 --- /dev/null +++ b/site/templates/partials/end.tera.html @@ -0,0 +1,3 @@ + + + diff --git a/site/templates/partials/footer.tera.html b/site/templates/partials/footer.tera.html index c8722618..bc5d6048 100644 --- a/site/templates/partials/footer.tera.html +++ b/site/templates/partials/footer.tera.html @@ -1,14 +1,14 @@ -