diff --git a/.github/workflows/website-deploy.yml b/.github/workflows/website-deploy.yml index cbdc490b..835b7ccb 100644 --- a/.github/workflows/website-deploy.yml +++ b/.github/workflows/website-deploy.yml @@ -51,6 +51,12 @@ jobs: mv tailwindcss "${HOME}/.local/bin/tailwindcss" echo "${HOME}/.local/bin" >> $GITHUB_PATH + # Install the latest major version of Deno. + - name: Install Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + # Setup GitHub Pages # # Specifically, this sets some variables we can use in later steps that @@ -70,6 +76,8 @@ jobs: cd site zola build tailwindcss -i styles/main.css -o public/main.css + cd scripts + deno task bundle # Upload the output of the build as an Actions artifact so the deploy # step can pick it up and use it. diff --git a/.github/workflows/website-test.yml b/.github/workflows/website-test.yml index 751ba2ba..b57f73a5 100644 --- a/.github/workflows/website-test.yml +++ b/.github/workflows/website-test.yml @@ -41,9 +41,17 @@ jobs: mv tailwindcss "${HOME}/.local/bin/tailwindcss" echo "${HOME}/.local/bin" >> $GITHUB_PATH + # Install the latest major version of Deno. + - name: Install Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + # Build the actual site with Zola and Tailwind. - name: Build Hipcheck Website run: | cd site zola build tailwindcss -i styles/main.css -o public/main.css + cd scripts + deno task bundle 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..88861256 100644 --- a/site/config.toml +++ b/site/config.toml @@ -1,56 +1,66 @@ # The URL of the side. base_url = "https://mitre.github.io/hipcheck" +# Generate an Atom feed for the blog. +generate_feeds = true + # 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_json" + [markdown] # Use syntax highlighting. highlight_code = true +highlight_theme = "ayu-light" [extra] +# Backup title used in meta tags and social sharing cards. +title = "Hipcheck" + +# Backup description used in meta tags and social sharing cards. +description = "Helping maintainers assess software packages for long-term risk." + # 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/blog/2024-06-11-test-blog-post.md b/site/content/blog/2024-06-11-test-blog-post.md deleted file mode 100644 index ccfc83bd..00000000 --- a/site/content/blog/2024-06-11-test-blog-post.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Test Blog Post ---- - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin et suscipit ante. Duis bibendum eros eu eros iaculis vehicula. Cras dictum at libero sed suscipit. Sed condimentum metus id sem condimentum malesuada. Praesent ut velit eget nisl elementum tristique a vitae urna. Fusce eros purus, scelerisque sit amet arcu non, finibus condimentum nisi. Nam sodales pharetra risus eu dictum. Fusce tempor nunc ut enim mollis, sit amet pharetra felis euismod. Integer ac nunc sed sapien pharetra egestas id sit amet elit. - - - -Mauris turpis elit, mattis nec porttitor eu, gravida vitae lacus. Praesent eu libero eget mauris vestibulum finibus. Integer viverra scelerisque nibh vel fringilla. Aliquam efficitur eros sit amet ipsum vestibulum porta. Suspendisse potenti. Proin quis interdum sem. Nulla elementum ultricies viverra. - -Morbi nunc ex, varius sed arcu quis, suscipit posuere lectus. Vivamus elit nunc, ornare et dui ut, vestibulum sagittis neque. Proin molestie justo sed venenatis euismod. Ut luctus odio nec laoreet elementum. Proin et risus bibendum, pellentesque justo in, tincidunt ante. Nullam lacinia enim dui, consectetur varius eros tincidunt sed. Sed id varius leo. Donec non leo in nulla efficitur aliquet et a lacus. Suspendisse potenti. Vivamus at velit suscipit, porta eros dapibus, maximus odio. Suspendisse mollis sodales ante eu fringilla. Cras at ex in ante tempor ullamcorper. - -In varius felis nibh, eget laoreet erat cursus a. Nam porttitor enim justo, eu pulvinar lacus ornare et. Suspendisse et interdum justo, eget ullamcorper lacus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sagittis scelerisque turpis ut molestie. Proin feugiat vel orci quis tempus. Mauris laoreet, odio eleifend dictum commodo, orci magna feugiat ipsum, vitae cursus dolor nisi a justo. Donec tellus augue, consectetur vitae semper id, finibus nec velit. Praesent ullamcorper augue sit amet diam vehicula pellentesque. Nullam at lacinia lectus. Vestibulum augue lacus, malesuada id libero sit amet, cursus ornare velit. Donec tincidunt, purus vel auctor volutpat, nisi ligula venenatis nunc, vel sollicitudin augue risus in mauris. Morbi dapibus sagittis turpis, rutrum efficitur ligula sollicitudin vel. Nullam blandit metus convallis convallis malesuada. - -Phasellus eu faucibus quam. Cras ut auctor ex. Curabitur sit amet quam ut nunc mollis elementum. Nunc non risus aliquet urna pulvinar scelerisque in eu diam. Fusce fermentum ultricies orci, a congue augue iaculis et. Duis odio ante, suscipit rhoncus nisi sit amet, suscipit sagittis ante. Sed lacus lorem, pulvinar et dignissim non, bibendum nec mauris. In venenatis, ligula quis consequat vestibulum, lectus ligula fermentum elit, eu congue augue risus sit amet lectus. Proin id est facilisis, lacinia augue id, pretium orci. Aenean sed tincidunt erat. Sed at justo vitae risus lacinia viverra eu sit amet velit. Ut elementum magna felis. diff --git a/site/content/blog/2024-11-04-hipcheck-3.8.0-release.md b/site/content/blog/2024-11-04-hipcheck-3.8.0-release.md new file mode 100644 index 00000000..31c1026d --- /dev/null +++ b/site/content/blog/2024-11-04-hipcheck-3.8.0-release.md @@ -0,0 +1,82 @@ +--- +title: Hipcheck 3.8.0 Release +authors: + - Andrew Lilley Brinker +extra: + author_img: "images/authors/andrew.jpg" +--- + +Hipcheck 3.8.0 is out, with a completed transition to our new plugin system, +and a lot of polish for the plugin user experience. + + + +--- + +## End of a Half-Measure + +In 3.6.2, we launched the initial version of Hipcheck's new plugin system! +This is a new mechanism that enables anyone to add support for new data sources +and new analyses. + +As part of the move to plugins in 3.6.2, we introduced a "policy file" +format, written with KDL syntax, and began to phase out the older "configuration +file" written in TOML. You could (and can still in 3.8.0) continue to use +the TOML file, though you get a warning, and under the hood this TOML file +is converted to the KDL format. + +In the KDL policy file, you specify plugins to run with a "plugin specifier" +that looks like `mitre/activity`, or more generally `/`. From +3.6.2 to 3.7.0, any of the existing analyses which can be built into Hipcheck +were exposed as `mitre`-namespaced plugins in the policy file. + +However, under the hood, these did _not_ actually run those analyses _as +plugins_, but rather continued to run code that was built in to Hipcheck. Even +at the time, we knew this was a half measure to get the initial support for +plugins out into the wild while we continued to work on splitting existing +data sources and analyses out. + +With 3.8.0, that splitting-out is complete! There are no more built in analyses +or data sources in Hipcheck. This is good news, both for making the `hc` binary +itself smaller, _and_ because it means we no longer privilege plugins made +by the Hipcheck team! They run with the same Rust plugin SDK we make available +to anyone who wants to write a Hipcheck plugin. + +This dog-fooding of our own SDK and plugin API also helped us to identify both +bugfixes and future improvements to make. + +## New Plugins + +| Plugin Name | Top-Level? | Download Manifest Location | +|:--------------------|:-----------|:---------------------------| +| `mitre/activity` | Yes | TODO | +| `mitre/affiliation` | Yes | TODO | +| `mitre/binary` | Yes | TODO | +| `mitre/churn` | Yes | TODO | +| `mitre/entropy` | Yes | TODO | +| `mitre/fuzz` | Yes | TODO | +| `mitre/git` | - | TODO | +| `mitre/github` | - | TODO | +| `mitre/identity` | Yes | TODO | +| `mitre/linguist` | - | TODO | +| `mitre/npm` | - | TODO | +| `mitre/review` | Yes | TODO | +| `mitre/typo` | Yes | TODO | + +TODO: Explain what "top-level" means, what the download link is for, and how +to use these new plugins. + +## Changelogs + +### `hc` + +- Add `patch` field to policy files +- Introduce clear schema for `target` for default queries +- Add support for specifying local plugins +- Add support for datetimes and spans in policy expressions + +### `hipcheck-sdk` Rust Crate + +- Add ability to mock query responses for testing. +- Add concern reporting support for queries. +- FIX: Ensure the SDK sends error messages over gRPC in addition to logging them. diff --git a/site/content/blog/_index.md b/site/content/blog/_index.md index efaae4ec..a5b2a902 100644 --- a/site/content/blog/_index.md +++ b/site/content/blog/_index.md @@ -2,8 +2,5 @@ title: Blog sort_by: date template: blog.html +page_template: blog_post.html --- - -# Hipcheck Development Blog - -Updates from the people building Hipcheck. 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/analyses.md b/site/content/docs/guide/analyses.md deleted file mode 100644 index 96bf1a06..00000000 --- a/site/content/docs/guide/analyses.md +++ /dev/null @@ -1,332 +0,0 @@ ---- -title: Analyses ---- - -# Analyses - -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 -performed, what its current limitations are, and how the results of that -analysis are thresholded based on the configuration: - -- [Activity](#activity) -- [Affiliation](#affiliation) -- [Binary](#binary) -- [Churn](#churn) -- [Entropy](#entropy) -- [Fuzz](#fuzz) -- [Identity](#identity) -- [Review](#review) -- [Typo](#typo) - -## Activity - -* Configuration name: `analysis.practices.activity` -* Data source: Git (committed date of most recent commit to `HEAD` branch) - -Activity analysis looks at the date of the most recent commit to the branch -pointed to by `HEAD` in the repository. In the case of a local repository -source, that may be a branch other than the default. In the case of a remote -repository, it will always be the default branch on the remote host. - -Hipcheck identifies the committed date of the most recent commit, and -calculates the number of weeks between that commit and the day Hipcheck is -performing this analysis. It then compares that duration against the -configured threshold (default configuration: 71 weeks / one year). If the -duration in the repository is greater than the configured threshold, then -the analysis will be marked as a failure. - -### Limitations - -* __Cases where lack of updates is warranted__: Sometimes work on a piece of - software stops because it is complete, and there is no longer a need to - update it. In this case, a repository being flagged as failing this analysis - may not be truly risky for lack of activity. However, _most of the time_ - we expect that lack of updates ought to be concern, and so considering this - metric when analyzing software supply chain risk is reasonable. If you - are in a context where lack of updates is desirable or not concerning, you - may consider changing the configuration to a different duration, or disabling - the analysis entirely. - -## Affiliation - -* Configuration name: `analysis.attacks.commit.affiliation` -* Data source: Git (commit author identities) - -Affiliation analysis tries to identify when commit authors or committers -may be affiliated or unaffiliated with some list of organizations. -This determination is based on the email address associated with authors or -committers on each Git commit, compared against a configured list of web hosts -associated with organizations of concern. - -The construction of the list is based on an "orgs file," whose path is provided -in the configuration of this form of analysis. This orgs file defines two -things: 1) a list of organizations, including web hosts associated with them, -and the name of the country to which they primarily belong, and 2) a "strategy" -for how the list of to-be-flagged hosts should be constructed. - -The strategy defines the list of organizations to be included in the list of -those considered when checking affiliation, and whether the analysis should -flag commits from those _affiliated_ with the list of organizations, or -_independent_ from the list of organizations (for completeness, it also -permits _all_ or _none_, which would flag all commits, or none of them). - -If the `strategy` key is used in the configuration, then all organizations -listed in the "orgs file" are implicitly included in the list of organizations -to consider. - -If the `strategy_spec` table is used, then `strategy_spec.mode` and -`strategy_spec.list` keys must be defined. The `strategy_spec.mode` key accepts -the same set of values (`affiliated`, `independent`, `all`, or `none`) as the -`strategy` key, while `list` accepts an array of strings in one of two forms: -`"country:"` or `"org:"`. The first form will include -in the list of organizations all those organizations which are associated with -the named country, while the second form will include in the list a single -organization with the given name. - -To illustrate this, imagine the following strategy specification: - -```toml -[strategy_spec] -mode = "affiliated" -kind = ["country:United States", "org:MITRE"] -``` - -This strategy spec would flag any commits those authors or committers can be -identified as being affiliated with any American company listed in the file or -with MITRE specifically. - -### Limitations - -* __The orgs file is limited__: The current construction requires the manual - definition, in the "orgs file," of companies, their associated web hosts, and - their primary affiliated country. This manual work is laborious, possibly - error-prone, requires updating over time, and is less complete than accessing - more authoritative sources of corporate information. -* __Limits in git's identity system__: Git's identity system is, by default, - quite weak. Commit author or committer data may be freely spoofed or filled - with junk information which make identifying the true author or committer of - a commit impossible. Git _does_ support commit signing, to at least confirm - that a commit has been authored by the person who owns the relevant signing - key. This signing can then be checked against known signatures, including - sources like Keybase which provide easy distribution and checking of known - signatures against known identities. However, commit signing and checking of - signatures incorporates the complexity and limitations of cryptographic - signatures as a technical mechanism for trust, including questions of how to - handle failures to sign, changes in keys or loss of trust in previously - trusted keys, and so on. This is an important issue, and incorporation of - commit signing information in Hipcheck is intended for the future, but - currently Hipcheck does not use commit signatures in any way. When - integrated into Hipcheck, the question of how to handle the very common - case of signatures _not_ being used would arise as well. How to resolve - this here is an open question. -* __Questions about the best default configuration__: Additionally, there is a - question in the default configuration of this analysis regarding whether to - flag commits affiliated with organizations of concern, or commits - unaffiliated with any known organization. This question relies on assumptions - of the behavior of malicious actors in this context, and whether malicious - contributions would be made in commits authored or committed by those using - their corporate emails. - -## Binary - -* Configuration name: `analysis.practices.binary` -* Data source: cloned repository (all files in cloned repo filetree) - -Binary analysis searches through all of the files in the repository for binary -files (i.e. files not in readable text) that may contain code. There is a high -liklihood that these are deliberately malicious insertions. The precense of such -files could indicate the precense of malicious code in the repository and is a -cause for suspicion. - -The analysis works by searching through the entire repository filetree. It -identifies all binary files and filters out files that are obviously not code -(e.g. images or audio files). If, after filtering, more binary files remain than -the configured thershold amount, the repository fails this analysis. - -The analysis displays the internal filetree location of each suspicious binary file. -The user can then examine each file to determine if it is malicious or not. - -### Limitations - -* __Not all binary files may be malicious__: The repo may use certain binary - files (beyond image and audio files) for legitimate purposes. This - analysis does not investigate what the files do, only that they exist. - -* __No additional information on binary files__: Hipcheck does not currently - return any additional information about the suspcious files, only their - locations in the repo filetree. The user must search for them manually if - they wish to learn more about them. - -## Churn - -* Configuration name: `analysis.attacks.commit.churn` -* Data source: Git (commit diff lines added / deleted, and patch contents) - -Churn analysis attempts to identify the high prevalence of very large commits -which may increase the risk of successful malicious contribution. The notion -here being that it's easier to hide malicious content in a large commit than -in a small one, as malicious contribution relies on getting malicious changes -through a normal submission / review process (assuming review is performed). - -Churn analysis works by determining the total number of lines and files -changed across all commits containing changes to code in a repository, and -from that the percentage, per commit, of those totals. For each commit, the -file percentage and line percentage are then combined, as file frequency times -line frequency squared, times 1,000,000, to produce a score. These scores are -then normalized into Z-scores, to produce the final churn value for each commit. -These churn values therefore represent how much the size of a given commit -differs from the average for the repository. - -Churn cannot run if a repository contains only one commit (or only one commit -that affects a source file). Churn analysis will always give an error when run -against a repo with a single commit. - -### Limitations - -* __Whether churn surfaces malicious contributions is an open question__: - We have ongoing work to confirm that churn does help identify the presence - of malicious contributions, and therefore is a useful metric for assessing - supply chain risk against malicious contribution attacks, but at the - moment this is an assumption made by Hipcheck. -* __Churn's statistical calculations may be insufficient__: There is ongoing - work to assess the statistical qualities of the churn metric and determine - whether it needs to be changed. - -## Entropy - -* Configuration name: `analysis.attacks.commit.entropy` -* Data source: Git (commit diff lines added / deleted, and patch contents) - -Entropy analysis attempts to identify commits which contain a high degree of -textual randomness, in the believe that high textual randomness may indicate -the presence of packed malware or obfuscated code which ought to be assessed -for possible malicious content. - -Entropy analysis works by determining the total number of occurrences for all -unicode graphemes which appear in a repository's Git diffs for commits which -include code. In then converts these occurence counts into frequencies based on -the total number of each individual grapheme divided by the total number of -all graphemes in the combined set of Git diffs. It also determines grapheme -frequencies for each commit individually. These individual and total grapheme -frequencies are then combined into a score as an individual frequency times -the log base 2 of the individual frequency divided by the total frequency. -These individual grapheme scores are then summed to produce a per-commit score, -which is normalized into a Z-score same as the churn metric. These entropy -values therefore represent how much the grapheme frequency map of a given -commit differs from the average set of grapheme frequencies across all commits. - -Entropy cannot run if a repository contains only one commit (or only one commit -that affects a source file). Entropy analysis will always give an error when run -against a repo with a single commit. - -### Limitations - -* __Whether entropy surfaces malicious contributions is an open question__: - We have ongoing work to confirm that entropy does help identify the presence - of malicious contributions, and therefore is a useful metric for assessing - supply chain risk against malicious contribution attacks, but at the - moment this is an assumption made by Hipcheck. -* __Entropy's statistical calculations may be insufficient__: There is ongoing - work to assess the statistical qualities of the entropy metric and determine - whether it needs to be changed. - -## Fuzz - -* Configuration name: `analysis.practices.fuzz` - -Repos being checked by Hipcheck may receive regular fuzz testing. This analysis -checks if the repo is participating in the OSS Fuzz program. If it is fuzzed, -this is considered a signal of a repository being lower risk. - -### Limitations - -* __Not all languagues supported__: Robust fuzzing tools do not exist for every - language. It is possible fuzz testing was not done because no good option for it - existed at the time. Lack of fuzzing in those cases would still indicate a higher - risk, but it would not necessarily indicate bad software development practices. - -* __Only OSS Fuzz checked__: At this time, Hipcheck only checks if the repo - participates in Google's OSS Fuzz. Other fuzz testing programs exist, but a repo - will not pass this analysis if it uses one of those instead. - -## Identity - -* Configuration name: `analysis.practices.identity` -* Data source: Git (commit author and committer identities) - -Identity analysis looks at whether the author and committer identities for -each commit are the same, as part of gauging the likelihood that commits -are receiving some degree of review before being merged into a repository. - -When author and committer identity are the same, that may indicate that a -commit did _not_ receive review, which could be a cause for concern. At the -larger level, having a large percentage of commits with the same author -and committer identities may indicate a project that lacks code review. - -### Limitations - -* __Not every project uses a workflow that accords with this analysis__: - While some Git projects may use a workflow that involves the generation - of patchfiles to then be reviewed and applied by project maintainers, - many may not. In some cases, a workflow may produce final commits where - the author and committer identity are the same, even though the commit - received review. - -## Review - -* Configuration name: `analysis.practices.review` -* Data source: remote Git host API (currently supports: GitHub) - -Review analysis looks at whether pull requests on GitHub (currently the -only supported remote host for this analysis) receive at least one -review prior to being merged. - -If too few pull requests receive review prior to merging, then this -analysis will flag that as a supply chain risk. - -This works with the GitHub API, and requires a token in the configuration. -Hipcheck only needs permissions for accessing public repository data, so -those are the only permissions to assign to your generated token. - -### Limitations - -* __Not every project uses GitHub__: While GitHub is a very popular host - for Git repositories, it is by no means the _only_ host. This analysis' - current limitation to GitHub makes it less useful than it could be. -* __Projects which do use GitHub may not use GitHub Reviews for code review__: - GitHub Reviews is a specific GitHub feature for performing code reviews - which projects may not all use. There may be repositories which are older - than the availability of this feature, and so don't have reviews on older - pull requests. - -## Typo - -* Configuration name: `analysis.attacks.typo` -* Data source: dependency definition for repository (currently supports: NPM \[JavaScript\]) - -Typo analysis attempts to identify possible typosquatting attacks in the -dependency list for any projects which are analyzed and use a supported -language (currently: JavaScript w/ the NPM package manager). - -The analysis works by identifying a programming language based on the presence -of a dependency file in the root of the repository, then attempting to get the -full list of direct and transitive dependencies for that project. It then -compares that list against a list of known popular repositories for that -language to see if any in the dependencies list are possible typos of popular -package name. - -Typo detection is based on the generation of possible typos for known names, -according to a collection of typo possibilities, including single-character -deletion, substitution, swapping, and more. - -### Limitations - -* __Only works for some languages__: Right now, this analysis only supports - JavaScript projects. It requires the implementation of language-specific code - to work with different dependency files and generate the full list of - 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/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..74242a27 --- /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 active 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/plugins/_index.md b/site/content/docs/guide/plugins/_index.md new file mode 100644 index 00000000..ceef4549 --- /dev/null +++ b/site/content/docs/guide/plugins/_index.md @@ -0,0 +1,67 @@ +--- +title: Plugins +template: docs.html +page_template: docs_page.html +weight: 5 +sort_by: title +--- + +# Plugins + +This section covers the plugins currently produced by MITRE. + +
+ +{% waypoint(title="mitre/activity", path="@/docs/guide/plugins/mitre-activity.md", icon="box") %} +Plugin for checking whether a project is actively maintained. +{% end %} + +{% waypoint(title="mitre/affiliation", path="@/docs/guide/plugins/mitre-affiliation.md", icon="box") %} +Plugin for detecting contributors affiliated with an organization of concern. +{% end %} + +{% waypoint(title="mitre/binary", path="@/docs/guide/plugins/mitre-binary.md", icon="box") %} +Plugin for detecting binaries checked into source repositories. +{% end %} + +{% waypoint(title="mitre/churn", path="@/docs/guide/plugins/mitre-churn.md", icon="box") %} +Plugin for detecting unusually large changes in a project's history. +{% end %} + +{% waypoint(title="mitre/entropy", path="@/docs/guide/plugins/mitre-entropy.md", icon="box") %} +Plugin for detecting textually unusual changes in a project's history. +{% end %} + +{% waypoint(title="mitre/fuzz", path="@/docs/guide/plugins/mitre-fuzz.md", icon="box") %} +Plugin for checking if a project uses fuzz testing. +{% end %} + +{% waypoint(title="mitre/git", path="@/docs/guide/plugins/mitre-git.md", icon="git-pull-request") %} +Plugin for accessing Git commit history data. +{% end %} + +{% waypoint(title="mitre/github", path="@/docs/guide/plugins/mitre-github.md", icon="github") %} +Plugin for accessing data from the GitHub API. +{% end %} + +{% waypoint(title="mitre/identity", path="@/docs/guide/plugins/mitre-identity.md", icon="box") %} +Plugin for accessing Git contributor identity data. +{% end %} + +{% waypoint(title="mitre/linguist", path="@/docs/guide/plugins/mitre-linguist.md", icon="box") %} +Plugin for detecting text file language data. +{% end %} + +{% waypoint(title="mitre/npm", path="@/docs/guide/plugins/mitre-npm.md", icon="box") %} +Plugin for accessing package data from the NPM API. +{% end %} + +{% waypoint(title="mitre/review", path="@/docs/guide/plugins/mitre-review.md", icon="box") %} +Plugin for checking if a project practices code review. +{% end %} + +{% waypoint(title="mitre/typo", path="@/docs/guide/plugins/mitre-typo.md", icon="box") %} +Plugin for detecting possible typosquatting in dependencies. +{% end %} + +
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..3b5044b7 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-activity.md @@ -0,0 +1,46 @@ +--- +title: "mitre/activity" +extra: + nav_title: "mitre/activity" +--- + +# `mitre/activity` + +Activity analysis looks at the date of the most recent commit to the branch +pointed to by `HEAD` in the repository. In the case of a local repository +source, that may be a branch other than the default. In the case of a remote +repository, it will always be the default branch on the remote host. + +Hipcheck identifies the committed date of the most recent commit, and +calculates the number of weeks between that commit and the day Hipcheck is +performing this analysis. It then compares that duration against the +configured threshold (default configuration: 71 weeks / one year). If the +duration in the repository is greater than the configured threshold, then +the analysis will be marked as a failure. + +## Queries + +### `mitre/activity` (default query) + +Returns a `Span` representing the time from the most recent commit to now. + +## Configuration + +- `weeks`: An `Integer` of the permitted number of weeks before a project is + considered inactive. + +## Default Policy Expression + +`lte $ P{config.weeks}w` + +## Limitations + +* __Cases where lack of updates is warranted__: Sometimes work on a piece of + software stops because it is complete, and there is no longer a need to + update it. In this case, a repository being flagged as failing this analysis + may not be truly risky for lack of activity. However, _most of the time_ + we expect that lack of updates ought to be concern, and so considering this + metric when analyzing software supply chain risk is reasonable. If you + are in a context where lack of updates is desirable or not concerning, you + may consider changing the configuration to a different duration, or disabling + the analysis entirely. 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..2617f7f2 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-affiliation.md @@ -0,0 +1,98 @@ +--- +title: "mitre/affiliation" +extra: + nav_title: "mitre/affiliation" +--- + +# `mitre/affiliation` + +Affiliation analysis tries to identify when commit authors or committers +may be affiliated or unaffiliated with some list of organizations. +This determination is based on the email address associated with authors or +committers on each Git commit, compared against a configured list of web hosts +associated with organizations of concern. + +The construction of the list is based on an "orgs file," whose path is provided +in the configuration of this form of analysis. This orgs file defines two +things: 1) a list of organizations, including web hosts associated with them, +and the name of the country to which they primarily belong, and 2) a "strategy" +for how the list of to-be-flagged hosts should be constructed. + +The strategy defines the list of organizations to be included in the list of +those considered when checking affiliation, and whether the analysis should +flag commits from those _affiliated_ with the list of organizations, or +_independent_ from the list of organizations (for completeness, it also +permits _all_ or _none_, which would flag all commits, or none of them). + +If the `strategy` key is used in the configuration, then all organizations +listed in the "orgs file" are implicitly included in the list of organizations +to consider. + +If the `strategy_spec` table is used, then `strategy_spec.mode` and +`strategy_spec.list` keys must be defined. The `strategy_spec.mode` key accepts +the same set of values (`affiliated`, `independent`, `all`, or `none`) as the +`strategy` key, while `list` accepts an array of strings in one of two forms: +`"country:"` or `"org:"`. The first form will include +in the list of organizations all those organizations which are associated with +the named country, while the second form will include in the list a single +organization with the given name. + +To illustrate this, imagine the following strategy specification: + +```toml +[strategy_spec] +mode = "affiliated" +kind = ["country:United States", "org:MITRE"] +``` + +This strategy spec would flag any commits those authors or committers can be +identified as being affiliated with any American company listed in the file or +with MITRE specifically. + +## Queries + +### `mitre/affiliation` (default query) + +Returns the number of commits flagged for having concerning contributors. + +## Configuration + +- `orgs_spec`: specifies the policy for matching contributors +- `count_threshold`: an `Integer` expressing the permitted number of concerning + contributors. + +## Default Policy Expression + +`lte $ {config.count_threshold}` + +## Limitations + +* __The orgs file is limited__: The current construction requires the manual + definition, in the "orgs file," of companies, their associated web hosts, and + their primary affiliated country. This manual work is laborious, possibly + error-prone, requires updating over time, and is less complete than accessing + more authoritative sources of corporate information. +* __Limits in git's identity system__: Git's identity system is, by default, + quite weak. Commit author or committer data may be freely spoofed or filled + with junk information which make identifying the true author or committer of + a commit impossible. Git _does_ support commit signing, to at least confirm + that a commit has been authored by the person who owns the relevant signing + key. This signing can then be checked against known signatures, including + sources like Keybase which provide easy distribution and checking of known + signatures against known identities. However, commit signing and checking of + signatures incorporates the complexity and limitations of cryptographic + signatures as a technical mechanism for trust, including questions of how to + handle failures to sign, changes in keys or loss of trust in previously + trusted keys, and so on. This is an important issue, and incorporation of + commit signing information in Hipcheck is intended for the future, but + currently Hipcheck does not use commit signatures in any way. When + integrated into Hipcheck, the question of how to handle the very common + case of signatures _not_ being used would arise as well. How to resolve + this here is an open question. +* __Questions about the best default configuration__: Additionally, there is a + question in the default configuration of this analysis regarding whether to + flag commits affiliated with organizations of concern, or commits + unaffiliated with any known organization. This question relies on assumptions + of the behavior of malicious actors in this context, and whether malicious + contributions would be made in commits authored or committed by those using + their corporate emails. 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..28cde609 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-binary.md @@ -0,0 +1,32 @@ +--- +title: "mitre/binary" +extra: + nav_title: "mitre/binary" +--- + +# `mitre/binary` + +Binary analysis searches through all of the files in the repository for binary +files (i.e. files not in readable text) that may contain code. There is a high +liklihood that these are deliberately malicious insertions. The precense of such +files could indicate the precense of malicious code in the repository and is a +cause for suspicion. + +The analysis works by searching through the entire repository filetree. It +identifies all binary files and filters out files that are obviously not code +(e.g. images or audio files). If, after filtering, more binary files remain than +the configured thershold amount, the repository fails this analysis. + +The analysis displays the internal filetree location of each suspicious binary file. +The user can then examine each file to determine if it is malicious or not. + +## Limitations + +* __Not all binary files may be malicious__: The repo may use certain binary + files (beyond image and audio files) for legitimate purposes. This + analysis does not investigate what the files do, only that they exist. + +* __No additional information on binary files__: Hipcheck does not currently + return any additional information about the suspcious files, only their + locations in the repo filetree. The user must search for them manually if + they wish to learn more about them. 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..483913f9 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-churn.md @@ -0,0 +1,37 @@ +--- +title: "mitre/churn" +extra: + nav_title: "mitre/churn" +--- + +# `mitre/churn` + +Churn analysis attempts to identify the high prevalence of very large commits +which may increase the risk of successful malicious contribution. The notion +here being that it's easier to hide malicious content in a large commit than +in a small one, as malicious contribution relies on getting malicious changes +through a normal submission / review process (assuming review is performed). + +Churn analysis works by determining the total number of lines and files +changed across all commits containing changes to code in a repository, and +from that the percentage, per commit, of those totals. For each commit, the +file percentage and line percentage are then combined, as file frequency times +line frequency squared, times 1,000,000, to produce a score. These scores are +then normalized into Z-scores, to produce the final churn value for each commit. +These churn values therefore represent how much the size of a given commit +differs from the average for the repository. + +Churn cannot run if a repository contains only one commit (or only one commit +that affects a source file). Churn analysis will always give an error when run +against a repo with a single commit. + +## Limitations + +* __Whether churn surfaces malicious contributions is an open question__: + We have ongoing work to confirm that churn does help identify the presence + of malicious contributions, and therefore is a useful metric for assessing + supply chain risk against malicious contribution attacks, but at the + moment this is an assumption made by Hipcheck. +* __Churn's statistical calculations may be insufficient__: There is ongoing + work to assess the statistical qualities of the churn metric and determine + whether it needs to be changed. 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..8fb8580c --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-entropy.md @@ -0,0 +1,40 @@ +--- +title: "mitre/entropy" +extra: + nav_title: "mitre/entropy" +--- + +# `mitre/entropy` + +Entropy analysis attempts to identify commits which contain a high degree of +textual randomness, in the believe that high textual randomness may indicate +the presence of packed malware or obfuscated code which ought to be assessed +for possible malicious content. + +Entropy analysis works by determining the total number of occurrences for all +unicode graphemes which appear in a repository's Git diffs for commits which +include code. In then converts these occurence counts into frequencies based on +the total number of each individual grapheme divided by the total number of +all graphemes in the combined set of Git diffs. It also determines grapheme +frequencies for each commit individually. These individual and total grapheme +frequencies are then combined into a score as an individual frequency times +the log base 2 of the individual frequency divided by the total frequency. +These individual grapheme scores are then summed to produce a per-commit score, +which is normalized into a Z-score same as the churn metric. These entropy +values therefore represent how much the grapheme frequency map of a given +commit differs from the average set of grapheme frequencies across all commits. + +Entropy cannot run if a repository contains only one commit (or only one commit +that affects a source file). Entropy analysis will always give an error when run +against a repo with a single commit. + +## Limitations + +* __Whether entropy surfaces malicious contributions is an open question__: + We have ongoing work to confirm that entropy does help identify the presence + of malicious contributions, and therefore is a useful metric for assessing + supply chain risk against malicious contribution attacks, but at the + moment this is an assumption made by Hipcheck. +* __Entropy's statistical calculations may be insufficient__: There is ongoing + work to assess the statistical qualities of the entropy metric and determine + whether it needs to be changed. 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..24d808f6 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-fuzz.md @@ -0,0 +1,21 @@ +--- +title: "mitre/fuzz" +extra: + nav_title: "mitre/fuzz" +--- + +# `mitre/fuzz` + +Repos being checked by Hipcheck may receive regular fuzz testing. This analysis +checks if the repo is participating in the OSS Fuzz program. If it is fuzzed, +this is considered a signal of a repository being lower risk. + +## Limitations + +* __Not all languagues supported__: Robust fuzzing tools do not exist for every + language. It is possible fuzz testing was not done because no good option for it + existed at the time. Lack of fuzzing in those cases would still indicate a higher + risk, but it would not necessarily indicate bad software development practices. +* __Only OSS Fuzz checked__: At this time, Hipcheck only checks if the repo + participates in Google's OSS Fuzz. Other fuzz testing programs exist, but a repo + will not pass this analysis if it uses one of those instead. 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..1c2a68d5 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-identity.md @@ -0,0 +1,25 @@ +--- +title: "mitre/identity" +extra: + nav_title: "mitre/identity" +--- + +# `mitre/identity` + +Identity analysis looks at whether the author and committer identities for +each commit are the same, as part of gauging the likelihood that commits +are receiving some degree of review before being merged into a repository. + +When author and committer identity are the same, that may indicate that a +commit did _not_ receive review, which could be a cause for concern. At the +larger level, having a large percentage of commits with the same author +and committer identities may indicate a project that lacks code review. + +## Limitations + +* __Not every project uses a workflow that accords with this analysis__: + While some Git projects may use a workflow that involves the generation + of patchfiles to then be reviewed and applied by project maintainers, + many may not. In some cases, a workflow may produce final commits where + the author and committer identity are the same, even though the commit + received review. diff --git a/site/content/docs/guide/plugins/mitre-linguist.md b/site/content/docs/guide/plugins/mitre-linguist.md new file mode 100644 index 00000000..c6964fd6 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-linguist.md @@ -0,0 +1,9 @@ +--- +title: "mitre/linguist" +extra: + nav_title: "mitre/linguist" +--- + +# `mitre/linguist` + +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..a0ea35fc --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-review.md @@ -0,0 +1,29 @@ +--- +title: "mitre/review" +extra: + nav_title: "mitre/review" +--- + +# `mitre/review` + +Review analysis looks at whether pull requests on GitHub (currently the +only supported remote host for this analysis) receive at least one +review prior to being merged. + +If too few pull requests receive review prior to merging, then this +analysis will flag that as a supply chain risk. + +This works with the GitHub API, and requires a token in the configuration. +Hipcheck only needs permissions for accessing public repository data, so +those are the only permissions to assign to your generated token. + +## Limitations + +* __Not every project uses GitHub__: While GitHub is a very popular host + for Git repositories, it is by no means the _only_ host. This analysis' + current limitation to GitHub makes it less useful than it could be. +* __Projects which do use GitHub may not use GitHub Reviews for code review__: + GitHub Reviews is a specific GitHub feature for performing code reviews + which projects may not all use. There may be repositories which are older + than the availability of this feature, and so don't have reviews on older + pull requests. 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..aba34d98 --- /dev/null +++ b/site/content/docs/guide/plugins/mitre-typo.md @@ -0,0 +1,31 @@ +--- +title: "mitre/typo" +extra: + nav_title: "mitre/typo" +--- + +# `mitre/typo` + +Typo analysis attempts to identify possible typosquatting attacks in the +dependency list for any projects which are analyzed and use a supported +language (currently: JavaScript w/ the NPM package manager). + +The analysis works by identifying a programming language based on the presence +of a dependency file in the root of the repository, then attempting to get the +full list of direct and transitive dependencies for that project. It then +compares that list against a list of known popular repositories for that +language to see if any in the dependencies list are possible typos of popular +package name. + +Typo detection is based on the generation of possible typos for known names, +according to a collection of typo possibilities, including single-character +deletion, substitution, swapping, and more. + +## Limitations + +* __Only works for some languages__: Right now, this analysis only supports + JavaScript projects. It requires the implementation of language-specific code + to work with different dependency files and generate the full list of + dependencies, and requires legwork to produce the list of popular package + names, which are not currently pulled from any external API or authoritative + source. 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/docs.ts b/site/scripts/src/footer/docs.ts new file mode 100644 index 00000000..07b6e0cc --- /dev/null +++ b/site/scripts/src/footer/docs.ts @@ -0,0 +1,52 @@ +import { updateIcon } from "../util/icon.ts"; +import { querySelectorAll } from "../util/web.ts"; + +/** + * Sets up the collapse/expand functionality for docs navigation. + */ +export function setupDocsNav() { + let $toggles; + + try { + $toggles = querySelectorAll(".docs-nav-toggle"); + } catch (_e) { + // Swallow these errors. + return; + } + + $toggles.forEach(($toggle) => { + // Get the icon inside the toggle. + const $toggleIcon = $toggle.querySelector( + ".toggle-icon use", + ) as HTMLElement; + if ($toggleIcon === null) { + console.log(`no icon found for toggle: '${$toggle}'`); + return; + } + + $toggle.addEventListener("click", (e) => { + e.preventDefault(); + + // Find the subsection list associated with the current toggle. + const $parent = $toggle.parentElement?.parentElement; + if ($parent === null || $parent === undefined) return; + const $subsection = $parent.querySelector( + ".docs-nav-section", + ) as HTMLElement; + if ($subsection === null) return; + + // Hide or show it. + $subsection.classList.toggle("hidden"); + + if (sectionIsHidden($subsection)) { + updateIcon($toggleIcon, "chevron-right", "chevron-down"); + } else { + updateIcon($toggleIcon, "chevron-down", "chevron-right"); + } + }); + }); +} + +function sectionIsHidden($subsection: HTMLElement): boolean { + return $subsection.classList.contains("hidden"); +} diff --git a/site/scripts/src/footer/installer.ts b/site/scripts/src/footer/installer.ts new file mode 100644 index 00000000..ba42224e --- /dev/null +++ b/site/scripts/src/footer/installer.ts @@ -0,0 +1,102 @@ +import { updateIcon } from "../util/icon.ts"; +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); + updateIcon($copyIcon, "clipboard", "check"); + // Set the icon back on a timer. + setTimeout(() => updateIcon($copyIcon, "check", "clipboard"), 1_500); + }); +} + +/** + * 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"}'`, + ); + } +} diff --git a/site/scripts/src/footer/main.ts b/site/scripts/src/footer/main.ts new file mode 100644 index 00000000..5010aa1d --- /dev/null +++ b/site/scripts/src/footer/main.ts @@ -0,0 +1,34 @@ +import { setupDocsNav } from "./docs.ts"; +import { setupInstallerPicker } from "./installer.ts"; +import { setupSmoothScrolling } from "./scroll.ts"; +import { setupSearch, setupSearchModal } 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(); + setupSearchModal(); + setupSearch(); + setupSmoothScrolling(); + setupDocsNav(); +} + +/** + * 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..6febdcc4 --- /dev/null +++ b/site/scripts/src/footer/search.ts @@ -0,0 +1,348 @@ +import { debounce } from "../util/event.ts"; +import { elem } from "../util/html.ts"; +import { querySelector } from "../util/web.ts"; + +/** + * Sets up functionality for opening and closing the search modal. + */ +export function setupSearchModal() { + 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"); + const $searchInput = querySelector("#search-input"); + + // 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, $searchInput), + ); + $modalShroud.addEventListener( + "click", + (e) => toggleModal(e, $modal, $searchInput), + ); + $modalClose.addEventListener( + "click", + (e) => toggleModal(e, $modal, $searchInput), + ); + $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; + } + + if (modalIsOpen($modal)) { + switch (e.key) { + case "Escape": + e.preventDefault(); + $modalShroud.click(); + return; + case "ArrowDown": + // TODO: Focus the next result in the search results. + break; + case "ArrowUp": + // TODO: Focus the prior result in the search result. + break; + default: + break; + } + } + }); +} + +/** + * Toggle whether the modal is open or not. + */ +function toggleModal( + e: MouseEvent, + $modal: HTMLElement, + $searchInput: HTMLElement, +) { + e.preventDefault(); + $modal.classList.toggle("hidden"); + if (modalIsOpen($modal)) $searchInput.focus(); +} + +/** + * Check if the modal is open. + */ +function modalIsOpen($modal: HTMLElement): boolean { + return !$modal.classList.contains("hidden"); +} + +/** + * Elasticlunr is the library Zola uses for search integration, and it doesn't + * provide TypeScript type definitions. So the definitions here are just enough + * to get Deno's linter to stop complaining, but they do mean we don't really + * get type-checking protection for interacting with the Elasticluner API. + * + * In the future if we wanted type-checking for this API we could replace the + * 'any' with an actual description of the relevant types. + */ + +// Define the Index type. +type Index = { + // deno-lint-ignore no-explicit-any + load: (data: Promise) => Promise; + // deno-lint-ignore no-explicit-any + search: (query: string, options?: any) => SearchResult; +}; + +/** + * Data returned from the Elasticlunr search function + */ +type SearchResult = { + ref: string; + score: number; + doc: { + body: string; + id: string; + title: string; + }; +}; + +// Define the "elasticlunr" global. +declare global { + let elasticlunr: { + Index: Index; + // deno-lint-ignore no-explicit-any + stemmer: any; + }; +} + +/** + * The path to the search index created by Zola. + */ +const SEARCH_INDEX: string = "/search_index.en.json"; + +/** + * The maximum number of results to show in searches. + */ +const MAX_ITEMS: number = 6; + +/** + * Setup the search operation within the search modal. + */ +export function setupSearch() { + const $searchInput = querySelector("#search-input") as HTMLInputElement; + const $searchResults = querySelector("#search-results"); + const $searchResultsItems = querySelector("#search-results-items"); + + // The search index, representing the content of the site. + let index: Promise; + + // The current term being searched by the user. + let currentTerm = ""; + + const initIndex = async function () { + // If no index, then asynchronously load it from the index file. + if (index === undefined) { + index = fetch(SEARCH_INDEX) + .then( + async function (response) { + return await elasticlunr.Index.load(await response.json()); + }, + ); + } + + return await index; + }; + + $searchInput.addEventListener( + "keyup", + debounce(150, async function () { + const term = $searchInput.value.trim(); + if (term === currentTerm) return; + + $searchResults.style.display = term === "" ? "none" : "block"; + $searchResultsItems.innerHTML = ""; + + currentTerm = term; + if (currentTerm === "") return; + + const results: SearchResult[] = (await initIndex()) + .search(term, { + bool: "AND", + fields: { + title: { boost: 2 }, + body: { boost: 1 }, + }, + }); + + if (results.length === 0) { + $searchResults.style.display = "none"; + return; + } + + for (let i = 0; i < Math.min(results.length, MAX_ITEMS); ++i) { + const entry = buildListEntry(results[i], currentTerm.split(" ")); + $searchResultsItems.appendChild(entry); + } + }), + ); +} + +/** + * Build an HTML element for each item in the search results. + */ +function buildListEntry(data: SearchResult, terms: string[]): HTMLElement { + return elem("li", {}, { + classList: ["border-t", "border-neutral-300", "dark:border-neutral-500"], + }, [ + elem("div", {}, {}, [ + elem("a", { href: data.ref }, { + classList: [ + "block", + "px-5", + "py-2", + "hover:bg-blue-50", + "dark:hover:bg-blue-500", + "hover:text-blue-500", + "dark:hover:text-white", + "group", + ], + }, [ + elem("span", {}, { + classList: ["block", "text-base", "mb-1", "font-medium"], + }, [ + data.doc.title, + ]), + elem("span", {}, { + classList: [ + "block", + "text-neutral-500", + "text-sm", + "group-hover:text-blue-500", + ], + }, [ + makeTeaser(data.doc.body, terms), + ]), + ]), + ]), + ]); +} + +/** + * Construct a usable preview of the body that matched the search term. + * + * This code adapted from Zola's sample search code, itself adapted from mdbook. + * Licensed under the terms of the MIT license. + * + * https://github.com/getzola/zola/blob/master/LICENSE + */ +function makeTeaser(body: string, terms: string[]): HTMLElement { + const TERM_WEIGHT = 40; + const NORMAL_WORD_WEIGHT = 2; + const FIRST_WORD_WEIGHT = 8; + const TEASER_MAX_WORDS = 15; + + const stemmedTerms = terms.map(function (w) { + return elasticlunr.stemmer(w.toLowerCase()); + }); + + let termFound = false; + let index = 0; + // contains elements of ["word", weight, index_in_document] + const weighted: ([string, number, number])[] = []; + + // split in sentences, then words + const sentences = body.toLowerCase().split(". "); + + for (const i in sentences) { + const words = sentences[i].split(" "); + let value = FIRST_WORD_WEIGHT; + + for (const j in words) { + const word = words[j]; + + if (word.length > 0) { + for (const k in stemmedTerms) { + if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) { + value = TERM_WEIGHT; + termFound = true; + } + } + + weighted.push([word, value, index]); + value = NORMAL_WORD_WEIGHT; + } + + index += word.length; + // ' ' or '.' if last word in sentence + index += 1; + } + + // because we split at a two-char boundary '. ' + index += 1; + } + + if (weighted.length === 0) { + const final = body; + const span = elem("span", {}, {}, []); + span.innerHTML = final; + return span; + } + + const windowWeights: number[] = []; + const windowSize = Math.min(weighted.length, TEASER_MAX_WORDS); + // We add a window with all the weights first + let curSum = 0; + for (let i = 0; i < windowSize; i++) { + curSum += weighted[i][1]; + } + windowWeights.push(curSum); + + for (let i = 0; i < weighted.length - windowSize; i++) { + curSum -= weighted[i][1]; + curSum += weighted[i + windowSize][1]; + windowWeights.push(curSum); + } + + // If we didn't find the term, just pick the first window + let maxSumIndex = 0; + if (termFound) { + let maxFound = 0; + // backwards + for (let i = windowWeights.length - 1; i >= 0; i--) { + if (windowWeights[i] > maxFound) { + maxFound = windowWeights[i]; + maxSumIndex = i; + } + } + } + + const teaser: string[] = []; + let startIndex = weighted[maxSumIndex][2]; + for (let i = maxSumIndex; i < maxSumIndex + windowSize; i++) { + const word = weighted[i]; + if (startIndex < word[2]) { + // missing text from index to start of `word` + teaser.push(body.substring(startIndex, word[2])); + startIndex = word[2]; + } + + // add around search terms + if (word[1] === TERM_WEIGHT) { + teaser.push(""); + } + startIndex = word[2] + word[0].length; + teaser.push(body.substring(word[2], startIndex)); + + if (word[1] === TERM_WEIGHT) { + teaser.push(""); + } + } + teaser.push("…"); + const final = teaser.join(""); + const span = elem("span", {}, {}, []); + span.innerHTML = final; + return span; +} 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/event.ts b/site/scripts/src/util/event.ts new file mode 100644 index 00000000..987c430d --- /dev/null +++ b/site/scripts/src/util/event.ts @@ -0,0 +1,14 @@ +/** + * Debounce an event handler by waiting `waitFor` number of milliseconds before + * permitting the event to be triggered again. + */ +export function debounce) => ReturnType>( + waitFor: number, + func: F, +): (...args: Parameters) => void { + let timeout: number; + return (...args: Parameters): void => { + clearTimeout(timeout); + timeout = setTimeout(() => func(...args), waitFor); + }; +} diff --git a/site/scripts/src/util/html.ts b/site/scripts/src/util/html.ts new file mode 100644 index 00000000..7fdf7714 --- /dev/null +++ b/site/scripts/src/util/html.ts @@ -0,0 +1,75 @@ +/** + * This code adapted from char's "rainbow" project. + * + * https://github.com/char/rainbow/blob/47721ad574c61d59921435a2bff41b9fd582540d/src/util/elem.ts + * + * It's licensed under the terms of the WTFPL. + * + * https://bsky.app/profile/pet.bun.how/post/3l7vv6ddjn426 + */ + +// deno-lint-ignore-file ban-types + +export type ElemProps = { + [K in keyof E as E[K] extends Function ? never : K]?: E[K]; +}; + +// Just a shorthand for a long type name. +type HTMLElemTagNameMap = HTMLElementTagNameMap; + +// Children can be built from an existing element, string, or text. +type IntoChild = Element | string | Text; + +// We permit adding classes and data attributes. +type Extras = { + classList?: string[]; + dataset?: Partial>; +}; + +// Attributes to add. +type Attrs = + | ElemProps + | ElemProps[]; + +// A little way to reduce an object down to only defined entries, since +// entries can have a key but an undefined value. +function removeUndefinedValues(x: object): object { + const entries = Object.entries(x).filter(([_k, v]) => v !== undefined); + return Object.fromEntries(entries); +} + +/** + * Construct a new HTMLElement + */ +export function elem( + tag: K, + attrs: Attrs = {}, + extras: Extras = {}, + children: IntoChild[] = [], +): HTMLElemTagNameMap[K] { + // Create the new element. + const element = document.createElement(tag); + + // Assign any defined values from `attrs`. + Object.assign(element, removeUndefinedValues(attrs)); + + // Fill in any provided classes. + if (extras.classList) { + extras.classList.forEach((c) => element.classList.add(c)); + } + + // Fill in any assigned data attributes. + if (extras.dataset) { + Object.entries(extras.dataset) + .filter(([_k, v]) => v !== undefined) + .forEach(([k, v]) => (element.dataset[k] = v)); + } + + // Populate any children. + const nodes = children.map( + (e) => (typeof e === "string" ? document.createTextNode(e) : e) + ); + element.append(...nodes); + + return element; +} diff --git a/site/scripts/src/util/icon.ts b/site/scripts/src/util/icon.ts new file mode 100644 index 00000000..4534ad19 --- /dev/null +++ b/site/scripts/src/util/icon.ts @@ -0,0 +1,39 @@ +export function updateIcon( + $node: HTMLElement, + oldName: string, + newName: string, +) { + const iconUrl = getIconUrl($node); + const newIconUrl = iconUrl.replace(`#icon-${oldName}`, `#icon-${newName}`); + setIconUrl($node, newIconUrl); +} + +/** + * 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"; + +/** + * 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/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/192.png b/site/static/192.png new file mode 100644 index 00000000..544ab1ca Binary files /dev/null and b/site/static/192.png differ diff --git a/site/static/512.png b/site/static/512.png new file mode 100644 index 00000000..2197cace Binary files /dev/null and b/site/static/512.png differ diff --git a/site/static/apple-touch-icon.png b/site/static/apple-touch-icon.png new file mode 100644 index 00000000..82c146d1 Binary files /dev/null and b/site/static/apple-touch-icon.png differ diff --git a/site/static/dl/install.ps1 b/site/static/dl/install.ps1 new file mode 100755 index 00000000..ff748794 --- /dev/null +++ b/site/static/dl/install.ps1 @@ -0,0 +1,9 @@ + +# This installer delegates to the "real" installer included with each new +# release of Hipcheck. + +$hc_version = "3.7.0" +$repo = "https://github.com/mitre/hipcheck" +$installer = "$repo/releases/download/hipcheck-v${hc_version}/hipcheck-installer.ps1" + +irm "$installer" | iex "$Args" diff --git a/site/static/dl/install.sh b/site/static/dl/install.sh new file mode 100755 index 00000000..fcc7cc3e --- /dev/null +++ b/site/static/dl/install.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh + +# This installer delegates to the "real" installer included with each new +# release of Hipcheck. + +HC_VERSION="3.7.0" +REPO="https://github.com/mitre/hipcheck" +INSTALLER="$REPO/releases/download/hipcheck-v$HC_VERSION/hipcheck-installer.sh" + +# Check that curl is installed and error out if it isn't. +if ! command -v curl >/dev/null; then + echo "error: 'curl' is required to run the installer" 1>&2 + exit 1 +fi + +curl -LsSf "$INSTALLER" | sh "$@" diff --git a/site/static/favicon.ico b/site/static/favicon.ico new file mode 100644 index 00000000..084c1e8e Binary files /dev/null and b/site/static/favicon.ico differ diff --git a/site/static/fonts/plex/IBMPlexMono-Bold.woff2 b/site/static/fonts/plex/IBMPlexMono-Bold.woff2 new file mode 100644 index 00000000..1a30540c Binary files /dev/null and b/site/static/fonts/plex/IBMPlexMono-Bold.woff2 differ diff --git a/site/static/fonts/plex/IBMPlexMono-Text.woff2 b/site/static/fonts/plex/IBMPlexMono-Text.woff2 new file mode 100644 index 00000000..26d72642 Binary files /dev/null and b/site/static/fonts/plex/IBMPlexMono-Text.woff2 differ diff --git a/site/static/icon.svg b/site/static/icon.svg new file mode 100644 index 00000000..dfad0c88 --- /dev/null +++ b/site/static/icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/site/static/images/authors/andrew.jpg b/site/static/images/authors/andrew.jpg new file mode 100644 index 00000000..2860c708 Binary files /dev/null and b/site/static/images/authors/andrew.jpg differ diff --git a/site/static/js/footer.mjs b/site/static/js/footer.mjs new file mode 100644 index 00000000..dc1c361b --- /dev/null +++ b/site/static/js/footer.mjs @@ -0,0 +1,2 @@ +function g(e,t,o){let r=V(e).replace(`#icon-${t}`,`#icon-${o}`);Y(e,r)}function V(e){let t=e.getAttributeNS(_,"href");if(t===null)throw new S;return t}function Y(e,t){e.setAttributeNS(_,"href",t)}var _="http://www.w3.org/1999/xlink",S=class extends Error{constructor(){super("could not find copy icon")}};function a(e){let t=document.querySelector(e);if(t===null)throw new k(`could not find ${e}`);return t}function m(e){let t=document.querySelectorAll(e);if(t===null)throw new k(`could not find all '${e}'`);return Array.from(t)}function D(e){navigator.clipboard.writeText(e).then(null,t=>{throw new I(t)})}var I=class extends Error{constructor(t){super(`clipboard copy rejected: '${t}'`)}},k=class extends Error{constructor(t){super(t)}};function N(){let e;try{e=m(".docs-nav-toggle")}catch{return}e.forEach(t=>{let o=t.querySelector(".toggle-icon use");if(o===null){console.log(`no icon found for toggle: '${t}'`);return}t.addEventListener("click",s=>{s.preventDefault();let r=t.parentElement?.parentElement;if(r==null)return;let l=r.querySelector(".docs-nav-section");l!==null&&(l.classList.toggle("hidden"),J(l)?g(o,"chevron-right","chevron-down"):g(o,"chevron-down","chevron-right"))})})}function J(e){return e.classList.contains("hidden")}function K(){let e,t,o,s;try{e=m(".installer-button"),t=a("#installer-cmd"),o=a("#installer-copy"),s=a("#installer-copy > svg > use")}catch{return}e.forEach(r=>{r.addEventListener("click",l=>{l.preventDefault(),e.forEach(n=>delete n.dataset.active),r.dataset.active="true",t.innerText=$(r.dataset.platform)}),r.dataset.active&&r.dataset.active==="true"&&(t.innerText=$(r.dataset.platform))}),o.addEventListener("click",r=>{r.preventDefault(),D(t.innerText),g(s,"clipboard","check"),setTimeout(()=>g(s,"check","clipboard"),1500)})}function $(e){if(e===void 0)throw new x(e);switch(e){case"macos":return W;case"linux":return W;case"windows":return Z;default:throw new x(e)}}var P=`${globalThis.window.location.protocol}//${globalThis.window.location.host}`,W=`curl -LsSf ${P}/dl/install.sh | sh`,Z=`irm ${P}/dl/install.ps1 | iex`,x=class extends Error{constructor(t){super(`could not determine platform: '${t||"undefined"}'`)}};function q(){m('a[href^="#"]').forEach(e=>{e.addEventListener("click",t=>{if(t.preventDefault(),t.currentTarget===null)return;let o=t.currentTarget.getAttribute("href");if(o===null)return;let r=a(o).getBoundingClientRect().top,l=globalThis.window.scrollY,n=r+l;globalThis.window.scrollTo({top:n,behavior:"smooth"})})})}function O(e,t){let o;return(...s)=>{clearTimeout(o),o=setTimeout(()=>t(...s),e)}}function Q(e){let t=Object.entries(e).filter(([o,s])=>s!==void 0);return Object.fromEntries(t)}function p(e,t={},o={},s=[]){let r=document.createElement(e);Object.assign(r,Q(t)),o.classList&&o.classList.forEach(n=>r.classList.add(n)),o.dataset&&Object.entries(o.dataset).filter(([n,d])=>d!==void 0).forEach(([n,d])=>r.dataset[n]=d);let l=s.map(n=>typeof n=="string"?document.createTextNode(n):n);return r.append(...l),r}function C(){let e=a("#search-button"),t=a("#search-modal"),o=a("#search-modal-close"),s=a("#search-modal-shroud"),r=a("#search-modal-box"),l=a("#search-input");e.addEventListener("click",n=>H(n,t,l)),s.addEventListener("click",n=>H(n,t,l)),o.addEventListener("click",n=>H(n,t,l)),r.addEventListener("click",n=>n.stopPropagation()),document.addEventListener("keydown",n=>{if(n.metaKey===!0&&n.shiftKey===!1&&n.key==="k"){n.preventDefault(),e.click();return}if(F(t))switch(n.key){case"Escape":n.preventDefault(),s.click();return;case"ArrowDown":break;case"ArrowUp":break;default:break}})}function H(e,t,o){e.preventDefault(),t.classList.toggle("hidden"),F(t)&&o.focus()}function F(e){return!e.classList.contains("hidden")}var ee="/search_index.en.json",te=6;function j(){let e=a("#search-input"),t=a("#search-results"),o=a("#search-results-items"),s,r="",l=async function(){return s===void 0&&(s=fetch(ee).then(async function(n){return await elasticlunr.Index.load(await n.json())})),await s};e.addEventListener("keyup",O(150,async function(){let n=e.value.trim();if(n===r||(t.style.display=n===""?"none":"block",o.innerHTML="",r=n,r===""))return;let d=(await l()).search(n,{bool:"AND",fields:{title:{boost:2},body:{boost:1}}});if(d.length===0){t.style.display="none";return}for(let f=0;f0){for(let z in n)elasticlunr.stemmer(b).startsWith(n[z])&&(v=40,d=!0);u.push([b,v,f]),v=2}f+=b.length,f+=1}f+=1}if(u.length===0){let c=e,i=p("span",{},{},[]);return i.innerHTML=c,i}let T=[],y=Math.min(u.length,15),E=0;for(let c=0;c=0;i--)T[i]>c&&(c=T[i],w=i)}let h=[],L=u[w][2];for(let c=w;c"),L=i[2]+i[0].length,h.push(e.substring(i[2],L)),i[1]===40&&h.push("")}h.push("\u2026");let X=h.join(""),R=p("span",{},{},[]);return R.innerHTML=X,R}function G(e){if(e===void 0)throw new M(e);switch(e){case"system":switch(localStorage.removeItem("theme"),se()){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 M(e)}oe(e)}function oe(e){m(".theme-option").forEach(t=>{t.dataset.theme===e?t.dataset.active="true":delete t.dataset.active})}function se(){return globalThis.window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}var M=class extends Error{constructor(t){super(`could not determine theme: '${t||"undefined"}'`)}};function U(){m(".theme-option").forEach(e=>{e.addEventListener("click",t=>{t.preventDefault(),G(e.dataset.theme)})})}function ce(){U(),K(),C(),j(),q(),N()}(function(){try{ce()}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..47c61a7e --- /dev/null +++ b/site/static/js/footer.mjs.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../scripts/src/util/icon.ts", "../../scripts/src/util/web.ts", "../../scripts/src/footer/docs.ts", "../../scripts/src/footer/installer.ts", "../../scripts/src/footer/scroll.ts", "../../scripts/src/util/event.ts", "../../scripts/src/util/html.ts", "../../scripts/src/footer/search.ts", "../../scripts/src/util/theme.ts", "../../scripts/src/footer/theme.ts", "../../scripts/src/footer/main.ts"], + "sourcesContent": ["export function updateIcon(\n $node: HTMLElement,\n oldName: string,\n newName: string,\n) {\n const iconUrl = getIconUrl($node);\n const newIconUrl = iconUrl.replace(`#icon-${oldName}`, `#icon-${newName}`);\n setIconUrl($node, newIconUrl);\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 * 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", "/**\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 { updateIcon } from \"../util/icon.ts\";\nimport { querySelectorAll } from \"../util/web.ts\";\n\n/**\n * Sets up the collapse/expand functionality for docs navigation.\n */\nexport function setupDocsNav() {\n let $toggles;\n\n try {\n $toggles = querySelectorAll(\".docs-nav-toggle\");\n } catch (_e) {\n // Swallow these errors.\n return;\n }\n\n $toggles.forEach(($toggle) => {\n // Get the icon inside the toggle.\n const $toggleIcon = $toggle.querySelector(\n \".toggle-icon use\",\n ) as HTMLElement;\n if ($toggleIcon === null) {\n console.log(`no icon found for toggle: '${$toggle}'`);\n return;\n }\n\n $toggle.addEventListener(\"click\", (e) => {\n e.preventDefault();\n\n // Find the subsection list associated with the current toggle.\n const $parent = $toggle.parentElement?.parentElement;\n if ($parent === null || $parent === undefined) return;\n const $subsection = $parent.querySelector(\n \".docs-nav-section\",\n ) as HTMLElement;\n if ($subsection === null) return;\n\n // Hide or show it.\n $subsection.classList.toggle(\"hidden\");\n\n if (sectionIsHidden($subsection)) {\n updateIcon($toggleIcon, \"chevron-right\", \"chevron-down\");\n } else {\n updateIcon($toggleIcon, \"chevron-down\", \"chevron-right\");\n }\n });\n });\n}\n\nfunction sectionIsHidden($subsection: HTMLElement): boolean {\n return $subsection.classList.contains(\"hidden\");\n}\n", "import { updateIcon } from \"../util/icon.ts\";\nimport {\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 updateIcon($copyIcon, \"clipboard\", \"check\");\n // Set the icon back on a timer.\n setTimeout(() => updateIcon($copyIcon, \"check\", \"clipboard\"), 1_500);\n });\n}\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", "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", "/**\n * Debounce an event handler by waiting `waitFor` number of milliseconds before\n * permitting the event to be triggered again.\n */\nexport function debounce) => ReturnType>(\n waitFor: number,\n func: F,\n): (...args: Parameters) => void {\n let timeout: number;\n return (...args: Parameters): void => {\n clearTimeout(timeout);\n timeout = setTimeout(() => func(...args), waitFor);\n };\n}\n", "/**\n * This code adapted from char's \"rainbow\" project.\n *\n * https://github.com/char/rainbow/blob/47721ad574c61d59921435a2bff41b9fd582540d/src/util/elem.ts\n *\n * It's licensed under the terms of the WTFPL.\n *\n * https://bsky.app/profile/pet.bun.how/post/3l7vv6ddjn426\n */\n\n// deno-lint-ignore-file ban-types\n\nexport type ElemProps = {\n [K in keyof E as E[K] extends Function ? never : K]?: E[K];\n};\n\n// Just a shorthand for a long type name.\ntype HTMLElemTagNameMap = HTMLElementTagNameMap;\n\n// Children can be built from an existing element, string, or text.\ntype IntoChild = Element | string | Text;\n\n// We permit adding classes and data attributes.\ntype Extras = {\n classList?: string[];\n dataset?: Partial>;\n};\n\n// Attributes to add.\ntype Attrs =\n | ElemProps\n | ElemProps[];\n\n// A little way to reduce an object down to only defined entries, since\n// entries can have a key but an undefined value.\nfunction removeUndefinedValues(x: object): object {\n const entries = Object.entries(x).filter(([_k, v]) => v !== undefined);\n return Object.fromEntries(entries);\n}\n\n/**\n * Construct a new HTMLElement\n */\nexport function elem(\n tag: K,\n attrs: Attrs = {},\n extras: Extras = {},\n children: IntoChild[] = [],\n): HTMLElemTagNameMap[K] {\n // Create the new element.\n const element = document.createElement(tag);\n\n // Assign any defined values from `attrs`.\n Object.assign(element, removeUndefinedValues(attrs));\n\n // Fill in any provided classes.\n if (extras.classList) {\n extras.classList.forEach((c) => element.classList.add(c));\n }\n\n // Fill in any assigned data attributes.\n if (extras.dataset) {\n Object.entries(extras.dataset)\n .filter(([_k, v]) => v !== undefined)\n .forEach(([k, v]) => (element.dataset[k] = v));\n }\n\n // Populate any children.\n const nodes = children.map(\n (e) => (typeof e === \"string\" ? document.createTextNode(e) : e)\n );\n element.append(...nodes);\n\n return element;\n}\n", "import { debounce } from \"../util/event.ts\";\nimport { elem } from \"../util/html.ts\";\nimport { querySelector } from \"../util/web.ts\";\n\n/**\n * Sets up functionality for opening and closing the search modal.\n */\nexport function setupSearchModal() {\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 const $searchInput = querySelector(\"#search-input\");\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(\n \"click\",\n (e) => toggleModal(e, $modal, $searchInput),\n );\n $modalShroud.addEventListener(\n \"click\",\n (e) => toggleModal(e, $modal, $searchInput),\n );\n $modalClose.addEventListener(\n \"click\",\n (e) => toggleModal(e, $modal, $searchInput),\n );\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 if (modalIsOpen($modal)) {\n switch (e.key) {\n case \"Escape\":\n e.preventDefault();\n $modalShroud.click();\n return;\n case \"ArrowDown\":\n // TODO: Focus the next result in the search results.\n break;\n case \"ArrowUp\":\n // TODO: Focus the prior result in the search result.\n break;\n default:\n break;\n }\n }\n });\n}\n\n/**\n * Toggle whether the modal is open or not.\n */\nfunction toggleModal(\n e: MouseEvent,\n $modal: HTMLElement,\n $searchInput: HTMLElement,\n) {\n e.preventDefault();\n $modal.classList.toggle(\"hidden\");\n if (modalIsOpen($modal)) $searchInput.focus();\n}\n\n/**\n * Check if the modal is open.\n */\nfunction modalIsOpen($modal: HTMLElement): boolean {\n return !$modal.classList.contains(\"hidden\");\n}\n\n/**\n * Elasticlunr is the library Zola uses for search integration, and it doesn't\n * provide TypeScript type definitions. So the definitions here are just enough\n * to get Deno's linter to stop complaining, but they do mean we don't really\n * get type-checking protection for interacting with the Elasticluner API.\n *\n * In the future if we wanted type-checking for this API we could replace the\n * 'any' with an actual description of the relevant types.\n */\n\n// Define the Index type.\ntype Index = {\n // deno-lint-ignore no-explicit-any\n load: (data: Promise) => Promise;\n // deno-lint-ignore no-explicit-any\n search: (query: string, options?: any) => SearchResult;\n};\n\n/**\n * Data returned from the Elasticlunr search function\n */\ntype SearchResult = {\n ref: string;\n score: number;\n doc: {\n body: string;\n id: string;\n title: string;\n };\n};\n\n// Define the \"elasticlunr\" global.\ndeclare global {\n let elasticlunr: {\n Index: Index;\n // deno-lint-ignore no-explicit-any\n stemmer: any;\n };\n}\n\n/**\n * The path to the search index created by Zola.\n */\nconst SEARCH_INDEX: string = \"/search_index.en.json\";\n\n/**\n * The maximum number of results to show in searches.\n */\nconst MAX_ITEMS: number = 6;\n\n/**\n * Setup the search operation within the search modal.\n */\nexport function setupSearch() {\n const $searchInput = querySelector(\"#search-input\") as HTMLInputElement;\n const $searchResults = querySelector(\"#search-results\");\n const $searchResultsItems = querySelector(\"#search-results-items\");\n\n // The search index, representing the content of the site.\n let index: Promise;\n\n // The current term being searched by the user.\n let currentTerm = \"\";\n\n const initIndex = async function () {\n // If no index, then asynchronously load it from the index file.\n if (index === undefined) {\n index = fetch(SEARCH_INDEX)\n .then(\n async function (response) {\n return await elasticlunr.Index.load(await response.json());\n },\n );\n }\n\n return await index;\n };\n\n $searchInput.addEventListener(\n \"keyup\",\n debounce(150, async function () {\n const term = $searchInput.value.trim();\n if (term === currentTerm) return;\n\n $searchResults.style.display = term === \"\" ? \"none\" : \"block\";\n $searchResultsItems.innerHTML = \"\";\n\n currentTerm = term;\n if (currentTerm === \"\") return;\n\n const results: SearchResult[] = (await initIndex())\n .search(term, {\n bool: \"AND\",\n fields: {\n title: { boost: 2 },\n body: { boost: 1 },\n },\n });\n\n if (results.length === 0) {\n $searchResults.style.display = \"none\";\n return;\n }\n\n for (let i = 0; i < Math.min(results.length, MAX_ITEMS); ++i) {\n const entry = buildListEntry(results[i], currentTerm.split(\" \"));\n $searchResultsItems.appendChild(entry);\n }\n }),\n );\n}\n\n/**\n * Build an HTML element for each item in the search results.\n */\nfunction buildListEntry(data: SearchResult, terms: string[]): HTMLElement {\n return elem(\"li\", {}, {\n classList: [\"border-t\", \"border-neutral-300\", \"dark:border-neutral-500\"],\n }, [\n elem(\"div\", {}, {}, [\n elem(\"a\", { href: data.ref }, {\n classList: [\n \"block\",\n \"px-5\",\n \"py-2\",\n \"hover:bg-blue-50\",\n \"dark:hover:bg-blue-500\",\n \"hover:text-blue-500\",\n \"dark:hover:text-white\",\n \"group\",\n ],\n }, [\n elem(\"span\", {}, {\n classList: [\"block\", \"text-base\", \"mb-1\", \"font-medium\"],\n }, [\n data.doc.title,\n ]),\n elem(\"span\", {}, {\n classList: [\n \"block\",\n \"text-neutral-500\",\n \"text-sm\",\n \"group-hover:text-blue-500\",\n ],\n }, [\n makeTeaser(data.doc.body, terms),\n ]),\n ]),\n ]),\n ]);\n}\n\n/**\n * Construct a usable preview of the body that matched the search term.\n *\n * This code adapted from Zola's sample search code, itself adapted from mdbook.\n * Licensed under the terms of the MIT license.\n *\n * https://github.com/getzola/zola/blob/master/LICENSE\n */\nfunction makeTeaser(body: string, terms: string[]): HTMLElement {\n const TERM_WEIGHT = 40;\n const NORMAL_WORD_WEIGHT = 2;\n const FIRST_WORD_WEIGHT = 8;\n const TEASER_MAX_WORDS = 15;\n\n const stemmedTerms = terms.map(function (w) {\n return elasticlunr.stemmer(w.toLowerCase());\n });\n\n let termFound = false;\n let index = 0;\n // contains elements of [\"word\", weight, index_in_document]\n const weighted: ([string, number, number])[] = [];\n\n // split in sentences, then words\n const sentences = body.toLowerCase().split(\". \");\n\n for (const i in sentences) {\n const words = sentences[i].split(\" \");\n let value = FIRST_WORD_WEIGHT;\n\n for (const j in words) {\n const word = words[j];\n\n if (word.length > 0) {\n for (const k in stemmedTerms) {\n if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {\n value = TERM_WEIGHT;\n termFound = true;\n }\n }\n\n weighted.push([word, value, index]);\n value = NORMAL_WORD_WEIGHT;\n }\n\n index += word.length;\n // ' ' or '.' if last word in sentence\n index += 1;\n }\n\n // because we split at a two-char boundary '. '\n index += 1;\n }\n\n if (weighted.length === 0) {\n const final = body;\n const span = elem(\"span\", {}, {}, []);\n span.innerHTML = final;\n return span;\n }\n\n const windowWeights: number[] = [];\n const windowSize = Math.min(weighted.length, TEASER_MAX_WORDS);\n // We add a window with all the weights first\n let curSum = 0;\n for (let i = 0; i < windowSize; i++) {\n curSum += weighted[i][1];\n }\n windowWeights.push(curSum);\n\n for (let i = 0; i < weighted.length - windowSize; i++) {\n curSum -= weighted[i][1];\n curSum += weighted[i + windowSize][1];\n windowWeights.push(curSum);\n }\n\n // If we didn't find the term, just pick the first window\n let maxSumIndex = 0;\n if (termFound) {\n let maxFound = 0;\n // backwards\n for (let i = windowWeights.length - 1; i >= 0; i--) {\n if (windowWeights[i] > maxFound) {\n maxFound = windowWeights[i];\n maxSumIndex = i;\n }\n }\n }\n\n const teaser: string[] = [];\n let startIndex = weighted[maxSumIndex][2];\n for (let i = maxSumIndex; i < maxSumIndex + windowSize; i++) {\n const word = weighted[i];\n if (startIndex < word[2]) {\n // missing text from index to start of `word`\n teaser.push(body.substring(startIndex, word[2]));\n startIndex = word[2];\n }\n\n // add around search terms\n if (word[1] === TERM_WEIGHT) {\n teaser.push(\"\");\n }\n startIndex = word[2] + word[0].length;\n teaser.push(body.substring(word[2], startIndex));\n\n if (word[1] === TERM_WEIGHT) {\n teaser.push(\"\");\n }\n }\n teaser.push(\"\u2026\");\n const final = teaser.join(\"\");\n const span = elem(\"span\", {}, {}, []);\n span.innerHTML = final;\n return span;\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 { setupDocsNav } from \"./docs.ts\";\nimport { setupInstallerPicker } from \"./installer.ts\";\nimport { setupSmoothScrolling } from \"./scroll.ts\";\nimport { setupSearch, setupSearchModal } 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 setupSearchModal();\n setupSearch();\n setupSmoothScrolling();\n setupDocsNav();\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": "AAAO,SAASA,EACdC,EACAC,EACAC,EACA,CAEA,IAAMC,EADUC,EAAWJ,CAAK,EACL,QAAQ,SAASC,CAAO,GAAI,SAASC,CAAO,EAAE,EACzEG,EAAWL,EAAOG,CAAU,CAC9B,CAKA,SAASC,EAAWJ,EAA4B,CAC9C,IAAMM,EAAUN,EAAM,eAAeO,EAAU,MAAM,EACrD,GAAID,IAAY,KAAM,MAAM,IAAIE,EAChC,OAAOF,CACT,CAKA,SAASD,EAAWL,EAAoBS,EAAa,CACnDT,EAAM,eAAeO,EAAU,OAAQE,CAAG,CAC5C,CAKA,IAAMF,EAAmB,+BAKnBC,EAAN,cAAwB,KAAM,CAC5B,aAAc,CACZ,MAAM,0BAA0B,CAClC,CACF,ECnCO,SAASE,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,ECrCO,SAASC,GAAe,CAC7B,IAAIC,EAEJ,GAAI,CACFA,EAAWC,EAAiB,kBAAkB,CAChD,MAAa,CAEX,MACF,CAEAD,EAAS,QAASE,GAAY,CAE5B,IAAMC,EAAcD,EAAQ,cAC1B,kBACF,EACA,GAAIC,IAAgB,KAAM,CACxB,QAAQ,IAAI,8BAA8BD,CAAO,GAAG,EACpD,MACF,CAEAA,EAAQ,iBAAiB,QAAUE,GAAM,CACvCA,EAAE,eAAe,EAGjB,IAAMC,EAAUH,EAAQ,eAAe,cACvC,GAAIG,GAAY,KAA+B,OAC/C,IAAMC,EAAcD,EAAQ,cAC1B,mBACF,EACIC,IAAgB,OAGpBA,EAAY,UAAU,OAAO,QAAQ,EAEjCC,EAAgBD,CAAW,EAC7BE,EAAWL,EAAa,gBAAiB,cAAc,EAEvDK,EAAWL,EAAa,eAAgB,eAAe,EAE3D,CAAC,CACH,CAAC,CACH,CAEA,SAASI,EAAgBD,EAAmC,CAC1D,OAAOA,EAAY,UAAU,SAAS,QAAQ,CAChD,CCtCO,SAASG,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,EAC9BS,EAAWP,EAAW,YAAa,OAAO,EAE1C,WAAW,IAAMO,EAAWP,EAAW,QAAS,WAAW,EAAG,IAAK,CACrE,CAAC,CACH,CAKA,SAASK,EAAqBG,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,ECnGO,SAASK,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,CCtBO,SAASC,EACdC,EACAC,EACkC,CAClC,IAAIC,EACJ,MAAO,IAAIC,IAA8B,CACvC,aAAaD,CAAO,EACpBA,EAAU,WAAW,IAAMD,EAAK,GAAGE,CAAI,EAAGH,CAAO,CACnD,CACF,CCsBA,SAASI,EAAsBC,EAAmB,CAChD,IAAMC,EAAU,OAAO,QAAQD,CAAC,EAAE,OAAO,CAAC,CAACE,EAAIC,CAAC,IAAMA,IAAM,MAAS,EACrE,OAAO,OAAO,YAAYF,CAAO,CACnC,CAKO,SAASG,EACdC,EACAC,EAAkB,CAAC,EACnBC,EAAiB,CAAC,EAClBC,EAAwB,CAAC,EACF,CAEvB,IAAMC,EAAU,SAAS,cAAcJ,CAAG,EAG1C,OAAO,OAAOI,EAASV,EAAsBO,CAAK,CAAC,EAG/CC,EAAO,WACTA,EAAO,UAAU,QAASG,GAAMD,EAAQ,UAAU,IAAIC,CAAC,CAAC,EAItDH,EAAO,SACT,OAAO,QAAQA,EAAO,OAAO,EAC1B,OAAO,CAAC,CAACL,EAAIC,CAAC,IAAMA,IAAM,MAAS,EACnC,QAAQ,CAAC,CAACQ,EAAGR,CAAC,IAAOM,EAAQ,QAAQE,CAAC,EAAIR,CAAE,EAIjD,IAAMS,EAAQJ,EAAS,IACpBK,GAAO,OAAOA,GAAM,SAAW,SAAS,eAAeA,CAAC,EAAIA,CAC/D,EACA,OAAAJ,EAAQ,OAAO,GAAGG,CAAK,EAEhBH,CACT,CCnEO,SAASK,GAAmB,CACjC,IAAMC,EAAUC,EAAc,gBAAgB,EACxCC,EAASD,EAAc,eAAe,EACtCE,EAAcF,EAAc,qBAAqB,EACjDG,EAAeH,EAAc,sBAAsB,EACnDI,EAAYJ,EAAc,mBAAmB,EAC7CK,EAAeL,EAAc,eAAe,EAKlDD,EAAQ,iBACN,QACCO,GAAMC,EAAYD,EAAGL,EAAQI,CAAY,CAC5C,EACAF,EAAa,iBACX,QACCG,GAAMC,EAAYD,EAAGL,EAAQI,CAAY,CAC5C,EACAH,EAAY,iBACV,QACCI,GAAMC,EAAYD,EAAGL,EAAQI,CAAY,CAC5C,EACAD,EAAU,iBAAiB,QAAUE,GAAMA,EAAE,gBAAgB,CAAC,EAG9D,SAAS,iBAAiB,UAAYA,GAAM,CAE1C,GAAIA,EAAE,UAAY,IAAQA,EAAE,WAAa,IAASA,EAAE,MAAQ,IAAK,CAC/DA,EAAE,eAAe,EACjBP,EAAQ,MAAM,EACd,MACF,CAEA,GAAIS,EAAYP,CAAM,EACpB,OAAQK,EAAE,IAAK,CACb,IAAK,SACHA,EAAE,eAAe,EACjBH,EAAa,MAAM,EACnB,OACF,IAAK,YAEH,MACF,IAAK,UAEH,MACF,QACE,KACJ,CAEJ,CAAC,CACH,CAKA,SAASI,EACP,EACAN,EACAI,EACA,CACA,EAAE,eAAe,EACjBJ,EAAO,UAAU,OAAO,QAAQ,EAC5BO,EAAYP,CAAM,GAAGI,EAAa,MAAM,CAC9C,CAKA,SAASG,EAAYP,EAA8B,CACjD,MAAO,CAACA,EAAO,UAAU,SAAS,QAAQ,CAC5C,CA6CA,IAAMQ,GAAuB,wBAKvBC,GAAoB,EAKnB,SAASC,GAAc,CAC5B,IAAMN,EAAeL,EAAc,eAAe,EAC5CY,EAAiBZ,EAAc,iBAAiB,EAChDa,EAAsBb,EAAc,uBAAuB,EAG7Dc,EAGAC,EAAc,GAEZC,EAAY,gBAAkB,CAElC,OAAIF,IAAU,SACZA,EAAQ,MAAML,EAAY,EACvB,KACC,eAAgBQ,EAAU,CACxB,OAAO,MAAM,YAAY,MAAM,KAAK,MAAMA,EAAS,KAAK,CAAC,CAC3D,CACF,GAGG,MAAMH,CACf,EAEAT,EAAa,iBACX,QACAa,EAAS,IAAK,gBAAkB,CAC9B,IAAMC,EAAOd,EAAa,MAAM,KAAK,EAOrC,GANIc,IAASJ,IAEbH,EAAe,MAAM,QAAUO,IAAS,GAAK,OAAS,QACtDN,EAAoB,UAAY,GAEhCE,EAAcI,EACVJ,IAAgB,IAAI,OAExB,IAAMK,GAA2B,MAAMJ,EAAU,GAC9C,OAAOG,EAAM,CACZ,KAAM,MACN,OAAQ,CACN,MAAO,CAAE,MAAO,CAAE,EAClB,KAAM,CAAE,MAAO,CAAE,CACnB,CACF,CAAC,EAEH,GAAIC,EAAQ,SAAW,EAAG,CACxBR,EAAe,MAAM,QAAU,OAC/B,MACF,CAEA,QAASS,EAAI,EAAGA,EAAI,KAAK,IAAID,EAAQ,OAAQV,EAAS,EAAG,EAAEW,EAAG,CAC5D,IAAMC,EAAQC,GAAeH,EAAQC,CAAC,EAAGN,EAAY,MAAM,GAAG,CAAC,EAC/DF,EAAoB,YAAYS,CAAK,CACvC,CACF,CAAC,CACH,CACF,CAKA,SAASC,GAAeC,EAAoBC,EAA8B,CACxE,OAAOC,EAAK,KAAM,CAAC,EAAG,CACpB,UAAW,CAAC,WAAY,qBAAsB,yBAAyB,CACzE,EAAG,CACDA,EAAK,MAAO,CAAC,EAAG,CAAC,EAAG,CAClBA,EAAK,IAAK,CAAE,KAAMF,EAAK,GAAI,EAAG,CAC5B,UAAW,CACT,QACA,OACA,OACA,mBACA,yBACA,sBACA,wBACA,OACF,CACF,EAAG,CACDE,EAAK,OAAQ,CAAC,EAAG,CACf,UAAW,CAAC,QAAS,YAAa,OAAQ,aAAa,CACzD,EAAG,CACDF,EAAK,IAAI,KACX,CAAC,EACDE,EAAK,OAAQ,CAAC,EAAG,CACf,UAAW,CACT,QACA,mBACA,UACA,2BACF,CACF,EAAG,CACDC,GAAWH,EAAK,IAAI,KAAMC,CAAK,CACjC,CAAC,CACH,CAAC,CACH,CAAC,CACH,CAAC,CACH,CAUA,SAASE,GAAWC,EAAcH,EAA8B,CAM9D,IAAMI,EAAeJ,EAAM,IAAI,SAAUK,EAAG,CAC1C,OAAO,YAAY,QAAQA,EAAE,YAAY,CAAC,CAC5C,CAAC,EAEGC,EAAY,GACZjB,EAAQ,EAENkB,EAAyC,CAAC,EAG1CC,EAAYL,EAAK,YAAY,EAAE,MAAM,IAAI,EAE/C,QAAWP,KAAKY,EAAW,CACzB,IAAMC,EAAQD,EAAUZ,CAAC,EAAE,MAAM,GAAG,EAChCc,EAAQ,EAEZ,QAAWC,KAAKF,EAAO,CACrB,IAAMG,EAAOH,EAAME,CAAC,EAEpB,GAAIC,EAAK,OAAS,EAAG,CACnB,QAAWC,KAAKT,EACV,YAAY,QAAQQ,CAAI,EAAE,WAAWR,EAAaS,CAAC,CAAC,IACtDH,EAAQ,GACRJ,EAAY,IAIhBC,EAAS,KAAK,CAACK,EAAMF,EAAOrB,CAAK,CAAC,EAClCqB,EAAQ,CACV,CAEArB,GAASuB,EAAK,OAEdvB,GAAS,CACX,CAGAA,GAAS,CACX,CAEA,GAAIkB,EAAS,SAAW,EAAG,CACzB,IAAMO,EAAQX,EACRY,EAAOd,EAAK,OAAQ,CAAC,EAAG,CAAC,EAAG,CAAC,CAAC,EACpC,OAAAc,EAAK,UAAYD,EACVC,CACT,CAEA,IAAMC,EAA0B,CAAC,EAC3BC,EAAa,KAAK,IAAIV,EAAS,OAAQ,EAAgB,EAEzDW,EAAS,EACb,QAAStB,EAAI,EAAGA,EAAIqB,EAAYrB,IAC9BsB,GAAUX,EAASX,CAAC,EAAE,CAAC,EAEzBoB,EAAc,KAAKE,CAAM,EAEzB,QAAStB,EAAI,EAAGA,EAAIW,EAAS,OAASU,EAAYrB,IAChDsB,GAAUX,EAASX,CAAC,EAAE,CAAC,EACvBsB,GAAUX,EAASX,EAAIqB,CAAU,EAAE,CAAC,EACpCD,EAAc,KAAKE,CAAM,EAI3B,IAAIC,EAAc,EAClB,GAAIb,EAAW,CACb,IAAIc,EAAW,EAEf,QAAS,EAAIJ,EAAc,OAAS,EAAG,GAAK,EAAG,IACzCA,EAAc,CAAC,EAAII,IACrBA,EAAWJ,EAAc,CAAC,EAC1BG,EAAc,EAGpB,CAEA,IAAME,EAAmB,CAAC,EACtBC,EAAaf,EAASY,CAAW,EAAE,CAAC,EACxC,QAASvB,EAAIuB,EAAavB,EAAIuB,EAAcF,EAAYrB,IAAK,CAC3D,IAAMgB,EAAOL,EAASX,CAAC,EACnB0B,EAAaV,EAAK,CAAC,IAErBS,EAAO,KAAKlB,EAAK,UAAUmB,EAAYV,EAAK,CAAC,CAAC,CAAC,EAC/CU,EAAaV,EAAK,CAAC,GAIjBA,EAAK,CAAC,IAAM,IACdS,EAAO,KAAK,KAAK,EAEnBC,EAAaV,EAAK,CAAC,EAAIA,EAAK,CAAC,EAAE,OAC/BS,EAAO,KAAKlB,EAAK,UAAUS,EAAK,CAAC,EAAGU,CAAU,CAAC,EAE3CV,EAAK,CAAC,IAAM,IACdS,EAAO,KAAK,MAAM,CAEtB,CACAA,EAAO,KAAK,QAAG,EACf,IAAMP,EAAQO,EAAO,KAAK,EAAE,EACtBN,EAAOd,EAAK,OAAQ,CAAC,EAAG,CAAC,EAAG,CAAC,CAAC,EACpC,OAAAc,EAAK,UAAYD,EACVC,CACT,CCpUO,SAASQ,EAAaC,EAA2B,CACtD,GAAIA,IAAU,OAAW,MAAM,IAAIC,EAAWD,CAAK,EAEnD,OAAQA,EAAO,CACb,IAAK,SAEH,OADA,aAAa,WAAW,OAAO,EACvBE,GAAe,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,GAAWH,CAAK,CAClB,CAiBA,SAASG,GAAWH,EAAmB,CACrCI,EAAiB,eAAe,EAAE,QAASC,GAAY,CACjDA,EAAQ,QAAQ,QAAUL,EAAOK,EAAQ,QAAQ,OAAS,OACzD,OAAOA,EAAQ,QAAQ,MAC9B,CAAC,CACH,CAKA,SAASH,IAA8B,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,CCFA,SAASG,IAAQ,CACfC,EAAqB,EACrBC,EAAqB,EACrBC,EAAiB,EACjBC,EAAY,EACZC,EAAqB,EACrBC,EAAa,CACf,EAKC,UAAY,CACX,GAAI,CACFN,GAAM,CACR,OAAS,EAAG,CACV,QAAQ,MAAM,CAAC,CACjB,CACF,GAAG", + "names": ["updateIcon", "$node", "oldName", "newName", "newIconUrl", "getIconUrl", "setIconUrl", "iconUrl", "XLINK_NS", "IconError", "url", "querySelector", "selector", "$elem", "QueryError", "querySelectorAll", "$elems", "copyToClipboard", "text", "reason", "ClipboardError", "source", "msg", "setupDocsNav", "$toggles", "querySelectorAll", "$toggle", "$toggleIcon", "e", "$parent", "$subsection", "sectionIsHidden", "updateIcon", "setupInstallerPicker", "$buttons", "$cmd", "$copy", "$copyIcon", "querySelectorAll", "querySelector", "$button", "e", "installerForPlatform", "copyToClipboard", "updateIcon", "platform", "UnknownPlatformError", "UNIX_INSTALLER", "WINDOWS_INSTALLER", "HOST", "setupSmoothScrolling", "querySelectorAll", "anchor", "e", "targetHeader", "headerPosition", "querySelector", "scrollAmount", "offsetPosition", "debounce", "waitFor", "func", "timeout", "args", "removeUndefinedValues", "x", "entries", "_k", "v", "elem", "tag", "attrs", "extras", "children", "element", "c", "k", "nodes", "e", "setupSearchModal", "$button", "querySelector", "$modal", "$modalClose", "$modalShroud", "$modalBox", "$searchInput", "e", "toggleModal", "modalIsOpen", "SEARCH_INDEX", "MAX_ITEMS", "setupSearch", "$searchResults", "$searchResultsItems", "index", "currentTerm", "initIndex", "response", "debounce", "term", "results", "i", "entry", "buildListEntry", "data", "terms", "elem", "makeTeaser", "body", "stemmedTerms", "w", "termFound", "weighted", "sentences", "words", "value", "j", "word", "k", "final", "span", "windowWeights", "windowSize", "curSum", "maxSumIndex", "maxFound", "teaser", "startIndex", "setPageTheme", "theme", "ThemeError", "preferredTheme", "setButtons", "querySelectorAll", "$option", "setupThemeController", "querySelectorAll", "$option", "e", "setPageTheme", "setup", "setupThemeController", "setupInstallerPicker", "setupSearchModal", "setupSearch", "setupSmoothScrolling", "setupDocsNav"] +} 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/static/manifest.webmanifest b/site/static/manifest.webmanifest new file mode 100644 index 00000000..55dae3ec --- /dev/null +++ b/site/static/manifest.webmanifest @@ -0,0 +1,6 @@ +{ + "icons": [ + { "src": "/192.png", "type": "image/png", "sizes": "192x192" }, + { "src": "/512.png", "type": "image/png", "sizes": "512x512" } + ] +} 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..7b6c9643 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/blog.tera.html b/site/templates/bases/blog.tera.html new file mode 100644 index 00000000..dc175114 --- /dev/null +++ b/site/templates/bases/blog.tera.html @@ -0,0 +1,48 @@ +{% 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 %} +
+ +
+
+
+ + {% block blog_header %} +
+ Blog Messages from the maintainers + Subscribe {{ ic::icon(name="rss") }} +
+ {% endblock %} + +
+ {% block content %}{% 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..0a5d083f --- /dev/null +++ b/site/templates/bases/docs.tera.html @@ -0,0 +1,163 @@ +{% 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 %} +
+ +
+ +
+
+ {% 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 %} +
+
+
+ +
+ + {% 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..50bab5d7 100644 --- a/site/templates/blog.html +++ b/site/templates/blog.html @@ -1,8 +1,4 @@ - -{% import "macros/icon.tera.html" as ic %} -{% import "macros/toc.tera.html" as toc %} - -{% extends "bases/base.tera.html" %} +{% extends "bases/blog.tera.html" %} {% block title %} {% if section.title %} @@ -12,27 +8,37 @@ {% endif %} {% endblock %} -{% block sidebar %} -{% endblock %} - {% block content %} {{ section.content | safe }} - {% for page in section.pages %} - -
-
-

{{ page.title }}

-

Posted on {{ page.date | date(format="%B %-d, %Y") }}

- - {% if page.summary %} - {{ page.summary | safe }} - {% endif %} - -
- -
- - {% endfor %} + {% endblock %} diff --git a/site/templates/blog_post.html b/site/templates/blog_post.html new file mode 100644 index 00000000..c73bf333 --- /dev/null +++ b/site/templates/blog_post.html @@ -0,0 +1,39 @@ +{% extends "bases/blog.tera.html" %} + +{% block title %} + {% if section.title %} + {{ section.title }} + {% else %} + Hipcheck + {% endif %} +{% endblock %} + +{% block blog_header %} +
+
+

{{ page.title }}

+
+
+
+ +
+
+
+

Written by {{ page.authors.0 }}

+

Posted on {{ page.date | date(format="%B %-d, %Y") }}

+
+
+ +
+
+{% endblock %} + +{% block content %} +
+
+
+ {{ page.content | safe }} +
+
+
+{% endblock %} 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..4b938d5e 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,93 +8,152 @@ {% 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 -

+ long term risk + + +
    +
  • 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
  • +
-

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!

+ {% 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

- -
-
-
-
- {{ ic::icon(name="info", classes="-mt-1 text-blue-500") }} +
+
+
+ Analyzing +
+
+ {{ ic::icon(name="link") }} +
+ pkg:github/example/project +
+
+
+ +
+
+
+ {{ 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 +
+ Are there binaries in the repo? + Warning: found prebuilt, prebuilt.exe +
+ {{ ic::icon(name="alert-circle", classes="fill-yellow-800") }} Investigate + 20% weight
-
-

Make Dependencies Manageable

-

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

-

Learn about using Hipcheck →

+
+
+ +
+
+ + 0.2 + Risk Score + + is + + ≤ 0.5 + Risk Policy + + so + + {{ ic::icon(name="check", classes="") }} Pass + Result + +
+
+
+
+{% endblock %} + +{% block extra %} +
+
+ +
+
+
+

Maintainers don't need drive-by comments with + + + best practice + scanner results, they need insights to make + dependencies work for them.

+

We’re not building Yet Another Analysis Tool, + we’re building an analysis swiss army knife + you can extend, modify, and own.

+

Hipcheck’s plugin system means anyone can add + new data sources and analyses, and users control + what runs and what gets recommended.

+

We wear our values + on our sleeves, and we’re proud to be working + for regular maintainers.

+
diff --git a/site/templates/macros/breadcrumbs.tera.html b/site/templates/macros/breadcrumbs.tera.html index 5d9f8815..6345ea8a 100644 --- a/site/templates/macros/breadcrumbs.tera.html +++ b/site/templates/macros/breadcrumbs.tera.html @@ -2,32 +2,47 @@ {# 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..72abf8b5 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..7e75db0a --- /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..5e082503 100644 --- a/site/templates/partials/footer.tera.html +++ b/site/templates/partials/footer.tera.html @@ -1,19 +1,19 @@ -