Skip to content

Commit d5b6afa

Browse files
authored
Merge pull request #13 from WebFuzzing/nullable-optionals
nullable optionals
2 parents 164a796 + 72de59a commit d5b6afa

File tree

10 files changed

+85
-33
lines changed

10 files changed

+85
-33
lines changed

src/main/resources/wfc/schemas/report.yaml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ properties:
5151
#OPTIONAL
5252
extra:
5353
description: "Extra, optional coverage information, collected by different tools."
54-
type: array
54+
type: [array, "null"]
5555
items:
5656
$ref: "#/$defs/Coverage"
5757

@@ -75,7 +75,7 @@ $defs:
7575
context:
7676
description: "An optional context for the fault. The same fault type could be manifested in different ways, \
7777
and we use this property to differentiate among them."
78-
type: string
78+
type: [string, "null"]
7979
required: ["code"]
8080
TestFilePath:
8181
description: "A relative path used to unique locate a test suite file."
@@ -89,16 +89,17 @@ $defs:
8989
testCaseId:
9090
$ref: "#/$defs/TestCaseId"
9191
httpStatus:
92-
description: "As in a test case the same endpoint could be called more than once, here we report all of the
93-
obtained HTTP status codes"
94-
type: array
92+
description: "As in a test case the same endpoint could be called more than once, here we report all of the \
93+
obtained HTTP status codes. If for any reason a call does not return any response (e.g., the TCP connection \
94+
does timeout), then this HTTP status array would be either null or empty."
95+
type: [array,"null"]
9596
items:
9697
$ref: "#/$defs/HttpStatus"
97-
minItems: 1
98+
minItems: 0
9899
uniqueItems: true
99100
required: ["endpointId","testCaseId","httpStatus"]
100101
HttpStatus:
101-
type: integer
102+
type: [integer,"null"]
102103
minimum: 0
103104
maximum: 599
104105
FoundFault:
@@ -198,6 +199,6 @@ $defs:
198199
minimum: 0
199200
total:
200201
description: "Optional number of all testing targets for this criterion. For some criteria, this number can be unknown."
201-
type: integer
202+
type: [integer, "null"]
202203
minimum: 0
203204
required: ["name","covered"]

