Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SolidityScan API response schema validation #2190

Merged
merged 3 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deploy/values/review/values.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ frontend:
NEXT_PUBLIC_AD_BANNER_PROVIDER: slise
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: "['/apps']"
PROMETHEUS_METRICS_ENABLED: true
envFromSecret:
NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
Expand Down
5 changes: 3 additions & 2 deletions lib/api/buildUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ export default function buildUrl<R extends ResourceName>(
resourceName: R,
pathParams?: ResourcePathParams<R>,
queryParams?: Record<string, string | Array<string> | number | boolean | null | undefined>,
noProxy?: boolean,
): string {
const resource: ApiResource = RESOURCES[resourceName];
const baseUrl = isNeedProxy() ? config.app.baseUrl : (resource.endpoint || config.api.endpoint);
const baseUrl = !noProxy && isNeedProxy() ? config.app.baseUrl : (resource.endpoint || config.api.endpoint);
const basePath = resource.basePath !== undefined ? resource.basePath : config.api.basePath;
const path = isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
const path = !noProxy && isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
const url = new URL(compile(path)(pathParams), baseUrl);

queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
Expand Down
5 changes: 2 additions & 3 deletions lib/api/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ import type { BackendVersionConfig } from 'types/api/configs';
import type {
SmartContract,
SmartContractVerificationConfigRaw,
SolidityscanReport,
SmartContractSecurityAudits,
} from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
Expand Down Expand Up @@ -480,7 +479,7 @@ export const RESOURCES = {
path: '/api/v2/smart-contracts/:hash/verification/via/:method',
pathParams: [ 'hash' as const, 'method' as const ],
},
contract_solidityscan_report: {
contract_solidity_scan_report: {
path: '/api/v2/smart-contracts/:hash/solidityscan-report',
pathParams: [ 'hash' as const ],
},
Expand Down Expand Up @@ -1038,7 +1037,7 @@ Q extends 'quick_search' ? Array<SearchResultItem> :
Q extends 'search' ? SearchResult :
Q extends 'search_check_redirect' ? SearchRedirectResult :
Q extends 'contract' ? SmartContract :
Q extends 'contract_solidityscan_report' ? SolidityscanReport :
Q extends 'contract_solidity_scan_report' ? unknown :
Q extends 'verified_contracts' ? VerifiedContractsResponse :
Q extends 'verified_contracts_counters' ? VerifiedContractsCounters :
Q extends 'visualize_sol2uml' ? visualizer.VisualizeResponse :
Expand Down
1 change: 1 addition & 0 deletions lib/metadata/getPageOgType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/login': 'Regular page',
'/sprite': 'Regular page',
'/api/metrics': 'Regular page',
'/api/monitoring/invalid-api-schema': 'Regular page',
'/api/log': 'Regular page',
'/api/media-type': 'Regular page',
'/api/proxy': 'Regular page',
Expand Down
1 change: 1 addition & 0 deletions lib/metadata/templates/description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/login': DEFAULT_TEMPLATE,
'/sprite': DEFAULT_TEMPLATE,
'/api/metrics': DEFAULT_TEMPLATE,
'/api/monitoring/invalid-api-schema': DEFAULT_TEMPLATE,
'/api/log': DEFAULT_TEMPLATE,
'/api/media-type': DEFAULT_TEMPLATE,
'/api/proxy': DEFAULT_TEMPLATE,
Expand Down
1 change: 1 addition & 0 deletions lib/metadata/templates/title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/login': '%network_name% login',
'/sprite': '%network_name% SVG sprite',
'/api/metrics': '%network_name% node API prometheus metrics',
'/api/monitoring/invalid-api-schema': '%network_name% node API prometheus metrics',
'/api/log': '%network_name% node API request log',
'/api/media-type': '%network_name% node API media type',
'/api/proxy': '%network_name% node API proxy',
Expand Down
1 change: 1 addition & 0 deletions lib/mixpanel/getPageType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/login': 'Login',
'/sprite': 'Sprite',
'/api/metrics': 'Node API: Prometheus metrics',
'/api/monitoring/invalid-api-schema': 'Node API: Prometheus metrics',
'/api/log': 'Node API: Request log',
'/api/media-type': 'Node API: Media type',
'/api/proxy': 'Node API: Proxy',
Expand Down
8 changes: 7 additions & 1 deletion lib/monitoring/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ const metrics = (() => {

promClient.register.clear();

const invalidApiSchema = new promClient.Counter({
name: 'invalid_api_schema',
help: 'Number of invalid external API schema events',
labelNames: [ 'resource', 'url' ] as const,
});

const socialPreviewBotRequests = new promClient.Counter({
name: 'social_preview_bot_requests_total',
help: 'Number of incoming requests from social preview bots',
Expand All @@ -27,7 +33,7 @@ const metrics = (() => {
buckets: [ 0.2, 0.5, 1, 3, 10 ],
});

return { socialPreviewBotRequests, searchEngineBotRequests, apiRequestDuration };
return { invalidApiSchema, socialPreviewBotRequests, searchEngineBotRequests, apiRequestDuration };
})();

export default metrics;
25 changes: 25 additions & 0 deletions lib/solidityScan/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as v from 'valibot';

export const SolidityScanIssueSeverityDistributionSchema = v.object({
critical: v.number(),
gas: v.number(),
high: v.number(),
informational: v.number(),
low: v.number(),
medium: v.number(),
});

export const SolidityScanSchema = v.object({
scan_report: v.object({
contractname: v.string(),
scan_status: v.string(),
scan_summary: v.object({
score_v2: v.string(),
issue_severity_distribution: SolidityScanIssueSeverityDistributionSchema,
}),
scanner_reference_url: v.string(),
}),
});

export type SolidityScanReport = v.InferOutput<typeof SolidityScanSchema>;
export type SolidityScanReportSeverityDistribution = v.InferOutput<typeof SolidityScanIssueSeverityDistributionSchema>;
51 changes: 51 additions & 0 deletions lib/solidityScan/useFetchReport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import * as v from 'valibot';

import buildUrl from 'lib/api/buildUrl';
import useApiQuery from 'lib/api/useApiQuery';
import { SOLIDITY_SCAN_REPORT } from 'stubs/contract';

import { SolidityScanSchema } from './schema';

interface Params {
hash: string;
}

const RESOURCE_NAME = 'contract_solidity_scan_report';
const ERROR_NAME = 'Invalid response schema';

export default function useFetchReport({ hash }: Params) {
const query = useApiQuery(RESOURCE_NAME, {
pathParams: { hash },
queryOptions: {
select: (response) => {
const parsedResponse = v.safeParse(SolidityScanSchema, response);

if (!parsedResponse.success) {
throw Error(ERROR_NAME);
}

return parsedResponse.output;
},
enabled: Boolean(hash),
placeholderData: SOLIDITY_SCAN_REPORT,
retry: 0,
},
});

const errorMessage = query.error && 'message' in query.error ? query.error.message : undefined;

React.useEffect(() => {
if (errorMessage === ERROR_NAME) {
fetch('/node-api/monitoring/invalid-api-schema', {
method: 'POST',
body: JSON.stringify({
resource: RESOURCE_NAME,
url: buildUrl(RESOURCE_NAME, { hash }, undefined, true),
}),
});
}
}, [ errorMessage, hash ]);

return query;
}
20 changes: 4 additions & 16 deletions mocks/contract/solidityscanReport.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SolidityscanReport } from 'types/api/contract';
import type { SolidityScanReport } from 'lib/solidityScan/schema';

export const solidityscanReportAverage: SolidityscanReport = {
export const solidityscanReportAverage: SolidityScanReport = {
scan_report: {
contractname: 'foo',
scan_status: 'scan_done',
Expand All @@ -13,17 +13,13 @@ export const solidityscanReportAverage: SolidityscanReport = {
low: 2,
medium: 0,
},
lines_analyzed_count: 18,
scan_time_taken: 1,
score: '3.61',
score_v2: '72.22',
threat_score: '94.74',
},
scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout',
},
};

export const solidityscanReportGreat: SolidityscanReport = {
export const solidityscanReportGreat: SolidityScanReport = {
scan_report: {
contractname: 'foo',
scan_status: 'scan_done',
Expand All @@ -36,17 +32,13 @@ export const solidityscanReportGreat: SolidityscanReport = {
low: 0,
medium: 0,
},
lines_analyzed_count: 18,
scan_time_taken: 1,
score: '3.61',
score_v2: '100',
threat_score: '94.74',
},
scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout',
},
};

export const solidityscanReportLow: SolidityscanReport = {
export const solidityscanReportLow: SolidityScanReport = {
scan_report: {
contractname: 'foo',
scan_status: 'scan_done',
Expand All @@ -59,11 +51,7 @@ export const solidityscanReportLow: SolidityscanReport = {
low: 2,
medium: 10,
},
lines_analyzed_count: 18,
scan_time_taken: 1,
score: '3.61',
score_v2: '22.22',
threat_score: '94.74',
},
scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout',
},
Expand Down
1 change: 1 addition & 0 deletions nextjs/nextjs-routes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/api/log">
| StaticRoute<"/api/media-type">
| StaticRoute<"/api/metrics">
| StaticRoute<"/api/monitoring/invalid-api-schema">
| StaticRoute<"/api/proxy">
| StaticRoute<"/api/sprite">
| StaticRoute<"/api-docs">
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"react-scroll": "^1.8.7",
"swagger-ui-react": "^5.9.0",
"use-font-face-observer": "^1.2.1",
"valibot": "0.38.0",
"viem": "2.10.9",
"wagmi": "2.9.2",
"xss": "^1.0.14"
Expand Down
19 changes: 19 additions & 0 deletions pages/api/monitoring/invalid-api-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import * as v from 'valibot';

import metrics from 'lib/monitoring/metrics';

const PayloadSchema = v.object({
resource: v.string(),
url: v.string(),
});

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const payload = JSON.parse(req.body);
metrics?.invalidApiSchema.inc(v.parse(PayloadSchema, payload));
} catch (error) {

}
res.status(200).json({ status: 'ok' });
}
10 changes: 4 additions & 6 deletions stubs/contract.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { SmartContract, SolidityscanReport } from 'types/api/contract';
import type { SmartContract } from 'types/api/contract';
import type { VerifiedContract, VerifiedContractsCounters } from 'types/api/contracts';

import type { SolidityScanReport } from 'lib/solidityScan/schema';

import { ADDRESS_PARAMS } from './addressParams';

export const CONTRACT_CODE_UNVERIFIED = {
Expand Down Expand Up @@ -78,7 +80,7 @@ export const VERIFIED_CONTRACTS_COUNTERS: VerifiedContractsCounters = {
new_verified_smart_contracts_24h: '1234',
};

export const SOLIDITYSCAN_REPORT: SolidityscanReport = {
export const SOLIDITY_SCAN_REPORT: SolidityScanReport = {
scan_report: {
contractname: 'BullRunners',
scan_status: 'scan_done',
Expand All @@ -91,11 +93,7 @@ export const SOLIDITYSCAN_REPORT: SolidityscanReport = {
low: 2,
medium: 0,
},
lines_analyzed_count: 18,
scan_time_taken: 1,
score: '3.61',
score_v2: '72.22',
threat_score: '94.74',
},
scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout',
},
Expand Down
26 changes: 0 additions & 26 deletions types/api/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,32 +107,6 @@ export interface SmartContractVerificationError {
name?: Array<string>;
}

// it's an external API proxy, we can't guarantee the responce types
export type SolidityscanReport = {
scan_report?: {
contractname?: string;
scan_status?: string;
scan_summary?: {
issue_severity_distribution?: SolidityscanReportSeverityDistribution;
lines_analyzed_count?: number;
scan_time_taken?: number;
score?: string;
score_v2?: string;
threat_score?: string;
};
scanner_reference_url?: string;
};
}

export type SolidityscanReportSeverityDistribution = {
critical?: number;
gas?: number;
high?: number;
informational?: number;
low?: number;
medium?: number;
};

type SmartContractSecurityAudit = {
audit_company_name: string;
audit_publish_date: string;
Expand Down
6 changes: 3 additions & 3 deletions types/client/marketplace.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SolidityscanReport, SolidityscanReportSeverityDistribution } from 'types/api/contract';
import type { SolidityScanReport, SolidityScanReportSeverityDistribution } from 'lib/solidityScan/schema';

export type MarketplaceAppPreview = {
id: string;
Expand Down Expand Up @@ -54,12 +54,12 @@ export type MarketplaceAppSecurityReport = {
solidityScanContractsNumber: number;
securityScore: number;
totalIssues?: number;
issueSeverityDistribution: SolidityscanReportSeverityDistribution;
issueSeverityDistribution: SolidityScanReportSeverityDistribution;
};
contractsData: Array<{
address: string;
isVerified: boolean;
solidityScanReport?: SolidityscanReport['scan_report'] | null;
solidityScanReport?: SolidityScanReport['scan_report'] | null;
}>;
}

Expand Down
6 changes: 3 additions & 3 deletions ui/address/SolidityscanReport.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const addressHash = 'hash';

test('average report +@dark-mode +@mobile', async({ render, mockApiResponse, page }) => {
await mockApiResponse(
'contract_solidityscan_report',
'contract_solidity_scan_report',
solidityscanReportMock.solidityscanReportAverage,
{ pathParams: { hash: addressHash } },
);
Expand All @@ -23,7 +23,7 @@ test('average report +@dark-mode +@mobile', async({ render, mockApiResponse, pag

test('great report', async({ render, mockApiResponse, page }) => {
await mockApiResponse(
'contract_solidityscan_report',
'contract_solidity_scan_report',
solidityscanReportMock.solidityscanReportGreat,
{ pathParams: { hash: addressHash } },
);
Expand All @@ -41,7 +41,7 @@ test('great report', async({ render, mockApiResponse, page }) => {

test('low report', async({ render, mockApiResponse, page }) => {
await mockApiResponse(
'contract_solidityscan_report',
'contract_solidity_scan_report',
solidityscanReportMock.solidityscanReportLow,
{ pathParams: { hash: addressHash } },
);
Expand Down
Loading
Loading