Skip to content

Commit

Permalink
feat(rule): add secrets rule
Browse files Browse the repository at this point in the history
  • Loading branch information
Oleksiivanov committed Aug 13, 2023
1 parent 5b1d1f6 commit 4838844
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 3 deletions.
7 changes: 6 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ const camundaCloud12Rules = withConfig(camundaCloud11Rules, { version: '1.2' });
const camundaCloud13Rules = withConfig(camundaCloud12Rules, { version: '1.3' });

const camundaCloud80Rules = withConfig({
...omit(camundaCloud13Rules, 'no-template')
...omit(camundaCloud13Rules, [
'no-template',
'secrets'
]),
'secrets': 'error'
}, { version: '8.0' });

const camundaCloud81Rules = withConfig({
Expand Down Expand Up @@ -93,6 +97,7 @@ const rules = {
'no-task-schedule': './rules/camunda-cloud/no-task-schedule',
'no-template': './rules/camunda-cloud/no-template',
'no-zeebe-properties': './rules/camunda-cloud/no-zeebe-properties',
'secrets': './rules/camunda-cloud/secrets',
'sequence-flow-condition': './rules/camunda-cloud/sequence-flow-condition',
'signal-reference': './rules/camunda-cloud/signal-reference',
'start-form': './rules/camunda-cloud/start-form',
Expand Down
81 changes: 81 additions & 0 deletions rules/camunda-cloud/secrets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const { isString } = require('min-dash');
const { reportErrors } = require('../utils/reporter');
const { ERROR_TYPES } = require('../utils/error-types');
const { skipInNonExecutableProcess } = require('../utils/rule');
const { findExtensionElement } = require('../utils/element');

module.exports = skipInNonExecutableProcess(checkNodeForErrors);

function checkNodeForErrors() {
function check(node, reporter) {
const errors = validateZeebeProperties(node);
reportIfErrorsExist(node, reporter, errors);
}

return {
check
};
}

function reportIfErrorsExist(node, reporter, errors) {
if (errors && errors.length) {
reportErrors(node, reporter, errors);
}
}

function validateSubscription(subscription, node) {
const correlationKey = subscription.get('correlationKey');
return containInvalidSecret(correlationKey) ?
[ createErrorForInvalidSecret('correlationKey', subscription, node) ] :
[];
}

function validateIoMapping(ioMapping, node) {
return (ioMapping.inputParameters || ioMapping.$children)
.filter(parameter => containInvalidSecret(parameter.source))
.map(parameter => createErrorForInvalidSecret(parameter.target, parameter, node));
}

function validateProperties(properties, node) {
return (properties.properties || properties.$children)
.filter(parameter => containInvalidSecret(parameter.value))
.map(parameter => createErrorForInvalidSecret(parameter.name, parameter, node));
}

function validateZeebeProperties(node) {
const errors = [];
const extensionElements = {
'zeebe:subscription': validateSubscription,
'zeebe:Subscription': validateSubscription,
'zeebe:ioMapping': validateIoMapping,
'zeebe:IoMapping': validateIoMapping,
'zeebe:properties': validateProperties,
'zeebe:Properties': validateProperties
};

for (const [ type, validationFunction ] of Object.entries(extensionElements)) {
const element = findExtensionElement(node, type);
if (element) {
errors.push(...validationFunction(element, node));
}
}

return errors;
}

function createErrorForInvalidSecret(propertyName, node, parentNode) {
return {
message: `Element of type <${node.$type}> contains an invalid secret format in property '${propertyName}'. Must be {{secrets.YOUR_SECRET}}`,
path: null,
data: {
type: ERROR_TYPES.INVALID_SECRET_FORMAT,
node,
parentNode: parentNode,
invalidProperty: propertyName
}
};
}

function containInvalidSecret(value) {
return isString(value) && value.includes('secrets.') && !/{{secrets\.[a-zA-Z0-9_]+}}/.test(value);
}
3 changes: 2 additions & 1 deletion rules/utils/error-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ module.exports.ERROR_TYPES = Object.freeze({
PROPERTY_TYPE_NOT_ALLOWED: 'camunda.propertyTypeNotAllowed',
PROPERTY_VALUE_DUPLICATED: 'camunda.propertyValueDuplicated',
PROPERTY_VALUE_NOT_ALLOWED: 'camunda.propertyValueNotAllowed',
PROPERTY_VALUE_REQUIRED: 'camunda.propertyValueRequired'
PROPERTY_VALUE_REQUIRED: 'camunda.propertyValueRequired',
INVALID_SECRET_FORMAT: 'camunda.invalidSecretFormat'
});
20 changes: 20 additions & 0 deletions test/camunda-cloud/integration/secrets-errors.bpmn
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_10ra88m" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.4.2" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.2.0">
<bpmn:process id="Process_17ex1mt" isExecutable="true">
<bpmn:intermediateCatchEvent id="Event_1l9mlt3">
<bpmn:messageEventDefinition id="MessageEventDefinition_1xe3smg" messageRef="Message_2doqff2" />
</bpmn:intermediateCatchEvent>
</bpmn:process>
<bpmn:message id="Message_2doqff2" name="Message_2doqff2">
<bpmn:extensionElements>
<zeebe:subscription correlationKey="=secrets.KEY}" />
</bpmn:extensionElements>
</bpmn:message>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_17ex1mt">
<bpmndi:BPMNShape id="Event_018wlgk_di" bpmnElement="Event_1l9mlt3">
<dc:Bounds x="152" y="79" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
20 changes: 20 additions & 0 deletions test/camunda-cloud/integration/secrets.bpmn
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_10ra88m" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.4.2" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.1.0">
<bpmn:process id="Process_17ex1mt" isExecutable="true">
<bpmn:intermediateCatchEvent id="Event_1l9mlt3">
<bpmn:messageEventDefinition id="MessageEventDefinition_1xe3smg" messageRef="Message_2doqff2" />
</bpmn:intermediateCatchEvent>
</bpmn:process>
<bpmn:message id="Message_2doqff2" name="Message_2doqff2">
<bpmn:extensionElements>
<zeebe:subscription correlationKey="=foo" />
</bpmn:extensionElements>
</bpmn:message>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_17ex1mt">
<bpmndi:BPMNShape id="Event_018wlgk_di" bpmnElement="Event_1l9mlt3">
<dc:Bounds x="152" y="79" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
71 changes: 71 additions & 0 deletions test/camunda-cloud/integration/secrets.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const { expect } = require('chai');

const Linter = require('bpmnlint/lib/linter');

const NodeResolver = require('bpmnlint/lib/resolver/node-resolver');

const { readModdle } = require('../../helper');

const versions = [
'8.0',
'8.1',
'8.2'
];

describe('integration - secrets', function() {

versions.forEach(function(version) {

let linter;

beforeEach(function() {
linter = new Linter({
config: {
extends: `plugin:camunda-compat/camunda-cloud-${ version.replace('.', '-') }`
},
resolver: new NodeResolver()
});
});


describe(`Camunda Cloud ${ version }`, function() {

describe('no errors', function() {

it('should not have errors', async function() {

// given
const { root } = await readModdle('test/camunda-cloud/integration/secrets.bpmn');

// when
const reports = await linter.lint(root);

// then
expect(reports[ 'camunda-compat/secrets' ]).not.to.exist;
});

});


describe('errors', function() {

it('should have errors', async function() {

// given
const { root } = await readModdle('test/camunda-cloud/integration/secrets-errors.bpmn');

// when
const reports = await linter.lint(root);

// then
console.log('mylog', reports[ 'camunda-compat/secrets' ]);
expect(reports[ 'camunda-compat/secrets' ]).to.exist;
});

});

});

});

});
143 changes: 143 additions & 0 deletions test/camunda-cloud/secrets.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
const RuleTester = require('bpmnlint/lib/testers/rule-tester');
const rule = require('../../rules/camunda-cloud/secrets');
const { createDefinitions, createModdle } = require('../helper');
const { ERROR_TYPES } = require('../../rules/utils/element');


const valid = [
{
name: 'task with valid zeebe:properties',
moddleElement: createModdle(createDefinitions(`
<bpmn:process id="Process_1" isExecutable="true">
<bpmn:task id="Task_1" />
<bpmn:extensionElements>
<zeebe:properties>
<zeebe:property name="validName" value="{{secrets.VALID_SECRET}}" />
</zeebe:properties>
</bpmn:extensionElements>
</bpmn:process>
`))
}
];

const invalid = [
{
name: 'invalid secrets format in correlation key',
moddleElement: createModdle(createDefinitions(`
<bpmn:process id="Process_1" isExecutable="true">
<bpmn:subProcess id="SubProcess_1" triggeredByEvent="true">
<bpmn:startEvent id="StartEvent_1" isInterrupting="false">
<bpmn:messageEventDefinition id="MessageEventDefinition_1" messageRef="Message_1" />
</bpmn:startEvent>
</bpmn:subProcess>
</bpmn:process>
<bpmn:message id="Message_1" name="Message_1">
<bpmn:extensionElements>
<zeebe:subscription correlationKey="=secrets.KEY}" />
</bpmn:extensionElements>
</bpmn:message>
`)),
report: [
{
id: 'Message_1',
message: 'Element of type <zeebe:Subscription> contains an invalid secret format in property \'correlationKey\'. Must be {{secrets.YOUR_SECRET}}',
path: null,
data: {
type: ERROR_TYPES.INVALID_SECRET_FORMAT,
node: 'zeebe:Subscription',
parentNode: 'Message_1',
invalidProperty: 'correlationKey'
},
name: 'Message_1'
}
]
},
{
name: 'invalid secrets format in zeebe:input',
moddleElement: createModdle(createDefinitions(`
<bpmn:process id="Process_1yug3vp" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1a2mt2i</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1a2mt2i" sourceRef="StartEvent_1" targetRef="Activity_0gcnr68" />
<bpmn:endEvent id="Event_14xw0vt">
<bpmn:incoming>Flow_1i92ojz</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1i92ojz" sourceRef="Activity_0gcnr68" targetRef="Event_14xw0vt" />
<bpmn:serviceTask id="Activity_0gcnr68" zeebe:modelerTemplate="io.camunda.connectors.HttpJson.v2" zeebe:modelerTemplateVersion="1" zeebe:modelerTemplateIcon="data:image/svg+xml;utf8,%3Csvg%20width%3D%2218%22%20height%3D%2218%22%20viewBox%3D%220%200%2018%2018%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M17.0335%208.99997C17.0335%2013.4475%2013.4281%2017.0529%208.98065%2017.0529C4.53316%2017.0529%200.927765%2013.4475%200.927765%208.99997C0.927765%204.55248%204.53316%200.947083%208.98065%200.947083C13.4281%200.947083%2017.0335%204.55248%2017.0335%208.99997Z%22%20fill%3D%22%23505562%22%2F%3E%0A%3Cpath%20d%3D%22M4.93126%2014.1571L6.78106%203.71471H10.1375C11.1917%203.71471%2011.9824%203.98323%2012.5095%204.52027C13.0465%205.04736%2013.315%205.73358%2013.315%206.57892C13.315%207.44414%2013.0714%208.15522%2012.5841%208.71215C12.1067%209.25913%2011.4553%209.63705%2010.6298%209.8459L12.0619%2014.1571H10.3315L9.03364%2010.0249H7.24351L6.51254%2014.1571H4.93126ZM7.49711%208.59281H9.24248C9.99832%208.59281%2010.5901%208.42374%2011.0177%208.08561C11.4553%207.73753%2011.6741%207.26513%2011.6741%206.66842C11.6741%206.19106%2011.5249%205.81811%2011.2265%205.54959C10.9282%205.27113%2010.4558%205.1319%209.80936%205.1319H8.10874L7.49711%208.59281Z%22%20fill%3D%22white%22%2F%3E%0A%3C%2Fsvg%3E%0A">
<bpmn:extensionElements>
<zeebe:taskDefinition type="io.camunda:http-json:1" />
<zeebe:ioMapping>
<zeebe:input source="get" target="method" />
<zeebe:input source="{secrets.url}" target="url" />
</zeebe:ioMapping>
<zeebe:taskHeaders>
<zeebe:header key="resultVariable" value="res" />
</zeebe:taskHeaders>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1a2mt2i</bpmn:incoming>
<bpmn:outgoing>Flow_1i92ojz</bpmn:outgoing>
</bpmn:serviceTask>
</bpmn:process>
`)),
report: [
{
id: 'Activity_0gcnr68',
message: 'Element of type <zeebe:Input> contains an invalid secret format in property \'url\'. Must be {{secrets.YOUR_SECRET}}',
path: null,
data: {
type: ERROR_TYPES.INVALID_SECRET_FORMAT,
node: 'zeebe:Input',
parentNode: 'Activity_0gcnr68',
invalidProperty: 'url'
}
}
]
},
{
name: 'invalid secrets format in zeebe:Properties',
moddleElement: createModdle(createDefinitions(`
<bpmn:process id="Process_0wiwn7y" isExecutable="true">
<bpmn:startEvent id="Event_1xpo622">
<bpmn:outgoing>Flow_1yuqdrv</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1yuqdrv" sourceRef="Event_1xpo622" targetRef="Event_0e1cp3m" />
<bpmn:endEvent id="Event_1ovheam">
<bpmn:incoming>Flow_1aiiv9w</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1aiiv9w" sourceRef="Event_0e1cp3m" targetRef="Event_1ovheam" />
<bpmn:intermediateCatchEvent id="Event_0e1cp3m" zeebe:modelerTemplate="io.camunda.connectors.AWSEventBridge.intermediate.v1" zeebe:modelerTemplateVersion="1">
<bpmn:extensionElements>
<zeebe:properties>
<zeebe:property name="inbound.type" value="io.camunda:webhook:1" />
<zeebe:property name="inbound.shouldValidateHmac" value="disabled" />
<zeebe:property name="inbound.authorizationType" value="BASIC" />
<zeebe:property name="inbound.basic.username" value="secrets.USERNAME" />
</zeebe:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1yuqdrv</bpmn:incoming>
<bpmn:outgoing>Flow_1aiiv9w</bpmn:outgoing>
<bpmn:messageEventDefinition id="MessageEventDefinition_0ux4vg3" messageRef="Message_0qsf7ae" />
</bpmn:intermediateCatchEvent>
</bpmn:process>
`)),
report: [
{
id: 'Event_0e1cp3m',
message: 'Element of type <zeebe:Property> contains an invalid secret format in property \'inbound.basic.username\'. Must be {{secrets.YOUR_SECRET}}',
path: null,
data: {
type: ERROR_TYPES.INVALID_SECRET_FORMAT,
node: 'zeebe:Property',
parentNode: 'Event_0e1cp3m',
invalidProperty: 'inbound.basic.username'
}
}
]
}
];

RuleTester.verify('secrets', rule, {
valid,
invalid
});
Loading

0 comments on commit 4838844

Please sign in to comment.