Rule Sets¶
Rule sets are collections of rules that can be checked against a data-flow diagram to model potential threats. FlowStrider comes with two built-in rule sets: STRIDE and BSI. Each rule set includes a set of rules along with descriptions, possible mitigations, and the conditions under which each rule applies.
STRIDE¶
The STRIDE rule set is a generic collection of rules that covers the areas of spoofing,
tampering, repudiation, information disclosure, denial of service, and elevation of
privileges. For more information about STRIDE, refer to the book “Threat Modeling:
Designing for Security” by Adam Shostack. The tag for using STRIDE for a dfd is
stride.
BSI¶
The BSI rule set is based on standards issued by the German Federal Office for
Information Security (BSI). It contains a set of 13 representative rules. Compared to
the STRIDE rule set, this set is more specific and leverages security-relevant metadata
from the elements of the data-flow diagram. The tag for using the BSI rules is
bsi_rules.
LINDDUN¶
Based on the LINDDUN Go cards, this rule set focuses on privacy threats. The 33 rules
each implement one LINDDUN card and are divided into Linkability of data to users,
Identifying individuals, Non Repudiation for users, Detecting of data or
transmissions, Data Disclosure of personal data, Unawareness and
Unintervenability of users and Non Compliance to privacy standards. The tag for
using the LINDDUN rules is linddun_rules
How to Create Your Own Rule Set¶
This section explains how to add your own rule set to FlowStrider. It contains information regarding the general architecture, where to place your files and how to register your rules so they can be used. Each rule defines the requirements under which it applies and generates a threat. As rule sets are responsible for generating threats, they are also sometimes called threat catalogs or rule collections.
General Architecture¶
FlowStrider uses a modular rule system. Each ruleset (e.g., BSI, STRIDE) is implemented as a Python module containing:
Node rules
Edge rules
A rule set class that groups these rules
All available rule sets are registered in collections.py, so FlowStrider knows which rule sets to use.
Adding Your Own Rules¶
Create a New Rule Set
Place your new rule set in a new or existing folder under flowstrider/rules/builtin/
Example: flowstrider/rules/builtin/my_rules/my_rule_collection.py
Define Your Rules
To define your own rules, follow the established structure used in existing rule sets such as bsi_rule_collection.py or kubernetes_rule_collection_test.py. Each rule should be implemented as a class derived from either NodeRule or EdgeRule. First the BASE_SEVERITY attribute has to be set for the rule with 0.0 being the lowest severity. The severity attribute can then take on that value if all threats generated by this rule should be assigned this severity value. Alternatively the severity can be adjusted for each threat of a rule seperately in the _test method. The core logic of your rule belongs in a _test classmethod, while human-readable texts and descriptions are set in a separate init_texts classmethod, which contains the following:
display_name: The name of the rule
short_description: A brief summary of the rule, describing the bad outcome
long_description: A detailed explanation of the rule
mitigation_options: A list of suggested mitigations
requirement: A string describing the requirement being checked
- attribute_names: A list of strings with the attributes that are going to be needed
to determine if there is a threat or not (see the file attributes_dict.py for the attributes)
General Structure of a Rule Class
The following example illustrates the typical structure of a rule class. Here the logic is encapsulated in the _test classmethod, which should return True if the rule is violated (i.e. if a finding should be reported). The syntax prefix cls. is used, to access class-level variables, which are set in init_texts:
class MyExampleNodeRule(NodeRule): BASE_SEVERITY = 1.0 severity = BASE_SEVERITY @classmethod def init_texts(cls): _ = settings.lang_out.gettext cls.display_name = _("Example Rule") cls.short_description = _("Node is not a STRIDE process.") # Describes the unfavorable outcome cls.long_description = _( "This rule checks if a node carries the 'STRIDE:Process' tag. " "Nodes representing processes should always be tagged accordingly to " "ensure correct threat modeling." ) cls.mitigation_options = [_("Add the 'STRIDE:Process' tag to the node.")] cls.requirement = _("'STRIDE:Process' tag must be set.") cls.attribute_names = [] @classmethod def _test(cls, node, dfd): cls.severity = 1.2 * cls.BASE_SEVERITY is_stride_process = "STRIDE:Process" in node.tags return not is_stride_process # Return True if the rule is violatedThe
_testmethod receives the relevant object (node or edge) and the data-flow diagram. It is best practice to return a boolean expression that directly reflects the rule’s intent, as shown above.Once the rules are defined, group them in a collection class derived from DataflowDiagramRuleCollection:
class MyRuleCollection(DataflowDiagramRuleCollection): tags = {"my_rules"} # Tags in the dfd json for which this rule set applies @classmethod def init_texts(cls): _ = settings.lang_out.gettext cls.name = _("My rule set") cls.references = [ ("https://www.my-reference.tld") ] node_rules = [MyExampleNodeRule] edge_rules = [] dfd_rules = [] __all__ = ["MyRuleCollection"]
Add an `__init__.py`
In your rule set folder (e.g., my_rules/…) add an __init__.py file that imports your collection:
from .my_rule_collection import MyRuleCollection __all__ = ["MyRuleCollection"]
Register Your Rule Collection
Edit flowstrider/rules/collections.py and import your collection at the top:
from .builtin.my_rules import MyRuleCollectionAdd your collection to the all_collections list:
all_collections = [ BSIRuleCollection, GenericSTRIDERuleCollection, MyRuleCollection, # <-- New rule goes here ]
Tag Your JSON Models
In your DFD JSON files, add the tag for your rule set (e.g., “my_rules”) to the “tags” list at the bottom, this tells FlowStrider to apply your rule set to this data flow diagram in the
elicitcalls.{ "dfd": { "id": "Example", "_comment": "*rest of the JSON content*" "tags": [ "stride", "my_rules" ], "attributes": {} } }
Add Attributes (Optional)
All available attributes are centrally defined in flowstrider/rules/attributes_dict.py in the attributes dictionary and each entry specifies:
A short display name
A description
The entity types the attribute applies to (e.g., “Node: DataStore”, “Edge: Dataflow”)
The list of accepted values (if applicable)
For example:
attributes["handles_confidential_data"] = Attribute( _("Handles confidential data"), _("Whether the entity handles confidential data."), ["Node: DataStore", "Node: Process", "Edge: Dataflow"], [True, False], ) attributes["encryption_method"] = Attribute( _("Encryption method"), _("Which method of encryption will be used to encrypt the data."), ["Node: DataStore"], ["AES_128", "AES_192", "AES_256"], ) # More attributes ... }To add a new attribute, add a new entry to the dictionary while following the same structure, but reuse existing attributes where possible. If your attribute is only relevant for a specific ruleset, you may also organize it in a separate file (e.g., my_attributes.py).
Using Attributes in JSON Models
Attributes are set in the DFD JSON files under the attributes key for each node or edge. For example:
"Node1": { "id": "Node1", "name": "Database", "tags": ["STRIDE:DataStore"], "attributes": { "handles_confidential_data": true, "encryption_method": "AES-256" } }(While this step can be done manually, in practice it’s easier handled by using the tool functions
metadataandupdateas seen in Usage)
Accessing Attributes in Rules
Within the rule classes, attributes of elements can be accessed through the attributes dictionary of the node or edge. For example:
class MyEncryptionRule(NodeRule): @classmethod def init_texts(cls): cls.display_name = "Example Rule" cls.short_description = "..." cls.long_description = "..." cls.mitigation_options = ["..."] cls.attribute_names = ["handles_confidential_data", "encryption_method"] cls.allowed_encryption = attributes_dict.attributes[cls.attribute_names[1]][3] cls.requirement = attributes_dict.attributes[cls.attribute_names[1]][0] + _(": one of {") + ", ".join(cls.allowed_encryption) + "}" @classmethod def _test(cls, node, dfd): handles_confidential = node.attributes.get(cls.attribute_names[0], True) # With True if unknown encryption = node.attributes.get(cls.attribute_names[1], "") # With default = empty string uses_allowed_encryption = meet_any_requirement(encryption, cls.allowed_encryption) return handles_confidential and not uses_allowed_encryption