Skip to content

Commit

Permalink
feat: support private Sonatype Nexus Repository instances as OSS Regi…
Browse files Browse the repository at this point in the history
…stries (#49)

Signed-off-by: Paul Horton <[email protected]>
Co-authored-by: maurycupitt <[email protected]>
  • Loading branch information
madpah and maurycupitt authored Aug 10, 2023
1 parent 37298a4 commit 141e66e
Show file tree
Hide file tree
Showing 36 changed files with 306,069 additions and 237 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# production
build
*.zip

# misc
.DS_Store
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ The Sonatype Platform Browser Extension supercedes the [Nexus IQ Evaluation Exte
- [Installation on Microsoft Edge](#installation-on-microsoft-edge)
- [Supported Languages](#supported-languages)
- [Configuration](#configuration)
- [Advanced Configuration](#advanced-configuration)
- [Support for Sonatype Nexus Repository](#support-for-sonatype-nexus-repository)
- [Usage](#usage)
- [Pinning the Extension](#pinning-the-extension)
- [Opening the Extension](#opening-the-extension)
Expand Down Expand Up @@ -123,6 +125,27 @@ You can now enter your credentials for your Sonatype IQ Server and click "Connec

That's it - you have configured the Sonatype Platform Browser Extension. You can close the configuration tab. If you need to make changes to the configuration

## Advanced Configuration

### Support for Sonatype Nexus Repository

If your organisation runs one or more instances of Sonatype Nexus Repository, you can add these under Advanced Options.

![Configure Sonatype Nexus Repository](./docs/images/configure-add-nxrm.png)

> **_NOTE:_** The Sonatype Nexus Repository instance must be accessible via `http://` or `https://`
When browsing Sonatype Nexus Repository instances you have added, this extension will look to provide insight for Open Source Components for the following format repositories:

- CocoaPods
- Maven (Java)
- NPM (Javascript)
- PyPi (Python)
- R (CRAN)
- RubyGems

![Browsing Sonatype Nexus Repository](./docs/images/browse-nxrm.png)

## Usage

When you browse to a website that is supported by the Sonatype Platform Browser Extension, such as [Maven Central](https://central.sonatype.com/) the extension will assess the component you are viewing and alert you if there are known issues.
Expand Down
Binary file added docs/images/browse-nxrm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/configure-add-nxrm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/sonatype-repo-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -64,5 +64,5 @@
"matches": ["<all_urls>"]
}
],
"optional_host_permissions": ["<all_urls>"]
"optional_host_permissions": ["https://*/*", "http://*/*"]
}
321 changes: 302 additions & 19 deletions src/components/Options/General/GeneralOptionsPage.tsx

Large diffs are not rendered by default.

44 changes: 34 additions & 10 deletions src/components/Options/IQServer/IQServerOptionsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
} from '@sonatype/react-shared-components'
import React, { useEffect, useState, useContext } from 'react'
import './IQServerOptionsPage.css'
import { faExternalLink, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'
import { faExternalLink, 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'
Expand Down Expand Up @@ -63,6 +63,7 @@ export default function IQServerOptionsPage(props: IqServerOptionsPageInterface)
const [iqAuthenticated, setIqAuthenticated] = useState<boolean | undefined>()
const [iqServerApplicationList, setiqServerApplicationList] = useState<Array<ApiApplicationDTO>>([])
const setExtensionConfig = props.setExtensionConfig
const [checkingConnection, setCheckingConnection] = useState(false)

/**
* Hook to check whether we already have permissions to IQ Server Host
Expand Down Expand Up @@ -144,6 +145,7 @@ export default function IQServerOptionsPage(props: IqServerOptionsPageInterface)
}

function handleLoginCheck() {
setCheckingConnection(true)
_browser.runtime
.sendMessage({
type: MESSAGE_REQUEST_TYPE.GET_APPLICATIONS,
Expand All @@ -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)
Expand Down Expand Up @@ -368,7 +371,7 @@ export default function IQServerOptionsPage(props: IqServerOptionsPageInterface)
<strong>2)</strong> {_browser.i18n.getMessage('OPTIONS_PAGE_SONATYPE_POINT_2')}
</p>
<div className='nx-form-row'>
<NxFormGroup label={_browser.i18n.getMessage('LABEL_USERNAME') } isRequired>
<NxFormGroup label={_browser.i18n.getMessage('LABEL_USERNAME')} isRequired>
<NxStatefulTextInput
defaultValue={extensionSettings?.user}
validator={nonEmptyValidator}
Expand All @@ -385,6 +388,12 @@ export default function IQServerOptionsPage(props: IqServerOptionsPageInterface)
</NxFormGroup>
<NxButton variant='primary' onClick={handleLoginCheck}>
{_browser.i18n.getMessage('OPTIONS_PAGE_SONATYPE_BUTTON_CONNECT_IQ')}
{checkingConnection === true && (
<React.Fragment>
&nbsp;&nbsp;&nbsp;
<NxFontAwesomeIcon icon={faSpinner as IconDefinition} spin={true} />
</React.Fragment>
)}
</NxButton>
</div>
</div>
Expand Down Expand Up @@ -425,14 +434,29 @@ export default function IQServerOptionsPage(props: IqServerOptionsPageInterface)
</NxFormGroup>
{/* <NxFormGroup
label={_browser.i18n.getMessage('LABEL_SONATYPE_APPLICATION')}> */}

<a href="https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-core/2.12.1" target='_blank' className="nx-btn">Maven {_browser.i18n.getMessage('EXAMPLE')}
{' '}<NxFontAwesomeIcon icon={faExternalLink as IconDefinition} /></a>
<a href="https://www.npmjs.com/package/handlebars/v/4.7.5" target='_blank' className="nx-btn">npmjs {_browser.i18n.getMessage('EXAMPLE')}
{' '}<NxFontAwesomeIcon icon={faExternalLink as IconDefinition} /></a>
<a href="https://pypi.org/project/feedparser/6.0.10/" target='_blank' className="nx-btn">PyPI {_browser.i18n.getMessage('EXAMPLE')}
{' '}<NxFontAwesomeIcon icon={faExternalLink as IconDefinition} /></a>


<a
href='https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-core/2.12.1'
target='_blank'
className='nx-btn'>
Maven {_browser.i18n.getMessage('EXAMPLE')}{' '}
<NxFontAwesomeIcon icon={faExternalLink as IconDefinition} />
</a>
<a
href='https://www.npmjs.com/package/handlebars/v/4.7.5'
target='_blank'
className='nx-btn'>
npmjs {_browser.i18n.getMessage('EXAMPLE')}{' '}
<NxFontAwesomeIcon icon={faExternalLink as IconDefinition} />
</a>
<a
href='https://pypi.org/project/feedparser/6.0.10/'
target='_blank'
className='nx-btn'>
PyPI {_browser.i18n.getMessage('EXAMPLE')}{' '}
<NxFontAwesomeIcon icon={faExternalLink as IconDefinition} />
</a>

{/* </NxFormGroup> */}
</React.Fragment>
)}
Expand Down
46 changes: 25 additions & 21 deletions src/components/Popup/ExtensionPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
})
})
}
})
}, [])
Expand Down
110 changes: 73 additions & 37 deletions src/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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({
Expand All @@ -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 ${window.location.href}`,
},
})
} else {
sendResponse({
status: MESSAGE_RESPONSE_STATUS.SUCCESS,
data: {
purl: purl.toString(),
},
})
}
} else {
const purl = getArtifactDetailsFromDOM(repoType, window.location.href)
if (purl === undefined) {
Expand Down Expand Up @@ -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
}
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
}

const domElement = $(repoType.titleSelector)
if (domElement.length > 0) {
removeClasses(domElement)
domElement.addClass('sonatype-iq-extension-vuln')
domElement.addClass(vulnClass)
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)
}
}
}
}
})
}
}

Expand All @@ -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')
}
Loading

0 comments on commit 141e66e

Please sign in to comment.