web-report/src/AppProvider.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,29 @@ export const AppProvider = ({ children }: AppProviderProps) => {
4444
setInvalidReportErrors(report.error.issues);
4545
return;
4646
}
47-
setData(jsonData);
47+
48+
const result = jsonData?.problemDetails?.rest?.coveredHttpStatus?.map(status => {
49+
const httpStatus = status?.httpStatus ?? [null];
50+
const httpStatusArray = Array.isArray(httpStatus) ? httpStatus : [httpStatus];
51+
const updatedHttpStatus = httpStatusArray.map(code => code === null ? -1 : code);
52+
return {
53+
...status,
54+
httpStatus: updatedHttpStatus
55+
};
56+
}) ?? [];
57+
58+
const updatedJsonData = {
59+
...jsonData,
60+
problemDetails: {
61+
...jsonData.problemDetails,
62+
rest: {
63+
...jsonData.problemDetails.rest,
64+
coveredHttpStatus: result
65+
}
66+
}
67+
} as WebFuzzingCommonsReport;
68+
69+
setData(updatedJsonData);
4870
} catch (error: Error | unknown) {
4971
if (error instanceof Error) {
5072
setError("Could not load the report file. Please check if the file exists and is accessible in main folder.");

web-report/src/components/EndpointAccordion.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export const EndpointAccordion: React.FC<IEndpointAccordionProps> = ({
6666
<div className="flex flex-wrap justify-end gap-2 mr-4">
6767
{sortedStatusCodes.map((code, idx) => (
6868
<Badge key={`_${idx}`} className={`${getColor(code.code, true, false)} font-mono`}>
69-
H{code.code}
69+
{code.code == -1 ? "NO-RESPONSE" : `H${code.code}`}
7070
</Badge>
7171
))}
7272
{sortedFaults.map((code, idx) => (
@@ -87,7 +87,7 @@ export const EndpointAccordion: React.FC<IEndpointAccordionProps> = ({
8787
setIsFault(false);
8888
}}
8989
className={`${getColor(code.code, true, false)} ${getSelectedStyle(code.code, false)} hover:bg-green-600 cursor-pointer text-white font-mono text-base border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]`}>
90-
{code.code}
90+
{code.code == -1 ? "NO-RESPONSE" : `H${code.code}`}
9191
</Badge>
9292
))
9393
}

web-report/src/components/StatusCodeFilterButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function StatusCodeFilterButton({ code, initialState = "inactive", onChan
1717
if(isFault) {
1818
return "bg-red-500"
1919
}
20-
20+
if (code == -1) return "bg-gray-500"
2121
if (code >= 200 && code < 300) return "bg-green-500"
2222
if (code >= 300 && code < 400) return "bg-blue-500"
2323
if (code >= 400 && code < 500) return "bg-orange-500"
@@ -49,7 +49,7 @@ export function StatusCodeFilterButton({ code, initialState = "inactive", onChan
4949

5050
return (
5151
<Badge className={getStyles()} onClick={toggleState}>
52-
{isFault ? `F${-1 * code}`: `H${code}`}
52+
{isFault ? `F${-1 * code}`: code == -1 ? `NO-RESPONSE`:`H${code}`}
5353
</Badge>
5454
)
5555
}

web-report/src/components/StatusCodeFilters.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {useState} from "react"
22
import {StatusCodeFilterButton} from "./StatusCodeFilterButton"
33
import {ITransformedReport} from "@/lib/utils.tsx";
4+
import {StatusCodeModal} from "@/components/StatusCodeModal.tsx";
45

56
type FilterState = "inactive" | "active" | "removed"
67

@@ -38,6 +39,9 @@ export function StatusCodeFilters({data, onFiltersChange}: StatusCodeFiltersProp
3839
onFiltersChange(newFilters)
3940
}
4041

42+
const [isModalOpen, setIsModalOpen] = useState(false)
43+
44+
4145
return (
4246
<div className="mb-6">
4347
<div className="items-center mb-2">
@@ -68,7 +72,11 @@ export function StatusCodeFilters({data, onFiltersChange}: StatusCodeFiltersProp
6872
</div>
6973
</div>
7074
<div className="text-xs text-gray-500 mt-1">Click to toggle: Default → Active → Removed → Default</div>
75+
<div className="items-center mb-2 flex justify-end">
76+
<div className="text-sm font-mono text-red-500 mt-1 cursor-help hover:text-green-300" onClick={() => setIsModalOpen(true)}>Code Documentation</div>
77+
</div>
7178
<div className="border-t border-black my-2"></div>
79+
<StatusCodeModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} statusCode={-1} />
7280
</div>
7381
)
7482
}

web-report/src/components/TestCaseButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function TestCaseButton({ testName, statusCode, onClick, isFault }: TestC
3131
<span className="font-mono text-sm text-left">{testName}</span>
3232
</div>
3333
<div className="flex items-center">
34-
<span className={`font-bold mr-2 ${getColor(statusCode, false, isFault)}`}>{statusCode}</span>
34+
<span className={`font-bold mr-2 ${getColor(statusCode, false, isFault)}`}>{statusCode == -1 ? "NO-RESPONSE" : statusCode}</span>
3535
<ChevronRight
3636
className={`${
3737
isHovered ? "text-blue-600 transform translate-x-1" : "text-gray-400"

web-report/src/lib/utils.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@ export function cn(...inputs: ClassValue[]) {
66
return twMerge(clsx(inputs))
77
}
88

9-
export const getColor = (code: string | number, isBackground: boolean, isFault: boolean) => {
9+
export const getColor = (code: string | number | null | undefined, isBackground: boolean, isFault: boolean) => {
1010
if (isFault) {
1111
return isBackground ? "bg-red-500" : "text-red-500";
1212
}
13+
14+
if(code === null || code === undefined || code === "") {
15+
return isBackground ? "bg-gray-500" : "text-gray-500";
16+
}
17+
1318
if (typeof code === "number") {
1419
return getColorNumber(code, isBackground);
1520
}
@@ -69,6 +74,7 @@ export const extractCodeLines = (
6974

7075
export const calculateAllStatusCounts = (coveredHttpStatus: CoveredEndpoint[], endpointIds:string[]) => {
7176
const allStatusCounts ={
77+
"NO_RESPONSE": 0,
7278
"2XX": 0,
7379
"3XX": 0,
7480
"4XX": 0,
@@ -84,6 +90,7 @@ export const calculateAllStatusCounts = (coveredHttpStatus: CoveredEndpoint[], e
8490
const uniqueStatusCodes = [...new Set(allStatusCodes)];
8591

8692
const isContainStatusCode = {
93+
"NO_RESPONSE": false,
8794
"2XX": false,
8895
"3XX": false,
8996
"4XX": false,
@@ -92,7 +99,10 @@ export const calculateAllStatusCounts = (coveredHttpStatus: CoveredEndpoint[], e
9299

93100
uniqueStatusCodes.map(
94101
(status) => {
95-
if (status >= 200 && status < 300) {
102+
if(status == null) {
103+
isContainStatusCode["NO_RESPONSE"] = true;
104+
}
105+
else if (status >= 200 && status < 300) {
96106
isContainStatusCode["2XX"] = true;
97107
} else if (status >= 300 && status < 400) {
98108
isContainStatusCode["3XX"] = true;
@@ -103,7 +113,9 @@ export const calculateAllStatusCounts = (coveredHttpStatus: CoveredEndpoint[], e
103113
}
104114
}
105115
)
106-
116+
if(isContainStatusCode["NO_RESPONSE"]){
117+
allStatusCounts["NO_RESPONSE"]++;
118+
}
107119
if (isContainStatusCode["2XX"]) {
108120
allStatusCounts["2XX"]++;
109121
}
@@ -259,12 +271,15 @@ export const transformWebFuzzingReport = (original: WebFuzzingCommonsReport | nu
259271

260272
const endpointData = endpointMap.get(status.endpointId);
261273

262-
status.httpStatus.forEach(code => {
274+
status.httpStatus?.forEach(code => {
263275
if (!endpointData) {
264276
return;
265277
}
266278
let existingStatus = endpointData.httpStatusCodes.find((s: { code: number; }) => s.code === code);
267279
if (!existingStatus) {
280+
if(code === null || code === undefined) {
281+
code = -1;
282+
}
268283
existingStatus = {code, testCases: []};
269284
endpointData.httpStatusCodes.push(existingStatus);
270285
}

web-report/src/pages/TestResults.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,16 @@ export const TestResults: React.FC<IProps> = ({testCaseName}) => {
2828
const allFaultCodes = relatedFaults.map((fault) =>
2929
fault.faultCategories.map((f) => f.code)).flat();
3030
const uniqueFaultCodes = [...new Set(allFaultCodes)].sort((a, b) => a - b);
31-
3231
const allStatusCodes = relatedHttpStatus.map((status) =>
33-
status.httpStatus.map((s) => s)).flat();
34-
const uniqueStatusCodes = [...new Set(allStatusCodes)].sort((a, b) => a - b);
32+
status.httpStatus?.map((s) => s)).flat();
33+
const uniqueStatusCodes = [...new Set(allStatusCodes)].sort((a, b) => {
34+
const codeA = Number(a);
35+
const codeB = Number(b);
36+
if (isNaN(codeA) || isNaN(codeB)) {
37+
return String(a).localeCompare(String(b));
38+
}
39+
return codeA - codeB;
40+
});
3541
const currentFile = testFiles.find((file) => file.name === testCase?.filePath);
3642

3743

web-report/src/types/GeneratedTypes.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export type OperationId = string;
1313
* A unique identifier for a test case. It could include its name and file location.
1414
*/
1515
export type TestCaseId = string;
16-
export type HttpStatus = number;
16+
export type HttpStatus = number | null;
1717
/**
1818
* A relative path used to unique locate a test suite file.
1919
*/
@@ -59,7 +59,7 @@ export interface WebFuzzingCommonsReport {
5959
/**
6060
* Extra, optional coverage information, collected by different tools.
6161
*/
62-
extra?: Coverage[];
62+
extra?: Coverage[] | null;
6363
[k: string]: unknown;
6464
}
6565
export interface Faults {
@@ -96,7 +96,7 @@ export interface FaultCategoryId {
9696
/**
9797
* An optional context for the fault. The same fault type could be manifested in different ways, and we use this property to differentiate among them.
9898
*/
99-
context?: string;
99+
context?: string | null;
100100
[k: string]: unknown;
101101
}
102102
export interface RESTReport {
@@ -121,11 +121,11 @@ export interface CoveredEndpoint {
121121
endpointId: OperationId;
122122
testCaseId: TestCaseId;
123123
/**
124-
* As in a test case the same endpoint could be called more than once, here we report all of the obtained HTTP status codes
124+
* As in a test case the same endpoint could be called more than once, here we report all of the obtained HTTP status codes. If for any reason a call does not return any response (e.g., the TCP connection does timeout), then this HTTP status array would be either null or empty.
125125
*
126-
* @minItems 1
126+
* @minItems 0
127127
*/
128-
httpStatus: [HttpStatus, ...HttpStatus[]];
128+
httpStatus: HttpStatus[] | null;
129129
[k: string]: unknown;
130130
}
131131
export interface TestCase {
@@ -165,6 +165,6 @@ export interface CoverageCriterion {
165165
/**
166166
* Optional number of all testing targets for this criterion. For some criteria, this number can be unknown.
167167
*/
168-
total?: number;
168+
total?: number | null;
169169
[k: string]: unknown;
170170
}

web-report/src/types/GeneratedTypesZod.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export const operationIdSchema = z.string();
55

66
export const testCaseIdSchema = z.string();
77

8-
export const httpStatusSchema = z.number();
8+
export const httpStatusSchema = z.number().nullable();
99

1010
export const testFilePathSchema = z.string();
1111

@@ -22,23 +22,23 @@ export const testCaseSchema = z.record(z.unknown()).and(
2222
export const faultCategoryIdSchema = z.record(z.unknown()).and(
2323
z.object({
2424
code: z.number(),
25-
context: z.string().optional(),
25+
context: z.string().optional().nullable(),
2626
}),
2727
);
2828

2929
export const coveredEndpointSchema = z.record(z.unknown()).and(
3030
z.object({
3131
endpointId: operationIdSchema,
3232
testCaseId: testCaseIdSchema,
33-
httpStatus: z.tuple([httpStatusSchema]).rest(httpStatusSchema),
33+
httpStatus: z.array(httpStatusSchema).nullable(),
3434
}),
3535
);
3636

3737
export const coverageCriterionSchema = z.record(z.unknown()).and(
3838
z.object({
3939
name: z.string(),
4040
covered: z.number(),
41-
total: z.number().optional(),
41+
total: z.number().optional().nullable(),
4242
}),
4343
);
4444

@@ -89,6 +89,6 @@ export const webFuzzingCommonsReportSchema = z.record(z.unknown()).and(
8989
totalTests: z.number(),
9090
testFilePaths: z.array(testFilePathSchema),
9191
testCases: z.array(testCaseSchema),
92-
extra: z.array(coverageSchema).optional(),
92+
extra: z.array(coverageSchema).optional().nullable(),
9393
}),
9494
);

0 commit comments

Comments
 (0)