diff --git a/.gitignore b/.gitignore index e17ff7e..07a5ca3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # production build +*.zip # misc .DS_Store diff --git a/package.json b/package.json index 224f445..9b4874e 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "classnames": "^2.3.2", "localforage": "^1.10.0", "node-window-polyfill": "^1.0.2", - "packageurl-js": "^1.0.0", + "packageurl-js": "^1.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", "ts-deepmerge": "^6.1.0", diff --git a/public/manifest.json b/public/manifest.json index 78894a0..81e33b4 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -38,10 +38,10 @@ } ], "homepage_url": "https://github.com/sonatype-nexus-community/sonatype-platform-browser-extension/", - "minimum_chrome_version": "93", + "minimum_chrome_version": "102", "offline_enabled": false, "options_page": "options.html", - "permissions": ["activeTab", "declarativeContent", "background", "storage", "tabs"], + "permissions": ["activeTab", "declarativeContent", "background", "scripting", "storage", "tabs"], "short_name": "Sonatype", "background": { "service_worker": "extension_service_worker.js" @@ -64,5 +64,5 @@ "matches": [""] } ], - "optional_host_permissions": [""] + "optional_host_permissions": ["https://*/*", "http://*/*"] } diff --git a/src/components/Options/General/GeneralOptionsPage.tsx b/src/components/Options/General/GeneralOptionsPage.tsx index 6291de6..8b03160 100644 --- a/src/components/Options/General/GeneralOptionsPage.tsx +++ b/src/components/Options/General/GeneralOptionsPage.tsx @@ -14,11 +14,31 @@ * limitations under the License. */ -import { NxFormGroup, NxFormSelect, NxPageMain, NxPageTitle, NxTile } from '@sonatype/react-shared-components' -import React, { useContext } from 'react' -import { ExtensionConfiguration } from '../../../types/ExtensionConfiguration' +import { + NxDescriptionList, + NxDivider, + NxErrorAlert, + NxFontAwesomeIcon, + NxFormGroup, + NxFormSelect, + NxGrid, + NxPageMain, + NxPageTitle, + NxSmallTag, + NxTextInput, + NxTile, + nxTextInputStateHelpers, +} from '@sonatype/react-shared-components' +import React, { useContext, useState } from 'react' +import { faSpinner } from '@fortawesome/free-solid-svg-icons' +import { IconDefinition } from '@fortawesome/fontawesome-svg-core' +import { ExtensionConfiguration, SonatypeNexusRepostitoryHost } from '../../../types/ExtensionConfiguration' import { ExtensionConfigurationContext } from '../../../context/ExtensionConfigurationContext' -import { LogLevel } from '../../../logger/Logger' +import { LogLevel, logger } from '../../../logger/Logger' +import { isHttpUriValidator } from '../../Common/Validators' +// import { simpleHash } from '../../../utils/Helpers' + +const { initialState, userInput } = nxTextInputStateHelpers // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-explicit-any const _browser: any = chrome ? chrome : browser @@ -29,6 +49,136 @@ export default function GeneralOptionsPage({ setExtensionConfig: (settings: ExtensionConfiguration) => void }) { const extensionSettings = useContext(ExtensionConfigurationContext) + const [addNxrmHostState, setAddNxrmHostState] = useState(initialState('')) + const [checkingNxrmConnection, setCheckingNxrmConnection] = useState(false) + const [errorNxrm, setErrorNxrm] = useState(undefined) + + /** + * Field onChange Handlers + */ + function handleAddNxrmHostChange(host: string) { + setAddNxrmHostState( + userInput((val) => { + if (!isHttpUriValidator(val)) { + return 'Must be a valid URL' + } else { + return null + } + }, host) + ) + } + + function enableAddNxrmHostButton(): boolean { + if (addNxrmHostState.trimmedValue.length > 4) { + if (addNxrmHostState.validationErrors !== undefined && addNxrmHostState.validationErrors?.length == 0) { + return true + } else if (addNxrmHostState.validationErrors !== null) { + return false + } else { + return true + } + } + return false + } + + const askForPermissions = () => { + logger.logMessage(`Requesting Browser Permission for: ${addNxrmHostState.trimmedValue}`, LogLevel.INFO) + + if (addNxrmHostState.trimmedValue !== undefined) { + const newNxrmHost = addNxrmHostState.trimmedValue.endsWith('/') + ? addNxrmHostState.trimmedValue + : `${addNxrmHostState.trimmedValue}/` + + const existingNxrmHostCheck = extensionSettings.sonatypeNexusRepositoryHosts.find( + (nxrm) => nxrm.id == newNxrmHost.replace('://', '-') + ) + + if (existingNxrmHostCheck !== undefined) { + logger.logMessage( + `Attempt to add duplicate Sonatype Nexus Repository Host: ${newNxrmHost}`, + LogLevel.WARN + ) + return + } + + setCheckingNxrmConnection(true) + + logger.logMessage(`Requesting permission to Origin ${newNxrmHost}`, LogLevel.DEBUG) + if (extensionSettings.sonatypeNexusRepositoryHosts.length == 0) { + _browser.scripting + .registerContentScripts([ + { + id: 'content', + css: ['/css/pagestyle.css'], + js: ['/static/js/content.js'], + matches: [`${newNxrmHost}*`], + runAt: 'document_end', + }, + ]) + .then(recordRegisteredNxrmHost(newNxrmHost)) + } else { + const allNxrmHosts = extensionSettings.sonatypeNexusRepositoryHosts + .map((nxrm) => { + return nxrm.url + }) + .concat([newNxrmHost]) + _browser.scripting + .updateContentScripts([ + { + id: 'content', + css: ['/css/pagestyle.css'], + js: ['/static/js/content.js'], + matches: allNxrmHosts.map((url: string) => { + return url + '*' + }), + runAt: 'document_end', + world: 'MAIN', + }, + ]) + .then(recordRegisteredNxrmHost(newNxrmHost)) + } + } + } + + function recordRegisteredNxrmHost(host: string): void { + logger.logMessage(`Successfully registered ${host}`, LogLevel.INFO) + _browser.permissions + .request({ + origins: [host], + }) + .then((success: boolean) => { + if (success) { + fetch(host + 'service/rest/swagger.json') + .then((response) => { + response + .json() + .then((swaggerJson) => { + logger.logMessage(`Successfully registered ${host}`, LogLevel.INFO, swaggerJson) + const newExtensionSettings = extensionSettings as ExtensionConfiguration + newExtensionSettings.sonatypeNexusRepositoryHosts.push({ + id: host.replace('://', '-'), + url: host, + version: swaggerJson['info']['version'], + }) + setExtensionConfig(newExtensionSettings) + setAddNxrmHostState(userInput(null, '')) + }) + .catch(() => { + setErrorNxrm('This does not appear to be a Sonatype Nexus Repository 3 Server.') + }) + .finally(() => { + setCheckingNxrmConnection(false) + }) + }) + .catch(() => { + setCheckingNxrmConnection(false) + }) + } else { + setErrorNxrm('You need to Allow your browser permission to add a Sonatype Nexus Repository server.') + setCheckingNxrmConnection(false) + } + }) + } function handleLogLevelChange(e) { const newExtensionSettings = extensionSettings as ExtensionConfiguration @@ -44,21 +194,75 @@ export default function GeneralOptionsPage({ -
- - - {Object.keys(LogLevel) - .filter((key) => !isNaN(Number(LogLevel[key]))) - .map((val, key) => { - return ( - - ) - })} - - -
+ +
+
+ + + + +
+ {errorNxrm !== undefined && ( +
+ {errorNxrm} +
+ )} +
+
+

