diff --git a/public/js/components/package.info.js b/public/js/components/package.info.js deleted file mode 100644 index 3935c66b..00000000 --- a/public/js/components/package.info.js +++ /dev/null @@ -1,806 +0,0 @@ -// Import Third-party Dependencies -import prettyBytes from "pretty-bytes"; -import { getFlagsEmojisInlined, getJSON } from "@nodesecure/vis-network"; -import { locationToString } from "@nodesecure/utils"; - -// Import Internal Dependencies -import * as utils from "../utils.js"; -import { Bundlephobia } from "./bundlephobia.js"; -import { UnpkgCodeFetcher } from "./unpkgCodeFetcher.js"; - -const kSocketDevLink = 'https://socket.dev/npm/package/'; -const kSnykAdvisorLink = 'https://snyk.io/advisor/npm-package/'; -const kScorecardVisualizer = (repo) => `https://kooltheba.github.io/openssf-scorecard-api-visualizer/#/projects/github.com/${repo}`; - -export class PackageInfo { - static DOMElementName = "package-info"; - static StopSimulationTimeout = null; - - static close() { - const domElement = document.getElementById(PackageInfo.DOMElementName); - if (domElement.classList.contains("slide-in")) { - domElement.setAttribute("class", "slide-out"); - } - } - - /** - * @param {*} dependencyVersionData - * @param {*} dependency - * @param {*} nsn - */ - constructor(dependencyVersionData, currentNode, dependency, nsn) { - this.codeCache = new Map(); - this.nsn = nsn; - this.currentNode = currentNode; - this.dom = document.getElementById(PackageInfo.DOMElementName); - this.dom.innerHTML = ""; - this.menus = new Map(); - - const { name, version } = dependencyVersionData; - this.dependencyVersion = dependencyVersionData; - this.dependency = dependency; - - const template = document.getElementById("package-info-template"); - /** @type {HTMLTemplateElement} */ - const clone = document.importNode(template.content, true); - - this.activeNavigation = clone.querySelector(".package-navigation > div.active"); - this.setupNavigation(clone); - this.hydrateAndGenerate(clone); - - for (const domElement of clone.querySelectorAll(".open-wiki")) { - domElement.addEventListener("click", () => { - window.wiki.header.setNewActiveView("warnings"); - window.wiki.open(); - }); - } - - this.dom.appendChild(clone); - this.enableNavigation(window.settings.config.defaultPackageMenu); - this.open(); - - // Fetch Github stats - if (this.links.github.href !== null) { - this.fetchGithubStats().catch(console.error); - } - - // Fetch bundlephobia size stats - new Bundlephobia(name, version) - .fetchDataOnHttpServer() - .catch(console.error); - } - - get isLocalProject() { - return this.currentNode === 0 || this.dependencyVersion.flags.includes("isGit"); - } - - open() { - this.dom.setAttribute("class", "slide-in"); - } - - async fetchGithubStats() { - const github = new URL(this.links.github.href); - const repoName = github.pathname.slice(1, github.pathname.includes(".git") ? -4 : github.pathname.length); - - const { stargazers_count, open_issues_count, forks_count } = await fetch(`https://api.github.com/repos/${repoName}`) - .then((value) => value.json()); - - document.querySelector(".github-stars").innerHTML = ` ${stargazers_count}`; - document.querySelector(".github-issues").textContent = open_issues_count; - document.querySelector(".github-forks").textContent = forks_count; - } - - /** - * @param {!HTMLTemplateElement} clone - */ - setupNavigation(clone) { - for (const div of clone.querySelectorAll(".package-navigation > div")) { - const dataMenu = div.getAttribute("data-menu"); - this.menus.set(dataMenu, div); - - div.addEventListener("click", () => this.enableNavigation(dataMenu)); - } - } - - setupNavigationSignal(navElement, count = 0) { - if (count === 0) { - navElement.classList.add("disabled"); - } - else { - const counter = navElement.querySelector(".signal"); - counter.style.display = "flex"; - counter.appendChild(document.createTextNode(count)); - } - } - - enableNavigation(name) { - const div = this.menus.has(name) ? this.menus.get(name) : this.menus.get("info"); - - const isActive = div.classList.contains("active"); - const isDisabled = div.classList.contains("disabled"); - const dataTitle = div.getAttribute("data-title"); - - if (isActive || isDisabled) { - return; - } - - div.classList.add("active"); - this.activeNavigation.classList.remove("active"); - - const targetPan = document.getElementById(`pan-${name}`); - const currentPan = document.getElementById(`pan-${this.activeNavigation.getAttribute("data-menu")}`); - targetPan.classList.remove("hidden"); - currentPan.classList.add("hidden"); - document.querySelector(".container-title").textContent = dataTitle; - - this.activeNavigation = div; - } - - get lastUpdateAt() { - return Intl.DateTimeFormat("en-GB", { - day: "2-digit", month: "short", year: "numeric", hour: "numeric", minute: "numeric", second: "numeric" - }).format(new Date(this.dependency.metadata.lastUpdateAt)); - } - - get author() { - const author = this.dependencyVersion.author; - const flatAuthorFullname = typeof author === "string" ? author : (author?.name ?? "Unknown"); - - return flatAuthorFullname.length > 26 ? `${flatAuthorFullname.slice(0, 26)}...` : flatAuthorFullname; - } - - /** - * @param {!HTMLTemplateElement} clone - */ - hydrateAndGenerate(clone) { - const { name, version, size, composition, warnings, usedBy, engines, flags } = this.dependencyVersion; - const { metadata, vulnerabilities } = this.dependency; - - const [maintainersDomElement, licensesDomElement, warningsDomElement, scriptsDomElement, vulnDomElement, ossfScorecardDomElement] = [ - clone.querySelector(".package-maintainers"), - clone.getElementById("pan-licenses"), - clone.getElementById("pan-warnings"), - clone.querySelector(".package-scripts"), - clone.querySelector(".packages-vuln"), - clone.getElementById("pan-scorecard") - ]; - const [fieldsDefault, fieldsReleases] = [ - clone.querySelector(".fields"), - clone.querySelector(".fields.releases") - ]; - - this.generateHeader(clone); - if (flags.includes("hasScript")) { - this.setupNavigationSignal(clone.getElementById("dependencies-nav-menu"), "!"); - } - { - const warningsLength = warnings.filter((warning) => !window.settings.warnings.has(warning.kind)).length; - this.setupNavigationSignal(clone.getElementById("warnings-nav-menu"), warningsLength); - } - - this.setupNavigationSignal(clone.getElementById("vulnerabilities-nav-menu"), vulnerabilities.length); - - { - const doc = document.createDocumentFragment(); - - if (this.links.homepage.href !== null) { - doc.appendChild(utils.createLiField("Homepage", this.links.homepage.href, { isLink: true })); - } - doc.appendChild(utils.createLiField("Author", this.author)); - doc.appendChild(utils.createLiField("Size on system", prettyBytes(size))); - doc.appendChild(utils.createLiField("Number of dependencies", metadata.dependencyCount)); - doc.appendChild(utils.createLiField("Number of files", composition.files.length)); - doc.appendChild(utils.createLiField("README.md", composition.files.some((file) => /README\.md/gi.test(file)) ? "✔️" : "❌")); - doc.appendChild(utils.createLiField("TS Typings", composition.files.some((file) => /d\.ts/gi.test(file)) ? "✔️" : "❌")); - if ("node" in engines) { - doc.appendChild(utils.createLiField("Node.js compatibility", engines.node)); - } - if ("npm" in engines) { - doc.appendChild(utils.createLiField("NPM compatibility", engines.npm)); - } - - fieldsDefault.appendChild(doc); - } - { - const doc = document.createDocumentFragment(); - - doc.appendChild(utils.createLiField("Last release version", metadata.lastVersion)); - doc.appendChild(utils.createLiField("Last release date", this.lastUpdateAt)); - doc.appendChild(utils.createLiField("Number of published releases", metadata.publishedCount)); - doc.appendChild(utils.createLiField("Number of publisher(s)", metadata.publishers.length)); - - fieldsReleases.appendChild(doc); - } - - utils.createItemsList(clone.getElementById("nodedep"), composition.required_nodejs, { - hideItemsLength: 8, - onclick: (event, coreLib) => { - const lib = coreLib.startsWith('node:') ? coreLib.slice(5) : coreLib; - - window.open(`https://nodejs.org/dist/latest/docs/api/${lib}.html`, "_blank").focus(); - } - }); - - const onclick = (_, fileName) => { - if (fileName === "../" || fileName === "./") { - return; - } - const cleanedFile = fileName.startsWith("./") ? fileName.slice(2) : fileName; - window.open(`https://unpkg.com/${name}@${version}/${cleanedFile}`, "_blank").focus(); - }; - - utils.createItemsList(clone.getElementById("extensions"), composition.extensions); - utils.createItemsList(clone.getElementById("tarballfiles"), composition.files, { - onclick, hideItems: true, hideItemsLength: 3 - }); - - utils.createItemsList(clone.getElementById("minifiedfiles"), composition.minified, { - onclick, hideItems: true - }); - utils.createItemsList(clone.getElementById("unuseddep"), composition.unused); - utils.createItemsList(clone.getElementById("missingdep"), composition.missing); - - const onclickFocusNetworkNode = (_, packageName) => this.nsn.focusNodeByName(packageName); - utils.createItemsList(clone.getElementById("requireddep"), composition.required_thirdparty, { - onclick: onclickFocusNetworkNode, - hideItems: true - }); - utils.createItemsList(clone.getElementById("usedby"), Object.keys(usedBy), { onclick: onclickFocusNetworkNode, hideItems: true }); - utils.createItemsList(clone.getElementById("internaldep"), composition.required_files, { - onclick, - hideItems: true, - hideItemsLength: 3 - }); - licensesDomElement.appendChild(this.generateLicenses()); - maintainersDomElement.appendChild(this.generateMaintainers()); - warningsDomElement.appendChild(this.generateWarnings()); - scriptsDomElement.appendChild(this.generateScripts()); - vulnDomElement.appendChild(this.generateVulnerabilities()); - - this.generateOssfScorecard(name).then( - (ossfScorecardElementChildren) => { - if (ossfScorecardElementChildren) { - ossfScorecardDomElement.appendChild(ossfScorecardElementChildren); - document.getElementById('scorecard-menu').style.display = 'flex'; - } - } - ); - - const strategy = window.vulnerabilityStrategy; - clone.querySelector(".vuln-strategy .name").textContent = strategy; - - /** @type {HTMLImageElement} */ - const strategyLogo = clone.querySelector(".vuln-strategy img"); - if (strategy === "none") { - strategyLogo.style.display = "none"; - } - else { - strategyLogo.src = strategy === "npm" ? "npm-icon.svg" : `${strategy}.png`; - } - - const btnShow = clone.getElementById("show-hide-dependency"); - if (this.currentNode === 0) { - btnShow.classList.add("disabled"); - - return; - } - btnShow.innerHTML = this.dependencyVersion.hidden ? " show" : " hide"; - - if (this.dependency.metadata.dependencyCount === 0) { - btnShow.classList.add("disabled"); - } - else { - btnShow.addEventListener("click", () => { - const currBtn = document.getElementById("show-hide-dependency"); - currBtn.classList.toggle("active"); - const hidden = !this.dependencyVersion.hidden; - - currBtn.innerHTML = hidden ? " show" : " hide"; - - this.nsn.highlightNodeNeighbour(this.currentNode, hidden); - if (PackageInfo.StopSimulationTimeout !== null) { - clearTimeout(PackageInfo.StopSimulationTimeout); - } - PackageInfo.StopSimulationTimeout = setTimeout(() => { - this.nsn.network.stopSimulation(); - PackageInfo.StopSimulationTimeout = null; - }, 500); - this.dependencyVersion.hidden = !this.dependencyVersion.hidden; - }); - } - } - - /** - * @param {!HTMLTemplateElement} clone - */ - generateHeader(clone) { - const { license } = this.dependencyVersion; - const [nameDomElement, versionDomElement, descriptionDomElement, linksDomElement, flagsDomElement] = [ - clone.querySelector(".name"), - clone.querySelector(".version"), - clone.querySelector(".package-description"), - clone.querySelector(".package-links"), - clone.querySelector(".package-flags") - ] - - // Name and Version - nameDomElement.textContent = this.dependencyVersion.name; - if (this.dependencyVersion.name.length >= 18) { - nameDomElement.classList.add("lowsize"); - } - versionDomElement.textContent = `v${this.dependencyVersion.version}`; - - // Description - const description = this.dependencyVersion.description.trim(); - if (description === "") { - descriptionDomElement.style.display = "none"; - } - else { - descriptionDomElement.textContent = description; - } - - // Links - const packageHomePage = this.dependency.metadata.homepage || null; - const packageGithubPage = utils.parseRepositoryUrl( - this.dependencyVersion.repository, - packageHomePage !== null && new URL(packageHomePage).hostname === "github.com" ? packageHomePage : null - ); - - const hasNoLicense = license === "unkown license"; - this.links = { - npm: { - href: `https://www.npmjs.com/package/${this.dependencyVersion.name}/v/${this.dependencyVersion.version}`, - text: "NPM", - image: "npm-icon.svg", - showInHeader: true - }, - homepage: { - href: packageHomePage, - showInHeader: false - }, - github: { - href: packageGithubPage, - text: "GitHub", - image: "github-mark.png", - showInHeader: true - }, - unpkg: { - href: `https://unpkg.com/${this.dependencyVersion.name}@${this.dependencyVersion.version}/`, - text: "Unpkg", - icon: "icon-cubes", - showInHeader: true - }, - license: { - href: hasNoLicense ? "#" : (license.licenses[0]?.spdxLicenseLinks[0] ?? "#"), - text: hasNoLicense ? "unkown" : license.uniqueLicenseIds.join(", ").toUpperCase(), - icon: "icon-vcard", - showInHeader: true - }, - thirdParty: { - menu: this.externalToolsMenu(), - text: 'Tools', - icon: 'icon-link', - showInHeader: true - } - }; - - { - const linksFragment = document.createDocumentFragment(); - for (const [linkName, linkAttributes] of Object.entries(this.links)) { - if (!linkAttributes.showInHeader || linkAttributes.href === null) { - continue; - } - const linkImageOrIcon = linkAttributes.icon ? - utils.createDOMElement("i", { classList: [linkAttributes.icon] }) : - utils.createDOMElement("img", { - attributes: { src: linkAttributes.image, alt: linkName } - }); - - const linksChildren = [ - linkImageOrIcon, - ]; - if (linkAttributes.menu) { - linksChildren.push( - utils.createDOMElement("div", { - classList: ['package-info-header-menu'], - childs: linkAttributes.menu - }) - ); - } else { - linksChildren.push( - utils.createDOMElement("a", { - text: linkAttributes.text, - attributes: { - href: linkAttributes.href, - target: "_blank", - rel: "noopener noreferrer" - } - }) - ); - } - - linksFragment.appendChild(utils.createDOMElement("div", { - className: "link", childs: linksChildren - })); - } - - linksDomElement.appendChild(linksFragment); - } - - // Flags - { - const textContent = getFlagsEmojisInlined(this.dependencyVersion.flags, new Set(window.settings.config.ignore.flags)); - - if (textContent === "") { - flagsDomElement.style.display = "none"; - } - else { - const flagsMap = new Map( - Object.entries(this.nsn.secureDataSet.FLAGS).map(([name, row]) => [row.emoji, { ...row, name }]) - ); - - const flagsFragment = document.createDocumentFragment(); - for (const icon of textContent) { - if (flagsMap.has(icon)) { - const tooltipElement = utils.createTooltip(icon, flagsMap.get(icon).tooltipDescription); - tooltipElement.addEventListener("click", () => { - const { name } = flagsMap.get(icon); - - wiki.header.setNewActiveView("flags"); - wiki.navigation.flags.setNewActiveMenu(name); - wiki.open(); - }); - - flagsFragment.appendChild(tooltipElement); - } - } - flagsDomElement.appendChild(flagsFragment); - } - } - } - - generateLicenses() { - const licensesFragment = document.createDocumentFragment(); - if (typeof this.dependencyVersion.license === "string") { - return licensesFragment; - } - - for (const license of this.dependencyVersion.license.licenses) { - const [licenseName] = license.uniqueLicenseIds; - const [licenseLink] = license.spdxLicenseLinks; - - const spdx = Object.entries(license.spdx) - .map(([key, value]) => `${value ? "✔️" : "❌"} ${key}`); - - const boxContainer = utils.createDOMElement("div", { - classList: ["box-container-licenses"], - childs: spdx.map((text) => utils.createDOMElement("div", { text })) - }); - - const box = utils.createFileBox({ - title: licenseName, - fileName: license.from, - childs: [boxContainer], - titleHref: licenseLink, - fileHref: `${this.links.unpkg.href}${license.from}` - }); - licensesFragment.appendChild(box); - } - - return licensesFragment; - } - - generateMaintainers() { - const maintainersFragment = document.createDocumentFragment(); - for (const author of this.dependency.metadata.maintainers) { - const img = utils.createAvatarImageElement(author.email); - maintainersFragment.appendChild(utils.createDOMElement("div", { childs: [img] })); - } - - return maintainersFragment; - } - - generateWarnings() { - const warningsFragment = document.createDocumentFragment(); - const codeFetcher = new UnpkgCodeFetcher(this.links.unpkg.href); - - for (const warning of this.dependencyVersion.warnings) { - if (window.settings.warnings.has(warning.kind)) { - continue; - } - const multipleLocation = warning.kind === "encoded-literal" ? - warning.location.map((loc) => locationToString(loc)).join(" // ") : - locationToString(warning.location); - - const id = Math.random().toString(36).slice(2); - const hasNoInspection = - warning.file.includes(".min") && - warning.kind === "short-identifiers" && - warning.kind === "obfuscated-code"; - - const viewMoreElement = utils.createDOMElement("div", { - className: "view-more", - childs: [ - utils.createDOMElement("i", { className: "icon-code" }) - ] - }); - - if (this.isLocalProject || hasNoInspection) { - viewMoreElement.style.display = "none"; - } - else { - const location = warning.kind === "encoded-literal" ? warning.location[0] : warning.location; - - viewMoreElement.addEventListener("click", (event) => { - codeFetcher.fetchCodeLine(event, { file: warning.file, location, id }); - }); - } - - const boxContainer = utils.createDOMElement("div", { - classList: ["box-container-warning"], - childs: [ - utils.createDOMElement("div", { - className: "info", - childs: [ - utils.createDOMElement("p", { - className: "title", - text: "incrimined value" - }), - utils.createDOMElement("p", { - className: "value", - text: warning.value && warning.value.length > 200 ? `${warning.value.slice(0, 200)}...` : warning.value - }) - ] - }), - viewMoreElement - ] - }); - const boxPosition = utils.createDOMElement("div", { - className: "box-source-code-position", - childs: [ - utils.createDOMElement("p", { text: multipleLocation }) - ] - }); - - const box = utils.createFileBox({ - title: warning.kind, - fileName: warning.file.length > 20 ? `${warning.file.slice(0, 20)}...` : warning.file, - childs: [boxContainer, boxPosition], - titleHref: `https://github.com/NodeSecure/js-x-ray/blob/master/docs/${warning.kind}.md`, - fileHref: `${this.links.unpkg.href}${warning.file}`, - severity: warning.severity ?? "Information" - }) - warningsFragment.appendChild(box); - } - - return warningsFragment; - } - - generateScripts() { - const fragment = document.createDocumentFragment(); - const createPElement = (className, text) => utils.createDOMElement("p", { className, text }); - - const scripts = Object.entries(this.dependencyVersion.scripts); - const hideItemsLength = 4; - const hideItems = scripts.length > hideItemsLength; - - for (let id = 0; id < scripts.length; id++) { - const [key, value] = scripts[id]; - - const script = utils.createDOMElement("div", { - className: "script", - childs: [ - createPElement("name", key), - createPElement("value", value) - ] - }); - if (hideItems && id >= hideItemsLength) { - script.classList.add("hidden"); - } - - fragment.appendChild(script); - } - - if (hideItems) { - fragment.appendChild(utils.createExpandableSpan(hideItemsLength)); - } - - return fragment; - } - - generateVulnerabilities() { - const fragment = document.createDocumentFragment(); - const defaultHrefProperties = { target: "_blank", rel: "noopener noreferrer" }; - - for (const vuln of this.dependency.vulnerabilities) { - const severity = vuln.severity ?? "info"; - const vulnerableSemver = vuln.vulnerableRanges[0] ?? "N/A"; - - const header = utils.createDOMElement("div", { - childs: [ - utils.createDOMElement("div", { - classList: ["severity", severity], - text: severity.charAt(0).toUpperCase() - }), - utils.createDOMElement("p", { className: "name", text: vuln.package }), - utils.createDOMElement("span", { text: vulnerableSemver }) - ] - }); - const description = utils.createDOMElement("div", { - className: "description", - childs: [utils.createDOMElement("p", { text: vuln.title })] - }); - const links = utils.createDOMElement("div", { - className: "links", - childs: [ - utils.createDOMElement("i", { className: "icon-link" }), - utils.createDOMElement("a", { - text: vuln.url, - attributes: { href: vuln.url, ...defaultHrefProperties } - }) - ] - }); - - const vulnDomElement = utils.createDOMElement("div", { - classList: ["vuln", severity], - childs: [ - header, - description, - links - ] - }); - fragment.appendChild(vulnDomElement); - } - - return fragment; - } - - async generateOssfScorecard() { - if (!this.links.github.href) { - const scorecardMenu = document.getElementById('scorecard-menu'); - if (scorecardMenu) { - scorecardMenu.style.display = 'none'; - } - return; - } - - const github = new URL(this.links.github.href); - const repoName = github.pathname.slice(1, github.pathname.includes(".git") ? -4 : github.pathname.length); - - let data; - - try { - data = (await getJSON(`/scorecard/${repoName}`)).data; - } - catch (error) { - console.error(error); - document.getElementById('scorecard-menu').style.display = 'none'; - - return null; - } - - if (!data) { - return; - } - - const { score, checks } = data; - const checksContainerElement = utils.createDOMElement('div', { - classList: ['checks'], - }); - - function generateCheckElement(check) { - if (!check.score || check.score < 0) { - check.score = 0; - } - - const fragment = document.createDocumentFragment(); - fragment.appendChild( - utils.createDOMElement('div', { - classList: ['check'], - childs: [ - utils.createDOMElement('span', { - classList: ['name'], - text: check.name, - }), - utils.createDOMElement('div', { - classList: ['score'], - text: `${check.score}/10`, - }), - utils.createDOMElement('div', { - classList: ['info'], - childs: [ - utils.createDOMElement('div', { - classList: ['description'], - text: check.documentation.short, - }), - utils.createDOMElement('div', { - classList: ['reason'], - childs: [ - utils.createDOMElement('p', { - childs: [ - utils.createDOMElement('strong', { - text: "Reasoning", - }), - ], - }), - utils.createDOMElement('span', { - text: check.reason, - }), - ], - }), - ], - }), - ], - }) - ); - - for (const detail of check.details ?? []) { - fragment.querySelector('.info').appendChild( - utils.createDOMElement('div', { - classList: ['detail'], - text: detail, - }), - ); - } - - return fragment; - } - - for (const check of checks) { - checksContainerElement.append(generateCheckElement(check)); - } - - document.getElementById('ossf-score').innerText = score; - document.getElementById('head-score').innerText = score; - document.querySelector(".score-header .visualizer a").setAttribute('href', kScorecardVisualizer(repoName)); - - const checksNodes = checksContainerElement.childNodes; - checksNodes.forEach((check, checkKey) => { - check.addEventListener('click', () => { - if (check.children[2].classList.contains('visible')) { - check.children[2].classList.remove('visible'); - check.classList.remove('visible') - - return; - } - - check.classList.add('visible'); - check.children[2].classList.add('visible'); - - checksNodes.forEach((check, key) => { - if (checkKey !== key) { - check.classList.remove('visible'); - check.children[2].classList.remove('visible'); - } - }); - }); - }); - - return checksContainerElement; - } - - externalToolsMenu() { - return [ - utils.createDOMElement('span', { text: 'Tools' }), - utils.createDOMElement('div', { - classList: ['tools-menu'], - childs: [ - utils.createDOMElement('a', { - text: 'Snyk', - attributes: { - href: kSnykAdvisorLink + this.dependencyVersion.name, - target: "_blank", - } - }), - utils.createDOMElement('a', { - text: 'Socket.dev', - attributes: { - href: kSocketDevLink + this.dependencyVersion.name, - target: "_blank", - } - }) - ] - }) - ] - } -} diff --git a/public/js/components/bundlephobia.js b/public/js/components/package/bundlephobia.js similarity index 100% rename from public/js/components/bundlephobia.js rename to public/js/components/package/bundlephobia.js diff --git a/public/js/components/package/header.js b/public/js/components/package/header.js new file mode 100644 index 00000000..01c9de3b --- /dev/null +++ b/public/js/components/package/header.js @@ -0,0 +1,220 @@ +// Import Third-party Dependencies +import { getFlagsEmojisInlined } from "@nodesecure/vis-network"; + +// Import Internal Dependencies +import * as utils from "../../utils.js"; + +export class PackageHeader { + static ExternalLinks = { + socket: "https://socket.dev/npm/package/", + snykAdvisor: "https://snyk.io/advisor/npm-package/" + }; + + constructor(pkg) { + this.package = pkg; + this.nsn = this.package.nsn; + } + + /** + * @param {!HTMLTemplateElement} clone + */ + generate(clone) { + const { + name: packageName, + version: packageVersion, + description: packageDescription, + license, + repository, + flags + } = this.package.dependencyVersion; + + const [nameDomElement, versionDomElement, descriptionDomElement, linksDomElement, flagsDomElement] = [ + clone.querySelector(".name"), + clone.querySelector(".version"), + clone.querySelector(".package-description"), + clone.querySelector(".package-links"), + clone.querySelector(".package-flags") + ] + + // Name and Version + nameDomElement.textContent = packageName; + if (packageName.length >= 18) { + nameDomElement.classList.add("lowsize"); + } + versionDomElement.textContent = `v${packageVersion}`; + + // Description + const description = packageDescription.trim(); + if (description === "") { + descriptionDomElement.style.display = "none"; + } + else { + descriptionDomElement.textContent = description; + } + + // Links + const packageHomePage = this.package.dependency.metadata.homepage || null; + const packageGithubPage = utils.parseRepositoryUrl( + repository, + packageHomePage !== null && new URL(packageHomePage).hostname === "github.com" ? packageHomePage : null + ); + + const hasNoLicense = license === "unkown license"; + const links = { + npm: { + href: `https://www.npmjs.com/package/${packageName}/v/${packageVersion}`, + text: "NPM", + image: "npm-icon.svg", + showInHeader: true + }, + homepage: { + href: packageHomePage, + showInHeader: false + }, + github: { + href: packageGithubPage, + text: "GitHub", + image: "github-mark.png", + showInHeader: true + }, + unpkg: { + href: `https://unpkg.com/${packageName}@${packageVersion}/`, + text: "Unpkg", + icon: "icon-cubes", + showInHeader: true + }, + license: { + href: hasNoLicense ? "#" : (license.licenses[0]?.spdxLicenseLinks[0] ?? "#"), + text: hasNoLicense ? "unkown" : license.uniqueLicenseIds.join(", ").toUpperCase(), + icon: "icon-vcard", + showInHeader: true + }, + thirdParty: { + menu: this.renderToolsMenu(packageName), + text: 'Tools', + icon: 'icon-link', + showInHeader: true + } + }; + linksDomElement.appendChild(this.renderLinks(links)); + + // Flags + const flagFragment = this.renderFlags(flags); + if (flagFragment) { + flagsDomElement.appendChild(flagFragment); + } + else { + flagsDomElement.style.display = "none"; + } + + return links; + } + + renderLinks(links) { + const fragment = document.createDocumentFragment(); + for (const [linkName, linkAttributes] of Object.entries(links)) { + if (!linkAttributes.showInHeader || linkAttributes.href === null) { + continue; + } + + const linkImageOrIcon = linkAttributes.icon ? + utils.createDOMElement("i", { classList: [linkAttributes.icon] }) : + utils.createDOMElement("img", { + attributes: { src: linkAttributes.image, alt: linkName } + }); + + const linksChildren = [ + linkImageOrIcon, + ]; + if (linkAttributes.menu) { + linksChildren.push( + utils.createDOMElement("div", { + classList: ['package-info-header-menu'], + childs: linkAttributes.menu + }) + ); + } + else { + linksChildren.push( + utils.createDOMElement("a", { + text: linkAttributes.text, + attributes: { + href: linkAttributes.href, + target: "_blank", + rel: "noopener noreferrer" + } + }) + ); + } + + fragment.appendChild(utils.createDOMElement("div", { + className: "link", childs: linksChildren + })); + } + + return fragment; + } + + /** + * @param {!string} packageName + * @returns {HTMLElement[]} + */ + renderToolsMenu(packageName) { + const { snykAdvisor, socket } = PackageHeader.ExternalLinks; + + return [ + utils.createDOMElement('span', { text: 'Tools' }), + utils.createDOMElement('div', { + classList: ['tools-menu'], + childs: [ + utils.createDOMElement('a', { + text: 'Snyk', + attributes: { + href: snykAdvisor + packageName, + target: "_blank", + } + }), + utils.createDOMElement('a', { + text: 'Socket.dev', + attributes: { + href: socket + packageName, + target: "_blank", + } + }) + ] + }) + ] + } + + renderFlags(flags) { + const textContent = getFlagsEmojisInlined(flags, new Set(window.settings.config.ignore.flags)); + + if (textContent === "") { + return null; + } + + const flagsMap = new Map( + Object + .entries(this.package.nsn.secureDataSet.FLAGS) + .map(([name, row]) => [row.emoji, { ...row, name }]) + ); + + const fragment = document.createDocumentFragment(); + for (const icon of textContent) { + if (flagsMap.has(icon)) { + const tooltipElement = utils.createTooltip(icon, flagsMap.get(icon).tooltipDescription); + tooltipElement.addEventListener("click", () => { + const { name } = flagsMap.get(icon); + + wiki.header.setNewActiveView("flags"); + wiki.navigation.flags.setNewActiveMenu(name); + wiki.open(); + }); + + fragment.appendChild(tooltipElement); + } + } + + return fragment; + } +} diff --git a/public/js/components/package/package.js b/public/js/components/package/package.js new file mode 100644 index 00000000..0bfd2039 --- /dev/null +++ b/public/js/components/package/package.js @@ -0,0 +1,120 @@ +// Import Internal Dependencies +import { Bundlephobia } from "./bundlephobia.js"; +import { PackageHeader } from "./header.js"; +import * as Pannels from "./pannels/index.js"; + +export class PackageInfo { + static DOMElementName = "package-info"; + + static close() { + const domElement = document.getElementById(PackageInfo.DOMElementName); + if (domElement.classList.contains("slide-in")) { + domElement.setAttribute("class", "slide-out"); + } + } + + /** + * @param {*} dependencyVersionData + * @param {*} dependency + * @param {*} nsn + */ + constructor( + dependencyVersionData, + currentNode, + dependency, + nsn + ) { + this.codeCache = new Map(); + this.menus = new Map(); + this.nsn = nsn; + this.currentNode = currentNode; + this.dependencyVersion = dependencyVersionData; + this.dependency = dependency; + + this.initialize(); + } + + initialize() { + const packageHTMLElement = document.getElementById(PackageInfo.DOMElementName); + packageHTMLElement.innerHTML = ""; + packageHTMLElement.appendChild( + this.render() + ); + this.enableNavigation( + window.settings.config.defaultPackageMenu + ); + packageHTMLElement.setAttribute("class", "slide-in"); + + new Bundlephobia(this.dependencyVersion.name, this.dependencyVersion.version) + .fetchDataOnHttpServer() + .catch(console.error); + } + + /** + * @param {HTMLElement} navElement + * @param {number} [count=0] + */ + addNavigationSignal(navElement, count = 0) { + if (count === 0) { + navElement.classList.add("disabled"); + } + else { + const counter = navElement.querySelector(".signal"); + counter.style.display = "flex"; + counter.appendChild(document.createTextNode(count)); + } + } + + /** + * @param {!string} name + * @returns {void} + */ + enableNavigation(name) { + const div = this.menus.has(name) ? this.menus.get(name) : this.menus.get("info"); + + const isActive = div.classList.contains("active"); + const isDisabled = div.classList.contains("disabled"); + const dataTitle = div.getAttribute("data-title"); + + if (isActive || isDisabled) { + return; + } + + div.classList.add("active"); + this.activeNavigation.classList.remove("active"); + + const targetPan = document.getElementById(`pan-${name}`); + const currentPan = document.getElementById(`pan-${this.activeNavigation.getAttribute("data-menu")}`); + targetPan.classList.remove("hidden"); + currentPan.classList.add("hidden"); + document.querySelector(".container-title").textContent = dataTitle; + + this.activeNavigation = div; + } + + render() { + const template = document.getElementById("package-info-template"); + /** @type {HTMLTemplateElement} */ + const clone = document.importNode(template.content, true); + + this.activeNavigation = clone.querySelector(".package-navigation > div.active"); + for (const div of clone.querySelectorAll(".package-navigation > div")) { + const dataMenu = div.getAttribute("data-menu"); + this.menus.set(dataMenu, div); + + div.addEventListener("click", () => this.enableNavigation(dataMenu)); + } + + this.links = new PackageHeader(this).generate(clone); + + new Pannels.Overview(this).generate(clone); + new Pannels.Licenses(this).generate(clone) + new Pannels.Warnings(this).generate(clone) + new Pannels.Scripts(this).generate(clone) + new Pannels.Vulnerabilities(this).generate(clone) + new Pannels.Scorecard(this).generate(clone); + new Pannels.Files(this).generate(clone); + + return clone; + } +} diff --git a/public/js/components/package/pannels/files.js b/public/js/components/package/pannels/files.js new file mode 100644 index 00000000..5cf7b021 --- /dev/null +++ b/public/js/components/package/pannels/files.js @@ -0,0 +1,53 @@ +// Import Internal Dependencies +import * as utils from "../../../utils.js"; + +export class Files { + constructor(pkg) { + this.package = pkg; + } + + /** + * @param {!HTMLTemplateElement} clone + */ + generate(clone) { + const { name, version, composition } = this.package.dependencyVersion; + + const onclick = (_, fileName) => { + if (fileName === "../" || fileName === "./") { + return; + } + + const cleanedFile = fileName.startsWith("./") ? fileName.slice(2) : fileName; + window + .open(`https://unpkg.com/${name}@${version}/${cleanedFile}`, "_blank") + .focus(); + }; + + utils.createItemsList( + clone.getElementById("extensions"), + composition.extensions + ); + + utils.createItemsList( + clone.getElementById("tarballfiles"), + composition.files, + { onclick, hideItems: true, hideItemsLength: 3 } + ); + + utils.createItemsList( + clone.getElementById("minifiedfiles"), + composition.minified, + { onclick, hideItems: true } + ); + + utils.createItemsList( + clone.getElementById("internaldep"), + composition.required_files, + { + onclick, + hideItems: true, + hideItemsLength: 3 + } + ); + } +} diff --git a/public/js/components/package/pannels/index.js b/public/js/components/package/pannels/index.js new file mode 100644 index 00000000..dd06bd41 --- /dev/null +++ b/public/js/components/package/pannels/index.js @@ -0,0 +1,7 @@ +export * from "./licenses.js"; +export * from "./warnings.js"; +export * from "./vulnerabilities.js"; +export * from "./overview.js"; +export * from "./scripts.js"; +export * from "./scorecard.js"; +export * from "./files.js"; diff --git a/public/js/components/package/pannels/licenses.js b/public/js/components/package/pannels/licenses.js new file mode 100644 index 00000000..3100cd65 --- /dev/null +++ b/public/js/components/package/pannels/licenses.js @@ -0,0 +1,50 @@ +// Import Internal Dependencies +import * as utils from "../../../utils.js"; + +export class Licenses { + constructor(pkg) { + this.package = pkg; + } + + /** + * @param {!HTMLTemplateElement} clone + */ + generate(clone) { + clone.getElementById("pan-licenses") + .appendChild(this.renderLicenses()); + } + + renderLicenses() { + const { license: packageLicense } = this.package.dependencyVersion; + + const fragment = document.createDocumentFragment(); + if (typeof packageLicense === "string") { + return fragment; + } + + const unpkgRoot = this.package.links.unpkg.href; + for (const license of packageLicense.licenses) { + const [licenseName] = license.uniqueLicenseIds; + const [licenseLink] = license.spdxLicenseLinks; + + const spdx = Object.entries(license.spdx) + .map(([key, value]) => `${value ? "✔️" : "❌"} ${key}`); + + const boxContainer = utils.createDOMElement("div", { + classList: ["box-container-licenses"], + childs: spdx.map((text) => utils.createDOMElement("div", { text })) + }); + + const box = utils.createFileBox({ + title: licenseName, + fileName: license.from, + childs: [boxContainer], + titleHref: licenseLink, + fileHref: `${unpkgRoot}${license.from}` + }); + fragment.appendChild(box); + } + + return fragment; + } +} diff --git a/public/js/components/package/pannels/overview.js b/public/js/components/package/pannels/overview.js new file mode 100644 index 00000000..c7209129 --- /dev/null +++ b/public/js/components/package/pannels/overview.js @@ -0,0 +1,127 @@ +// Import Third-party Dependencies +import prettyBytes from "pretty-bytes"; + +// Import Internal Dependencies +import * as utils from "../../../utils.js"; + +// CONSTANTS +const kEnGBDateFormat = Intl.DateTimeFormat("en-GB", { + day: "2-digit", month: "short", year: "numeric", hour: "numeric", minute: "numeric", second: "numeric" +}); + +export class Overview { + constructor(pkg) { + this.package = pkg; + } + + get author() { + const author = this.package.dependencyVersion.author; + const flatAuthorFullname = typeof author === "string" ? author : (author?.name ?? "Unknown"); + + return flatAuthorFullname.length > 26 ? `${flatAuthorFullname.slice(0, 26)}...` : flatAuthorFullname; + } + + /** + * @param {!HTMLTemplateElement} clone + */ + generate(clone) { + const { usedBy } = this.package.dependencyVersion; + + clone.querySelector(".fields") + .appendChild(this.renderTopFields()); + clone.querySelector(".fields.releases") + .appendChild(this.renderReleases()); + + utils.createItemsList( + clone.getElementById("usedby"), + Object.keys(usedBy), + { + onclick: (_, packageName) => this.package.nsn.focusNodeByName(packageName), + hideItems: true + } + ); + + // Fetch Github stats + const githubLink = this.package.links.github; + if (githubLink.href !== null) { + this.fetchGithubStats(githubLink.href) + .catch(console.error); + } + + clone.querySelector(".package-maintainers") + .appendChild(this.renderMaintainers()); + } + + async fetchGithubStats(githubLink) { + const github = new URL(githubLink); + const repoName = github.pathname.slice( + 1, + github.pathname.includes(".git") ? -4 : github.pathname.length + ); + + const { + stargazers_count, + open_issues_count, + forks_count + } = await fetch(`https://api.github.com/repos/${repoName}`) + .then((value) => value.json()); + + document.querySelector(".github-stars").innerHTML = ` ${stargazers_count}`; + document.querySelector(".github-issues").textContent = open_issues_count; + document.querySelector(".github-forks").textContent = forks_count; + } + + renderTopFields() { + const { size, composition, engines } = this.package.dependencyVersion; + const { metadata } = this.package.dependency; + + const fragment = document.createDocumentFragment(); + + const { homepage } = this.package.links; + if (homepage.href !== null) { + fragment.appendChild(utils.createLiField("Homepage", homepage.href, { isLink: true })); + } + fragment.appendChild(utils.createLiField("Author", this.author)); + fragment.appendChild(utils.createLiField("Size on system", prettyBytes(size))); + fragment.appendChild(utils.createLiField("Number of dependencies", metadata.dependencyCount)); + fragment.appendChild(utils.createLiField("Number of files", composition.files.length)); + fragment.appendChild(utils.createLiField("README.md", composition.files.some((file) => /README\.md/gi.test(file)) ? "✔️" : "❌")); + fragment.appendChild(utils.createLiField("TS Typings", composition.files.some((file) => /d\.ts/gi.test(file)) ? "✔️" : "❌")); + if ("node" in engines) { + fragment.appendChild(utils.createLiField("Node.js compatibility", engines.node)); + } + if ("npm" in engines) { + fragment.appendChild(utils.createLiField("NPM compatibility", engines.npm)); + } + + return fragment; + } + + renderReleases() { + const { metadata } = this.package.dependency; + const fragment = document.createDocumentFragment(); + + const lastUpdatedAt = kEnGBDateFormat.format( + new Date(this.package.dependency.metadata.lastUpdateAt) + ); + + fragment.appendChild(utils.createLiField("Last release version", metadata.lastVersion)); + fragment.appendChild(utils.createLiField("Last release date", lastUpdatedAt)); + fragment.appendChild(utils.createLiField("Number of published releases", metadata.publishedCount)); + fragment.appendChild(utils.createLiField("Number of publisher(s)", metadata.publishers.length)); + + return fragment; + } + + renderMaintainers() { + const { metadata } = this.package.dependency; + const fragment = document.createDocumentFragment(); + + for (const author of metadata.maintainers) { + const img = utils.createAvatarImageElement(author.email); + fragment.appendChild(utils.createDOMElement("div", { childs: [img] })); + } + + return fragment; + } +} diff --git a/public/js/components/package/pannels/scorecard.js b/public/js/components/package/pannels/scorecard.js new file mode 100644 index 00000000..597a4d81 --- /dev/null +++ b/public/js/components/package/pannels/scorecard.js @@ -0,0 +1,163 @@ +// Import Third-party Dependencies +import { getJSON } from "@nodesecure/vis-network"; + +// Import Internal Dependencies +import * as utils from "../../../utils.js"; + +export class Scorecard { + static ExternalLinks = { + visualizer: "https://kooltheba.github.io/openssf-scorecard-api-visualizer/#/projects/github.com/" + } + + constructor(pkg) { + this.package = pkg; + } + + async fetchScorecardData(repoName) { + try { + const { data } = (await getJSON(`/scorecard/${repoName}`)); + if (!data) { + return null; + } + + return data; + } + catch (error) { + console.error(error); + + return null; + } + } + + hide() { + const scorecardMenu = document.getElementById('scorecard-menu'); + if (scorecardMenu) { + scorecardMenu.style.display = 'none'; + } + } + + /** + * @param {!HTMLTemplateElement} clone + */ + generate(clone) { + const githubURL = this.package.links.github; + if (!githubURL.href) { + return this.hide(); + } + + const github = new URL(githubURL.href); + const repoName = github.pathname.slice( + 1, + github.pathname.includes(".git") ? -4 : github.pathname.length + ); + + const pannel = clone.getElementById("pan-scorecard"); + this.fetchScorecardData(repoName).then((data) => { + if (!data) { + return this.hide(); + } + + pannel.appendChild(this.renderScorecard(data, repoName)); + document.getElementById('scorecard-menu').style.display = 'flex'; + }); + } + + renderScorecard(data, repoName) { + const { score, checks } = data; + + const container = utils.createDOMElement('div', { + classList: ['checks'], + }); + + for (const check of checks) { + container.append(generateCheckElement(check)); + } + + document.getElementById('ossf-score').innerText = score; + document.getElementById('head-score').innerText = score; + document + .querySelector(".score-header .visualizer a") + .setAttribute('href', Scorecard.ExternalLinks.visualizer + repoName); + + container.childNodes.forEach((check, checkKey) => { + check.addEventListener('click', () => { + if (check.children[2].classList.contains('visible')) { + check.children[2].classList.remove('visible'); + check.classList.remove('visible') + + return; + } + + check.classList.add('visible'); + check.children[2].classList.add('visible'); + + container.childNodes.forEach((check, key) => { + if (checkKey !== key) { + check.classList.remove('visible'); + check.children[2].classList.remove('visible'); + } + }); + }); + }); + + return container; + } +} + +function generateCheckElement(check) { + if (!check.score || check.score < 0) { + check.score = 0; + } + + const fragment = document.createDocumentFragment(); + fragment.appendChild( + utils.createDOMElement('div', { + classList: ['check'], + childs: [ + utils.createDOMElement('span', { + classList: ['name'], + text: check.name, + }), + utils.createDOMElement('div', { + classList: ['score'], + text: `${check.score}/10`, + }), + utils.createDOMElement('div', { + classList: ['info'], + childs: [ + utils.createDOMElement('div', { + classList: ['description'], + text: check.documentation.short, + }), + utils.createDOMElement('div', { + classList: ['reason'], + childs: [ + utils.createDOMElement('p', { + childs: [ + utils.createDOMElement('strong', { + text: "Reasoning", + }), + ], + }), + utils.createDOMElement('span', { + text: check.reason, + }), + ], + }), + ], + }), + ], + }) + ); + + for (const detail of check.details ?? []) { + fragment.querySelector('.info').appendChild( + utils.createDOMElement('div', { + classList: ['detail'], + text: detail, + }), + ); + } + + return fragment; +} diff --git a/public/js/components/package/pannels/scripts.js b/public/js/components/package/pannels/scripts.js new file mode 100644 index 00000000..9d42f02f --- /dev/null +++ b/public/js/components/package/pannels/scripts.js @@ -0,0 +1,145 @@ +// Import Internal Dependencies +import * as utils from "../../../utils.js"; + +export class Scripts { + static SimulationTimeout = null; + + constructor(pkg) { + this.package = pkg; + } + + /** + * @param {!HTMLTemplateElement} clone + */ + generate(clone) { + this.setupSignal(clone); + + clone.querySelector(".package-scripts") + .appendChild(this.renderScripts()); + this.renderDependencies(clone); + this.showHideDependenciesInTree(clone); + } + + /** + * @param {!HTMLTemplateElement} clone + */ + setupSignal(clone) { + const { flags } = this.package.dependencyVersion; + + if (flags.includes("hasScript")) { + this.package.addNavigationSignal( + clone.getElementById("dependencies-nav-menu"), + "!" + ); + } + } + + renderScripts() { + const fragment = document.createDocumentFragment(); + const createPElement = (className, text) => utils.createDOMElement("p", { className, text }); + + const scripts = Object.entries(this.package.dependencyVersion.scripts); + const hideItemsLength = 4; + const hideItems = scripts.length > hideItemsLength; + + for (let id = 0; id < scripts.length; id++) { + const [key, value] = scripts[id]; + + const script = utils.createDOMElement("div", { + className: "script", + childs: [ + createPElement("name", key), + createPElement("value", value) + ] + }); + if (hideItems && id >= hideItemsLength) { + script.classList.add("hidden"); + } + + fragment.appendChild(script); + } + + if (hideItems) { + fragment.appendChild(utils.createExpandableSpan(hideItemsLength)); + } + + return fragment; + } + + /** + * @param {!HTMLTemplateElement} clone + */ + renderDependencies(clone) { + const { composition } = this.package.dependencyVersion; + + utils.createItemsList( + clone.getElementById("nodedep"), + composition.required_nodejs, + { + hideItemsLength: 8, + onclick: (_, coreModuleName) => this.openNodeDocumentation(coreModuleName) + } + ); + + utils.createItemsList( + clone.getElementById("unuseddep"), + composition.unused + ); + utils.createItemsList( + clone.getElementById("missingdep"), + composition.missing + ); + + utils.createItemsList( + clone.getElementById("requireddep"), + composition.required_thirdparty, + { + onclick: (_, packageName) => this.package.nsn.focusNodeByName(packageName), + hideItems: true + } + ); + } + + openNodeDocumentation(coreModuleName) { + const name = coreModuleName.startsWith('node:') ? + coreModuleName.slice(5) : coreModuleName; + + window + .open(`https://nodejs.org/dist/latest/docs/api/${name}.html`, "_blank") + .focus(); + } + + showHideDependenciesInTree(clone) { + const btnShow = clone.getElementById("show-hide-dependency"); + if (this.package.currentNode === 0) { + btnShow.classList.add("disabled"); + + return; + } + btnShow.innerHTML = this.package.dependencyVersion.hidden ? + " show" : " hide"; + + if (this.package.dependency.metadata.dependencyCount === 0) { + btnShow.classList.add("disabled"); + } + else { + btnShow.addEventListener("click", () => { + const currBtn = document.getElementById("show-hide-dependency"); + currBtn.classList.toggle("active"); + const hidden = !this.package.dependencyVersion.hidden; + + currBtn.innerHTML = hidden ? " show" : " hide"; + + this.package.nsn.highlightNodeNeighbour(this.package.currentNode, hidden); + if (Scripts.SimulationTimeout !== null) { + clearTimeout(Scripts.SimulationTimeout); + } + Scripts.SimulationTimeout = setTimeout(() => { + this.package.nsn.network.stopSimulation(); + Scripts.SimulationTimeout = null; + }, 500); + this.package.dependencyVersion.hidden = !this.package.dependencyVersion.hidden; + }); + } + } +} diff --git a/public/js/components/package/pannels/vulnerabilities.js b/public/js/components/package/pannels/vulnerabilities.js new file mode 100644 index 00000000..dc776295 --- /dev/null +++ b/public/js/components/package/pannels/vulnerabilities.js @@ -0,0 +1,96 @@ +// Import Internal Dependencies +import * as utils from "../../../utils.js"; + +export class Vulnerabilities { + static href = { target: "_blank", rel: "noopener noreferrer" }; + + constructor(pkg) { + this.package = pkg; + } + + /** + * @param {!HTMLTemplateElement} clone + */ + setStrategy(clone) { + const strategy = window.vulnerabilityStrategy; + clone.querySelector(".vuln-strategy .name").textContent = strategy; + + /** @type {HTMLImageElement} */ + const strategyLogo = clone.querySelector(".vuln-strategy img"); + if (strategy === "none") { + strategyLogo.style.display = "none"; + } + else { + strategyLogo.src = strategy === "npm" ? "npm-icon.svg" : `${strategy}.png`; + } + } + + /** + * @param {!HTMLTemplateElement} clone + */ + generate(clone) { + this.setupSignal(clone); + this.setStrategy(clone); + + clone.querySelector(".packages-vuln") + .appendChild(this.renderVulnerabilies()); + } + + /** + * @param {!HTMLTemplateElement} clone + */ + setupSignal(clone) { + const { vulnerabilities } = this.package.dependency; + this.package.addNavigationSignal( + clone.getElementById("vulnerabilities-nav-menu"), + vulnerabilities.length + ); + } + + renderVulnerabilies() { + const { vulnerabilities } = this.package.dependency; + + const fragment = document.createDocumentFragment(); + for (const vuln of vulnerabilities) { + const severity = vuln.severity ?? "info"; + const vulnerableSemver = vuln.vulnerableRanges[0] ?? "N/A"; + + const header = utils.createDOMElement("div", { + childs: [ + utils.createDOMElement("div", { + classList: ["severity", severity], + text: severity.charAt(0).toUpperCase() + }), + utils.createDOMElement("p", { className: "name", text: vuln.package }), + utils.createDOMElement("span", { text: vulnerableSemver }) + ] + }); + const description = utils.createDOMElement("div", { + className: "description", + childs: [utils.createDOMElement("p", { text: vuln.title })] + }); + const links = utils.createDOMElement("div", { + className: "links", + childs: [ + utils.createDOMElement("i", { className: "icon-link" }), + utils.createDOMElement("a", { + text: vuln.url, + attributes: { href: vuln.url, ...Vulnerabilities.href } + }) + ] + }); + + const vulnDomElement = utils.createDOMElement("div", { + classList: ["vuln", severity], + childs: [ + header, + description, + links + ] + }); + fragment.appendChild(vulnDomElement); + } + + return fragment; + } +} diff --git a/public/js/components/package/pannels/warnings.js b/public/js/components/package/pannels/warnings.js new file mode 100644 index 00000000..5e179c67 --- /dev/null +++ b/public/js/components/package/pannels/warnings.js @@ -0,0 +1,125 @@ +// Import Third-party Dependencies +import { locationToString } from "@nodesecure/utils"; + +// Import Internal Dependencies +import { UnpkgCodeFetcher } from "../unpkgCodeFetcher.js"; +import * as utils from "../../../utils.js"; + +export class Warnings { + constructor(pkg) { + this.package = pkg; + } + + get isLocalProject() { + return this.package.currentNode === 0 || + this.package.dependencyVersion.flags.includes("isGit"); + } + + /** + * @param {!HTMLTemplateElement} clone + */ + generate(clone) { + this.setupSignal(clone); + clone.getElementById("pan-warnings") + .appendChild(this.renderWarnings()); + + clone.querySelectorAll(".open-wiki") + .forEach((element) => element.addEventListener("click", () => this.openWiki())); + } + + openWiki() { + window.wiki.header.setNewActiveView("warnings"); + window.wiki.open(); + } + + /** + * @param {!HTMLTemplateElement} clone + */ + setupSignal(clone) { + const { warnings } = this.package.dependencyVersion; + this.package.addNavigationSignal( + clone.getElementById("warnings-nav-menu"), + warnings.filter((warning) => !window.settings.warnings.has(warning.kind)).length + ); + } + + renderWarnings() { + const { warnings } = this.package.dependencyVersion; + + const fragment = document.createDocumentFragment(); + const unpkgRoot = this.package.links.unpkg.href; + + const codeFetcher = new UnpkgCodeFetcher(unpkgRoot); + + for (const warning of warnings) { + if (window.settings.warnings.has(warning.kind)) { + continue; + } + const multipleLocation = warning.kind === "encoded-literal" ? + warning.location.map((loc) => locationToString(loc)).join(" // ") : + locationToString(warning.location); + + const id = Math.random().toString(36).slice(2); + const hasNoInspection = + warning.file.includes(".min") && + warning.kind === "short-identifiers" && + warning.kind === "obfuscated-code"; + + const viewMoreElement = utils.createDOMElement("div", { + className: "view-more", + childs: [ + utils.createDOMElement("i", { className: "icon-code" }) + ] + }); + + if (this.isLocalProject || hasNoInspection) { + viewMoreElement.style.display = "none"; + } + else { + const location = warning.kind === "encoded-literal" ? warning.location[0] : warning.location; + + viewMoreElement.addEventListener("click", (event) => { + codeFetcher.fetchCodeLine(event, { file: warning.file, location, id }); + }); + } + + const boxContainer = utils.createDOMElement("div", { + classList: ["box-container-warning"], + childs: [ + utils.createDOMElement("div", { + className: "info", + childs: [ + utils.createDOMElement("p", { + className: "title", + text: "incrimined value" + }), + utils.createDOMElement("p", { + className: "value", + text: warning.value && warning.value.length > 200 ? `${warning.value.slice(0, 200)}...` : warning.value + }) + ] + }), + viewMoreElement + ] + }); + const boxPosition = utils.createDOMElement("div", { + className: "box-source-code-position", + childs: [ + utils.createDOMElement("p", { text: multipleLocation }) + ] + }); + + const box = utils.createFileBox({ + title: warning.kind, + fileName: warning.file.length > 20 ? `${warning.file.slice(0, 20)}...` : warning.file, + childs: [boxContainer, boxPosition], + titleHref: `https://github.com/NodeSecure/js-x-ray/blob/master/docs/${warning.kind}.md`, + fileHref: `${unpkgRoot}${warning.file}`, + severity: warning.severity ?? "Information" + }) + fragment.appendChild(box); + } + + return fragment; + } +} diff --git a/public/js/components/unpkgCodeFetcher.js b/public/js/components/package/unpkgCodeFetcher.js similarity index 100% rename from public/js/components/unpkgCodeFetcher.js rename to public/js/components/package/unpkgCodeFetcher.js diff --git a/public/js/master.js b/public/js/master.js index d93d7930..d0cdfbf7 100644 --- a/public/js/master.js +++ b/public/js/master.js @@ -5,7 +5,7 @@ import { NodeSecureDataSet, NodeSecureNetwork } from "@nodesecure/vis-network"; // Import UI Components import { ViewNavigation } from "./components/navigation.js"; -import { PackageInfo } from "./components/package.info.js"; +import { PackageInfo } from "./components/package/package.js"; import { Wiki } from "./components/wiki.js"; import { SearchBar } from "./components/searchbar.js"; import { Settings } from "./components/settings.js";