Skip to content

Commit

Permalink
Support for Checkmarx CxSAST as source for metrics is deprecated.
Browse files Browse the repository at this point in the history
Closes #10383.
  • Loading branch information
fniessink committed Dec 12, 2024
1 parent addb79b commit bf7ace7
Show file tree
Hide file tree
Showing 14 changed files with 82 additions and 12 deletions.
3 changes: 2 additions & 1 deletion components/frontend/src/metric/MetricDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export function MetricDetails({
const metric = subject.metrics[metric_uuid]
const lastMeasurement = measurements[measurements.length - 1]
let anyError = lastMeasurement?.sources.some((source) => source.connection_error || source.parse_error)
let anyWarning = Object.values(metric.sources).some((source) => dataModel.sources[source.type].deprecated)
anyError =
anyError ||
Object.values(metric.sources ?? {}).some(
Expand Down Expand Up @@ -156,7 +157,7 @@ export function MetricDetails({
changed_fields={changed_fields}
reload={reload}
/>,
{ iconName: "server", error: Boolean(anyError) },
{ iconName: "server", error: Boolean(anyError), warning: Boolean(anyWarning) },
),
tabPane(
"Technical debt",
Expand Down
6 changes: 6 additions & 0 deletions components/frontend/src/metric/MetricDetails.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const dataModel = {
sources: {
source_type: {
name: "The source",
deprecated: true,
parameters: {},
parameter_layout: {
all: {
Expand Down Expand Up @@ -166,6 +167,11 @@ it("displays whether sources have errors", async () => {
expect(screen.getByText(/Sources/)).toHaveClass("red label")
})

it("displays whether sources have warnings", async () => {
await renderMetricDetails()
expect(screen.getByText(/Sources/)).toHaveClass("yellow label")
})

it("moves the metric", async () => {
const mockCallback = jest.fn()
await renderMetricDetails(mockCallback)
Expand Down
3 changes: 2 additions & 1 deletion components/frontend/src/source/SourceType.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useContext } from "react"
import { DataModel } from "../context/DataModel"
import { EDIT_REPORT_PERMISSION } from "../context/Permissions"
import { SingleChoiceInput } from "../fields/SingleChoiceInput"
import { Header } from "../semantic_ui_react_wrappers"
import { Header, Label } from "../semantic_ui_react_wrappers"
import { dataModelPropType, sourceTypePropType } from "../sharedPropTypes"
import { Logo } from "./Logo"

Expand All @@ -29,6 +29,7 @@ function sourceTypeOption(key, sourceType) {
<Header.Content>
<Logo logo={key} alt={sourceType.name} />
{sourceType.name}
{sourceType.deprecated && <Label color="yellow">Deprecated</Label>}
<Header.Subheader>{sourceTypeDescription(sourceType)}</Header.Subheader>
</Header.Content>
</Header>
Expand Down
8 changes: 8 additions & 0 deletions components/frontend/src/source/SourceType.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const dataModel = {
},
gitlab: {
name: "GitLab",
deprecated: true,
},
unsupported: {
name: "Unsupported",
Expand Down Expand Up @@ -68,3 +69,10 @@ it("shows the supported source versions", async () => {
})
expect(screen.queryAllByText(/Supported SonarQube versions: >=8.2/).length).toBe(1)
})

it("shows sources as deprecated if they are deprecated", async () => {
await act(async () => {
renderSourceType("violations", "sonarqube")
})
expect(screen.getAllByText(/Deprecated/).length).toBe(1)
})
3 changes: 2 additions & 1 deletion components/frontend/src/source/SourceTypeHeader.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { string } from "prop-types"

import { Header, Icon } from "../semantic_ui_react_wrappers"
import { Header, Icon, Label } from "../semantic_ui_react_wrappers"
import { sourceTypePropType } from "../sharedPropTypes"
import { slugify } from "../utils"
import { HyperLink } from "../widgets/HyperLink"
Expand All @@ -18,6 +18,7 @@ export function SourceTypeHeader({ metricTypeId, sourceTypeId, sourceType }) {
<Header.Content>
<Logo logo={sourceTypeId} alt={sourceType.name} />
{sourceType.name}
{sourceType.deprecated && <Label color="yellow">Deprecated</Label>}
<Header.Subheader>
{`${sourceTypeDescription(sourceType)} `}
<HyperLink url={url}>
Expand Down
13 changes: 12 additions & 1 deletion components/frontend/src/source/SourceTypeHeader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { render, screen } from "@testing-library/react"

import { SourceTypeHeader } from "./SourceTypeHeader"

function renderSourceTypeHeader(documentation, metricTypeId) {
function renderSourceTypeHeader(documentation, metricTypeId, deprecated) {
render(
<SourceTypeHeader
metricTypeId={metricTypeId}
Expand All @@ -11,6 +11,7 @@ function renderSourceTypeHeader(documentation, metricTypeId) {
description: "Description",
documentation: documentation,
supported_versions_description: ">=1.0",
deprecated: deprecated,
}}
/>,
)
Expand Down Expand Up @@ -42,3 +43,13 @@ it("shows the supported source versions", () => {
renderSourceTypeHeader()
expect(screen.getAllByText(/Supported Source type versions: >=1.0/).length).toBe(1)
})

it("does not show the source as deprecated if it is not deprecated", () => {
renderSourceTypeHeader()
expect(screen.queryAllByText(/Deprecated/).length).toBe(0)
})

it("shows the source as deprecated if it is deprecated", () => {
renderSourceTypeHeader({}, null, true)
expect(screen.getAllByText(/Deprecated/).length).toBe(1)
})
10 changes: 8 additions & 2 deletions components/frontend/src/widgets/TabPane.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import { Menu } from "semantic-ui-react"
import { DarkMode } from "../context/DarkMode"
import { Icon, Label, Tab } from "../semantic_ui_react_wrappers"

function FocusableTab({ error, iconName, image, label }) {
function FocusableTab({ error, iconName, image, label, warning }) {
const className = useContext(DarkMode) ? "tabbutton inverted" : "tabbutton"
const tabLabel = error ? <Label color="red">{label}</Label> : label
let tabLabel = label
if (error || warning) {
const color = error ? "red" : "yellow"
tabLabel = <Label color={color}>{label}</Label>
}
return (
<>
{iconName ? <Icon name={iconName} size="large" /> : image}
Expand All @@ -22,6 +26,7 @@ FocusableTab.propTypes = {
iconName: string,
image: element,
label: oneOfType([element, string]),
warning: bool,
}

export function tabPane(label, pane, options) {
Expand All @@ -34,6 +39,7 @@ export function tabPane(label, pane, options) {
iconName={options?.iconName}
image={options?.image}
label={label}
warning={options?.warning}
/>
</Menu.Item>
),
Expand Down
5 changes: 5 additions & 0 deletions components/frontend/src/widgets/TabPane.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ it("shows the tab red when there is an error", () => {
expect(screen.getByText("Tab").className).toEqual(expect.stringContaining("red"))
})

it("shows the tab yellow when there is a warning", () => {
render(<Tab panes={[tabPane("Tab", <p>Pane</p>, { warning: true })]} />)
expect(screen.getByText("Tab").className).toEqual(expect.stringContaining("yellow"))
})

it("shows an icon", () => {
const { container } = render(<Tab panes={[tabPane("Tab", <p>Pane</p>, { iconName: "server" })]} />)
expect(container.firstChild.firstChild.firstChild.firstChild.className).toEqual(expect.stringContaining("server"))
Expand Down
9 changes: 5 additions & 4 deletions components/shared_code/.vulture_ignore_list.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
_.subject_uuids # unused attribute (src/shared/model/report.py:36)
_.check_description # unused method (src/shared_data_model/meta/base.py:37)
_.check_scales # unused method (src/shared_data_model/meta/data_model.py:22)
_.check_sources # unused method (src/shared_data_model/meta/data_model.py:38)
_.check_subjects # unused method (src/shared_data_model/meta/data_model.py:136)
_.check_scales # unused method (src/shared_data_model/meta/data_model.py:21)
_.check_sources # unused method (src/shared_data_model/meta/data_model.py:37)
_.check_subjects # unused method (src/shared_data_model/meta/data_model.py:135)
ERROR # unused variable (src/shared_data_model/meta/entity.py:17)
LEFT # unused variable (src/shared_data_model/meta/entity.py:37)
model_config # unused variable (src/shared_data_model/meta/entity.py:45)
Expand Down Expand Up @@ -32,7 +32,8 @@
parameter_layout # unused variable (src/shared_data_model/meta/source.py:26)
issue_tracker # unused variable (src/shared_data_model/meta/source.py:28)
supported_versions_description # unused variable (src/shared_data_model/meta/source.py:29)
_.check_parameters # unused method (src/shared_data_model/meta/source.py:31)
_.check_parameters # unused method (src/shared_data_model/meta/source.py:33)
_.check_deprecation_url # unused method (src/shared_data_model/meta/source.py:50)
DOWNVOTES # unused variable (src/shared_data_model/meta/unit.py:16)
min_value # unused variable (src/shared_data_model/parameters.py:28)
_.check_unit # unused method (src/shared_data_model/parameters.py:30)
Expand Down
12 changes: 11 additions & 1 deletion components/shared_code/src/shared_data_model/meta/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ class Source(DescribedModel):
entities: dict[str, Entity] = {}
issue_tracker: bool | None = False
supported_versions_description: str | None = None # The source versions that Quality-time supports, e.g. "≥10.2"
deprecated: bool | None = False
deprecation_url: HttpUrl | None = None # URL to a GitHub issue

@model_validator(mode="after")
def check_parameters(self) -> Self:
"""Check that if the source has a landing URL parameter it also has a URL parameter."""
"""Check the consistency of the source parameters."""
if "landing_url" in self.parameters and "url" not in self.parameters:
msg = f"Source {self.name} has a landing URL but no URL"
raise ValueError(msg)
Expand All @@ -44,3 +46,11 @@ def check_parameters(self) -> Self:
)
raise ValueError(msg)
return self

@model_validator(mode="after")
def check_deprecation_url(self) -> Self:
"""Check that deprecated sources have a deprecation URL."""
if self.deprecated and self.deprecation_url is None:
msg = f"Source {self.name} is deprecated but has no deprecation URL"
raise ValueError(msg)
return self
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
name="Checkmarx CxSAST",
description="Static analysis software to identify security vulnerabilities in both custom code and open source "
"components.",
deprecated=True,
deprecation_url=HttpUrl("https://github.com/ICTU/quality-time/issues/10383"),
url=HttpUrl("https://checkmarx.com/glossary/static-application-security-testing-sast/"),
parameters={
"project": StringParameter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,18 @@ def test_missing_parameter_to_validate_on(self):
)

def test_missing_url_when_landing_url(self):
"""Test that a source that has a landing url also has a url parameter."""
"""Test that a source that has a landing URL also has a URL parameter."""
extra_model_kwargs = {
"parameters": {
"landing_url": {"name": "URL", "type": "url", "metrics": ["metric"]},
},
}
self.check_source_validation_error("Source Source has a landing URL but no URL", **extra_model_kwargs)

def test_missing_url_when_deprecated(self):
"""Test that a source that is deprecated also has a deprecation URL parameter."""
extra_model_kwargs = {"deprecated": True, "parameters": {}}
self.check_source_validation_error(
"Source Source is deprecated but has no deprecation URL",
**extra_model_kwargs,
)
4 changes: 4 additions & 0 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ If your currently installed *Quality-time* version is not the latest version, pl
- When measuring test cases with Visual Studio TRX as source, search all test category items for test case ids, instead of cutting the search short after the first match. Fixes [#10460](https://github.com/ICTU/quality-time/issues/10460).
- Correctly parse empty Axe-core JSON report. Fixes [#10487](https://github.com/ICTU/quality-time/issues/10487).

### Changed

- Support for Checkmarx CxSAST as source for metrics is deprecated. Closes [#10383](https://github.com/ICTU/quality-time/issues/10383).

## v5.20.0 - 2024-12-05

### Added
Expand Down
6 changes: 6 additions & 0 deletions docs/src/create_reference_md.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ def source_section(source: Source, source_key: str, level: int) -> str:
if source.supported_versions_description:
title = f"Supported {source.name} versions"
markdown += admonition(source.supported_versions_description, title, "important")
if source.deprecated:
deprecation_message = (
f"Support for using {source.name} as source is deprecated. "
f"See this [GitHub issue]({source.deprecation_url}) for more information."
)
markdown += admonition(deprecation_message, "Deprecated", "caution")
supported_metrics_markdown = ""
metrics = [metric for metric in DATA_MODEL.metrics.values() if source_key in metric.sources]
for metric in sorted(metrics, key=get_model_name):
Expand Down

0 comments on commit bf7ace7

Please sign in to comment.