Repository: huhu/rust-search-extension (language: rust)

Issue:
Docs.rs enhancement: Show related security advisories 
When reading the docs, it's helpful to show related security advisories for current version.

<https://rustsec.org/advisories/>


Discussion / hints:
Great idea. 👍 

Fix patch:
diff --git a/extension/script/docs-rs.css b/extension/script/docs-rs.css
index 2108bd22..155432f8 100644
--- a/extension/script/docs-rs.css
+++ b/extension/script/docs-rs.css
@@ -11,21 +11,27 @@ div.nav-container form.landing-search-form-nav {
 
 /* Rust Search Extension classes */
 
-.feature-flags-content {
+.rse-dropdown-content {
     color: var(--color-navbar-standard);
     max-height: 600px;
     overflow: auto;
     max-width: 60rem;
 }
 
-.feature-flags-content a {
+.rse-dropdown-content a {
     color: var(--color-standard);
 }
 
-.feature-flags-content a:hover {
+.rse-dropdown-content a:hover {
     text-decoration: underline;
 }
 
+.rse-dropdown-content>ul {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+}
+
 .feature-flags-table {
     margin: 0.5rem;
     border-collapse: separate;
@@ -61,6 +67,25 @@ div.nav-container form.landing-search-form-nav {
     margin: 7px 0;
 }
 
+.advisory-item {
+    padding: 20px 0;
+    margin: 0 20px;
+}
+
+.advisory-item:not(:first-child) {
+    border-top: 1px solid var(--color-border);
+}
+
+.advisory-title {
+    color: var(--link-color) !important;
+    font-weight: bold;
+}
+
+.advisory-desc {
+    padding: 10px 0;
+    font-size: 13px;
+}
+
 .add-to-extension {
     line-height: 1;
     font-size: 0.8em;
diff --git a/extension/script/docs-rs.js b/extension/script/docs-rs.js
index 733d737a..c53f1cdd 100644
--- a/extension/script/docs-rs.js
+++ b/extension/script/docs-rs.js
@@ -108,9 +108,68 @@ document.addEventListener("DOMContentLoaded", async () => {
         featureFlagsMenu.classList.add("pure-menu-has-children", "pure-menu-allow-hover");
         await enhanceFeatureFlagsMenu(featureFlagsMenu);
     }
+
+    // Add a menu item to show the crate's security advisories.
+    let advisories = await getAdvisories() || [];
+    let advisoryMenu = document.createElement("li");
+    advisoryMenu.classList.add("pure-menu-item", "pure-menu-has-children", "pure-menu-allow-hover");
+    let html = advisories.map(({ advisory, versions }) => {
+        let affectedVersions = versions.patched.map(v => `<span class="stab"><code style="white-space: nowrap;">${v}</code></span>`).join(" , ");
+        return `<li class="advisory-item">
+            <div>
+                <img style="width: 15px;vertical-align: middle;margin-right: 5px;" src="https://rustsec.org/img/rustsec-logo-square.svg" alt="advisory">
+                <b>${advisory.date}</b>
+                <a class="advisory-title" href="https://rustsec.org/advisories/${advisory.id}.html" target="_blank">
+                    ${advisoryTitle(advisory)}
+                </a>
+            </div>
+            <div class="advisory-desc">${advisory.title}</div>
+            <div class="advisory-affected-versions">
+                ${affectedVersions}
+            </div>
+            </li>`;
+    }).join("");
+
+    advisoryMenu.innerHTML = `<a href="https://rustsec.org/${ advisories.length > 0 ? `packages/${crateName}.html` : ""}"
+                                class="pure-menu-link" target="_blank">
+                <span class="fa-svg" aria-hidden="true">${SVG_SHEILD}</span>
+                <span class="title">Security advisory ${advisories.length}</span>
+                </a>
+                <div class="pure-menu-children rse-dropdown-content" role="menu">
+                ${advisories.length > 0 ? `<ul>${html}</ul>` : `<div style="padding: 1rem;text-align: center;"><img style="width:100px;padding:1rem"src="https://rustsec.org/img/rustsec-logo-square.svg"/> <div>No security advisories found.</div></div>`}
+                </div>`;
+    featureFlagsMenu.insertAdjacentElement("afterend", advisoryMenu);
 });
 
+// Advisory title. Mirror to code of https://github.com/rustsec/rustsec repository.
+function advisoryTitle(advisory) {
+    let id = advisory.id;
+    let pkg = advisory.package;
+    let informational = advisory.informational;
+    if (informational) {
+        if (informational === "notice") {
+            return `${id}: Security notice about ${pkg}`;
+        } else if (informational === "unmaintained") {
+            return `${id}: ${pkg} is unmaintained`;
+        } else if (informational === "unsound") {
+            return `${id}: Unsoundness in ${pkg}`;
+        } else if (informational === "other") {
+            return `${id}: ${pkg} is ${s}`;
+        } else {
+            return `${id}: Advisory for ${pkg}`;
+        }
+    } else {
+        return `${id}: Vulnerability in ${pkg}`;
+    }
+}
+
 async function getFeatureFlagsMenuData() {
+    // Try to get the data from sessionStorage first.
+    let crateData = JSON.parse(sessionStorage.getItem(`${crateName}-${crateVersion}`));
+    if (crateData) {
+        return crateData;
+    }
+
     let crateAPIURL = `https://crates.io/api/v1/crates/${crateName}/${crateVersion}`;
     let response = await fetch(crateAPIURL);
     let content = await response.json();
@@ -121,16 +180,14 @@ async function getFeatureFlagsMenuData() {
     let depsContent = await depsResponse.json();
     let optionalDependencies = parseOptionalDependencies(depsContent);
 
-    return { features, optionalDependencies };
+    crateData = { features, optionalDependencies };
+    // Cache the data in sessionStorage.
+    sessionStorage.setItem(`${crateName}-${crateVersion}`, JSON.stringify(crateData));
+    return crateData;
 }
 
 async function enhanceFeatureFlagsMenu(menu) {
-    let crateData = JSON.parse(window.sessionStorage.getItem(`${crateName}-${crateVersion}`));
-
-    if (!crateData) {
-        crateData = await getFeatureFlagsMenuData();
-        window.sessionStorage.setItem(`${crateName}-${crateVersion}`, JSON.stringify(crateData));
-    }
+    let crateData = await getFeatureFlagsMenuData();
 
     const { features, optionalDependencies } = crateData;
     let html = `<div style="padding: 1rem"><p>
@@ -171,7 +228,7 @@ async function enhanceFeatureFlagsMenu(menu) {
         }
         </div>`;
     menu.firstElementChild.insertAdjacentHTML("afterend",
-        `<div class="pure-menu-children feature-flags-content" role="menu">
+        `<div class="pure-menu-children rse-dropdown-content" role="menu">
             ${html}
         </div>`);
 }
@@ -254,6 +311,28 @@ function insertAddToExtensionElement(state) {
     }
 }
 
+// Get vulnerability advisory for this crate.
+async function getAdvisories() {
+    // Get advisory from sessionStorage first.
+    let key = `advisory:${libName}`;
+    let advisory = sessionStorage.getItem(key);
+    if (advisory) {
+        return JSON.parse(advisory);
+    }
+
+    let resp = await fetch(`https://rust.extension.sh/advisory/${libName}.json`);
+    if (resp.status === 200) {
+        let advisory = await resp.json();
+        // Save advisory to sessionStorage.
+        sessionStorage.setItem(key, JSON.stringify(advisory));
+        return advisory;
+    } else {
+        sessionStorage.setItem(key, JSON.stringify([]));
+        console.log(`${libName} has no vulnerability advisory!`);
+        return null;
+    }
+}
+
 window.addEventListener("message", async function (event) {
     if (event.source === window &&
         event.data &&
diff --git a/extension/script/svgs.js b/extension/script/svgs.js
index 0b372428..a64a20a2 100644
--- a/extension/script/svgs.js
+++ b/extension/script/svgs.js
@@ -4,3 +4,4 @@ const SVG_CHECK_CIRCLE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5
 const SVG_FLAG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M349.565 98.783C295.978 98.783 251.721 64 184.348 64c-24.955 0-47.309 4.384-68.045 12.013a55.947 55.947 0 0 0 3.586-23.562C118.117 24.015 94.806 1.206 66.338.048 34.345-1.254 8 24.296 8 56c0 19.026 9.497 35.825 24 45.945V488c0 13.255 10.745 24 24 24h16c13.255 0 24-10.745 24-24v-94.4c28.311-12.064 63.582-22.122 114.435-22.122 53.588 0 97.844 34.783 165.217 34.783 48.169 0 86.667-16.294 122.505-40.858C506.84 359.452 512 349.571 512 339.045v-243.1c0-23.393-24.269-38.87-45.485-29.016-34.338 15.948-76.454 31.854-116.95 31.854z"/></svg>`;
 const SVG_PLUS_CIRCLE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm144 276c0 6.6-5.4 12-12 12h-92v92c0 6.6-5.4 12-12 12h-56c-6.6 0-12-5.4-12-12v-92h-92c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h92v-92c0-6.6 5.4-12 12-12h56c6.6 0 12 5.4 12 12v92h92c6.6 0 12 5.4 12 12v56z"/></svg>`;
 const SVG_ERROR = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z"/></svg>`;
+const SVG_SHEILD = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M240 0c4.6 0 9.2 1 13.4 2.9L441.7 82.8c22 9.3 38.4 31 38.3 57.2c-.5 99.2-41.3 280.7-213.6 363.2c-16.7 8-36.1 8-52.8 0C41.3 420.7 .5 239.2 0 140c-.1-26.2 16.3-47.9 38.3-57.2L226.7 2.9C230.8 1 235.4 0 240 0z"/></svg>`;


Test patch: