Skip to content
This repository has been archived by the owner on Aug 1, 2024. It is now read-only.

Tenable enhancement dev #249

Merged
merged 12 commits into from
Jul 30, 2024
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
ACCESS_KEY=tenable-access-key
SECRET_KEY=tenable-secret-key
ASSET_API_TIMEOUT_IN_MINUTES=30

# Configuration filters for Vulnerabilities
VULNERABILITY_API_TIMEOUT_IN_MINUTES=30
VULNERABILITY_SEVERITIES=info,low,medium,high,critical
VULNERABILITY_STATES=open,reopened,fixed

# Configuration filters for Compliance Findings
LAST_SEEN=15,30,60,90
COMPLIANCE_STATE=OPEN,REOPENED,FIXED
COMPLIANCE_RESULT=PASSED,FAILED,WARNING,SKIPPED,UNKNOWN,ERROR
NUM_FINDINGS=10000

2 changes: 2 additions & 0 deletions docs/jupiterone.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ The following entities are created:
| Account | `tenable_account` | `Account` |
| Agent | `tenable_agent` | `HostAgent` |
| Asset | `tenable_asset` | `Record` |
| Compliance Finding | `tenable_compliance_finding` | `Finding` |
| Container Finding | `tenable_container_finding` | `Finding` |
| Container Image | `tenable_container_image` | `Image` |
| Container Malware | `tenable_container_malware` | `Finding` |
Expand All @@ -113,6 +114,7 @@ The following relationships are created:
| `tenable_account` | **HAS** | `tenable_user` |
| `tenable_account` | **PROVIDES** | `tenable_scanner` |
| `tenable_agent` | **PROTECTS** | `tenable_asset` |
| `tenable_asset` | **HAS** | `tenable_compliance_finding` |
| `tenable_asset` | **HAS** | `tenable_vulnerability_finding` |
| `tenable_container_image` | **HAS** | `tenable_container_finding` |
| `tenable_container_image` | **HAS** | `tenable_container_malware` |
Expand Down
20 changes: 20 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ export interface IntegrationConfig extends IntegrationInstanceConfig {
accessKey: string;
secretKey: string;
vulnerabilityApiTimeoutInMinutes?: number;
complianceApiTimeoutInMinutes?: number;
gastonyelmini marked this conversation as resolved.
Show resolved Hide resolved
assetApiTimeoutInMinutes?: number;
vulnerabilitySeverities?: string;
vulnerabilityStates?: string;
lastSeen?: string;
complianceState?: string;
complianceResult?: string;
numFindings?: number;
}

export const instanceConfigFields: IntegrationInstanceConfigFieldMap = {
Expand All @@ -32,4 +37,19 @@ export const instanceConfigFields: IntegrationInstanceConfigFieldMap = {
vulnerabilityStates: {
type: 'string',
},
lastSeen: {
type: 'string',
},
complianceApiTimeoutInMinutes: {
type: 'string',
},
complianceState: {
type: 'string',
},
complianceResult: {
type: 'string',
},
numFindings: {
type: 'string',
},
};
52 changes: 52 additions & 0 deletions src/invocationValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import TenableClient from './tenable/TenableClient';
import {
VALID_VULNERABILITY_STATES,
VALID_VULNERABILITY_SEVERITIES,
VALID_COMPLIANCE_STATES,
VALID_COMPLIANCE_RESULT,
} from './tenable/client';
import { toNum } from './utils/dataType';

Expand Down Expand Up @@ -50,6 +52,38 @@ function validateVulnerabilityStates(states: string) {
}
}

function validateComplianceStates(states: string) {
const statesValues = states.replace(/\s+/g, '').split(',');
for (const state of statesValues) {
if (!(VALID_COMPLIANCE_STATES as unknown as string[]).includes(state)) {
throw new IntegrationValidationError(
`States - ${state} - is not valid. Valid Compliance states include ${VALID_COMPLIANCE_STATES.map(
(v) => v,
)}`,
);
}
}
}

function validateComplianceResults(complianceResult: string) {
const complianceResultValues = complianceResult
.replace(/\s+/g, '')
.split(',');
for (const complianceResult of complianceResultValues) {
if (
!(VALID_COMPLIANCE_RESULT as unknown as string[]).includes(
complianceResult,
)
) {
throw new IntegrationValidationError(
`complianceResult - ${complianceResult} - is not valid. Valid complianceResult include ${VALID_COMPLIANCE_RESULT.map(
(v) => v,
)}`,
);
}
}
}

/**
* Performs validation of the execution before the execution handler function is
* invoked.
Expand Down Expand Up @@ -118,6 +152,24 @@ export default async function validateInvocation(
validateVulnerabilityStates(vulnerabilityStates);
}

if (config.complianceStates) {
const complianceStates =
(executionContext.instance.config.complianceStates =
typeof config.complianceStates === 'string'
? config.complianceStates.replace(/\s+/g, '')
: config.complianceStates);
validateComplianceStates(complianceStates);
}

if (config.complianceResult) {
const complianceResult =
(executionContext.instance.config.complianceResult =
typeof config.complianceResult === 'string'
? config.complianceResult.replace(/\s+/g, '')
: config.complianceResult);
validateComplianceResults(complianceResult);
}

const provider = new TenableClient({
logger,
accessToken: config.accessKey,
Expand Down
38 changes: 38 additions & 0 deletions src/steps/compliance-finding/converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Entities } from '../constants';
import {
createIntegrationEntity,
Entity,
} from '@jupiterone/integration-sdk-core';
import { generateEntityKey } from '../../utils/generateKey';

export function createComplianceFindingEntity(complianceChunk): Entity {
return createIntegrationEntity({
entityData: {
source: complianceChunk,
assign: {
_class: Entities.COMPLIANCE_FINDINGS._class,
_type: Entities.COMPLIANCE_FINDINGS._type,
_key: generateEntityKey(
Entities.COMPLIANCE_FINDINGS._type,
complianceChunk.uuid,
),

// Schema required fields.
category: ['network', 'host'],
severity: ['low', 'medium'],
numericSeverity: [1, 2],
id: String(complianceChunk.id),
agentId: complianceChunk.id,
displayName: complianceChunk.name,
open: complianceChunk.state === 'OPEN',

// Entity additional data.
name: complianceChunk.name,
status: complianceChunk.status,
firstSeen: complianceChunk.first_seen,
lastSeen: complianceChunk.last_seen,
agentName: complianceChunk.agent_name,
},
},
});
}
54 changes: 54 additions & 0 deletions src/steps/compliance-finding/filters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { IntegrationConfig } from '../../config';
import {
ExportComplianceFindingsFilter,
complianceChunkState,
complianceChunkResult,
} from '../../tenable/client';
import { subDays, getUnixTime } from 'date-fns';

const DEFAULT_STATES: complianceChunkState[] = ['OPEN', 'REOPENED', 'FIXED'];
const DEFAULT_RESULTS: complianceChunkResult[] = [
'PASSED',
'FAILED',
'WARNING',
'SKIPPED',
'UNKNOWN',
'ERROR',
];
const DEFAULT_LAST_SEEN_DAYS = 30; // Default to 30 days if not provided

function parseComplianceStates(states: string): complianceChunkState[] {
return states.split(',') as complianceChunkState[];
}

function parseComplianceResults(results: string): complianceChunkResult[] {
return results.split(',') as complianceChunkResult[];
}

function calculateLastSeenTimestamp(daysAgo: number): number {
const lastSeenDate = subDays(new Date(), daysAgo);
return getUnixTime(lastSeenDate);
}

export function buildComplianceFilters(
config: IntegrationConfig,
): ExportComplianceFindingsFilter {
const lastSeenDays = config.lastSeen
? Number(config.lastSeen)
: DEFAULT_LAST_SEEN_DAYS;
if (isNaN(lastSeenDays)) {
throw new Error(`Invalid lastSeen value: ${config.lastSeen}`);
}

const lastSeenTimestamp = calculateLastSeenTimestamp(lastSeenDays);

return {
state: config.complianceState
? parseComplianceStates(config.complianceState)
: DEFAULT_STATES,
compliance_results: config.complianceResults
? parseComplianceResults(config.complianceResults)
: DEFAULT_RESULTS,
last_seen: lastSeenTimestamp,
};
}
54 changes: 54 additions & 0 deletions src/steps/compliance-finding/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
jest.setTimeout(50000);

import { StepIds } from '../constants';
import { buildStepTestConfig } from '../../../test/config';
import { executeStepWithDependencies } from '@jupiterone/integration-sdk-testing';
import { setupTenableRecording, Recording } from '../../../test/recording';

let recording: Recording;

afterEach(async () => {
if (recording) {
await recording.stop();
}
});

describe.skip('step-compliance-findings', () => {
test('success', async () => {
recording = setupTenableRecording({
name: 'step-compliance-findings',
directory: __dirname,
options: {
recordFailedRequests: false,
matchRequestsBy: {
order: true,
},
},
});

const stepConfig = buildStepTestConfig(StepIds.COMPLIANCE_FINDINGS);
const stepResults = await executeStepWithDependencies(stepConfig);
expect(stepResults).toMatchStepMetadata(stepConfig);
});
});

describe.skip('build-asset-compliance-findings-relationships', () => {
test('success', async () => {
recording = setupTenableRecording({
name: 'build-asset-compliance-findings-relationships',
directory: __dirname,
options: {
recordFailedRequests: false,
matchRequestsBy: {
order: true,
},
},
});

const stepConfig = buildStepTestConfig(
StepIds.ASSET_COMPLIANCE_FINDINGS_RELATIONSHIPS,
);
const stepResults = await executeStepWithDependencies(stepConfig);
expect(stepResults).toMatchStepMetadata(stepConfig);
});
});
Loading
Loading