Add OAS contract monitoring support (#325)

* Add OAS contract monitoring support

* Pass the contract failure reason to UI

* Fix the issues related to contract validation

* Fix rest of the issues in the UI

* Add documentation related to contract monitoring feature

* Fix a typo in the docs

* Unmarshal to `HTTPRequestResponsePair` only if the OAS validation is enabled

* Fix an issue caused by the merge commit

* Slightly change the logic in the `validateOAS` method

Change the `contractText` value to `No Breaches` or `Breach` and make the text `white-space: nowrap`.

* Retrieve and display the failure reason for both request and response

Also display the content of the contract/OAS file in the UI.

* Display the OAS under `CONTRACT` tab with syntax highlighting

Also fix the styling in the entry feed.

* Remove `EnforcePolicyFileDeprecated` constant

* Log the other errors as well

* Get context from caller instead

* Define a type for the contract status and make its values enum-like

* Remove an unnecessary `if` statement

* Validate OAS in the CLI before passing it to Agent

* Get rid of the `github.com/ghodss/yaml` dependency in `loadOAS` by using `LoadFromData`

* Fix an artifact from the merge conflict
This commit is contained in:
M. Mert Yıldıran
2021-10-19 14:24:22 +03:00
committed by GitHub
parent b7ff076571
commit 145e7cda01
29 changed files with 662 additions and 111 deletions

View File

@@ -71,7 +71,7 @@ export const EntryDetailed: React.FC<EntryDetailedProps> = ({entryData}) => {
/>
{entryData.data && <EntrySummary data={entryData.data}/>}
<>
{entryData.data && <EntryViewer representation={entryData.representation} isRulesEnabled={entryData.isRulesEnabled} rulesMatched={entryData.rulesMatched} elapsedTime={entryData.data.elapsedTime} color={entryData.protocol.backgroundColor}/>}
{entryData.data && <EntryViewer representation={entryData.representation} isRulesEnabled={entryData.isRulesEnabled} rulesMatched={entryData.rulesMatched} contractStatus={entryData.data.contractStatus} requestReason={entryData.data.contractRequestReason} responseReason={entryData.data.contractResponseReason} contractContent={entryData.data.contractContent} elapsedTime={entryData.data.elapsedTime} color={entryData.protocol.backgroundColor}/>}
</>
</>
};

View File

@@ -150,8 +150,6 @@ export const EntryTableSection: React.FC<EntrySectionProps> = ({title, color, ar
</React.Fragment>
}
interface EntryPolicySectionProps {
title: string,
color: string,
@@ -159,7 +157,6 @@ interface EntryPolicySectionProps {
arrayToIterate: any[],
}
interface EntryPolicySectionCollapsibleTitleProps {
label: string;
matched: string;
@@ -253,3 +250,28 @@ export const EntryTablePolicySection: React.FC<EntryPolicySectionProps> = ({titl
}
</React.Fragment>
}
interface EntryContractSectionProps {
color: string,
requestReason: string,
responseReason: string,
contractContent: string,
}
export const EntryContractSection: React.FC<EntryContractSectionProps> = ({color, requestReason, responseReason, contractContent}) => {
return <React.Fragment>
{requestReason && <EntrySectionContainer title="Request" color={color}>
{requestReason}
</EntrySectionContainer>}
{responseReason && <EntrySectionContainer title="Response" color={color}>
{responseReason}
</EntrySectionContainer>}
{contractContent && <EntrySectionContainer title="Contract" color={color}>
<SyntaxHighlighter
isWrapped={false}
code={contractContent}
language={"yaml"}
/>
</EntrySectionContainer>}
</React.Fragment>
}

View File

@@ -1,7 +1,7 @@
import React, {useState} from 'react';
import styles from './EntryViewer.module.sass';
import Tabs from "../UI/Tabs";
import {EntryTableSection, EntryBodySection, EntryTablePolicySection} from "./EntrySections";
import {EntryTableSection, EntryBodySection, EntryTablePolicySection, EntryContractSection} from "./EntrySections";
enum SectionTypes {
SectionTable = "table",
@@ -33,7 +33,7 @@ const SectionsRepresentation: React.FC<any> = ({data, color}) => {
return <>{sections}</>;
}
const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rulesMatched, elapsedTime, color}) => {
const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => {
var TABS = [
{
tab: 'Request'
@@ -50,6 +50,7 @@ const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rule
var responseTabIndex = 0;
var rulesTabIndex = 0;
var contractTabIndex = 0;
if (response) {
TABS.push(
@@ -69,11 +70,19 @@ const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rule
rulesTabIndex = TABS.length - 1;
}
if (contractStatus !== 0 && contractContent) {
TABS.push(
{
tab: 'Contract',
}
);
contractTabIndex = TABS.length - 1;
}
return <div className={styles.Entry}>
{<div className={styles.body}>
<div className={styles.bodyHeader}>
<Tabs tabs={TABS} currentTab={currentTab} color={color} onChange={setCurrentTab} leftAligned/>
{request?.url && <a className={styles.endpointURL} href={request.payload.url} target='_blank' rel="noreferrer">{request.payload.url}</a>}
</div>
{currentTab === TABS[0].tab && <React.Fragment>
<SectionsRepresentation data={request} color={color}/>
@@ -84,6 +93,9 @@ const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rule
{isRulesEnabled && currentTab === TABS[rulesTabIndex].tab && <React.Fragment>
<EntryTablePolicySection title={'Rule'} color={color} latency={elapsedTime} arrayToIterate={rulesMatched ? rulesMatched : []}/>
</React.Fragment>}
{contractStatus !== 0 && contractContent && currentTab === TABS[contractTabIndex].tab && <React.Fragment>
<EntryContractSection color={color} requestReason={requestReason} responseReason={responseReason} contractContent={contractContent}/>
</React.Fragment>}
</div>}
</div>;
}
@@ -92,12 +104,26 @@ interface Props {
representation: any;
isRulesEnabled: boolean;
rulesMatched: any;
contractStatus: number;
requestReason: string;
responseReason: string;
contractContent: string;
color: string;
elapsedTime: number;
}
const EntryViewer: React.FC<Props> = ({representation, isRulesEnabled, rulesMatched, elapsedTime, color}) => {
return <AutoRepresentation representation={representation} isRulesEnabled={isRulesEnabled} rulesMatched={rulesMatched} elapsedTime={elapsedTime} color={color}/>
const EntryViewer: React.FC<Props> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => {
return <AutoRepresentation
representation={representation}
isRulesEnabled={isRulesEnabled}
rulesMatched={rulesMatched}
contractStatus={contractStatus}
requestReason={requestReason}
responseReason={responseReason}
contractContent={contractContent}
elapsedTime={elapsedTime}
color={color}
/>
};
export default EntryViewer;

View File

@@ -38,6 +38,7 @@
.ruleNumberText
font-size: 12px
font-weight: 600
white-space: nowrap
.ruleNumberTextFailure
color: #DB2156
@@ -72,12 +73,17 @@
padding-left: 10px
flex-grow: 1
.directionContainer
.separatorRight
display: flex
border-right: 1px solid $data-background-color
padding: 4px
padding-right: 12px
.separatorLeft
display: flex
padding: 4px
padding-left: 12px
.port
font-size: 12px
color: $secondary-font-color

View File

@@ -26,6 +26,7 @@ interface Entry {
isOutgoing?: boolean;
latency: number;
rules: Rules;
contractStatus: number,
}
interface Rules {
@@ -64,7 +65,7 @@ export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntryId, isSel
}
}
let additionalRulesProperties = "";
let ruleSuccess: boolean;
let ruleSuccess = true;
let rule = 'latency' in entry.rules
if (rule) {
if (entry.rules.latency !== -1) {
@@ -91,11 +92,32 @@ export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntryId, isSel
}
}
}
var contractEnabled = true;
var contractText = "";
switch (entry.contractStatus) {
case 0:
contractEnabled = false;
break;
case 1:
additionalRulesProperties = styles.ruleSuccessRow
ruleSuccess = true
contractText = "No Breaches"
break;
case 2:
additionalRulesProperties = styles.ruleFailureRow
ruleSuccess = false
contractText = "Breach"
break;
default:
break;
}
return <>
<div
id={entry.id}
className={`${styles.row}
${isSelected && !rule ? styles.rowSelected : additionalRulesProperties}`}
${isSelected && !rule && !contractEnabled ? styles.rowSelected : additionalRulesProperties}`}
onClick={() => setFocusedEntryId(entry.id)}
style={{
border: isSelected ? `1px ${entry.protocol.backgroundColor} solid` : "1px transparent solid",
@@ -117,12 +139,19 @@ export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntryId, isSel
</div>
{
rule ?
<div className={`${styles.ruleNumberText} ${ruleSuccess ? styles.ruleNumberTextSuccess : styles.ruleNumberTextFailure}`}>
<div className={`${styles.ruleNumberText} ${ruleSuccess ? styles.ruleNumberTextSuccess : styles.ruleNumberTextFailure} ${rule && contractEnabled ? styles.separatorRight : ""}`}>
{`Rules (${numberOfRules})`}
</div>
: ""
}
<div className={styles.directionContainer}>
{
contractEnabled ?
<div className={`${styles.ruleNumberText} ${ruleSuccess ? styles.ruleNumberTextSuccess : styles.ruleNumberTextFailure} ${rule && contractEnabled ? styles.separatorLeft : ""}`}>
{contractText}
</div>
: ""
}
<div className={styles.separatorRight}>
<span className={styles.port} title="Source Port">{entry.sourcePort}</span>
{entry.isOutgoing ?
<img src={outgoingIcon} alt="Ingoing traffic" title="Ingoing"/>