Enabled Sonatype Nexus Repository servers:

+ {extensionSettings.sonatypeNexusRepositoryHosts.length == 0 && None added yet} + {extensionSettings.sonatypeNexusRepositoryHosts.length > 0 && ( + + {extensionSettings.sonatypeNexusRepositoryHosts.map( + (nxrmHost: SonatypeNexusRepostitoryHost) => { + return ( + + + {nxrmHost.url}{' '} + {nxrmHost.version} + + + ) + } + )} + + )} +
+
+ + +
+ + + {Object.keys(LogLevel) + .filter((key) => !isNaN(Number(LogLevel[key]))) + .map((val, key) => { + return ( + + ) + })} + + +
+
diff --git a/src/components/Options/IQServer/IQServerOptionsPage.tsx b/src/components/Options/IQServer/IQServerOptionsPage.tsx index 5e0cbab..5a6f3eb 100644 --- a/src/components/Options/IQServer/IQServerOptionsPage.tsx +++ b/src/components/Options/IQServer/IQServerOptionsPage.tsx @@ -30,11 +30,11 @@ import { NxTile, NxTextLink, NxDivider, - NxTag, + NxTag } from '@sonatype/react-shared-components' import React, { useEffect, useState, useContext } from 'react' import './IQServerOptionsPage.css' -import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons' +import { faQuestionCircle, faSpinner } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { MESSAGE_REQUEST_TYPE, MESSAGE_RESPONSE_STATUS, MessageResponse } from '../../../types/Message' import { DEFAULT_EXTENSION_SETTINGS, ExtensionConfiguration } from '../../../types/ExtensionConfiguration' @@ -63,6 +63,7 @@ export default function IQServerOptionsPage(props: IqServerOptionsPageInterface) const [iqAuthenticated, setIqAuthenticated] = useState() const [iqServerApplicationList, setiqServerApplicationList] = useState>([]) const setExtensionConfig = props.setExtensionConfig + const [checkingConnection, setCheckingConnection] = useState(false) /** * Hook to check whether we already have permissions to IQ Server Host @@ -94,7 +95,7 @@ export default function IQServerOptionsPage(props: IqServerOptionsPageInterface) function hasOriginPermission() { if (extensionSettings.host !== undefined && isHttpUriValidator(extensionSettings.host)) { - chrome.permissions.contains( + _browser.permissions.contains( { origins: [extensionSettings.host], }, @@ -144,6 +145,7 @@ export default function IQServerOptionsPage(props: IqServerOptionsPageInterface) } function handleLoginCheck() { + setCheckingConnection(true) _browser.runtime .sendMessage({ type: MESSAGE_REQUEST_TYPE.GET_APPLICATIONS, @@ -153,6 +155,7 @@ export default function IQServerOptionsPage(props: IqServerOptionsPageInterface) }) // eslint-disable-next-line @typescript-eslint/no-explicit-any .then((response: any) => { + setCheckingConnection(false) // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (_browser.runtime.lastError) { logger.logMessage('Error handleLoginCheck', LogLevel.ERROR) @@ -385,6 +388,12 @@ export default function IQServerOptionsPage(props: IqServerOptionsPageInterface) {_browser.i18n.getMessage('OPTIONS_PAGE_SONATYPE_BUTTON_CONNECT_IQ')} + {checkingConnection === true && ( + +     + + + )} diff --git a/src/components/Popup/ExtensionPopup.tsx b/src/components/Popup/ExtensionPopup.tsx index de33ba7..ce3ba6b 100644 --- a/src/components/Popup/ExtensionPopup.tsx +++ b/src/components/Popup/ExtensionPopup.tsx @@ -40,6 +40,7 @@ import { ApiComponentRemediationDTO, ApiLicenseLegalComponentReportDTO, } from '@sonatype/nexus-iq-api-client' +import { findRepoType } from '../../utils/UrlParsing' // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-explicit-any const _browser: any = chrome ? chrome : browser @@ -77,27 +78,30 @@ export default function ExtensionPopup() { setPopupContext((c) => merge(c, newPopupContextWithTab)) logger.logMessage(`Requesting PURL from Tab ${tab.url}`, LogLevel.DEBUG) if (tab.status != 'unloaded') { - _browser.tabs - .sendMessage(tab.id, { - type: MESSAGE_REQUEST_TYPE.CALCULATE_PURL_FOR_PAGE, - params: { - tabId: tab.id, - url: tab.url, - }, - }) - .catch((err) => { - logger.logMessage(`Error caught calculating PURL from Tab`, LogLevel.DEBUG, err) - }) - .then((response) => { - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (_browser.runtime.lastError) { - console.error('ERROR in here', _browser.runtime.lastError.message, response) - } - logger.logMessage('Calc Purl Response: ', LogLevel.INFO, response) - if (response.status == MESSAGE_RESPONSE_STATUS.SUCCESS) { - setPurl(PackageURL.fromString(response.data.purl)) - } - }) + findRepoType(tab.url).then((repoType) => { + _browser.tabs + .sendMessage(tab.id, { + type: MESSAGE_REQUEST_TYPE.CALCULATE_PURL_FOR_PAGE, + params: { + repoType: repoType, + tabId: tab.id, + url: tab.url, + }, + }) + .catch((err) => { + logger.logMessage(`Error caught calculating PURL from Tab`, LogLevel.DEBUG, err) + }) + .then((response) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (_browser.runtime.lastError) { + console.error('ERROR in here', _browser.runtime.lastError.message, response) + } + logger.logMessage('Calc Purl Response: ', LogLevel.INFO, response) + if (response.status == MESSAGE_RESPONSE_STATUS.SUCCESS) { + setPurl(PackageURL.fromString(response.data.purl)) + } + }) + }) } }) }, []) diff --git a/src/components/Popup/Popup.tsx b/src/components/Popup/Popup.tsx index 5a9d2ae..70d089e 100644 --- a/src/components/Popup/Popup.tsx +++ b/src/components/Popup/Popup.tsx @@ -101,7 +101,7 @@ function IqPopup() { }}> diff --git a/src/content.ts b/src/content.ts index fdb131e..3b20ac3 100644 --- a/src/content.ts +++ b/src/content.ts @@ -20,7 +20,8 @@ import { findRepoType } from './utils/UrlParsing' import { MESSAGE_REQUEST_TYPE, MESSAGE_RESPONSE_STATUS, MessageRequest, MessageResponseFunction } from './types/Message' import { logger, LogLevel } from './logger/Logger' import { ComponentState } from './types/Component' -import { RepoType } from './utils/Constants' +import { FORMATS, RepoType } from './utils/Constants' +import { getArtifactDetailsFromNxrmDom } from './utils/PageParsing/NexusRepositoryPageParsing' // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-explicit-any const _browser: any = chrome ? chrome : browser @@ -45,7 +46,11 @@ function handle_message_received_calculate_purl_for_page( if (request.type == MESSAGE_REQUEST_TYPE.CALCULATE_PURL_FOR_PAGE) { logger.logMessage('Content Script - Handle Received Message', LogLevel.INFO, request.type) logger.logMessage('Deriving PackageURL', LogLevel.INFO, request.params) - const repoType = findRepoType(window.location.href) + + let repoType: RepoType | undefined + if (request.params !== undefined && 'repoType' in request.params) { + repoType = request.params.repoType as RepoType + } if (repoType === undefined) { sendResponse({ @@ -54,6 +59,25 @@ function handle_message_received_calculate_purl_for_page( message: `Repository not supported: ${window.location.href}`, }, }) + } else if (repoType.repoFormat == FORMATS.NXRM) { + logger.logMessage(`Calculating PURL for a Sonatype Nexus Repository`, LogLevel.DEBUG) + const purl = getArtifactDetailsFromNxrmDom(repoType, window.location.href) + + if (purl === undefined) { + sendResponse({ + status: MESSAGE_RESPONSE_STATUS.FAILURE, + status_detail: { + message: `Unable to determine PackageURL for Sonatype Nexus Repository ${request.params}`, + }, + }) + } else { + sendResponse({ + status: MESSAGE_RESPONSE_STATUS.SUCCESS, + data: { + purl: purl.toString(), + }, + }) + } } else { const purl = getArtifactDetailsFromDOM(repoType, window.location.href) if (purl === undefined) { @@ -82,43 +106,54 @@ function handle_message_received_calculate_purl_for_page( */ function handle_message_received_propogate_component_state(request: MessageRequest): void { if (request.type == MESSAGE_REQUEST_TYPE.PROPOGATE_COMPONENT_STATE) { - logger.logMessage('Content Script - Handle Received Message', LogLevel.INFO, request.type) - if (request.params !== undefined && 'componentState' in request.params) { - const repoType = findRepoType(window.location.href) as RepoType - const componentState = request.params.componentState as ComponentState - logger.logMessage('Adding CSS Classes', LogLevel.DEBUG, ComponentState) - let vulnClass = 'sonatype-iq-extension-vuln-unspecified' - switch (componentState) { - case ComponentState.CRITICAL: - vulnClass = 'sonatype-iq-extension-vuln-critical' - break - case ComponentState.SEVERE: - vulnClass = 'sonatype-iq-extension-vuln-severe' - break - case ComponentState.MODERATE: - vulnClass = 'sonatype-iq-extension-vuln-moderate' - break - case ComponentState.LOW: - vulnClass = 'sonatype-iq-extension-vuln-low' - break - case ComponentState.NONE: - vulnClass = 'sonatype-iq-extension-vuln-none' - break - case ComponentState.EVALUATING: - vulnClass = 'sonatype-iq-extension-vuln-evaluating' - break - case ComponentState.INCOMPLETE_CONFIG: - vulnClass = 'sonatype-iq-extension-vuln-invalid-config' - break - } - - const domElement = $(repoType.titleSelector) - if (domElement.length > 0) { - removeClasses(domElement) - domElement.addClass('sonatype-iq-extension-vuln') - domElement.addClass(vulnClass) + logger.logMessage('Content Script - Handle Received Message', LogLevel.DEBUG, request.type) + findRepoType(window.location.href).then((repoType) => { + if (repoType !== undefined) { + logger.logMessage('Propogate - Repo Type', LogLevel.DEBUG, repoType) + if (request.params !== undefined && 'componentState' in request.params) { + const domElement = $(repoType.titleSelector) + const componentState = request.params.componentState as ComponentState + + if (componentState == ComponentState.CLEAR) { + removeClasses(domElement) + return + } + + logger.logMessage('Adding CSS Classes', LogLevel.DEBUG, ComponentState) + let vulnClass = 'sonatype-iq-extension-vuln-unspecified' + switch (componentState) { + case ComponentState.CRITICAL: + vulnClass = 'sonatype-iq-extension-vuln-critical' + break + case ComponentState.SEVERE: + vulnClass = 'sonatype-iq-extension-vuln-severe' + break + case ComponentState.MODERATE: + vulnClass = 'sonatype-iq-extension-vuln-moderate' + break + case ComponentState.LOW: + vulnClass = 'sonatype-iq-extension-vuln-low' + break + case ComponentState.NONE: + vulnClass = 'sonatype-iq-extension-vuln-none' + break + case ComponentState.EVALUATING: + vulnClass = 'sonatype-iq-extension-vuln-evaluating' + break + case ComponentState.INCOMPLETE_CONFIG: + vulnClass = 'sonatype-iq-extension-vuln-invalid-config' + break + } + + logger.logMessage('Propogate - domElement', LogLevel.DEBUG, domElement) + if (domElement.length > 0) { + removeClasses(domElement) + domElement.addClass('sonatype-iq-extension-vuln') + domElement.addClass(vulnClass) + } + } } - } + }) } } @@ -132,4 +167,5 @@ const removeClasses = (element) => { element.removeClass('sonatype-iq-extension-vuln-none') element.removeClass('sonatype-iq-extension-vuln-evaluating') element.removeClass('sonatype-iq-extension-vuln-invalid-config') + element.removeClass('sonatype-iq-extension-vuln-unspecified') } diff --git a/src/extension_service_worker.ts b/src/extension_service_worker.ts index 47093a5..16366a3 100644 --- a/src/extension_service_worker.ts +++ b/src/extension_service_worker.ts @@ -84,143 +84,164 @@ function enableDisableExtensionForUrl(url: string, tabId: number): void { * Check if URL matches an ecosystem we support, and only then do something * */ - const repoType = findRepoType(url) + findRepoType(url).then((repoType) => { + /** + * Make sure we get a valid PURL before we ENABLE - this may require DOM access (via Message) + */ - /** - * Make sure we get a valid PURL before we ENABLE - this may require DOM access (via Message) - */ + if (repoType !== undefined) { + // We support this Repository! + logger.logMessage(`Enabling Sonatype Browser Extension for ${url}`, LogLevel.DEBUG) + _browser.tabs + .sendMessage(tabId, { + type: MESSAGE_REQUEST_TYPE.CALCULATE_PURL_FOR_PAGE, + params: { + repoType: repoType, + tabId: tabId, + url: url, + }, + }) + .catch((err) => { + logger.logMessage(`Error caught calling CALCULATE_PURL_FOR_PAGE`, LogLevel.DEBUG, err) + }) + .then((response) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (_browser.runtime.lastError) { + logger.logMessage('Error response from CALCULATE_PURL_FOR_PAGE', LogLevel.ERROR, response) + } + logger.logMessage('Calc Purl Response: ', LogLevel.INFO, response) - if (repoType !== undefined) { - // We support this Repository! - logger.logMessage(`Enabling Sonatype Browser Extension for ${url}`, LogLevel.DEBUG) - propogateCurrentComponentState(tabId, ComponentState.EVALUATING) - _browser.tabs - .sendMessage(tabId, { - type: MESSAGE_REQUEST_TYPE.CALCULATE_PURL_FOR_PAGE, - params: { - tabId: tabId, - url: url, - }, - }) - .catch((err) => { - logger.logMessage(`Error caught calling CALCULATE_PURL_FOR_PAGE`, LogLevel.DEBUG, err) - }) - .then((response) => { - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (_browser.runtime.lastError) { - logger.logMessage('Error response from CALCULATE_PURL_FOR_PAGE', LogLevel.ERROR, response) - } - logger.logMessage('Calc Purl Response: ', LogLevel.INFO, response) - - // chrome.sidePanel.setPanelBehavior({ - // openPanelOnActionClick: true, - // }) - - if (response !== undefined && response.status == MESSAGE_RESPONSE_STATUS.SUCCESS) { - requestComponentEvaluationByPurls({ - type: MESSAGE_REQUEST_TYPE.REQUEST_COMPONENT_EVALUATION_BY_PURLS, - params: { - purls: [response.data.purl], - }, - }).then((r2) => { - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (_browser.runtime.lastError) { - logger.logMessage('Error handling requestComponentEvaluationByPurls', LogLevel.ERROR) - } - - const evaluateRequestTicketResponse = r2.data as ApiComponentEvaluationTicketDTOV2 - - const { promise, stopPolling } = pollForComponentEvaluationResult( - evaluateRequestTicketResponse.applicationId === undefined - ? '' - : evaluateRequestTicketResponse.applicationId, - evaluateRequestTicketResponse.resultId === undefined - ? '' - : evaluateRequestTicketResponse.resultId, - 1000 - ) + // chrome.sidePanel.setPanelBehavior({ + // openPanelOnActionClick: true, + // }) - promise - .then((evalResponse) => { - const componentDetails = ( - evalResponse as ApiComponentEvaluationResultDTOV2 - ).results?.pop() + if (response !== undefined && response.status == MESSAGE_RESPONSE_STATUS.SUCCESS) { + propogateCurrentComponentState(tabId, ComponentState.EVALUATING) - let componentState: ComponentState = ComponentState.UNKNOWN - if (componentDetails?.matchState != null && componentDetails.matchState != 'unknown') { - componentState = getForComponentPolicyViolations(componentDetails?.policyData) + requestComponentEvaluationByPurls({ + type: MESSAGE_REQUEST_TYPE.REQUEST_COMPONENT_EVALUATION_BY_PURLS, + params: { + purls: [response.data.purl], + }, + }) + .then((r2) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (_browser.runtime.lastError) { + logger.logMessage( + 'Error handling requestComponentEvaluationByPurls', + LogLevel.ERROR + ) } - propogateCurrentComponentState(tabId, componentState) - - _browser.action.enable(tabId, () => { - _browser.action.setIcon({ - tabId: tabId, - path: getIconForComponentState(componentState), - }) - }) + const evaluateRequestTicketResponse = r2.data as ApiComponentEvaluationTicketDTOV2 - logger.logMessage( - `${extension.name} ENABLED for ${url} : ${response.data.purl}`, - LogLevel.INFO + const { promise, stopPolling } = pollForComponentEvaluationResult( + evaluateRequestTicketResponse.applicationId === undefined + ? '' + : evaluateRequestTicketResponse.applicationId, + evaluateRequestTicketResponse.resultId === undefined + ? '' + : evaluateRequestTicketResponse.resultId, + 1000 ) - _browser.storage.local - .set({ - componentDetails: componentDetails, + promise + .then((evalResponse) => { + const componentDetails = ( + evalResponse as ApiComponentEvaluationResultDTOV2 + ).results?.pop() + + let componentState: ComponentState = ComponentState.UNKNOWN + if ( + componentDetails?.matchState != null && + componentDetails.matchState != 'unknown' + ) { + componentState = getForComponentPolicyViolations( + componentDetails?.policyData + ) + } + + propogateCurrentComponentState(tabId, componentState) + + _browser.action.enable(tabId, () => { + _browser.action.setIcon({ + tabId: tabId, + path: getIconForComponentState(componentState), + }) + }) + + logger.logMessage( + `${extension.name} ENABLED for ${url} : ${response.data.purl}`, + LogLevel.INFO + ) + + _browser.storage.local + .set({ + componentDetails: componentDetails, + }) + .then(() => { + logger.logMessage( + 'We wrote to the session', + LogLevel.DEBUG, + componentDetails + ) + }) + }) + .catch((err) => { + logger.logMessage(`Error in Poll: ${err}`, LogLevel.ERROR) }) - .then(() => { - logger.logMessage('We wrote to the session', LogLevel.DEBUG, componentDetails) + .finally(() => { + logger.logMessage('Stopping poll for results - they are in!', LogLevel.INFO) + stopPolling() }) }) .catch((err) => { - logger.logMessage(`Error in Poll: ${err}`, LogLevel.ERROR) - }) - .finally(() => { - logger.logMessage('Stopping poll for results - they are in!', LogLevel.INFO) - stopPolling() + if (err instanceof IncompleteConfigurationError) { + logger.logMessage(`Incomplete Extension Configuration: ${err}`, LogLevel.ERROR) + propogateCurrentComponentState(tabId, ComponentState.INCOMPLETE_CONFIG) + logger.logMessage( + `Disabling ${extension.name} - Incompolete Extension Configuration: ${err}`, + LogLevel.ERROR + ) + _browser.action.disable(tabId, () => { + _browser.action.setIcon({ + tabId: tabId, + path: getIconForComponentState(ComponentState.UNKNOWN), + }) + }) + } + logger.logMessage(`Error in r2: ${err}`, LogLevel.ERROR) }) - }).catch((err) => { - if (err instanceof IncompleteConfigurationError) { - logger.logMessage(`Incomplete Extension Configuration: ${err}`, LogLevel.ERROR) - propogateCurrentComponentState(tabId, ComponentState.INCOMPLETE_CONFIG) - logger.logMessage( - `Disabling ${extension.name} - Incompolete Extension Configuration: ${err}`, - LogLevel.ERROR - ) - _browser.action.disable(tabId, () => { - _browser.action.setIcon({ - tabId: tabId, - path: getIconForComponentState(ComponentState.UNKNOWN), - }) + } else { + logger.logMessage( + `Disabling Sonatype Browser Extension for ${url} - Could not determine PURL.`, + LogLevel.DEBUG + ) + propogateCurrentComponentState(tabId, ComponentState.CLEAR) + _browser.action.disable(tabId, () => { + logger.logMessage(`Sonatype Extension DISABLED for ${url}`, LogLevel.INFO) + _browser.action.setIcon({ + tabId: tabId, + path: getIconForComponentState(ComponentState.UNKNOWN), }) - } - logger.logMessage(`Error in r2: ${err}`, LogLevel.ERROR) - }) - } else { - logger.logMessage( - `Disabling Sonatype Browser Extension for ${url} - Could not determine PURL.`, - LogLevel.DEBUG - ) - _browser.action.disable(tabId, () => { - logger.logMessage(`Sonatype Extension DISABLED for ${url}`, LogLevel.INFO) - _browser.action.setIcon({ - tabId: tabId, - path: getIconForComponentState(ComponentState.UNKNOWN), }) - }) - } - }) - } else { - logger.logMessage(`Disabling Sonatype Browser Extension for ${url} - Not a supported Registry.`, LogLevel.DEBUG) - _browser.action.disable(tabId, () => { - logger.logMessage(`Sonatype Extension DISABLED for ${url}`, LogLevel.INFO) - _browser.action.setIcon({ - tabId: tabId, - path: getIconForComponentState(ComponentState.UNKNOWN), + } + }) + } else { + logger.logMessage( + `Disabling Sonatype Browser Extension for ${url} - Not a supported Registry.`, + LogLevel.DEBUG + ) + propogateCurrentComponentState(tabId, ComponentState.CLEAR) + _browser.action.disable(tabId, () => { + logger.logMessage(`Sonatype Extension DISABLED for ${url}`, LogLevel.INFO) + _browser.action.setIcon({ + tabId: tabId, + path: getIconForComponentState(ComponentState.UNKNOWN), + }) }) - }) - } + } + }) } /** diff --git a/src/messages/IqMessages.ts b/src/messages/IqMessages.ts index a0c28bd..115cbcc 100644 --- a/src/messages/IqMessages.ts +++ b/src/messages/IqMessages.ts @@ -23,6 +23,7 @@ import { GetSuggestedRemediationForComponentOwnerTypeEnum, LicenseLegalMetadataApi, GetLicenseLegalComponentReportOwnerTypeEnum, + ApiComponentDTOV2, } from '@sonatype/nexus-iq-api-client' import { logger, LogLevel } from '../logger/Logger' import { readExtensionConfiguration } from '../messages/SettingsMessages' @@ -31,6 +32,7 @@ import { IncompleteConfigurationError, InvalidConfigurationError } from '../erro import { MessageRequest, MessageResponse, MESSAGE_RESPONSE_STATUS } from '../types/Message' import { DATA_SOURCE } from '../utils/Constants' import { UserAgentHelper } from '../utils/UserAgentHelper' +import { PackageURL } from 'packageurl-js' // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-explicit-any const _browser: any = chrome ? chrome : browser @@ -67,9 +69,7 @@ export async function requestComponentEvaluationByPurls(request: MessageRequest) { applicationId: applicationId, apiComponentEvaluationRequestDTOV2: { - components: purls.map((purl) => { - return { packageUrl: purl } - }), + components: purls.map(mapPurlToComponentEvaluationRequestDTOV2), }, }, { credentials: 'omit' } @@ -90,6 +90,17 @@ export async function requestComponentEvaluationByPurls(request: MessageRequest) }) } +function mapPurlToComponentEvaluationRequestDTOV2(purl: string): ApiComponentDTOV2 { + const packageUrl = PackageURL.fromString(purl) + + if (packageUrl.qualifiers && 'checksum' in packageUrl.qualifiers) { + logger.logMessage(`PURL contains a checksum: ${purl}`, LogLevel.DEBUG) + return { packageUrl: purl } + } else { + return { packageUrl: purl } + } +} + export function pollForComponentEvaluationResult(applicationId: string, resultId: string, time: number) { let polling = false // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/types/Component.ts b/src/types/Component.ts index 38a16a7..7f915d7 100644 --- a/src/types/Component.ts +++ b/src/types/Component.ts @@ -25,6 +25,7 @@ export enum ComponentState { EVALUATING, UNKNOWN, INCOMPLETE_CONFIG, + CLEAR, } export function getMaxThreatLevelForPolicyData(policydata: ApiComponentPolicyViolationListDTOV2): number { diff --git a/src/types/ExtensionConfiguration.ts b/src/types/ExtensionConfiguration.ts index fd93447..51cc7c7 100644 --- a/src/types/ExtensionConfiguration.ts +++ b/src/types/ExtensionConfiguration.ts @@ -17,6 +17,12 @@ import { LogLevel } from '../logger/Logger' import { DATA_SOURCE } from '../utils/Constants' +export interface SonatypeNexusRepostitoryHost { + id: string + url: string + version: string +} + export interface ExtensionConfiguration { dataSource: DATA_SOURCE host?: string @@ -25,6 +31,7 @@ export interface ExtensionConfiguration { iqApplicationInternalId?: string iqApplicationPublidId?: string logLevel: LogLevel + sonatypeNexusRepositoryHosts: Array supportsFirewall: boolean supportsLifecycle: boolean supportsLifecycleAlp: boolean @@ -33,6 +40,7 @@ export interface ExtensionConfiguration { export const DEFAULT_EXTENSION_SETTINGS: ExtensionConfiguration = { dataSource: DATA_SOURCE.NEXUSIQ, logLevel: LogLevel.DEBUG, + sonatypeNexusRepositoryHosts: [], supportsFirewall: false, supportsLifecycle: false, supportsLifecycleAlp: false, diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index c6a5ff8..369ea8c 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -54,6 +54,7 @@ export const FORMATS = { nuget: 'nuget', pypi: 'pypi', rpm: 'rpm', + NXRM: 'NXRM', } export const REPOS = { diff --git a/src/utils/Helpers.ts b/src/utils/Helpers.ts index be0fb00..a1c4eb7 100644 --- a/src/utils/Helpers.ts +++ b/src/utils/Helpers.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +// import crypto from 'crypto' import { logger, LogLevel } from '../logger/Logger' // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-explicit-any @@ -26,6 +28,14 @@ function ensure(argument: T | undefined | null, message = 'This value was pro return argument } +// function simpleHash(input: string): string { +// console.log('HASHING: ', input) +// if (input.length > 0) { +// return crypto.createHash('sha1').update(input).digest('hex') +// } +// throw new Error('Cannot SHA empty string') +// } + function stripHtmlComments(html: string): string { return html.replace(/)/g, '') } diff --git a/src/utils/PageParsing/NexusRepositoryPageParsing.test.ts b/src/utils/PageParsing/NexusRepositoryPageParsing.test.ts new file mode 100644 index 0000000..c215418 --- /dev/null +++ b/src/utils/PageParsing/NexusRepositoryPageParsing.test.ts @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2019-present Sonatype, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, expect, test } from '@jest/globals' +import { readFileSync } from 'fs' +import { join } from 'path' +import { FORMATS } from '../Constants' +import { getArtifactDetailsFromNxrmDom } from './NexusRepositoryPageParsing' +import exp from 'constants' + +describe('NXRM3 Page Parsing', () => { + const repoType = { + url: 'https://repo.tld/', + repoFormat: FORMATS.NXRM, + repoID: 'NXRM-https://repo.tld/', + } + + /** + * CocoaPods FORMAT TESTS + */ + + test('#browse/browse:cocoapods-proxy:Specs%2Fc%2Fb%2F0%2FIgor%2F0.5.0', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-cocoapod-folder.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:cocoapods-proxy:Specs%2Fc%2Fb%2F0%2FIgor%2F0.5.0' + ) + + expect(packageURL).toBeUndefined() + }) + + test('#browse/browse:cocoapods-proxy:Specs%2Fc%2Fb%2F0%2FIgor%2F0.5.0%2FIgor.podspec.json', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-cocoapod.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:cocoapods-proxy:Specs%2Fc%2Fb%2F0%2FIgor%2F0.5.0%2FIgor.podspec.json' + ) + + expect(packageURL).toBeDefined() + expect(packageURL?.type).toBe(FORMATS.cocoapods) + expect(packageURL?.namespace).toBeUndefined() + expect(packageURL?.name).toBe('Igor') + expect(packageURL?.version).toBe('0.5.0') + expect(packageURL?.qualifiers).toBeUndefined() + }) + + /** + * MAVEN(2) FORMAT TESTS + */ + + test('#browse/browse:maven-central:commons-collections', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-maven2-folder.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:maven-central:commons-collections' + ) + + expect(packageURL).toBeUndefined() + }) + + test('#browse/browse:maven-central:commons-collections%2Fcommons-collections%2F2.0', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-maven2-component.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + '#browse/browse:maven-central:commons-collections%2Fcommons-collections%2F2.0' + ) + + expect(packageURL).toBeUndefined() + }) + + test('#browse/browse:maven-central:commons-logging%2Fcommons-logging%2F1.1.3%2Fcommons-logging-1.1.3.jar', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-maven2.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + '#browse/browse:maven-central:commons-logging%2Fcommons-logging%2F1.1.3%2Fcommons-logging-1.1.3.jar' + ) + + expect(packageURL).toBeDefined() + expect(packageURL?.type).toBe(FORMATS.maven) + expect(packageURL?.namespace).toBe('commons-logging') + expect(packageURL?.name).toBe('commons-logging') + expect(packageURL?.version).toBe('1.1.3') + expect(packageURL?.qualifiers).toEqual({ type: 'jar' }) + }) + + test('#browse/browse:maven-central:org%2Fapache%2Flogging%2Flog4j%2Flog4j-core%2F2.12.0%2Flog4j-core-2.12.0.jar', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-maven2.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + '#browse/browse:maven-central:org%2Fapache%2Flogging%2Flog4j%2Flog4j-core%2F2.12.0%2Flog4j-core-2.12.0.jar' + ) + + expect(packageURL).toBeDefined() + expect(packageURL?.type).toBe(FORMATS.maven) + expect(packageURL?.namespace).toBe('org.apache.logging.log4j') + expect(packageURL?.name).toBe('log4j-core') + expect(packageURL?.version).toBe('2.12.0') + expect(packageURL?.qualifiers).toEqual({ type: 'jar' }) + }) + + /** + * NuGet FORMAT TESTS + */ + + test('#browse/browse:nuget-proxy:azure.core', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-nuget-folder.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:nuget-proxy:azure.core' + ) + + expect(packageURL).toBeUndefined() + }) + + test('#browse/browse:nuget-proxy:azure.core%2F1.0.2', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-nuget-version.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:nuget-proxy:azure.core%2F1.0.2' + ) + + expect(packageURL).toBeUndefined() + }) + + test('#browse/browse:nuget-proxy:azure.core%2F1.0.2%2Fazure.core-1.0.2.nupkg', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-nuget.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:nuget-proxy:azure.core%2F1.0.2%2Fazure.core-1.0.2.nupkg' + ) + + expect(packageURL).toBeDefined() + expect(packageURL?.type).toBe(FORMATS.nuget) + expect(packageURL?.namespace).toBeUndefined() + expect(packageURL?.name).toBe('azure.core') + expect(packageURL?.version).toBe('1.0.2') + expect(packageURL?.qualifiers).toEqual({ checksum: 'sha1:4fd5ec10371c391919dddda1849436aaa41893b9' }) + expect(packageURL?.toString()).toBe( + 'pkg:nuget/azure.core@1.0.2?checksum=sha1%3A4fd5ec10371c391919dddda1849436aaa41893b9' + ) + }) + + /** + * NPM FORMAT TESTS + */ + + test('#browse/browse:npm-proxy:%40sonatype', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-npm-folder.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:npm-proxy:%40sonatype' + ) + + expect(packageURL).toBeUndefined() + }) + + test('#browse/browse:npm-proxy:braces%2Fbraces-1.8.5.tgz', () => { + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:npm-proxy:braces%2Fbraces-1.8.5.tgz' + ) + + expect(packageURL).toBeDefined() + expect(packageURL?.type).toBe(FORMATS.npm) + expect(packageURL?.namespace).toBeUndefined() + expect(packageURL?.name).toBe('braces') + expect(packageURL?.version).toBe('1.8.5') + }) + + test('#browse/browse:npm-proxy:%40sonatype%2Fnexus-iq-api-client', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-npm-no-version.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:npm-proxy:%40sonatype%2Fnexus-iq-api-client' + ) + + expect(packageURL).toBeUndefined() + }) + + test('#browse/browse:npm-proxy:%40sonatype%2Fpolicy-demo%2Fpolicy-demo-2.0.0.tgz', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-npm.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:npm-proxy:%40sonatype%2Fpolicy-demo%2Fpolicy-demo-2.0.0.tgz' + ) + + expect(packageURL).toBeDefined() + expect(packageURL?.type).toBe(FORMATS.npm) + expect(packageURL?.namespace).toBe('%40sonatype') + expect(packageURL?.name).toBe('policy-demo') + expect(packageURL?.version).toBe('2.0.0') + }) + + /** + * PYPI FORMAT TESTS + */ + + test('#browse/browse:pupy-proxy:asynctest', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-pypi-folder.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:pupy-proxy:asynctest' + ) + + expect(packageURL).toBeUndefined() + }) + + test('#browse/browse:pupy-proxy:babel%2F2.12.1', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-pypi-component.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:pupy-proxy:babel%2F2.12.1' + ) + + expect(packageURL).toBeUndefined() + }) + + test('#browse/browse:pupy-proxy:asynctest%2F0.13.0%2Fasynctest-0.13.0-py3-none-any.whl', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-pypi.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + '#browse/browse:pupy-proxy:asynctest%2F0.13.0%2Fasynctest-0.13.0-py3-none-any.whl' + ) + + expect(packageURL).toBeDefined() + expect(packageURL?.type).toBe(FORMATS.pypi) + expect(packageURL?.namespace).toBeUndefined() + expect(packageURL?.name).toBe('asynctest') + expect(packageURL?.version).toBe('0.13.0') + expect(packageURL?.qualifiers).toEqual({ extension: 'tar.gz' }) + expect(packageURL?.toString()).toBe('pkg:pypi/asynctest@0.13.0?extension=tar.gz') + }) + + /** + * RubyGem FORMAT TESTS + */ + + test('#browse/browse:ruby-proxy:logstash-input-tcp', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-ruby-folder.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:ruby-proxy:logstash-input-tcp' + ) + + expect(packageURL).toBeUndefined() + }) + + test('#browse/browse:ruby-proxy:logstash-input-tcp%2F0.1.0', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-ruby-component.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:ruby-proxy:logstash-input-tcp%2F0.1.0' + ) + + expect(packageURL).toBeUndefined() + }) + + test('#browse/browse:ruby-proxy:logstash-input-tcp%2F0.1.1%2Flogstash-input-tcp-0.1.1.gemspec.rz', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-ruby.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:ruby-proxy:logstash-input-tcp%2F0.1.1%2Flogstash-input-tcp-0.1.1.gemspec.rz' + ) + + expect(packageURL).toBeDefined() + expect(packageURL?.type).toBe(FORMATS.gem) + expect(packageURL?.namespace).toBeUndefined() + expect(packageURL?.name).toBe('logstash-input-tcp') + expect(packageURL?.version).toBe('0.1.1') + expect(packageURL?.qualifiers).toBeUndefined() + expect(packageURL?.toString()).toBe('pkg:gem/logstash-input-tcp@0.1.1') + }) + + test('#browse/browse:ruby-proxy:logstash-input-tcp%2F6.0.9%2Flogstash-input-tcp-6.0.9-java.gemspec.rz', () => { + const html = readFileSync(join(__dirname, 'testdata/nxrm3/browse-ruby-with-platform.html')) + + window.document.body.innerHTML = html.toString() + + const packageURL = getArtifactDetailsFromNxrmDom( + repoType, + 'https://repo.tld/#browse/browse:ruby-proxy:logstash-input-tcp%2F6.0.9%2Flogstash-input-tcp-6.0.9-java.gemspec.rz' + ) + + expect(packageURL).toBeDefined() + expect(packageURL?.type).toBe(FORMATS.gem) + expect(packageURL?.namespace).toBeUndefined() + expect(packageURL?.name).toBe('logstash-input-tcp') + expect(packageURL?.version).toBe('6.0.9') + expect(packageURL?.qualifiers).toEqual({ platform: 'java' }) + expect(packageURL?.toString()).toBe('pkg:gem/logstash-input-tcp@6.0.9?platform=java') + }) +}) diff --git a/src/utils/PageParsing/NexusRepositoryPageParsing.ts b/src/utils/PageParsing/NexusRepositoryPageParsing.ts new file mode 100644 index 0000000..e2a09ca --- /dev/null +++ b/src/utils/PageParsing/NexusRepositoryPageParsing.ts @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2019-present Sonatype, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import $ from 'cash-dom' +import { PackageURL } from 'packageurl-js' +import { FORMATS, RepoType } from '..//Constants' +import { LogLevel, logger } from '../../logger/Logger' +import { generatePackageURLComplete } from './PurlUtils' + +const DOM_SELECTOR_BROWSE_REPO_FORMAT = 'div.nx-info > table > tbody > tr:nth-child(2) > td.nx-info-entry-value' +const DOM_SELECTOR_BROWSE_SHA1_SUM = + 'div.nx-coreui-component-assetattributes table:nth-child(2) tr:nth-child(2) td:nth-child(2) div' + +export const getArtifactDetailsFromNxrmDom = (repoType: RepoType, url: string): PackageURL | undefined => { + logger.logMessage('In getArtifactDetailsFromNxrmDom', LogLevel.DEBUG, repoType, url) + + const uriPath = url.replace(repoType.url, '') + logger.logMessage('Normalised URI Path: ', LogLevel.DEBUG, uriPath) + + if (uriPath.startsWith('#browse/browse')) { + // Browse Mode + const formatDomNode = $(DOM_SELECTOR_BROWSE_REPO_FORMAT) + if (formatDomNode === undefined) { + return undefined + } + + const format = formatDomNode.first().text().trim() + logger.logMessage(`Detected format ${format}`, LogLevel.DEBUG, formatDomNode) + + switch (format) { + case FORMATS.cocoapods: + return attemptPackageUrlCocoaPodsUrl(uriPath) + case 'maven2': + return attemptPackageUrlMavenUrl(uriPath) + case FORMATS.nuget: + return attemptPackageUrlNuGetUrl(uriPath) + case FORMATS.npm: + return attemptPackageUrlNpmUrl(uriPath) + case FORMATS.pypi: + return attemptPackageUrlPyPiUrl(uriPath) + case 'rubygems': + return attemptPackageUrlRubyUrl(uriPath) + } + } else if (uriPath.startsWith('#/browse/search')) { + // Search Mode + } + + return undefined +} + +function attemptPackageUrlCocoaPodsUrl(uriPath: string): PackageURL | undefined { + // #browse/browse:cocoapods-proxy:Specs%2Fc%2Fb%2F0%2FIgor%2F0.5.0%2FIgor.podspec.json + // ==> Specs/c/b/0/Igor/0.5.0/Igor.podspec.json + const urlParts = uriPath.split(':') + const componentParts = decodeURIComponent(urlParts.pop() as string).split('/') + + if (componentParts.length >= 4) { + const filename = componentParts.pop() as string + if (!filename.endsWith('.podspec.json')) { + return undefined + } + + const version = componentParts.pop() as string + const componentName = componentParts.pop() as string + + return generatePackageURLComplete( + FORMATS.cocoapods, + encodeURIComponent(componentName), + encodeURIComponent(version), + undefined, + undefined, + undefined + ) + } + + return undefined +} + +function attemptPackageUrlMavenUrl(uriPath: string): PackageURL | undefined { + // #browse/browse:maven-central:org%2Fapache%2Flogging%2Flog4j%2Flog4j-core%2F2.12.0%2Flog4j-core-2.12.0.jar + const urlParts = uriPath.split(':') + const componentParts = decodeURIComponent(urlParts.pop() as string).split('/') + + if (componentParts.length >= 4) { + const fileExtension = (componentParts.pop() as string).split('.').pop() as string + const version = componentParts.pop() as string + const componentName = componentParts.pop() as string + const componentGroup = componentParts.join('.') + + return generatePackageURLComplete( + FORMATS.maven, + encodeURIComponent(componentName), + encodeURIComponent(version), + encodeURIComponent(componentGroup), + { type: fileExtension }, + undefined + ) + } + return undefined +} + +function attemptPackageUrlNpmUrl(uriPath: string): PackageURL | undefined { + // #browse/browse:npm-proxy:%40sonatype%2Fpolicy-demo%2Fpolicy-demo-2.0.0.tgz + const urlParts = uriPath.split(':') + const componentParts = decodeURIComponent(urlParts.pop() as string).split('/') + + if (componentParts.length >= 2) { + const filename = componentParts.pop() as string + if (!filename.endsWith('.tgz')) { + return undefined + } + const componentName = componentParts.pop() as string + const componentNamespace = componentParts.pop() as string + + const filenameParts = (filename.split('-').pop() as string).split('.') + filenameParts.pop() + const version = filenameParts.join('.') + + return generatePackageURLComplete( + FORMATS.npm, + encodeURIComponent(componentName), + encodeURIComponent(version), + componentNamespace === undefined ? undefined : componentNamespace, + undefined, + undefined + ) + } + return undefined +} + +function attemptPackageUrlNuGetUrl(uriPath: string): PackageURL | undefined { + // #browse/browse:nuget-proxy:azure.core%2F1.0.2%2Fazure.core-1.0.2.nupkg + const urlParts = uriPath.split(':') + const componentParts = decodeURIComponent(urlParts.pop() as string).split('/') + + if (componentParts.length >= 2) { + const sha1DomNode = $(DOM_SELECTOR_BROWSE_SHA1_SUM) + + if (sha1DomNode === undefined) { + return undefined + } + + const sha1Hash = sha1DomNode.first().text().trim() + logger.logMessage(`Detected SHA-1: ${sha1Hash}`, LogLevel.DEBUG, sha1DomNode) + + const filename = componentParts.pop() as string + if (!filename.endsWith('.nupkg')) { + return undefined + } + const componentVersion = componentParts.pop() as string + const componentName = componentParts.pop() as string + + return generatePackageURLComplete( + FORMATS.nuget, + encodeURIComponent(componentName), + encodeURIComponent(componentVersion), + undefined, + { checksum: `sha1:${sha1Hash}` }, + undefined + ) + } + return undefined +} + +function attemptPackageUrlPyPiUrl(uriPath: string): PackageURL | undefined { + // #browse/browse:pupy-proxy:babel%2F2.12.1%2FBabel-2.12.1-py3-none-any.whl + const urlParts = uriPath.split(':') + const componentParts = decodeURIComponent(urlParts.pop() as string).split('/') + + if (componentParts.length >= 3) { + componentParts.pop() as string // drop filename + const version = componentParts.pop() as string + const componentName = componentParts.pop() as string + + return generatePackageURLComplete( + FORMATS.pypi, + encodeURIComponent(componentName), + encodeURIComponent(version), + undefined, + { extension: 'tar.gz' }, + undefined + ) + } + return undefined +} + +function attemptPackageUrlRubyUrl(uriPath: string): PackageURL | undefined { + // #browse/browse:ruby-proxy:logstash-input-tcp%2F6.0.9%2Flogstash-input-tcp-6.0.9-java.gemspec.rz + const urlParts = uriPath.split(':') + const componentParts = decodeURIComponent(urlParts.pop() as string).split('/') + + if (componentParts.length >= 3) { + const filename = componentParts.pop() as string + const version = componentParts.pop() as string + const componentName = componentParts.pop() as string + + let platform: string | undefined = undefined + const detectPlatformInFilename = filename.replace(`${componentName}-`, '').replace(`${version}`, '') + if (detectPlatformInFilename != '.gemspec.rz') { + platform = (detectPlatformInFilename.split('.').shift() as string).substring(1) + } + + return generatePackageURLComplete( + FORMATS.gem, + encodeURIComponent(componentName), + encodeURIComponent(version), + undefined, + platform !== undefined ? { platform: platform } : undefined, + undefined + ) + } + + return undefined +} diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-cocoapod-folder.html b/src/utils/PageParsing/testdata/nxrm3/browse-cocoapod-folder.html new file mode 100644 index 0000000..6b7cb9f --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-cocoapod-folder.html @@ -0,0 +1,14666 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-cocoapod.html b/src/utils/PageParsing/testdata/nxrm3/browse-cocoapod.html new file mode 100644 index 0000000..07061f3 --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-cocoapod.html @@ -0,0 +1,14666 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-maven2-component.html b/src/utils/PageParsing/testdata/nxrm3/browse-maven2-component.html new file mode 100644 index 0000000..03d98ce --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-maven2-component.html @@ -0,0 +1,16542 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-maven2-folder.html b/src/utils/PageParsing/testdata/nxrm3/browse-maven2-folder.html new file mode 100644 index 0000000..8273996 --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-maven2-folder.html @@ -0,0 +1,16140 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-maven2.html b/src/utils/PageParsing/testdata/nxrm3/browse-maven2.html new file mode 100644 index 0000000..00dd322 --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-maven2.html @@ -0,0 +1,17855 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-npm-folder.html b/src/utils/PageParsing/testdata/nxrm3/browse-npm-folder.html new file mode 100644 index 0000000..ae66f1c --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-npm-folder.html @@ -0,0 +1,16897 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-npm-no-version.html b/src/utils/PageParsing/testdata/nxrm3/browse-npm-no-version.html new file mode 100644 index 0000000..a6be3bd --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-npm-no-version.html @@ -0,0 +1,16418 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-npm.html b/src/utils/PageParsing/testdata/nxrm3/browse-npm.html new file mode 100644 index 0000000..69aa129 --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-npm.html @@ -0,0 +1,17937 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-nuget-folder.html b/src/utils/PageParsing/testdata/nxrm3/browse-nuget-folder.html new file mode 100644 index 0000000..1e1a47c --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-nuget-folder.html @@ -0,0 +1,17718 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-nuget-version.html b/src/utils/PageParsing/testdata/nxrm3/browse-nuget-version.html new file mode 100644 index 0000000..e666aac --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-nuget-version.html @@ -0,0 +1,18097 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-nuget.html b/src/utils/PageParsing/testdata/nxrm3/browse-nuget.html new file mode 100644 index 0000000..32220df --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-nuget.html @@ -0,0 +1,18098 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-pypi-component.html b/src/utils/PageParsing/testdata/nxrm3/browse-pypi-component.html new file mode 100644 index 0000000..d6d13d8 --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-pypi-component.html @@ -0,0 +1,16787 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-pypi-folder.html b/src/utils/PageParsing/testdata/nxrm3/browse-pypi-folder.html new file mode 100644 index 0000000..86688d2 --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-pypi-folder.html @@ -0,0 +1,16159 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-pypi.html b/src/utils/PageParsing/testdata/nxrm3/browse-pypi.html new file mode 100644 index 0000000..337168d --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-pypi.html @@ -0,0 +1,19309 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-ruby-component.html b/src/utils/PageParsing/testdata/nxrm3/browse-ruby-component.html new file mode 100644 index 0000000..7b1b886 --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-ruby-component.html @@ -0,0 +1,16623 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-ruby-folder.html b/src/utils/PageParsing/testdata/nxrm3/browse-ruby-folder.html new file mode 100644 index 0000000..bb4fbf3 --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-ruby-folder.html @@ -0,0 +1,16669 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-ruby-with-platform.html b/src/utils/PageParsing/testdata/nxrm3/browse-ruby-with-platform.html new file mode 100644 index 0000000..6b4ba6b --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-ruby-with-platform.html @@ -0,0 +1,17637 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/PageParsing/testdata/nxrm3/browse-ruby.html b/src/utils/PageParsing/testdata/nxrm3/browse-ruby.html new file mode 100644 index 0000000..9355ab0 --- /dev/null +++ b/src/utils/PageParsing/testdata/nxrm3/browse-ruby.html @@ -0,0 +1,17694 @@ + + + Browse - Nexus Repository Manager + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/src/utils/UrlParsing.ts b/src/utils/UrlParsing.ts index f75dc30..d707f46 100644 --- a/src/utils/UrlParsing.ts +++ b/src/utils/UrlParsing.ts @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { RepoType, REPO_TYPES } from './Constants' +import { RepoType, REPO_TYPES, FORMATS } from './Constants' import { LogLevel, logger } from '../logger/Logger' +import { readExtensionConfiguration } from '../messages/SettingsMessages' +import { MESSAGE_RESPONSE_STATUS } from '../types/Message' +import { ExtensionConfiguration } from '../types/ExtensionConfiguration' -const findRepoType = (url: string): RepoType | undefined => { +const findRepoType = async (url: string): Promise => { for (let i = 0; i < REPO_TYPES.length; i++) { if (url.search(REPO_TYPES[i].url) >= 0) { logger.logMessage(`Current URL ${url} matches ${REPO_TYPES[i].repoID}`, LogLevel.INFO) @@ -24,7 +27,30 @@ const findRepoType = (url: string): RepoType | undefined => { } } - return undefined + return await findNxrmRepoType(url) +} + +function findNxrmRepoType(url: string): Promise { + return readExtensionConfiguration().then((response) => { + logger.logMessage(`Checking if ${url} matches a configured Sonatype Nexus Repository`, LogLevel.DEBUG) + if (response.status == MESSAGE_RESPONSE_STATUS.SUCCESS) { + const extensionConfig = response.data as ExtensionConfiguration + if (extensionConfig !== undefined && extensionConfig.sonatypeNexusRepositoryHosts.length > 0) { + for (const nxrmHost of extensionConfig.sonatypeNexusRepositoryHosts) { + logger.logMessage(`Checking ${url} against ${nxrmHost.url}...`, LogLevel.DEBUG) + if (url.startsWith(nxrmHost.url)) { + return { + url: nxrmHost.url, + repoFormat: FORMATS.NXRM, + repoID: `NXRM-${nxrmHost.id}`, + titleSelector: "[id^='nx-coreui-component-componentassetinfo'][id$='header-title-textEl']", + } + } + } + } + } + return undefined + }) } export { findRepoType } diff --git a/yarn.lock b/yarn.lock index 4ba3ba1..17ca76a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6147,7 +6147,7 @@ p-try@^2.0.0: resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -packageurl-js@^1.0.0: +packageurl-js@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/packageurl-js/-/packageurl-js-1.0.2.tgz#c568a569848c66be8f2b467ac41b0f1427672b00" integrity sha512-fWC4ZPxo80qlh3xN5FxfIoQD3phVY4+EyzTIqyksjhKNDmaicdpxSvkWwIrYTtv9C1/RcUN6pxaTwGmj2NzS6A==