diff --git a/docs/adr/0002-model-processes-with-behavior-trees.md b/docs/adr/0002-model-processes-with-behavior-trees.md new file mode 100644 index 00000000..f5430914 --- /dev/null +++ b/docs/adr/0002-model-processes-with-behavior-trees.md @@ -0,0 +1,62 @@ +--- +status: accepted +date: 2023-10-23 +deciders: adh +--- +# Model Processes with Behavior Trees + +## Context and Problem Statement + +The Vultron protocol is defined as a set of three interacting state machines for each agent: + +- Report Management +- Embargo Management +- Case State Management + +We have a need to simulate the behavior of these state machines in order to test the protocol. +However, the interactions between them can be complex, so we need a way to model those interactions. + +## Decision Drivers + +- Need to simulate complex interactions between state machines +- Need to explore and exercise state space of the protocol even if some states are rarely reached in practice + +## Considered Options + +- Object-oriented Agent implementation +- Behavior Tree Agent implementation + +## Decision Outcome + +Chosen option: "Behavior Tree Agent implementation", because behavior trees allow us to model complex interactions +between state machines. By building in stochastic behaviors, they also allow us to explore and exercise the state space +of the protocol even if some states are rarely reached in practice. + +### Consequences + +- Behavior trees are a new technology for the team, so there will be a learning curve. +- Simulating changes to process logic should be easier with behavior trees than with object-oriented code. + +## Pros and Cons of the Options + +### Object-oriented implementation + +Good, because: + +- Standard OO python approach understood by the team +- State management could be implemented as a set of classes for each state machine + +Bad, because: + +- Complex interactions between state machines would be difficult to model and maintain +- Reactive behaviors (e.g., based on state changes in outside world) would be difficult to model + +## More Information + +- Michele Colledanchise, Petter Ögren: Behavior Trees in Robotics and AI + - [book @ Amazon](https://www.amazon.com/Behavior-Trees-Robotics-Introduction-Intelligence/dp/1138593737) + - [pdf @ arXiv](https://arxiv.org/abs/1709.00084) +- Petter Ögren's YouTube channel has a number of good videos on Behavior Trees + - +- Wikipedia + - [Behavior Tree (artificial intelligence, robotics and control)](https://en.wikipedia.org/wiki/Behavior_tree_(artificial_intelligence,_robotics_and_control)) diff --git a/docs/adr/0003-build-custom-python-bt-engine.md b/docs/adr/0003-build-custom-python-bt-engine.md new file mode 100644 index 00000000..e250bb6e --- /dev/null +++ b/docs/adr/0003-build-custom-python-bt-engine.md @@ -0,0 +1,85 @@ +--- +status: accepted +date: 2023-10-23 +deciders: adh +--- +# Build our own Behavior Tree engine in Python + +## Context and Problem Statement + +We need a Behavior Tree engine to support our Behavior Tree Agent implementation. + +## Decision Drivers + +- Need to support stochastic behaviors +- Team is most familiar with Python +- Need to support quick prototyping and experimentation + +## Considered Options + +- BehaviorTree.CPP +- py_trees +- Build our own + +## Decision Outcome + +Chosen option: "Build our own", because + +- It's a good opportunity to learn about Behavior Trees +- py_trees is closely tied to robot operating system (ROS), so it might not be a good fit for our use case +- BehaviorTree.CPP is written in C++, so it would be difficult to integrate with the rest of our Python codebase + +This decision can/should be revisited if + +- the complexity of building our own Behavior Tree engine becomes too high +- we discover that existing Behavior Tree engines already support features that would be difficult for us to build + +In order to mitigate the risk of building our own Behavior Tree engine, we will maintain a modular design +that allows us to swap out our engine for an existing engine if necessary. + +### Consequences + +Good, because: + +- Provides an opportunity to learn more about Behavior Trees +- We can build it in Python, so it should be easy to integrate with the rest of our codebase +- We can build it in a way that supports stochastic behaviors +- We can customize it to our needs + +Neutral, because: + +- We might discover that we need features that are already supported by existing Behavior Tree engines +- We might find that other Behavior Tree engines are better suited to our needs + +Bad, because: + +- We have to build and maintain our own Behavior Tree engine +- It's another dependency we have to own and maintain + +## Pros and Cons of the Options + +### BehaviorTree.CPP + +Good, because: + +- It's a mature, well-tested Behavior Tree engine + +Bad, because: + +- It's written in C++, so it would be difficult to integrate with the rest of our Python codebase + +### py_trees + +Good, because: + +- It's written in Python, so it should be easy to integrate with the rest of our codebase +- It seems to be a mature, well-tested Behavior Tree engine + +Neutral-to-bad, because: + +- It's closely tied to robot operating system (ROS), so it might not be a good fit for our use case + +## More Information + +- [BehaviorTree.CPP](https://www.behaviortree.dev/) [GitHub](https://github.com/BehaviorTree/BehaviorTree.CPP) +- [py_trees](https://py-trees.readthedocs.io/en/devel/) [GitHub](https://github.com/splintered-reality/py_trees) diff --git a/docs/adr/0004-use-factory-methods-for-common-bt-node-types.md b/docs/adr/0004-use-factory-methods-for-common-bt-node-types.md new file mode 100644 index 00000000..afa245cb --- /dev/null +++ b/docs/adr/0004-use-factory-methods-for-common-bt-node-types.md @@ -0,0 +1,70 @@ +--- +# These are optional elements. Feel free to remove any of them. +status: accepted +date: 2023-10-24 +deciders: adh +--- +# Use factory methods for common BT node types + +## Context and Problem Statement + +We have a number of common BT node types that are used in multiple trees. We want to be able to create these nodes +in a consistent way, and we want to be able to easily change the implementation of these nodes without having to +change the code that creates them. + +## Decision Drivers + +* We want to be able to create these nodes in a consistent way +* Preserve future flexibility to change the underlying BT node implementation without having to change the code that creates them + +## Considered Options + +* No factory methods, directly subclass the BT node types +* Use factory methods + +## Decision Outcome + +Chosen option: "Use factory methods", because it allows us to create the nodes in a consistent way, and it allows us to +change the underlying implementation without having to change the code that creates them. + +### Consequences + +Good because: + +* retains flexibility to change the underlying implementation without having to change the code that creates them +* allows us to create the nodes in a consistent way +* allows us to keep the `vultron.bt.base` module clean and focused on the base classes +* allows us to keep the rest of the `vultron.bt` module focused on Vultron-specific needs + +Neutral because: + +* Adds a central place to maintain the factory methods + +Bad because: + +* less pythonic than just subclassing the BT node types + +## Pros and Cons of the Options + +### No factory methods + +Good because: + +* more pythonic +* + +Neutral because: + +Bad because: + +* Harder to enforce consistency in how the nodes are created + +## More Information + +This decision was inspired in part by the `py_trees` [documentation](https://py-trees.readthedocs.io/en/devel/the_crazy_hospital.html) +(in the context of composite nodes): + +> Don’t subclass merely to auto-populate it, build a create__subtree() library instead + +Which got us thinking about using factory methods to help maintain a clean separation between the `vultron.bt.base` +module and the things that live above it. diff --git a/docs/reference/code/bt/behavior_trees.md b/docs/reference/code/bt/behavior_trees.md new file mode 100644 index 00000000..58580e20 --- /dev/null +++ b/docs/reference/code/bt/behavior_trees.md @@ -0,0 +1,15 @@ +# Behavior Trees + +::: vultron.bt + +::: vultron.bt.base + +::: vultron.bt.behaviors + +::: vultron.bt.fuzzer + +::: vultron.bt.states + +::: vultron.bt.errors + +::: vultron.bt.common diff --git a/docs/reference/code/bt/bt_base.md b/docs/reference/code/bt/bt_base.md new file mode 100644 index 00000000..e3adf9ed --- /dev/null +++ b/docs/reference/code/bt/bt_base.md @@ -0,0 +1,13 @@ +# Behavior Tree Base + +The Vultron behavior tree base module defines the basic building blocks of any behavior tree. +This page covers the basic Tree and Blackboard classes, as well as the basic error classes. +Detailed descriptions of Node types are provided in [Behavior Tree Basic Node Types](bt_base_nodes.md). + +::: vultron.bt.base + +::: vultron.bt.base.bt + +::: vultron.bt.base.blackboard + +::: vultron.bt.base.errors diff --git a/docs/reference/code/bt/bt_base_nodes.md b/docs/reference/code/bt/bt_base_nodes.md new file mode 100644 index 00000000..9b65808d --- /dev/null +++ b/docs/reference/code/bt/bt_base_nodes.md @@ -0,0 +1,15 @@ +# Behavior Tree Basic Node Types + +We have defined a number of basic node types. +These are the building blocks of any behavior tree. +They are defined in the `vultron.bt.base` module. + +::: vultron.bt.base.node_status + +::: vultron.bt.base.bt_node + +::: vultron.bt.base.composites + +::: vultron.bt.base.decorators + +::: vultron.bt.base.fuzzer diff --git a/docs/reference/code/bt/case_states.md b/docs/reference/code/bt/case_states.md new file mode 100644 index 00000000..ef80f163 --- /dev/null +++ b/docs/reference/code/bt/case_states.md @@ -0,0 +1,5 @@ +# Vultron Case State Behaviors + +::: vultron.bt.case_state.conditions + +::: vultron.bt.case_state.transitions diff --git a/docs/reference/code/bt/pacman_demo.md b/docs/reference/code/bt/pacman_demo.md new file mode 100644 index 00000000..a96b5db3 --- /dev/null +++ b/docs/reference/code/bt/pacman_demo.md @@ -0,0 +1,80 @@ +# Pacman Bot Behavior Tree Demo + +This is a demo of a behavior tree for a game playing bot. +It has no practical use for Vultron, rather it's an introduction to how to use the behavior tree +framework we've built. + +We're providing this as an example to show how to build a behavior tree that can be used +to implement some context-aware behavior. + +It implements a simplified Pacman game with a bot that eats pills and avoids ghosts. + +## Behaviors + +If no ghosts are nearby, Pacman eats one pill per tick. +But if a ghost is nearby, Pacman will chase it if it is scared, otherwise he will avoid it. +If he successfully avoids a ghost, he will eat a pill. +The game ends when Pacman has eaten all the pills or has been eaten by a ghost. + +## Scoring + +Scoring is as follows: + +- 10 points per pill +- 200 points for the first ghost, 400 for the second, 800 for the third, 1600 for the fourth + +There are 240 pills on the board. The max score is + +$$(240 \times 10) + (200 + 400 + 800 + 1600) = 5400$$ + +## Differences from the real thing + +If the game exceeds 1000 ticks, Pacman gets bored and quits (but statistically that should never happen). + +We did not model power pellets, fruit, or the maze. Ghosts just randomly get scared and then randomly +stop being scared. Ghosts and pills are just counters. Ghost location is just a random "nearby" check. + +## The Behavior Tree + +The tree structure is shown below. + +{% include-markdown './pacman_tree_diagram.md' %} + +Legend: + +| Symbol | Meaning | +|----------| ------- | +| ? | FallbackNode | +| → | SequenceNode | +| ⇅ | Invert | +| ▰ | ActionNode | +| ⬬ | ConditionNode | +| 🎲 | Fuzzer node (randomly succeeds or fails some percentage of the time) | + +## Demo Output + +!!! example + + ```shell + # if vultron package is installed + # run the demo + $ vultrabot --pacman + + # print tree and exit + $ vultrabot --pacman --print-tree + + # if vultron package is not installed + $ python -m vultron.bt.base.demo.pacman + ``` + +When the tree is run, it will look something like this: + +```text +{% include-markdown './pacman_tree_example.txt' %} +``` + +## Demo Code + +::: vultron.bt.base.demo.pacman + options: + heading_level: 3 diff --git a/docs/reference/code/bt/pacman_tree_diagram.md b/docs/reference/code/bt/pacman_tree_diagram.md new file mode 100644 index 00000000..3a7b04a3 --- /dev/null +++ b/docs/reference/code/bt/pacman_tree_diagram.md @@ -0,0 +1,36 @@ +```mermaid +graph TD + MaybeEatPills_1["→ MaybeEatPills"] + MaybeChaseOrAvoidGhost_2["? MaybeChaseOrAvoidGhost"] + MaybeEatPills_1 --> MaybeChaseOrAvoidGhost_2 + NoMoreGhosts_3["#8645; NoMoreGhosts"] + MaybeChaseOrAvoidGhost_2 --> NoMoreGhosts_3 + GhostsRemain_4["#11052; GhostsRemain"] + NoMoreGhosts_3 --> GhostsRemain_4 + NoGhostClose_5["#8645; NoGhostClose"] + MaybeChaseOrAvoidGhost_2 --> NoGhostClose_5 + GhostClose_6["#127922; GhostClose"] + NoGhostClose_5 --> GhostClose_6 + ChaseOrAvoidGhost_7["? ChaseOrAvoidGhost"] + MaybeChaseOrAvoidGhost_2 --> ChaseOrAvoidGhost_7 + ChaseIfScared_8["→ ChaseIfScared"] + ChaseOrAvoidGhost_7 --> ChaseIfScared_8 + GhostsScared_9["#11052; GhostsScared"] + ChaseIfScared_8 --> GhostsScared_9 + ChaseGhost_10["#127922; ChaseGhost"] + ChaseIfScared_8 --> ChaseGhost_10 + CaughtGhost_11["→ CaughtGhost"] + ChaseIfScared_8 --> CaughtGhost_11 + DecrGhostCount_12["#9648; DecrGhostCount"] + CaughtGhost_11 --> DecrGhostCount_12 + ScoreGhost_13["#9648; ScoreGhost"] + CaughtGhost_11 --> ScoreGhost_13 + IncrGhostScore_14["#9648; IncrGhostScore"] + CaughtGhost_11 --> IncrGhostScore_14 + GhostsScared_15["#11052; GhostsScared"] + ChaseOrAvoidGhost_7 --> GhostsScared_15 + AvoidGhost_16["#127922; AvoidGhost"] + ChaseOrAvoidGhost_7 --> AvoidGhost_16 + EatPill_17["#9648; EatPill"] + MaybeEatPills_1 --> EatPill_17 +``` diff --git a/docs/reference/code/bt/pacman_tree_example.txt b/docs/reference/code/bt/pacman_tree_example.txt new file mode 100644 index 00000000..47961b84 --- /dev/null +++ b/docs/reference/code/bt/pacman_tree_example.txt @@ -0,0 +1,127 @@ +=== Tick 1 === +(>) >_MaybeEatPills_1 + | (?) ?_MaybeChaseOrAvoidGhost_2 + | | (^) ^_NoMoreGhosts_3 + | | | (c) c_GhostsRemain_4 + | | | | = SUCCESS + | | | = FAILURE + | | (^) ^_NoGhostClose_5 + | | | (z) z_GhostClose_6 + | | | | = SUCCESS + | | | = FAILURE + | | (?) ?_ChaseOrAvoidGhost_7 + | | | (>) >_ChaseIfScared_8 + | | | | (c) c_GhostsScared_9 + | | | | | = FAILURE + | | | | = FAILURE + | | | (c) c_GhostsScared_15 + | | | | = FAILURE + | | | (z) z_AvoidGhost_16 + | | | | = SUCCESS + | | | = SUCCESS + | | = SUCCESS + | (a) a_EatPill_17 + | | = SUCCESS + | = SUCCESS +=== Tick 2 === +(>) >_MaybeEatPills_1 + | (?) ?_MaybeChaseOrAvoidGhost_2 + | | (^) ^_NoMoreGhosts_3 + | | | (c) c_GhostsRemain_4 + | | | | = SUCCESS + | | | = FAILURE + | | (^) ^_NoGhostClose_5 + | | | (z) z_GhostClose_6 + | | | | = FAILURE + | | | = SUCCESS + | | = SUCCESS + | (a) a_EatPill_17 + | | = SUCCESS + | = SUCCESS +=== Tick 3 === +Ghosts are scared! +(>) >_MaybeEatPills_1 + | (?) ?_MaybeChaseOrAvoidGhost_2 + | | (^) ^_NoMoreGhosts_3 + | | | (c) c_GhostsRemain_4 + | | | | = SUCCESS + | | | = FAILURE + | | (^) ^_NoGhostClose_5 + | | | (z) z_GhostClose_6 + | | | | = SUCCESS + | | | = FAILURE + | | (?) ?_ChaseOrAvoidGhost_7 + | | | (>) >_ChaseIfScared_8 + | | | | (c) c_GhostsScared_9 + | | | | | = SUCCESS + | | | | (z) z_ChaseGhost_10 + | | | | | = SUCCESS + | | | | (>) >_CaughtGhost_11 + | | | | | (a) a_DecrGhostCount_12 +Clyde was caught! + | | | | | | = SUCCESS + | | | | | (a) a_ScoreGhost_13 + | | | | | | = SUCCESS + | | | | | (a) a_IncrGhostScore_14 +Ghost score is now 400 + | | | | | | = SUCCESS + | | | | | = SUCCESS + | | | | = SUCCESS + | | | = SUCCESS + | | = SUCCESS + | (a) a_EatPill_17 + | | = SUCCESS + | = SUCCESS +=== Tick 4 === +(>) >_MaybeEatPills_1 + | (?) ?_MaybeChaseOrAvoidGhost_2 + | | (^) ^_NoMoreGhosts_3 + | | | (c) c_GhostsRemain_4 + | | | | = SUCCESS + | | | = FAILURE + | | (^) ^_NoGhostClose_5 + | | | (z) z_GhostClose_6 + | | | | = SUCCESS + | | | = FAILURE + | | (?) ?_ChaseOrAvoidGhost_7 + | | | (>) >_ChaseIfScared_8 + | | | | (c) c_GhostsScared_9 + | | | | | = FAILURE + | | | | = FAILURE + | | | (c) c_GhostsScared_15 + | | | | = FAILURE + | | | (z) z_AvoidGhost_16 + | | | | = SUCCESS + | | | = SUCCESS + | | = SUCCESS + | (a) a_EatPill_17 + | | = SUCCESS + | = SUCCESS +=== Tick 5 === +(>) >_MaybeEatPills_1 + | (?) ?_MaybeChaseOrAvoidGhost_2 + | | (^) ^_NoMoreGhosts_3 + | | | (c) c_GhostsRemain_4 + | | | | = SUCCESS + | | | = FAILURE + | | (^) ^_NoGhostClose_5 + | | | (z) z_GhostClose_6 + | | | | = SUCCESS + | | | = FAILURE + | | (?) ?_ChaseOrAvoidGhost_7 + | | | (>) >_ChaseIfScared_8 + | | | | (c) c_GhostsScared_9 + | | | | | = FAILURE + | | | | = FAILURE + | | | (c) c_GhostsScared_15 + | | | | = FAILURE + | | | (z) z_AvoidGhost_16 + | | | | = FAILURE + | | | = FAILURE + | | = FAILURE + | = FAILURE +Pacman died! He was eaten by Pinky! +Final score: 240 +Ticks: 5 +Dots Remaining: 236 +Ghosts Remaining: 3 (Blinky, Pinky, Inky) diff --git a/docs/reference/code/bt/robot_demo.md b/docs/reference/code/bt/robot_demo.md new file mode 100644 index 00000000..506d928e --- /dev/null +++ b/docs/reference/code/bt/robot_demo.md @@ -0,0 +1,64 @@ +# Robot Behavior Tree Demo + +This is a demo of a behavior tree for a robot. +It has no practical use for Vultron, rather it's an introduction to how to use the behavior tree +framework we've built. + +We're providing this as an example to show how to build a behavior tree that can be used +to implement some context-aware behavior. + +## Behaviors + +The robot has a number of behaviors: + +- If the ball is in the bin, it will stop. +- If the ball is in the robot's grasp and it is near the bin, it will put it in the bin. +- If the ball is in the robot's grasp and it is not near the bin, it will move toward the bin. +- If the ball is nearby, it will try to pick it up (and sometimes fail) +- If the ball is not nearby, it will move toward the ball. +- If the ball is not found, it will search for it. +- If it fails to complete its task, it will ask for help. + +## The Behavior Tree + +The tree structure is shown below. + +{% include-markdown './robot_demo_diagram.md' %} + +Legend: + +| Symbol | Meaning | +|----------| ------- | +| ? | FallbackNode | +| → | SequenceNode | +| ⇅ | Invert | +| ▰ | ActionNode | +| ⬬ | ConditionNode | +| 🎲 | Fuzzer node (randomly succeeds or fails some percentage of the time) | + +## Demo Output + +!!! example Running the Demo + ```shell + # if vultron package is installed + # run the demo + $ vultrabot --robot + + # print the tree and exit + $ vultrabot --robot --print-tree + + # if vultron package is not installed + python -m vultron.bt.base.demo.robot + ``` + +When the tree is run, it will look something like this: + +```text +{% include-markdown './robot_tree_example.txt' %} +``` + +## Demo Code + +::: vultron.bt.base.demo.robot + options: + heading_level: 3 diff --git a/docs/reference/code/bt/robot_demo_diagram.md b/docs/reference/code/bt/robot_demo_diagram.md new file mode 100644 index 00000000..1b5d3810 --- /dev/null +++ b/docs/reference/code/bt/robot_demo_diagram.md @@ -0,0 +1,64 @@ +```mermaid +graph LR + Robot_1["? Robot"] + BallPlaced_2["#11052; BallPlaced"] + Robot_1 --> BallPlaced_2 + MainSequence_3["→ MainSequence"] + Robot_1 --> MainSequence_3 + EnsureBallFound_4["? EnsureBallFound"] + MainSequence_3 --> EnsureBallFound_4 + BallFound_5["#11052; BallFound"] + EnsureBallFound_4 --> BallFound_5 + FindBall_6["→ FindBall"] + EnsureBallFound_4 --> FindBall_6 + UsuallySucceed_7["#127922; UsuallySucceed"] + FindBall_6 --> UsuallySucceed_7 + SetBallFound_8["#9648; SetBallFound"] + FindBall_6 --> SetBallFound_8 + EnsureBallClose_9["? EnsureBallClose"] + MainSequence_3 --> EnsureBallClose_9 + BallClose_10["#11052; BallClose"] + EnsureBallClose_9 --> BallClose_10 + ApproachBall_11["→ ApproachBall"] + EnsureBallClose_9 --> ApproachBall_11 + UsuallySucceed_12["#127922; UsuallySucceed"] + ApproachBall_11 --> UsuallySucceed_12 + SetBallClose_13["#9648; SetBallClose"] + ApproachBall_11 --> SetBallClose_13 + EnsureBallGrasped_14["? EnsureBallGrasped"] + MainSequence_3 --> EnsureBallGrasped_14 + BallGrasped_15["#11052; BallGrasped"] + EnsureBallGrasped_14 --> BallGrasped_15 + GraspBall_16["→ GraspBall"] + EnsureBallGrasped_14 --> GraspBall_16 + OftenFail_17["#127922; OftenFail"] + GraspBall_16 --> OftenFail_17 + SetBallGrasped_18["#9648; SetBallGrasped"] + GraspBall_16 --> SetBallGrasped_18 + EnsureBinClose_19["? EnsureBinClose"] + MainSequence_3 --> EnsureBinClose_19 + BinClose_20["#11052; BinClose"] + EnsureBinClose_19 --> BinClose_20 + ApproachBin_21["→ ApproachBin"] + EnsureBinClose_19 --> ApproachBin_21 + UsuallySucceed_22["#127922; UsuallySucceed"] + ApproachBin_21 --> UsuallySucceed_22 + SetBinClose_23["#9648; SetBinClose"] + ApproachBin_21 --> SetBinClose_23 + EnsureBallPlaced_24["? EnsureBallPlaced"] + MainSequence_3 --> EnsureBallPlaced_24 + BallPlaced_25["#11052; BallPlaced"] + EnsureBallPlaced_24 --> BallPlaced_25 + PlaceBall_26["→ PlaceBall"] + EnsureBallPlaced_24 --> PlaceBall_26 + UsuallySucceed_27["#127922; UsuallySucceed"] + PlaceBall_26 --> UsuallySucceed_27 + SetBallPlaced_28["#9648; SetBallPlaced"] + PlaceBall_26 --> SetBallPlaced_28 + MaybeAskForHelp_29["→ MaybeAskForHelp"] + Robot_1 --> MaybeAskForHelp_29 + TimeToAskForHelp_30["#11052; TimeToAskForHelp"] + MaybeAskForHelp_29 --> TimeToAskForHelp_30 + AskForHelp_31["#9648; AskForHelp"] + MaybeAskForHelp_29 --> AskForHelp_31 +``` diff --git a/docs/reference/code/bt/robot_tree_example.txt b/docs/reference/code/bt/robot_tree_example.txt new file mode 100644 index 00000000..36b5f375 --- /dev/null +++ b/docs/reference/code/bt/robot_tree_example.txt @@ -0,0 +1,132 @@ +(?) ?_Robot_1 + | (c) c_BallPlaced_2 + | | = FAILURE + | (>) >_MainSequence_3 + | | (?) ?_EnsureBallFound_4 + | | | (c) c_BallFound_5 + | | | | = FAILURE + | | | (>) >_FindBall_6 + | | | | (z) z_UsuallySucceed_7 + | | | | | = SUCCESS + | | | | (a) a_SetBallFound_8 + | | | | | = SUCCESS + | | | | = SUCCESS + | | | = SUCCESS + | | (?) ?_EnsureBallClose_9 + | | | (c) c_BallClose_10 + | | | | = FAILURE + | | | (>) >_ApproachBall_11 + | | | | (z) z_UsuallySucceed_12 + | | | | | = SUCCESS + | | | | (a) a_SetBallClose_13 + | | | | | = SUCCESS + | | | | = SUCCESS + | | | = SUCCESS + | | (?) ?_EnsureBallGrasped_14 + | | | (c) c_BallGrasped_15 + | | | | = FAILURE + | | | (>) >_GraspBall_16 + | | | | (z) z_OftenFail_17 + | | | | | = SUCCESS + | | | | (a) a_SetBallGrasped_18 + | | | | | = SUCCESS + | | | | = SUCCESS + | | | = SUCCESS + | | (?) ?_EnsureBinClose_19 + | | | (c) c_BinClose_20 + | | | | = FAILURE + | | | (>) >_ApproachBin_21 + | | | | (z) z_UsuallySucceed_22 + | | | | | = SUCCESS + | | | | (a) a_SetBinClose_23 + | | | | | = SUCCESS + | | | | = SUCCESS + | | | = SUCCESS + | | (?) ?_EnsureBallPlaced_24 + | | | (c) c_BallPlaced_25 + | | | | = FAILURE + | | | (>) >_PlaceBall_26 + | | | | (z) z_UsuallySucceed_27 + | | | | | = FAILURE + | | | | = FAILURE + | | | = FAILURE + | | = FAILURE + | (>) >_MaybeAskForHelp_29 + | | (c) c_TimeToAskForHelp_30 + | | | = FAILURE + | | = FAILURE + | = FAILURE +The ball was knocked out of the robot's grasp! +(?) ?_Robot_1 + | (c) c_BallPlaced_2 + | | = FAILURE + | (>) >_MainSequence_3 + | | (?) ?_EnsureBallFound_4 + | | | (c) c_BallFound_5 + | | | | = SUCCESS + | | | = SUCCESS + | | (?) ?_EnsureBallClose_9 + | | | (c) c_BallClose_10 + | | | | = SUCCESS + | | | = SUCCESS + | | (?) ?_EnsureBallGrasped_14 + | | | (c) c_BallGrasped_15 + | | | | = FAILURE + | | | (>) >_GraspBall_16 + | | | | (z) z_OftenFail_17 + | | | | | = SUCCESS + | | | | (a) a_SetBallGrasped_18 + | | | | | = SUCCESS + | | | | = SUCCESS + | | | = SUCCESS + | | (?) ?_EnsureBinClose_19 + | | | (c) c_BinClose_20 + | | | | = SUCCESS + | | | = SUCCESS + | | (?) ?_EnsureBallPlaced_24 + | | | (c) c_BallPlaced_25 + | | | | = FAILURE + | | | (>) >_PlaceBall_26 + | | | | (z) z_UsuallySucceed_27 + | | | | | = FAILURE + | | | | = FAILURE + | | | = FAILURE + | | = FAILURE + | (>) >_MaybeAskForHelp_29 + | | (c) c_TimeToAskForHelp_30 + | | | = FAILURE + | | = FAILURE + | = FAILURE +(?) ?_Robot_1 + | (c) c_BallPlaced_2 + | | = FAILURE + | (>) >_MainSequence_3 + | | (?) ?_EnsureBallFound_4 + | | | (c) c_BallFound_5 + | | | | = SUCCESS + | | | = SUCCESS + | | (?) ?_EnsureBallClose_9 + | | | (c) c_BallClose_10 + | | | | = SUCCESS + | | | = SUCCESS + | | (?) ?_EnsureBallGrasped_14 + | | | (c) c_BallGrasped_15 + | | | | = SUCCESS + | | | = SUCCESS + | | (?) ?_EnsureBinClose_19 + | | | (c) c_BinClose_20 + | | | | = SUCCESS + | | | = SUCCESS + | | (?) ?_EnsureBallPlaced_24 + | | | (c) c_BallPlaced_25 + | | | | = FAILURE + | | | (>) >_PlaceBall_26 + | | | | (z) z_UsuallySucceed_27 + | | | | | = SUCCESS + | | | | (a) a_SetBallPlaced_28 + | | | | | = SUCCESS + | | | | = SUCCESS + | | | = SUCCESS + | | = SUCCESS + | = SUCCESS +Robot completed its mission in 3 ticks. diff --git a/docs/reference/code/bt/vultrabot_demo.md b/docs/reference/code/bt/vultrabot_demo.md new file mode 100644 index 00000000..38c4635a --- /dev/null +++ b/docs/reference/code/bt/vultrabot_demo.md @@ -0,0 +1,39 @@ +# Vultrabot Behavior Tree Demo + +This demo implements a far more complex behavior tree than the simple +ones in the [Pacman](./pacman_demo.md) and [Robot](./robot_demo.md) demos. + +The Behavior Tree for this demo is essentially the entire tree defined in the +[Behavior Logic](../topics/behavior_logic/cvd_bt.md) section. + +## Demo Output + +!!! example "Usage" + + ```shell + # if vultron package is installed + # run the demo + $ vultrabot + # or + $ vultrabot --cvd + + # print the tree and exit + $ vultrabot --cvd --print-tree + + # if vultron package is not installed + $ python -m vultron.bt.base.demo.vultrabot + ``` + +When the tree is run, it will look something like this: + +```text +{% include-markdown './vultrabot_tree_example.txt' %} +``` + +The full tree is too large to display here, but you can run the demo to see it. + +## Demo Code + +::: vultron.demo.vultrabot + options: + heading_level: 3 diff --git a/docs/reference/code/bt/vultrabot_tree_example.txt b/docs/reference/code/bt/vultrabot_tree_example.txt new file mode 100644 index 00000000..b78921b2 --- /dev/null +++ b/docs/reference/code/bt/vultrabot_tree_example.txt @@ -0,0 +1,20 @@ + msgs_received_this_tick q_rm q_em q_cs msgs_emitted_this_tick CVD_role +1 () START NONE vfdpxa () FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +2 (RS, RS, CV, CV, RK, CV, RK, CK, CK, CK) START NONE vfdpxa (RS, CV, CV, RK, CV, RK, CK, CK, CK, RV) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +3 (RV, RK) VALID NONE Vfdpxa (RK, RA, EP) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +4 (RA, EP, RK, EK) ACCEPTED PROPOSED Vfdpxa (RK, EK, ER) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +5 (ER, EK) ACCEPTED NONE Vfdpxa (EK, EP) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +6 (EP, EK) ACCEPTED PROPOSED Vfdpxa (EK, EA) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +7 (EA, EK) ACCEPTED ACTIVE Vfdpxa (EK, EV) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +8 (EV, EK) ACCEPTED REVISE Vfdpxa (EK, EA) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +9 (EA, EK) ACCEPTED ACTIVE Vfdpxa (EK,) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +10 (CA, ET, CK, EK) ACCEPTED ACTIVE Vfdpxa (ET, CK, EK) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +11 () ACCEPTED EXITED VfdpxA () FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +12 (CP, CK) ACCEPTED EXITED VfdpxA (CK,) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +13 () ACCEPTED EXITED VfdPxA (CF,) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +14 (CF, CK) ACCEPTED EXITED VFdPxA (CK,) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +15 () ACCEPTED EXITED VFdPxA () FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +18 () ACCEPTED EXITED VFdPxA (CD,) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +19 (CD, CK) ACCEPTED EXITED VFDPxA (CK, RD) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +20 (RD, CA, RK, CK) DEFERRED EXITED VFDPxA (RK, CK, RC) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR +21 (RD, CA, RK, CK) CLOSED EXITED VFDPxA (RK, CK, RC) FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR diff --git a/mkdocs.yml b/mkdocs.yml index a72651c2..c1e1f5a3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -126,6 +126,13 @@ nav: - Code: - vultron: 'reference/code/index.md' - case_states: 'reference/code/case_states.md' + - Behavior Trees: + - BT: 'reference/code/bt/behavior_trees.md' + - Base: 'reference/code/bt//bt_base.md' + - Nodes: 'reference/code/bt/bt_base_nodes.md' + - Pacman Demo: 'reference/code/bt/pacman_demo.md' + - Robot Demo: 'reference/code/bt/robot_demo.md' + - Vultrabot Demo: 'reference/code/bt/vultrabot_demo.md' - ISO Crosswalk: - Introduction: 'reference/iso_crosswalk.md' - ISO 30111: 'reference/iso_30111_2019.md' diff --git a/not_working/__init__.py b/not_working/__init__.py new file mode 100644 index 00000000..e2731de6 --- /dev/null +++ b/not_working/__init__.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +""" +file: __init__.py +author: adh +created_at: 10/9/23 1:11 PM +""" + + +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +def main(): + pass + + +if __name__ == "__main__": + main() diff --git a/not_working/test_messaging_outbound_behaviors.py b/not_working/test_messaging_outbound_behaviors.py new file mode 100644 index 00000000..a1724d82 --- /dev/null +++ b/not_working/test_messaging_outbound_behaviors.py @@ -0,0 +1,83 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + +import unittest +from dataclasses import dataclass, field +from typing import Callable, List + +from vultron.sim.communications import Message + +from vultron.bt.messaging.outbound.behaviors import Emitters, _EmitMsg + + +@dataclass +class MockState: + emit_func: Callable = None + sender: str = None + msg_history: List[str] = field(default_factory=list) + msgs_emitted_this_tick: List[str] = field(default_factory=list) + incoming_messages: List[str] = field(default_factory=list) + name: str = "foo" + + +class TestEmitMsg(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_init(self): + em = _EmitMsg() + self.assertIn(em.name_pfx, em.name) + self.assertIn(em.__class__.__name__, em.name) + self.assertIn(str(em.__class__.msg_type), em.name) + self.assertEqual(em.__class__.msg_type, em.msg_type) + + def _test_emmitter(self, emitter_cls): + em = emitter_cls() + + em.bb = MockState() + + # we need a dummy emit function to receive messages + messages = [] + + def emitfunc(msg): + messages.append(msg) + + em.bb.emit_func = emitfunc + em.bb.sender = "TestSender" + + em._tick() + + self.assertGreater( + len(messages), 0, "Expected non-empty list of messages emitted" + ) + for msg in messages: + self.assertIsInstance(msg, Message) + self.assertEqual(em.msg_type, msg.msg_type) + self.assertIn(msg, em.bb.msg_history) + self.assertIn(msg.msg_type, em.bb.msgs_emitted_this_tick) + self.assertIn(msg, em.bb.incoming_messages) + + def test_tick(self): + # Emitters is a list of all the subclasses of _EmitMsg + # that are created in vultron.bt.messaging.outbount.behaviors + for emitter_cls in Emitters: + self._test_emmitter(emitter_cls) + + +if __name__ == "__main__": + unittest.main() diff --git a/not_working/test_report_to_others.py b/not_working/test_report_to_others.py new file mode 100644 index 00000000..14839d4c --- /dev/null +++ b/not_working/test_report_to_others.py @@ -0,0 +1,294 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + +import logging +import unittest +from itertools import product + +from vultron.cvd_states.states import CS +from vultron.sim.participants import ( + Coordinator, + Participant, + ParticipantTypes, + Vendor, +) + +import vultron.bt.report_management._behaviors.report_to_others as rto +import vultron.bt.report_management.fuzzer.report_to_others +from vultron.bt.base.node_status import NodeStatus +from vultron.bt.embargo_management.states import EM +from vultron.bt.messaging.states import MessageTypes +from vultron.bt.states import CapabilityFlag + + +class MockCase: + def __init__(self): + self.potential_participants = [] + + +class MockState: + capabilities = CapabilityFlag.NoCapability + reporting_effort_budget = 0 + participant_types = ParticipantTypes + add_participant_func = None + q_em = EM.NO_EMBARGO + q_cs = CS.vfdpxa + + def __init__(self): + self.case = MockCase() + self.currently_notifying = None + + +class MyTestCase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_have_rto_capability(self): + node = rto.HaveReportToOthersCapability() + node.bb = MockState() + node.setup() + + self.assertEqual(CapabilityFlag.NoCapability, node.bb.capabilities) + r = node._tick() + self.assertEqual(NodeStatus.FAILURE, r) + + # bestow the ability + node.bb.capabilities |= CapabilityFlag.ReportToOthers + self.assertEqual(CapabilityFlag.ReportToOthers, node.bb.capabilities) + r = node._tick() + self.assertEqual(NodeStatus.SUCCESS, r) + + def test_reporting_effort_available(self): + node = rto._ReportingEffortAvailable() + node.bb = MockState() + node.setup() + + # fail when 0 + self.assertEqual(0, node.bb.reporting_effort_budget) + r = node._tick() + self.assertEqual(NodeStatus.FAILURE, r) + + # succeed when > 0 + node.bb.reporting_effort_budget = 1 + self.assertEqual(1, node.bb.reporting_effort_budget) + r = node._tick() + self.assertEqual(NodeStatus.SUCCESS, r) + + def test_total_effort_limit_met(self): + node = rto._TotalEffortLimitMet() + node.bb = MockState() + node.setup() + + # succeed when 0 + self.assertEqual(0, node.bb.reporting_effort_budget) + r = node._tick() + self.assertEqual(NodeStatus.SUCCESS, r) + + # fail when > 0 + node.bb.reporting_effort_budget = 1 + self.assertEqual(1, node.bb.reporting_effort_budget) + r = node._tick() + self.assertEqual(NodeStatus.FAILURE, r) + + def test_choose_recipient(self): + # take a recipient from potential_participants + # put them in currently_notifying + node = rto.ChooseRecipient() + node.bb = MockState() + node.setup() + + # fail if potential_participants is empty + self.assertFalse(node.bb.case.potential_participants) + r = node._tick() + self.assertEqual(NodeStatus.FAILURE, r) + + # succeed if there's something to do + potential = node.bb.case.potential_participants + p = Participant() + potential.append(p) + self.assertTrue(node.bb.case.potential_participants) + r = node._tick() + self.assertEqual(NodeStatus.SUCCESS, r) + self.assertIs(node.bb.currently_notifying, p) + + def test_add_vendor(self): + node = ( + vultron.bt.report_management.fuzzer.report_to_others.InjectVendor() + ) + node.bb = MockState() + node.setup() + + self.assertEqual(len(node.bb.case.potential_participants), 0) + + for i in range(10): + node._tick() + self.assertEqual(len(node.bb.case.potential_participants), i + 1) + + for p in node.bb.case.potential_participants: + self.assertIsInstance(p, Vendor) + + def test_add_coordinator(self): + node = ( + vultron.bt.report_management.fuzzer.report_to_others.InjectCoordinator() + ) + node.bb = MockState() + node.setup() + + self.assertEqual(len(node.bb.case.potential_participants), 0) + + for i in range(10): + node._tick() + self.assertEqual(len(node.bb.case.potential_participants), i + 1) + + for p in node.bb.case.potential_participants: + self.assertIsInstance(p, Coordinator) + + def test_add_other(self): + node = ( + vultron.bt.report_management.fuzzer.report_to_others.InjectOther() + ) + node.bb = MockState() + node.setup() + + self.assertEqual(len(node.bb.case.potential_participants), 0) + + for i in range(10): + node._tick() + self.assertEqual(len(node.bb.case.potential_participants), i + 1) + + for p in node.bb.case.potential_participants: + self.assertIsInstance(p, Participant) + + def test_remove_recipient(self): + node = rto._RemoveRecipient() + node.bb = MockState() + node.setup() + + # succeed when it's already none, just a no-op + self.assertIsNone(node.bb.currently_notifying) + r = node._tick() + self.assertEqual(NodeStatus.SUCCESS, r) + + # succeed when p not in list + p = Participant() + node.bb.currently_notifying = p + self.assertNotIn(p, node.bb.case.potential_participants) + # not sure how you'd get here in the real world, so it logs a warning + with self.assertLogs(rto.logger, logging.WARNING) as lc: + r = node._tick() + self.assertEqual(NodeStatus.SUCCESS, r) + + p = Participant() + node.bb.case.potential_participants.append(p) + node.bb.currently_notifying = p + self.assertIsNotNone(node.bb.currently_notifying) + self.assertIn(p, node.bb.case.potential_participants) + r = node._tick() + self.assertEqual(NodeStatus.SUCCESS, r) + self.assertIsNone(node.bb.currently_notifying) + self.assertNotIn(p, node.bb.case.potential_participants) + + def test_report_to_new_participant(self): + node = rto._ReportToNewParticipant() + node.bb = MockState() + node.setup() + + msg = [] + rcpt = [] + + def mock_dm(message, recipient): + msg.append(message) + rcpt.append(recipient) + + node.bb.dm_func = mock_dm + + # fail if currently notifying is empty + with self.assertLogs(level=logging.WARNING): + self.assertIsNone(node.bb.currently_notifying) + r = node._tick() + self.assertEqual(NodeStatus.FAILURE, r) + + # success otherwise + p = Participant() + node.bb.currently_notifying = p + self.assertIsNotNone(node.bb.currently_notifying) + r = node._tick() + self.assertEqual(NodeStatus.SUCCESS, r) + # check that dm got called + self.assertIn(p, rcpt) + self.assertEqual(msg[0].msg_type, MessageTypes.RS) + + def test_connect_new_participant_to_case(self): + node = rto._ConnectNewParticipantToCase() + node.bb = MockState() + node.setup() + + # fail if currently notifying is empty + with self.assertLogs(level=logging.WARNING): + self.assertIsNone(node.bb.currently_notifying) + r = node._tick() + self.assertEqual(NodeStatus.FAILURE, r) + + p = Participant() + # fail if add_func is none + with self.assertLogs(level=logging.WARNING): + node.bb.currently_notifying = p + self.assertIsNone(node.bb.add_participant_func) + r = node._tick() + self.assertEqual(NodeStatus.FAILURE, r) + + participants = [] + + def mock_add_func(participant): + participants.append(participant) + + node.bb.add_participant_func = mock_add_func + + r = node._tick() + self.assertEqual(NodeStatus.SUCCESS, r) + self.assertIn(p, participants) + + def test_bring_new_participant_up_to_speed(self): + node = rto._BringNewParticipantUpToSpeed() + node.bb = MockState() + node.setup() + + # fail if currently notifying is empty + with self.assertLogs(level=logging.WARNING): + self.assertIsNone(node.bb.currently_notifying) + r = node._tick() + self.assertEqual(NodeStatus.FAILURE, r) + + # succeed otherwise + + for qem, qcs in product(EM, CS): + p = Participant() + node.bb.currently_notifying = p + node.bb.q_em = qem + node.bb.q_cs = qcs + + r = node._tick() + self.assertEqual(NodeStatus.SUCCESS, r) + self.assertEqual(node.bb.currently_notifying.bt.bb.q_em, qem) + + qcs_pxa = CS.PXA & qcs + self.assertEqual(node.bb.currently_notifying.bt.bb.q_cs, qcs_pxa) + + +if __name__ == "__main__": + unittest.main() diff --git a/pyproject.toml b/pyproject.toml index 8bb6ac9d..a46541f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,18 @@ dependencies = [ ] dynamic = ["version",] +[project.scripts] +vultrabot_pacman="vultron.bt.base.demo.pacman:main" +vultrabot_robot="vultron.bt.base.demo.robot:main" +vultrabot_cvd="vultron.demo.vultrabot:main" +vultrabot="vultron.scripts.vultrabot:main" + + +[project.urls] +"Homepage" = "https://certcc.github.io/Vultron" +"Project" = "https://github.com/CERTCC/Vultron" +"Bug Tracker" = "https://github.com/CERTCC/Vultron/issues" + [tool.setuptools.packages.find] where = ["."] # list of folders that contain the packages (["."] by default) include = ["vultron*"] # package names should match these glob patterns (["*"] by default) @@ -51,3 +63,10 @@ version_file = "vultron/_version.py" [tool.black] line-length = 79 target-version = ['py38', 'py39', 'py310', 'py311'] + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q" +testpaths = [ + "test", +] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index bbfe9525..697e182d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -mkdocs>=1.4.3 -mkdocs-include-markdown-plugin>=4.0.4 -mkdocs-material>=9.1.19 +mkdocs==1.5.3 +mkdocs-include-markdown-plugin==6.0.3 +mkdocs-material==9.4.6 mkdocs-material-extensions==1.3 -mkdocstrings>=0.23.0 -mkdocstrings-python>=1.6.2 -pandas -scipy -networkx -mkdocs-print-site-plugin +mkdocstrings==0.23.0 +mkdocstrings-python==1.7.3 +pandas==2.1.1 +scipy==1.11.3 +networkx==3.2 +mkdocs-print-site-plugin==2.3.6 diff --git a/test/test_bt/__init__.py b/test/test_bt/__init__.py new file mode 100644 index 00000000..b8d147d8 --- /dev/null +++ b/test/test_bt/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University diff --git a/test/test_bt/test_base/__init__.py b/test/test_bt/test_base/__init__.py new file mode 100644 index 00000000..9e7f55f9 --- /dev/null +++ b/test/test_bt/test_base/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +#!/usr/bin/env python diff --git a/test/test_bt/test_base/test_blackboard.py b/test/test_bt/test_base/test_blackboard.py new file mode 100644 index 00000000..871e5eb7 --- /dev/null +++ b/test/test_bt/test_base/test_blackboard.py @@ -0,0 +1,34 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +import unittest +from dataclasses import dataclass + +from vultron.bt.base.blackboard import Blackboard + + +class TestBlackBoard(unittest.TestCase): + def test_bb_has_dataclass_semantics(self): + # there really isn't anything to test since Blackboard doesn't have any + # defined fields. But we can test that it has dataclass semantics + + @dataclass(kw_only=True) + class BB(Blackboard): + foo: str = "bar" + + bb = BB() + self.assertEqual("bar", bb.foo) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_base/test_bt.py b/test/test_bt/test_base/test_bt.py new file mode 100644 index 00000000..b6caee6a --- /dev/null +++ b/test/test_bt/test_base/test_bt.py @@ -0,0 +1,248 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +import unittest + +import vultron.bt.base.fuzzer as btz +from vultron.bt.base import bt +from vultron.bt.base.bt_node import BtNode, ConditionCheck, CountTicks +from vultron.bt.base.node_status import NodeStatus + + +class MockState(dict): + pass + + +def func_true(): + return True + + +def func_false(): + return False + + +class MyTestCase(unittest.TestCase): + def setUp(self): + # reset the object counter + BtNode._objcount = 0 + + def tearDown(self): + pass + + def test_ConditionCheck2(self): + cc = ConditionCheck() + cc.bb = MockState() + + cc.func = func_true + result = cc._tick() + self.assertEqual(NodeStatus.SUCCESS, result) + + cc.func = func_false + result = cc._tick() + self.assertEqual(NodeStatus.FAILURE, result) + + def test_add_child(self): + parent = BtNode() + child = BtNode() + + self.assertEqual(None, child.parent) + self.assertNotIn(child, parent.children) + parent.add_child(child) + self.assertEqual(parent, child.parent) + self.assertIn(child, parent.children) + self.assertEqual(child.indent_level, parent.indent_level + 1) + + def test_state_follows_parent(self): + parent = BtNode() + child = BtNode() + parent.bb = MockState() + + self.assertTrue(isinstance(parent.bb, MockState)) + self.assertFalse(isinstance(child.bb, MockState)) + parent.add_child(child) + self.assertTrue(isinstance(parent.bb, MockState)) + self.assertTrue(isinstance(child.bb, MockState)) + self.assertEqual(parent.bb, child.bb) + + def test_setup(self): + class MyTree(bt.BehaviorTree): + bbclass = MockState + + tree = MyTree() + root = BtNode() + + n = 10 + root._children = [BtNode for _ in range(10)] + + self.assertEqual(0, len(root.children)) + root.add_children() + self.assertEqual(n, len(root.children)) + + self.assertIsInstance(tree.bb, MockState) + + tree.bb.foo = True + self.assertTrue(hasattr(tree.bb, "foo")) + self.assertTrue(tree.bb.foo) + + tree.add_root(root) + + # only root should get the blackboard so far + self.assertIsInstance(root.bb, MockState) + self.assertTrue(hasattr(root.bb, "foo")) + self.assertTrue(root.bb.foo) + + for node in root.children: + self.assertIsNone(node.bb) + self.assertFalse(hasattr(node.bb, "foo")) + + tree.setup() + + # now everything should have it + for node in root.children: + self.assertIsInstance(node.bb, MockState) + self.assertEqual(tree.bb, node.bb) + self.assertTrue(hasattr(node.bb, "foo")) + self.assertTrue(node.bb.foo) + + def test_add_children(self): + parent = BtNode() + n = 10 + children = [BtNode for i in range(n)] + + self.assertFalse(parent.children) + parent._children = children + parent.add_children() + self.assertEqual(n, len(parent.children)) + + for child_cls, child_inst in zip(children, parent.children): + self.assertIsInstance(child_inst, child_cls) + self.assertEqual(child_inst.indent_level, parent.indent_level + 1) + + def test_count_ticks(self): + c = CountTicks() + for i in range(1000): + self.assertEqual(i, c.counter) + c.tick() + + c = CountTicks() + c.counter = 374 + for i in range(374, 450): + self.assertEqual(i, c.counter) + c.tick() + + def test_always_succeed(self): + s = btz.AlwaysSucceed() + for i in range(1000): + self.assertEqual(NodeStatus.SUCCESS, s.tick()) + + def test_always_fail(self): + s = btz.AlwaysFail() + for i in range(1000): + self.assertEqual(NodeStatus.FAILURE, s.tick()) + + def test_always_running(self): + s = btz.AlwaysRunning() + for i in range(1000): + self.assertEqual(NodeStatus.RUNNING, s.tick()) + + def test_btnode_objcount(self): + # We expect that there is exactly one _objcount shared across all + # subclasses of BtNode. This test creates a bunch of BtNodes and checks + # that the _objcount is incremented correctly. + + n = 100 + self.assertEqual(0, BtNode._objcount) + for i in range(n): + self.assertEqual(i, BtNode._objcount) + BtNode() + self.assertEqual(i + 1, BtNode._objcount) + + def test_btnode_objcount_subclasses(self): + # We expect that there is exactly one _objcount shared across all + # subclasses of BtNode. This test creates a nested set of subclasses of + # BtNode and checks that the _objcount is incremented correctly. + def subclass(cls): + class Foo(cls): + pass + + return Foo + + depth = 100 # this is way deeper than we probably ever need to go + self.assertEqual(0, BtNode._objcount) + subcls = subclass(BtNode) + for i in range(depth): + self.assertEqual(i, BtNode._objcount) + # instantiate it + subcls() + self.assertEqual(i + 1, BtNode._objcount) + + # go one deeper + subcls = subclass(subcls) + + self.assertEqual(depth, BtNode._objcount) + + def test_bt_node_to_graph(self): + class G(BtNode): + pass + + class F(BtNode): + _children = [ + G, + ] + + class E(BtNode): + pass + + class D(BtNode): + pass + + class C(BtNode): + _children = [E, F] + + class B(BtNode): + pass + + class A(BtNode): + _children = [B, C, D] + + # +-- a + # |-> b + # |-> c + # | |-> e + # | L-> f + # | L-> g + # L-> d + + root = A() + + graph = root.to_graph() + + self.assertEqual(7, len(graph.nodes)) + nodes = {n[0]: n for n in graph.nodes} + + for ch in "ABCDEFG": + self.assertIn(ch, nodes) + + self.assertEqual(6, len(graph.edges)) + + edges = ["AB", "AC", "AD", "CE", "CF", "FG"] + for edge in edges: + u = nodes[edge[0]] + v = nodes[edge[1]] + + self.assertIn((u, v), graph.edges) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_base/test_bt_composites.py b/test/test_bt/test_base/test_bt_composites.py new file mode 100644 index 00000000..b382a8d3 --- /dev/null +++ b/test/test_bt/test_base/test_bt_composites.py @@ -0,0 +1,82 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +import unittest + +import vultron.bt.base.composites as composites +import vultron.bt.base.fuzzer as btz +from vultron.bt.base.node_status import NodeStatus + +fail = btz.AlwaysFail +succeed = btz.AlwaysSucceed +running = btz.AlwaysRunning + + +class TestComposites(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def _children_match_expected(self, expect, klass): + # Test that the children of the given class match the expected results + # for the given expect dictionary of (children, result) pairs where the children are tuples of nodes + for children, result in expect.items(): + + class Cls(klass): + _children = children + + instance = Cls() + self.assertEqual(result, instance.tick()) + + def test_sequence(self): + # SequenceNode should return the first failure, or success if all succeed + expect = { + (fail, fail): NodeStatus.FAILURE, + (fail, succeed): NodeStatus.FAILURE, + (fail, running): NodeStatus.FAILURE, + (succeed, fail): NodeStatus.FAILURE, + (succeed, succeed): NodeStatus.SUCCESS, + (succeed, running): NodeStatus.RUNNING, + (running, fail): NodeStatus.RUNNING, + (running, succeed): NodeStatus.RUNNING, + (running, running): NodeStatus.RUNNING, + } + + self._children_match_expected(expect, composites.SequenceNode) + + def test_fallback(self): + # FallbackNode should return the first success, or failure if all fail + expect = { + (fail, fail): NodeStatus.FAILURE, + (fail, succeed): NodeStatus.SUCCESS, + (fail, running): NodeStatus.RUNNING, + (succeed, fail): NodeStatus.SUCCESS, + (succeed, succeed): NodeStatus.SUCCESS, + (succeed, running): NodeStatus.SUCCESS, + (running, fail): NodeStatus.RUNNING, + (running, succeed): NodeStatus.RUNNING, + (running, running): NodeStatus.RUNNING, + } + + self._children_match_expected(expect, composites.FallbackNode) + + def test_parallel(self): + # todo: test parallel node + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_base/test_bt_decorators.py b/test/test_bt/test_base/test_bt_decorators.py new file mode 100644 index 00000000..b345ec95 --- /dev/null +++ b/test/test_bt/test_base/test_bt_decorators.py @@ -0,0 +1,167 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +import unittest + +import vultron.bt.base.fuzzer as btz +from vultron.bt.base.bt_node import BtNode +from vultron.bt.base.decorators import ( + ForceFailure, + ForceRunning, + ForceSuccess, + Invert, + RepeatN, + RepeatUntilFail, + RetryN, + RunningIsFailure, + RunningIsSuccess, +) +from vultron.bt.base.node_status import NodeStatus + +succeed = btz.AlwaysSucceed +fail = btz.AlwaysFail +running = btz.AlwaysRunning + + +class MyTestCase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_invert(self): + expect = { + succeed: NodeStatus.FAILURE, + fail: NodeStatus.SUCCESS, + running: NodeStatus.RUNNING, + } + + self._check_expected_results(expect, Invert) + + def test_running_is_failure(self): + expect = { + succeed: NodeStatus.SUCCESS, + fail: NodeStatus.FAILURE, + running: NodeStatus.FAILURE, + } + + self._check_expected_results(expect, RunningIsFailure) + + def test_running_is_success(self): + expect = { + succeed: NodeStatus.SUCCESS, + fail: NodeStatus.FAILURE, + running: NodeStatus.SUCCESS, + } + + self._check_expected_results(expect, RunningIsSuccess) + + def _check_expected_results(self, expect, klass): + for child, result in expect.items(): + + class Cls(klass): + _children = (child,) + + instance = Cls() + self.assertEqual(result, instance.tick()) + + def test_force_success(self): + expect = { + succeed: NodeStatus.SUCCESS, + fail: NodeStatus.SUCCESS, + running: NodeStatus.SUCCESS, + } + self._check_expected_results(expect, ForceSuccess) + + def test_force_failure(self): + expect = { + succeed: NodeStatus.FAILURE, + fail: NodeStatus.FAILURE, + running: NodeStatus.FAILURE, + } + self._check_expected_results(expect, ForceFailure) + + def test_force_running(self): + expect = { + succeed: NodeStatus.RUNNING, + fail: NodeStatus.RUNNING, + running: NodeStatus.RUNNING, + } + self._check_expected_results(expect, ForceRunning) + + def test_retry_n(self): + class Retry100(RetryN): + n = 100 + _children = (fail,) + + parent = Retry100() + self.assertEqual(0, parent.count) + parent.tick() + self.assertEqual(parent.n, parent.count) + parent.tick() + self.assertEqual(2 * parent.n, parent.count) + parent.tick() + self.assertEqual(3 * parent.n, parent.count) + + # counter resets every time + parent.reset = True + parent.tick() + self.assertEqual(parent.n, parent.count) + + def test_repeat_n(self): + for _n in range(1, 100): + + class Repeat_n(RepeatN): + n = _n + _children = (succeed,) + + parent = Repeat_n() + self.assertEqual(0, parent.count) + parent.tick() + self.assertEqual(parent.n, parent.count) + parent.tick() + self.assertEqual(2 * parent.n, parent.count) + parent.tick() + self.assertEqual(3 * parent.n, parent.count) + + # counter resets every time + parent.reset = True + parent.tick() + self.assertEqual(parent.n, parent.count) + + def test_repeat_until_fail(self): + for n in range(1, 100): + + class WinBeforeLose(BtNode): + # dummy class that succeeds n times before failure + i = 0 + + def _tick(self, depth=0): + self.i += 1 + if self.i < n: + return NodeStatus.SUCCESS + return NodeStatus.FAILURE + + class UntilFail(RepeatUntilFail): + _children = (WinBeforeLose,) + + parent = UntilFail() + self.assertEqual(0, parent.count) + parent.tick() + self.assertEqual(n, parent.count) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_base/test_factory.py b/test/test_bt/test_base/test_factory.py new file mode 100644 index 00000000..accccf09 --- /dev/null +++ b/test/test_bt/test_base/test_factory.py @@ -0,0 +1,206 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +import unittest + +from vultron.bt.base.bt_node import ActionNode, BtNode, ConditionCheck +from vultron.bt.base.composites import FallbackNode, ParallelNode, SequenceNode +from vultron.bt.base.decorators import Invert, RepeatUntilFail +from vultron.bt.base.factory import ( + action_node, + condition_check, + fallback_node, + fuzzer, + invert, + node_factory, + parallel_node, + repeat_until_fail, + sequence_node, +) +from vultron.bt.base.fuzzer import FuzzerNode, WeightedSuccess + + +class MyTestCase(unittest.TestCase): + def setUp(self): + children = [] + for i in range(3): + name = f"node{i}" + docstr = f"docstr{i}" + child = type(name, (BtNode,), {}) + child.__doc__ = docstr + children.append(child) + self.children = children + + def test_node_factory(self): + for node_cls in [ + BtNode, + ActionNode, + ConditionCheck, + FallbackNode, + ParallelNode, + SequenceNode, + Invert, + RepeatUntilFail, + FuzzerNode, + ]: + name = "foo" + docstr = "bar" + + children = [] + + node = node_factory(node_cls, name, docstr, children) + + # test that node is a subclass of node_cls + self.assertTrue(issubclass(node, node_cls)) + self.assertEqual(name, node.__name__) + self.assertEqual(docstr, node.__doc__) + + # can't test for instantiation here because some classes have requirements + # we haven't met yet. We'll do those in the individual tests below. + + def test_sequence(self): + for x in range(2): + # throw an error if less than two children + self.assertRaises( + ValueError, sequence_node, "foo", "bar", *self.children[:x] + ) + + node_cls = sequence_node("foo", "bar", *self.children) + + self.assertTrue(issubclass(node_cls, SequenceNode)) + self.assertEqual("foo", node_cls.__name__) + self.assertEqual("bar", node_cls.__doc__) + self.assertEqual(tuple(self.children), node_cls._children) + + self.assertIsInstance(node_cls(), node_cls) + + def test_fallback(self): + for x in range(1): + # throw an error if less than one child + self.assertRaises( + ValueError, fallback_node, "foo", "bar", *self.children[:x] + ) + + node_cls = fallback_node("foo", "bar", *self.children) + + self.assertTrue(issubclass(node_cls, FallbackNode)) + self.assertEqual("foo", node_cls.__name__) + self.assertEqual("bar", node_cls.__doc__) + self.assertEqual(tuple(self.children), node_cls._children) + + self.assertIsInstance(node_cls(), node_cls) + + def test_invert(self): + # invert should only take one child + self.assertGreater(len(self.children), 1) + self.assertRaises(ValueError, invert, "foo", "bar", *self.children) + + # so let's just use the first child + children = [ + self.children[0], + ] + node_cls = invert("foo", "bar", *children) + + self.assertTrue(issubclass(node_cls, Invert)) + self.assertEqual("foo", node_cls.__name__) + self.assertEqual("bar", node_cls.__doc__) + self.assertEqual(tuple(children), node_cls._children) + + self.assertIsInstance(node_cls(), node_cls) + + def test_fuzzer(self): + node_cls = fuzzer(WeightedSuccess, "foo", "bar") + + self.assertTrue(issubclass(node_cls, FuzzerNode)) + self.assertEqual("foo", node_cls.__name__) + self.assertEqual("bar", node_cls.__doc__) + + self.assertIsInstance(node_cls(), node_cls) + + def test_condition_check(self): + func = lambda: True + func.__doc__ = "bar" + node_cls = condition_check("foo", func) + + self.assertTrue(issubclass(node_cls, ConditionCheck)) + self.assertEqual("foo", node_cls.__name__) + self.assertEqual("bar", node_cls.__doc__) + self.assertEqual(func, node_cls.func) + + self.assertIsInstance(node_cls(), node_cls) + + def test_action_node(self): + func = lambda: True + func.__doc__ = "bar" + node_cls = action_node("foo", func) + + self.assertTrue(issubclass(node_cls, ActionNode)) + self.assertEqual("foo", node_cls.__name__) + self.assertEqual("bar", node_cls.__doc__) + + self.assertIsInstance(node_cls(), node_cls) + + def test_repeat_until_fail(self): + self.assertGreater(len(self.children), 1) + self.assertRaises( + ValueError, repeat_until_fail, "foo", "bar", *self.children + ) + + # so let's just use the first child + children = [ + self.children[0], + ] + node_cls = repeat_until_fail("foo", "bar", *children) + + self.assertTrue(issubclass(node_cls, RepeatUntilFail)) + self.assertEqual("foo", node_cls.__name__) + self.assertEqual("bar", node_cls.__doc__) + self.assertEqual(tuple(children), node_cls._children) + + self.assertIsInstance(node_cls(), node_cls) + + def test_parallel_node(self): + self.assertGreater(len(self.children), 1) + for x in range(2): + # throw an error if less than two children + self.assertRaises( + ValueError, parallel_node, "foo", "bar", 1, *self.children[:x] + ) + + bad_values = [0, -1, None, "foo", len(self.children) + 1] + for min_success in bad_values: + self.assertRaises( + ValueError, + parallel_node, + "foo", + "bar", + min_success, + *self.children, + ) + + # no empty children + self.assertRaises(ValueError, parallel_node, "foo", "bar", 1, *[]) + + for m in range(1, len(self.children) + 1): + node_cls = parallel_node("foo", "bar", m, *self.children) + + self.assertTrue(issubclass(node_cls, ParallelNode)) + self.assertEqual("foo", node_cls.__name__) + self.assertEqual("bar", node_cls.__doc__) + self.assertEqual(tuple(self.children), node_cls._children) + + self.assertIsInstance(node_cls(), node_cls) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_behaviortree/__init__.py b/test/test_bt/test_behaviortree/__init__.py new file mode 100644 index 00000000..b8d147d8 --- /dev/null +++ b/test/test_bt/test_behaviortree/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University diff --git a/test/test_bt/test_behaviortree/test_behavior/__init__.py b/test/test_bt/test_behaviortree/test_behavior/__init__.py new file mode 100644 index 00000000..f697e509 --- /dev/null +++ b/test/test_bt/test_behaviortree/test_behavior/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +"""file: __init__.py +author: adh +created_at: 2/22/23 1:42 PM +""" + + +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + + +def main(): + pass + + +if __name__ == "__main__": + main() diff --git a/test/test_bt/test_behaviortree/test_common.py b/test/test_bt/test_behaviortree/test_common.py new file mode 100644 index 00000000..7c32fd3b --- /dev/null +++ b/test/test_bt/test_behaviortree/test_common.py @@ -0,0 +1,130 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + +import logging +import unittest +from itertools import product + +from vultron.bt import common as c +from vultron.bt.base.composites import FallbackNode +from vultron.bt.base.node_status import NodeStatus + +logger = logging.getLogger() + +logger.addHandler(logging.StreamHandler()) + + +# logger.setLevel(logging.DEBUG) + + +class MockState: + foo = 0 + foo_history = [] + + +class MyTestCase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_to_end_state_factory(self): + bb = MockState() + + for key, state in product("abcdefghij", range(10)): + xclass = c.to_end_state_factory(key, state) + self.assertTrue(callable(xclass)) + + x = xclass() + self.assertIn(key, x.name) + self.assertIn(str(state), x.name) + + setattr(bb, key, 99999) + histkey = f"{key}_history" + setattr(bb, histkey, []) + x.bb = bb + + self.assertEqual(99999, getattr(x.bb, key)) + result = x.tick() + self.assertEqual(NodeStatus.SUCCESS, result) + self.assertEqual(state, getattr(x.bb, key)) + self.assertIn(state, getattr(x.bb, histkey)) + + def test_make_check_state(self): + bb = MockState() + + for key, state in product("abcdefghij", range(10)): + xclass = c.state_in(key, state) + self.assertTrue(callable(xclass)) + + x = xclass() + self.assertIn(key, x.name) + self.assertIn(str(state), x.name) + + x.bb = bb + # check that it returns false / FAILURE when false + setattr(bb, key, 99999) + self.assertFalse(x.func()) + self.assertEqual(NodeStatus.FAILURE, x.tick()) + + # check that it returns true / SUCCESS when true + setattr(bb, key, state) + self.assertTrue(x.func()) + self.assertEqual(NodeStatus.SUCCESS, x.tick()) + + def test_make_state_change(self): + bb = MockState() + start_states = list(range(5)) + + for key, end_state in product("abcdefghij", range(10)): + with self.subTest(key=key, end_state=end_state): + transition = c.EnumStateTransition(start_states, end_state) + xclass = c.state_change(key, transition) + self.assertTrue(callable(xclass)) + self.assertNotEqual("Node", xclass.__name__) + self.assertIn(key, xclass.__name__) + x = xclass() + + self.assertTrue(isinstance(x, FallbackNode)) + self.assertIn(key, x.name) + self.assertIn(str(end_state), x.name) + + x.bb = bb + x.setup() + + setattr(bb, key, 99999) + histkey = f"{key}_history" + setattr(bb, histkey, []) + + result = x.tick() + self.assertEqual(NodeStatus.FAILURE, result) + + # make sure all the start states are allowed + for i in range(15): + setattr(bb, key, i) + result = x.tick() + if i in start_states or i == end_state: + # node succeeds, transition allowed + self.assertEqual(NodeStatus.SUCCESS, result) + self.assertEqual(end_state, getattr(bb, key)) + else: + # node fails, transition disallowed, state does not change + self.assertEqual(NodeStatus.FAILURE, result) + self.assertEqual(i, getattr(bb, key)) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_behaviortree/test_messaging/__init__.py b/test/test_bt/test_behaviortree/test_messaging/__init__.py new file mode 100644 index 00000000..a31986cc --- /dev/null +++ b/test/test_bt/test_behaviortree/test_messaging/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +"""file: __init__.py +author: adh +created_at: 2/22/23 1:50 PM +""" + + +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + + +def main(): + pass + + +if __name__ == "__main__": + main() diff --git a/test/test_bt/test_behaviortree/test_messaging/test_conditions.py b/test/test_bt/test_behaviortree/test_messaging/test_conditions.py new file mode 100644 index 00000000..f67b4397 --- /dev/null +++ b/test/test_bt/test_behaviortree/test_messaging/test_conditions.py @@ -0,0 +1,118 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +import unittest + +import vultron.bt.messaging.conditions as vmc +from vultron.bt.base.node_status import NodeStatus +from vultron.bt.messaging.states import MessageTypes + + +class MockMsg: + msg_type = None + + +class MockState: + current_message = MockMsg() + + +_TO_TEST = { + # RS, RI, RV, RD, RA, RC, RK, RE + MessageTypes.RS: vmc.IsMsgTypeRS, + MessageTypes.RI: vmc.IsMsgTypeRI, + MessageTypes.RV: vmc.IsMsgTypeRV, + MessageTypes.RD: vmc.IsMsgTypeRD, + MessageTypes.RA: vmc.IsMsgTypeRA, + MessageTypes.RC: vmc.IsMsgTypeRC, + MessageTypes.RK: vmc.IsMsgTypeRK, + MessageTypes.RE: vmc.IsMsgTypeRE, + # EP ER EA EV EJ EC ET EK EE + MessageTypes.EP: vmc.IsMsgTypeEP, + MessageTypes.ER: vmc.IsMsgTypeER, + MessageTypes.EA: vmc.IsMsgTypeEA, + MessageTypes.EV: vmc.IsMsgTypeEV, + MessageTypes.EJ: vmc.IsMsgTypeEJ, + MessageTypes.EC: vmc.IsMsgTypeEC, + MessageTypes.ET: vmc.IsMsgTypeET, + MessageTypes.EK: vmc.IsMsgTypeEK, + MessageTypes.EE: vmc.IsMsgTypeEE, + # CV CF CD CP CX CA CK CE + MessageTypes.CV: vmc.IsMsgTypeCV, + MessageTypes.CF: vmc.IsMsgTypeCF, + MessageTypes.CD: vmc.IsMsgTypeCD, + MessageTypes.CP: vmc.IsMsgTypeCP, + MessageTypes.CX: vmc.IsMsgTypeCX, + MessageTypes.CA: vmc.IsMsgTypeCA, + MessageTypes.CK: vmc.IsMsgTypeCK, + MessageTypes.CE: vmc.IsMsgTypeCE, + # GI GK GE + MessageTypes.GI: vmc.IsMsgTypeGI, + MessageTypes.GK: vmc.IsMsgTypeGK, + MessageTypes.GE: vmc.IsMsgTypeGE, +} + + +class MyTestCase(unittest.TestCase): + def _test_is_msg_type_generic(self, cls, msg_type): + node = cls() + node.bb = MockState() + node.msg_type = msg_type + + for mtype in MessageTypes: + node.bb.current_message.msg_type = mtype + if mtype == msg_type: + with self.subTest(msg_type=mtype): + self.assertEqual(node.tick(), NodeStatus.SUCCESS) + else: + with self.subTest(msg_type=mtype): + self.assertEqual(node.tick(), NodeStatus.FAILURE) + + def test_is_msg_type(self): + self.assertEqual( + len(_TO_TEST), + len(MessageTypes), + msg="Not all message types are tested", + ) + + for msg_type, cls in _TO_TEST.items(): + self._test_is_msg_type_generic(cls, msg_type) + + def _test_msg_type_compound(self, cls, pfx): + # loop through all message types + for msg_type in MessageTypes: + node = cls() + node.bb = MockState() + node.bb.current_message.msg_type = msg_type + + if msg_type.value.startswith(pfx): + # if the message type starts with the prefix, it should succeed + self.assertEqual(NodeStatus.SUCCESS, node.tick()) + else: + # if the message type does not start with the prefix, it should fail + self.assertEqual(NodeStatus.FAILURE, node.tick()) + + def test_is_rm_msg(self): + self._test_msg_type_compound(vmc.IsRMMessage, "R") + + def test_is_em_msg(self): + self._test_msg_type_compound(vmc.IsEMMessage, "E") + + def test_is_cs_msg(self): + self._test_msg_type_compound(vmc.IsCSMessage, "C") + + def test_is_gm_msg(self): + self._test_msg_type_compound(vmc.IsGMMessage, "G") + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_behaviortree/test_messaging/test_cs_messages_inbound.py b/test/test_bt/test_behaviortree/test_messaging/test_cs_messages_inbound.py new file mode 100644 index 00000000..072fd661 --- /dev/null +++ b/test/test_bt/test_behaviortree/test_messaging/test_cs_messages_inbound.py @@ -0,0 +1,119 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +import unittest +from itertools import product + +# noinspection PyProtectedMember +import vultron.bt.messaging.inbound._behaviors.cs_messages as vmc +from vultron.bt.base.node_status import NodeStatus +from vultron.bt.messaging.states import MessageTypes as Mt +from vultron.case_states.states import CS + + +class MockMsg: + msg_type: Mt = None + + +class MockState: + current_message: MockMsg = None + q_cs: CS = None + name: str = "Foo" + emit_func: callable = lambda *x: None + msg_history: list = [] + msgs_emitted_this_tick: list = [] + incoming_messages: list = [] + q_cs_history: list = [] + + +class MyTestCase(unittest.TestCase): + def test_handle_cp(self): + self._test_loop(vmc._HandleCp, Mt.CP, "...P..") + + def test_handle_ca(self): + self._test_loop(vmc._HandleCa, Mt.CA, ".....A") + + def test_handle_cx(self): + """ + Test that the _HandleCx node transitions to the correct state based on the current message type and the current + case state. + """ + cls = vmc._HandleCx + expect_success_on = Mt.CX + pattern = "...PX." # pX is not usually valid because X implies P, so we should see a transition to P as well + + self._test_loop(cls, expect_success_on, pattern) + + def _test_loop(self, cls, expect_success_on, pattern): + for msg_type, q_cs in product(Mt, CS): + with self.subTest( + cls=cls, + msg_type=msg_type, + q_cs=q_cs, + expect_success_on=expect_success_on, + pattern=pattern, + ): + # set up the node + node = self._build_and_tick(cls, msg_type, q_cs) + + if node.bb.current_message.msg_type == expect_success_on: + self.assertEqual(NodeStatus.SUCCESS, node.status) + self.assertRegex( + node.bb.q_cs.name, + pattern, + f"{cls} {msg_type} {q_cs} {expect_success_on} {pattern}", + ) + else: + self.assertEqual(NodeStatus.FAILURE, node.status) + self.assertEqual(q_cs, node.bb.q_cs) + + def _build_and_tick(self, cls, msg_type, q_cs): + node = cls() + node.bb = MockState() + msg = MockMsg() + msg.msg_type = msg_type + node.bb.current_message = msg + node.bb.q_cs = q_cs + self.assertIsNone(node.status) + node.tick() + return node + + def test_handle_cv_cf_cd(self): + """ + Test that the _HandleCv, _HandleCf, and HandleCd nodes do not transition states. + """ + for expect_success_on, cls in zip( + [Mt.CV, Mt.CF, Mt.CD], + [vmc._HandleCv, vmc._HandleCf, vmc._HandleCd], + ): + for msg_type, q_cs in product(Mt, CS): + with self.subTest( + cls=cls, + msg_type=msg_type, + q_cs=q_cs, + expect_success_on=expect_success_on, + ): + # set up the node + node = self._build_and_tick(cls, msg_type, q_cs) + + # CV doesn't change the case state + self.assertEqual(q_cs, node.bb.q_cs) + + if node.bb.current_message.msg_type == expect_success_on: + self.assertEqual(NodeStatus.SUCCESS, node.status) + else: + self.assertEqual(NodeStatus.FAILURE, node.status) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_behaviortree/test_messaging/test_messaging_inbound_common.py b/test/test_bt/test_behaviortree/test_messaging/test_messaging_inbound_common.py new file mode 100644 index 00000000..3b41aeb8 --- /dev/null +++ b/test/test_bt/test_behaviortree/test_messaging/test_messaging_inbound_common.py @@ -0,0 +1,178 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + +import unittest +from collections import deque +from dataclasses import dataclass + +from vultron.bt.base.bt_node import ActionNode +from vultron.bt.base.node_status import NodeStatus +from vultron.bt.messaging.inbound._behaviors.common import ( + LogMsg, + PopMessage, + PushMessage, + UnsetCurrentMsg, +) + + +class MockState: + current_message = None + incoming_messages = deque() + msgs_received_this_tick = [] + + +@dataclass(kw_only=True) +class MockMsg: + msg_type: str = "gloop" + + +class MyTestCase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_pop_message_fails_if_current_message_set(self): + pm = PopMessage() + self.assertIsInstance(pm, ActionNode) + + # fail if current_message is already set + pm.bb = MockState() + pm.bb.current_message = 1 + self.assertIsNotNone(pm.bb.current_message) + self.assertEqual(NodeStatus.FAILURE, pm._tick()) + + def test_pop_message_fails_when_incoming_empty(self): + pm = PopMessage() + self.assertIsInstance(pm, ActionNode) + + pm.bb = MockState() + self.assertIsNone(pm.bb.current_message) + # incoming messages is a deque + # empty deque evaluates as False + self.assertFalse(pm.bb.incoming_messages) + # ticking pm must therefore fail + self.assertEqual(NodeStatus.FAILURE, pm._tick()) + + def test_pop_message_succeeds(self): + pm = PopMessage() + self.assertIsInstance(pm, ActionNode) + + msg = MockMsg() + pm.bb = MockState() + pm.bb.incoming_messages.append(msg) + self.assertIsNone(pm.bb.current_message) + # incoming messages is a deque + r = pm._tick() + + # check for success + self.assertEqual(NodeStatus.SUCCESS, r) + + # check post-conditions + # message is now current message + self.assertEqual(msg, pm.bb.current_message) + + def test_pop_message_fifo(self): + pm = PopMessage() + pm.bb = MockState() + + # test FIFO + messages = [MockMsg(msg_type=x) for x in "abcdefg"] + pm.bb.incoming_messages.extend(messages) + for msg in messages: + pm.bb.current_message = None + + r = pm._tick() + self.assertEqual(NodeStatus.SUCCESS, r) + self.assertEqual(msg, pm.bb.current_message) + + def test_push_message_succeeds_if_no_current_message(self): + pm = PushMessage() + pm.bb = MockState() + + self.assertIsNone(pm.bb.current_message) + r = pm._tick() + self.assertEqual(NodeStatus.SUCCESS, r) + + def test_push_message_succeeds_if_current_message(self): + pm = PushMessage() + pm.bb = MockState() + + self.assertIsNone(pm.bb.current_message) + self.assertEqual(len(pm.bb.incoming_messages), 0) + + messages = list("abcdefghijklmnopqrstuvwxyz") + + for msg in messages: + pm.bb.current_message = msg + r = pm._tick() + self.assertEqual(NodeStatus.SUCCESS, r) + self.assertIsNone(pm.bb.current_message) + self.assertEqual( + len(pm.bb.incoming_messages), + len(messages), + pm.bb.incoming_messages, + ) + + # push message puts it back on the queue for next time + messages.reverse() + for msg in messages: + m = pm.bb.incoming_messages.popleft() + self.assertEqual(msg, m) + + self.assertEqual( + len(pm.bb.incoming_messages), 0, pm.bb.incoming_messages + ) + + def test_unset_current_msg(self): + node = UnsetCurrentMsg() + node.bb = MockState() + msg = MockMsg() + self.assertIsNone(node.bb.current_message) + node.bb.current_message = msg + self.assertEqual(msg, node.bb.current_message) + + self.assertIsNone(node.status) + + node.tick() + + self.assertEqual(NodeStatus.SUCCESS, node.status) + self.assertIsNone(node.bb.current_message) + + def test_log_msg(self): + node = LogMsg() + node.bb = MockState() + msg = MockMsg() + self.assertIsNone(node.bb.current_message) + node.bb.current_message = msg + self.assertEqual(msg, node.bb.current_message) + + self.assertEqual(0, len(node.bb.msgs_received_this_tick)) + self.assertIsNone(node.status) + + node.tick() + + # node should succeed + self.assertEqual(NodeStatus.SUCCESS, node.status) + # current message should not change + self.assertEqual(msg, node.bb.current_message) + # but the message type should be logged + self.assertEqual(1, len(node.bb.msgs_received_this_tick)) + self.assertEqual(msg.msg_type, node.bb.msgs_received_this_tick[-1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_behaviortree/test_messaging/test_messaging_states.py b/test/test_bt/test_behaviortree/test_messaging/test_messaging_states.py new file mode 100644 index 00000000..514bb867 --- /dev/null +++ b/test/test_bt/test_behaviortree/test_messaging/test_messaging_states.py @@ -0,0 +1,155 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + +import unittest + +from vultron.bt.messaging.states import ( + CS_MESSAGE_TYPES, + EM_MESSAGE_TYPES, + GM_MESSAGE_TYPES, + MessageTypes, + RM_MESSAGE_TYPES, +) + + +class MyTestCase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_messaging_states(self): + """Test that the MessageTypes enum is set up correctly including the aliases + + Returns: + + """ + self.assertIs(MessageTypes.RS, MessageTypes.ReportSubmission) + self.assertIs(MessageTypes.RI, MessageTypes.ReportInvalid) + self.assertIs(MessageTypes.RV, MessageTypes.ReportValid) + self.assertIs(MessageTypes.RD, MessageTypes.ReportDeferred) + self.assertIs(MessageTypes.RA, MessageTypes.ReportAccepted) + self.assertIs(MessageTypes.RC, MessageTypes.ReportClosed) + self.assertIs(MessageTypes.RK, MessageTypes.ReportManagementAck) + self.assertIs(MessageTypes.RE, MessageTypes.ReportManagementError) + + self.assertIs(MessageTypes.EP, MessageTypes.EmbargoProposal) + self.assertIs(MessageTypes.ER, MessageTypes.EmbargoRejected) + self.assertIs(MessageTypes.EA, MessageTypes.EmbargoAccepted) + self.assertIs(MessageTypes.EV, MessageTypes.EmbargoRevisionProposal) + self.assertIs(MessageTypes.EJ, MessageTypes.EmbargoRevisionRejected) + self.assertIs(MessageTypes.EC, MessageTypes.EmbargoRevisionAccepted) + self.assertIs(MessageTypes.ET, MessageTypes.EmbargoTerminated) + self.assertIs(MessageTypes.EK, MessageTypes.EmbargoManagementAck) + self.assertIs(MessageTypes.EE, MessageTypes.EmbargoManagementError) + + self.assertIs(MessageTypes.CV, MessageTypes.CaseStateVendorAware) + self.assertIs(MessageTypes.CF, MessageTypes.CaseStateFixReady) + self.assertIs(MessageTypes.CD, MessageTypes.CaseStateFixDeployed) + self.assertIs(MessageTypes.CP, MessageTypes.CaseStatePublicAware) + self.assertIs(MessageTypes.CX, MessageTypes.CaseStateExploitPublished) + self.assertIs(MessageTypes.CA, MessageTypes.CaseStateAttacksObserved) + self.assertIs(MessageTypes.CK, MessageTypes.CaseStateAck) + self.assertIs(MessageTypes.CE, MessageTypes.CaseStateError) + + self.assertIs(MessageTypes.GI, MessageTypes.GeneralInformationRequest) + self.assertIs(MessageTypes.GK, MessageTypes.GeneralInformationAck) + self.assertIs(MessageTypes.GE, MessageTypes.GeneralInformationError) + + def _check_expected_in_list(self, expected, actual) -> None: + """Check that all expected items are in the actual list + + Args: + expected: list of expected items + actual: list of actual items + + Returns: + None + """ + for e in expected: + self.assertIn(e, actual) + + def test_report_management_message_types(self): + """Check that all expected report management message types are in the list of report management message types + + Returns: + + """ + rm_types = [ + MessageTypes.RS, + MessageTypes.RI, + MessageTypes.RV, + MessageTypes.RD, + MessageTypes.RA, + MessageTypes.RC, + MessageTypes.RK, + MessageTypes.RE, + ] + + self._check_expected_in_list(rm_types, RM_MESSAGE_TYPES) + + def test_embargo_management_message_types(self): + """Check that all expected embargo management message types are in the list of embargo management message types + + Returns: + + """ + em_types = [ + MessageTypes.EP, + MessageTypes.ER, + MessageTypes.EA, + MessageTypes.EV, + MessageTypes.EJ, + MessageTypes.EC, + MessageTypes.ET, + MessageTypes.EK, + MessageTypes.EE, + ] + + self._check_expected_in_list(em_types, EM_MESSAGE_TYPES) + + def test_case_state_message_types(self): + """Check that all expected case state message types are in the list of case state message types + + Returns: + + """ + cs_types = [ + MessageTypes.CV, + MessageTypes.CF, + MessageTypes.CD, + MessageTypes.CP, + MessageTypes.CX, + MessageTypes.CA, + MessageTypes.CK, + MessageTypes.CE, + ] + + self._check_expected_in_list(cs_types, CS_MESSAGE_TYPES) + + def test_general_information_message_types(self): + """Check that all expected general information message types are in the list of general information message types + + Returns: + + """ + gi_types = [MessageTypes.GI, MessageTypes.GK, MessageTypes.GE] + + self._check_expected_in_list(gi_types, GM_MESSAGE_TYPES) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_behaviortree/test_report_management/__init__.py b/test/test_bt/test_behaviortree/test_report_management/__init__.py new file mode 100644 index 00000000..845a4de2 --- /dev/null +++ b/test/test_bt/test_behaviortree/test_report_management/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +"""file: __init__.py +author: adh +created_at: 2/22/23 1:49 PM +""" + + +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + + +def main(): + pass + + +if __name__ == "__main__": + main() diff --git a/test/test_bt/test_behaviortree/test_report_management/test_conditions.py b/test/test_bt/test_behaviortree/test_report_management/test_conditions.py new file mode 100644 index 00000000..f4a41b50 --- /dev/null +++ b/test/test_bt/test_behaviortree/test_report_management/test_conditions.py @@ -0,0 +1,80 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + +import unittest + +import vultron.bt.report_management.conditions as rmc +from vultron.bt.base.node_status import NodeStatus + + +class MockState: + q_rm = None + pass + + +class MyTestCase(unittest.TestCase): + def setUp(self): + self.rmstates = ( + rmc.RM.R, + rmc.RM.I, + rmc.RM.V, + rmc.RM.A, + rmc.RM.C, + rmc.RM.D, + rmc.RM.S, + ) + self.checks = { + rmc.RM.S: rmc.RMinStateStart, + rmc.RM.R: rmc.RMinStateReceived, + rmc.RM.I: rmc.RMinStateInvalid, + rmc.RM.V: rmc.RMinStateValid, + rmc.RM.D: rmc.RMinStateDeferred, + rmc.RM.A: rmc.RMinStateAccepted, + rmc.RM.C: rmc.RMinStateClosed, + } + self.not_checks = { + rmc.RM.S: rmc.RMnotInStateStart, + rmc.RM.C: rmc.RMnotInStateClosed, + } + + def tearDown(self): + pass + + def test_q_rm_in_whatever(self): + for expect_success, check in self.checks.items(): + c = check() + c.bb = MockState() + c.bb.q_rm = expect_success + self.assertEqual(NodeStatus.SUCCESS, c.tick()) + + for state in [x for x in self.rmstates if x != expect_success]: + c.bb.q_rm = state + self.assertEqual(NodeStatus.FAILURE, c.tick()) + + def test_q_rm_not_in_whatever(self): + for expect_fail, check in self.not_checks.items(): + c = check() + c.bb = MockState() + c.setup() + c.bb.q_rm = expect_fail + self.assertEqual(NodeStatus.FAILURE, c.tick()) + + for state in [x for x in self.rmstates if x != expect_fail]: + c.bb.q_rm = state + self.assertEqual(NodeStatus.SUCCESS, c.tick()) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_behaviortree/test_report_management/test_report_states.py b/test/test_bt/test_behaviortree/test_report_management/test_report_states.py new file mode 100644 index 00000000..c923fcf9 --- /dev/null +++ b/test/test_bt/test_behaviortree/test_report_management/test_report_states.py @@ -0,0 +1,91 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + +import unittest + +from vultron.bt.report_management.states import ( + RM, + RM_ACTIVE, + RM_CLOSABLE, + RM_UNCLOSED, +) + + +class MyTestCase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_report_management_states(self): + self.assertIs(RM.START, RM.S) + self.assertIs(RM.RECEIVED, RM.R) + self.assertIs(RM.INVALID, RM.I) + self.assertIs(RM.VALID, RM.V) + self.assertIs(RM.DEFERRED, RM.D) + self.assertIs(RM.ACCEPTED, RM.A) + self.assertIs(RM.CLOSED, RM.C) + + def test_report_management_state_names(self): + self.assertEqual("REPORT_MANAGEMENT_START", RM.S.name) + self.assertEqual("REPORT_MANAGEMENT_RECEIVED", RM.R.name) + self.assertEqual("REPORT_MANAGEMENT_INVALID", RM.I.name) + self.assertEqual("REPORT_MANAGEMENT_VALID", RM.V.name) + self.assertEqual("REPORT_MANAGEMENT_DEFERRED", RM.D.name) + self.assertEqual("REPORT_MANAGEMENT_ACCEPTED", RM.A.name) + self.assertEqual("REPORT_MANAGEMENT_CLOSED", RM.C.name) + + def test_rm_closable(self): + self.assertNotIn(RM.S, RM_CLOSABLE) + self.assertNotIn(RM.R, RM_CLOSABLE) + + self.assertIn(RM.I, RM_CLOSABLE) + + self.assertNotIn(RM.V, RM_CLOSABLE) + + self.assertIn(RM.D, RM_CLOSABLE) + self.assertIn(RM.A, RM_CLOSABLE) + + self.assertNotIn(RM.C, RM_CLOSABLE) + + def test_rm_unclosed(self): + self.assertIn(RM.S, RM_UNCLOSED) + self.assertIn(RM.R, RM_UNCLOSED) + self.assertIn(RM.I, RM_UNCLOSED) + self.assertIn(RM.V, RM_UNCLOSED) + self.assertIn(RM.D, RM_UNCLOSED) + self.assertIn(RM.A, RM_UNCLOSED) + + self.assertNotIn(RM.C, RM_UNCLOSED) + + def test_rm_active(self): + self.assertNotIn(RM.S, RM_ACTIVE) + + self.assertIn(RM.R, RM_ACTIVE) + + self.assertNotIn(RM.I, RM_ACTIVE) + + self.assertIn(RM.V, RM_ACTIVE) + + self.assertNotIn(RM.D, RM_ACTIVE) + + self.assertIn(RM.A, RM_ACTIVE) + + self.assertNotIn(RM.C, RM_ACTIVE) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_behaviortree/test_report_management/test_transitions.py b/test/test_bt/test_behaviortree/test_report_management/test_transitions.py new file mode 100644 index 00000000..0db8c34f --- /dev/null +++ b/test/test_bt/test_behaviortree/test_report_management/test_transitions.py @@ -0,0 +1,106 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + +import unittest + +import vultron.bt.report_management.transitions as rmt +from vultron.bt.base.node_status import NodeStatus + + +class MockState: + q_rm = None + q_rm_history = [] + + +class TestRMTransitions(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def _test_rm_transition_factory(self, expect_success, end_state, factory): + # Make sure the end state is in the list of expected success states + if end_state not in expect_success: + expect_success.append(end_state) + + # Expect failure for all other states + expect_fail = [ + state for state in rmt.RM if state not in expect_success + ] + + for state in expect_success: + # set up the node + node = factory() + node.bb = MockState() + self.assertIsNotNone(node.bb) + self.assertIsNone(node.bb.q_rm) + node.bb.q_rm = state + node.setup() + self.assertIsNotNone(node.bb.q_rm) + + self.assertEqual(NodeStatus.SUCCESS, node.tick()) + + for state in expect_fail: + # set up the node + node = factory() + node.bb = MockState() + self.assertIsNotNone(node.bb) + self.assertIsNone(node.bb.q_rm) + node.bb.q_rm = state + node.setup() + self.assertIsNotNone(node.bb.q_rm) + + self.assertEqual(NodeStatus.FAILURE, node.tick()) + + def test_q_rm_to_R(self): + expect_success = rmt._to_R.start_states + end_state = rmt._to_R.end_state + factory = rmt.q_rm_to_R + self._test_rm_transition_factory(expect_success, end_state, factory) + + def test_q_rm_to_I(self): + expect_success = rmt._to_I.start_states + end_state = rmt._to_I.end_state + factory = rmt.q_rm_to_I + self._test_rm_transition_factory(expect_success, end_state, factory) + + def test_q_rm_to_V(self): + expect_success = rmt._to_V.start_states + end_state = rmt._to_V.end_state + factory = rmt.q_rm_to_V + self._test_rm_transition_factory(expect_success, end_state, factory) + + def test_q_rm_to_D(self): + expect_success = rmt._to_D.start_states + end_state = rmt._to_D.end_state + factory = rmt.q_rm_to_D + self._test_rm_transition_factory(expect_success, end_state, factory) + + def test_q_rm_to_A(self): + expect_success = rmt._to_A.start_states + end_state = rmt._to_A.end_state + factory = rmt.q_rm_to_A + self._test_rm_transition_factory(expect_success, end_state, factory) + + def test_q_rm_to_C(self): + expect_success = rmt._to_C.start_states + end_state = rmt._to_C.end_state + factory = rmt.q_rm_to_C + self._test_rm_transition_factory(expect_success, end_state, factory) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_case_states/__init__.py b/test/test_bt/test_case_states/__init__.py new file mode 100644 index 00000000..b8d147d8 --- /dev/null +++ b/test/test_bt/test_case_states/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University diff --git a/test/test_bt/test_case_states/test_conditions.py b/test/test_bt/test_case_states/test_conditions.py new file mode 100644 index 00000000..f53e6ce3 --- /dev/null +++ b/test/test_bt/test_case_states/test_conditions.py @@ -0,0 +1,195 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +import unittest + +import vultron.bt.case_state.conditions as csc +from vultron.bt.base.node_status import NodeStatus +from vultron.bt.states import ActorState +from vultron.case_states.states import CS + + +def all_casings(input_string): + if not input_string: + yield "" + else: + first = input_string[:1] + if first.lower() == first.upper(): + for sub_casing in all_casings(input_string[1:]): + yield first + sub_casing + else: + for sub_casing in all_casings(input_string[1:]): + yield first.lower() + sub_casing + yield first.upper() + sub_casing + + +class MyTestCase(unittest.TestCase): + def setUp(self) -> None: + self.bb = ActorState() + + def _ab( + self, node_cls: object, expect_true_when: str, extra: list = [] + ) -> None: + """ + Abstracts out the test for a CS state condition node + + Args: + node_cls: The bt condition node to test + expect_true_when: a string that should appear in the state name if the condition is true + extra: a list of other strings that might also cause the condition to resolve to true + + """ + + node = node_cls() + node.bb = self.bb + + for state in CS: + node.bb.q_cs = state + self.assertEqual(node.bb.q_cs, state) + + # do the thing + node.tick() + + self.assertEqual(node.bb.q_cs, state) + + if expect_true_when in state.name or any( + [x in state.name for x in extra] + ): + self.assertEqual( + NodeStatus.SUCCESS, + node.status, + f"{node.name} expected SUCCESS on state:{state.name}", + ) + continue + + # for each letter in expect_true, generate all the other case permutations and check for failure + casings = list(all_casings(expect_true_when)) + casings.remove(expect_true_when) + self.assertNotIn(expect_true_when, casings) + + for s in casings: + if s not in state.name: + continue + + if any([x in state.name for x in extra]): + continue + + print(s, state.name) + # if s in state.name: + self.assertEqual( + NodeStatus.FAILURE, + node.status, + f"{node.name} expected FAILURE on state:{state.name}", + ) + + def test_cs_in_state_vendor_aware(self): + self._ab(node_cls=csc.CSinStateVendorAware, expect_true_when="V") + + def test_cs_in_state_vendor_unaware(self): + self._ab(node_cls=csc.CSinStateVendorUnaware, expect_true_when="v") + + def test_cs_in_state_fix_ready(self): + self._ab(node_cls=csc.CSinStateFixReady, expect_true_when="F") + + def test_cs_in_state_fix_not_ready(self): + self._ab(node_cls=csc.CSinStateFixNotReady, expect_true_when="f") + + def test_cs_in_state_fix_deployed(self): + self._ab(node_cls=csc.CSinStateFixDeployed, expect_true_when="D") + + def test_cs_in_state_fix_not_deployed(self): + self._ab(node_cls=csc.CSinStateFixNotDeployed, expect_true_when="d") + + def test_cs_in_state_public_aware(self): + self._ab(node_cls=csc.CSinStatePublicAware, expect_true_when="P") + + def test_cs_in_state_public_unaware(self): + self._ab(node_cls=csc.CSinStatePublicUnaware, expect_true_when="p") + + def test_cs_in_state_exploit_public(self): + self._ab(node_cls=csc.CSinStateExploitPublic, expect_true_when="X") + + def test_cs_in_state_no_exploit_public(self): + self._ab(node_cls=csc.CSinStateNoExploitPublic, expect_true_when="x") + + def test_cs_in_state_attacks_observed(self): + self._ab(node_cls=csc.CSinStateAttacksObserved, expect_true_when="A") + + def test_cs_in_state_no_attacks_observed(self): + self._ab(node_cls=csc.CSinStateNoAttacksObserved, expect_true_when="a") + + def test_PX_combo(self): + self._ab( + node_cls=csc.CSinStatePublicAwareAndExploitPublic, + expect_true_when="PX", + ) + + def test_VF_combo(self): + self._ab( + node_cls=csc.CSinStateVendorAwareAndFixReady, expect_true_when="VF" + ) + + def test_VFD_combo(self): + self._ab( + node_cls=csc.CSinStateVendorAwareAndFixReadyAndFixDeployed, + expect_true_when="VFD", + ) + + def test_pxa_combo(self): + self._ab( + node_cls=csc.CSinStateNotPublicNoExploitNoAttacks, + expect_true_when="pxa", + ) + + def test_P_or_X_or_A_combo_P(self): + self._ab( + node_cls=csc.CSinStatePublicAwareOrExploitPublicOrAttacksObserved, + expect_true_when="P", + extra=["X", "A"], + ) + + def test_P_or_X_or_A_combo_X(self): + self._ab( + node_cls=csc.CSinStatePublicAwareOrExploitPublicOrAttacksObserved, + expect_true_when="X", + extra=["P", "A"], + ) + + def test_P_or_X_or_A_combo_A(self): + self._ab( + node_cls=csc.CSinStatePublicAwareOrExploitPublicOrAttacksObserved, + expect_true_when="A", + extra=["P", "X"], + ) + + def test_dpxa_combo(self): + self._ab( + node_cls=csc.CSinStateNotDeployedNotPublicNoExploitNoAttacks, + expect_true_when="dpxa", + ) + + def test_dP_combo(self): + self._ab( + node_cls=csc.CSinStateNotDeployedButPublicAware, + expect_true_when="dP", + ) + + def test_VFd_combo(self): + self._ab( + node_cls=csc.CSinStateVendorAwareFixReadyFixNotDeployed, + expect_true_when="VFd", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_case_states/test_states.py b/test/test_bt/test_case_states/test_states.py new file mode 100644 index 00000000..1d51a8fb --- /dev/null +++ b/test/test_bt/test_case_states/test_states.py @@ -0,0 +1,67 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +import unittest +from itertools import product + +import vultron.case_states.states as s + + +class MyTestCase(unittest.TestCase): + def setUp(self): + vfd = ["vfd", "Vfd", "VFd", "VFD"] + pxa = ["pxa", "Pxa", "pXa", "pxA", "PXa", "pXA", "PxA", "PXA"] + + self.states = [a + b for (a, b) in product(vfd, pxa)] + + self.assertEqual(32, len(self.states)) + + def test_state_string_to_enums(self): + for state_string in self.states: + (vfd, pxa) = s.state_string_to_enums(state_string) + self.assertEqual(state_string[:3], vfd.name) + self.assertEqual(state_string[3:], pxa.name) + + def test_state_string_to_enum2(self): + for state_string in self.states: + result = s.state_string_to_enum2(state_string) + # for each character in state string, check to see that the result is 0 if lowercase and 1 if uppercase + # we don't really care about the names of the enums, just that they are 0 or 1 + for i, c in enumerate(state_string): + if c.islower(): + self.assertEqual(0, result[i]) + else: + self.assertEqual(1, result[i]) + + def test_CS_vfdpxa(self): + for state_string in self.states: + vfd_str = state_string[:3] + pxa_str = state_string[3:] + + cs = getattr(s.CS, state_string) + self.assertEqual(state_string, cs.name) + + vfd = getattr(s.CS_vfd, vfd_str) + self.assertEqual(vfd_str, vfd.name) + self.assertEqual(vfd, cs.value.vfd_state) + self.assertEqual(vfd_str, cs.value.vfd_state.name) + + pxa = getattr(s.CS_pxa, pxa_str) + self.assertEqual(pxa_str, pxa.name) + self.assertEqual(pxa, cs.value.pxa_state) + self.assertEqual(pxa_str, cs.value.pxa_state.name) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_case_states/test_transitions.py b/test/test_bt/test_case_states/test_transitions.py new file mode 100644 index 00000000..f96d88fa --- /dev/null +++ b/test/test_bt/test_case_states/test_transitions.py @@ -0,0 +1,178 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +import unittest +from copy import deepcopy + +import vultron.bt.case_state.transitions as cst +from vultron.bt.base.node_status import NodeStatus +from vultron.bt.states import ActorState +from vultron.case_states.states import ( + AttackObservation, + CS, + ExploitPublication, + FixDeployment, + FixReadiness, + PublicAwareness, + VendorAwareness, +) + + +class MyTestCase(unittest.TestCase): + def setUp(self) -> None: + self.bb = ActorState() + + def _test_q_cs_to_something(self): + """ + Helper function to test the q_cs_to_* functions + + Expects the following class attributes to be set: + - cls2test: the class to test + - expected_value: the expected value of the state after the transition + - attrib2check: the variable to check for the expected value + + Returns: + + """ + node = self.cls2test() + node.bb = self.bb + + for state in CS: + with self.subTest(state=state): + original_state = deepcopy(state) + + expect_new_state = state.name.replace( + node.target_state.lower(), node.target_state + ) + valid_new_state = expect_new_state in CS.__members__ + + self.bb.q_cs = state + + self.assertEqual(state, node.bb.q_cs) + + node.tick() + + # should always succeed + self.assertEqual(NodeStatus.SUCCESS, node.status) + + if node.target_state in state.name: + # shouldn't change if we're already there + self.assertEqual(original_state, node.bb.q_cs) + return + + # if you got here, we're not already in the new state + if not valid_new_state: + # shouldn't change if new state is invalid + self.assertEqual(original_state, node.bb.q_cs) + return + + # if you got here, the new state is valid + + # should change if we're not already there + self.assertNotEqual(original_state, node.bb.q_cs) + + # only diff should be the V + self.assertEqual( + node.bb.q_cs.name, + original_state.name.replace( + node.target_state.lower(), node.target_state + ), + ) + + # dive in to find the actual state value we want to check + val2check = self.bb.q_cs.value + for attr in self.attrib2check.split("."): + val2check = getattr(val2check, attr) + + # make sure the actual state value is correct + self.assertEqual(self.expected_value, val2check) + + def test_q_cs_to_V(self): + self.cls2test = cst.q_cs_to_V + self.expected_value = VendorAwareness.VENDOR_AWARE + self.attrib2check = "vfd_state.value.vendor_awareness" + + self._test_q_cs_to_something() + + def test__q_cs_to_F(self): + self.cls2test = cst._q_cs_to_F + self.expected_value = FixReadiness.FIX_READY + self.attrib2check = "vfd_state.value.fix_readiness" + + self._test_q_cs_to_something() + + def _test_q_cs_to_something_with_precondition(self): + node = self.cls2test() + node.bb = self.bb + + for state in CS: + self.bb.q_cs = state + node.tick() + + if self.expect_fail_when in state.name: + # should not work + self.assertEqual(NodeStatus.FAILURE, node.status) + else: + self.assertEqual(NodeStatus.SUCCESS, node.status) + + attrib2check = self.bb.q_cs.value + for attr in self.attrib2check.split("."): + attrib2check = getattr(attrib2check, attr) + + self.assertEqual(self.expected_value, attrib2check) + + def test_q_cs_to_F(self): + self.cls2test = cst.q_cs_to_F + self.expected_value = FixReadiness.FIX_READY + self.attrib2check = "vfd_state.value.fix_readiness" + self.expect_fail_when = "v" + + self._test_q_cs_to_something_with_precondition() + + def test__q_cs_to_D(self): + self.cls2test = cst._q_cs_to_D + self.expected_value = FixDeployment.FIX_DEPLOYED + self.attrib2check = "vfd_state.value.fix_deployment" + + self._test_q_cs_to_something() + + def test_q_cs_to_D(self): + self.cls2test = cst.q_cs_to_D + self.expected_value = FixDeployment.FIX_DEPLOYED + self.attrib2check = "vfd_state.value.fix_deployment" + self.expect_fail_when = "f" + + self._test_q_cs_to_something_with_precondition() + + def test_q_cs_to_P(self): + self.cls2test = cst.q_cs_to_P + self.expected_value = PublicAwareness.PUBLIC_AWARE + self.attrib2check = "pxa_state.value.public_awareness" + + self._test_q_cs_to_something() + + def test_q_cs_to_X(self): + self.cls2test = cst.q_cs_to_X + self.expected_value = ExploitPublication.EXPLOIT_PUBLIC + self.attrib2check = "pxa_state.value.exploit_publication" + + self._test_q_cs_to_something() + + def test_q_cs_to_A(self): + self.cls2test = cst.q_cs_to_A + self.expected_value = AttackObservation.ATTACKS_OBSERVED + self.attrib2check = "pxa_state.value.attack_observation" + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_embargo_management/__init__.py b/test/test_bt/test_embargo_management/__init__.py new file mode 100644 index 00000000..845a4de2 --- /dev/null +++ b/test/test_bt/test_embargo_management/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +"""file: __init__.py +author: adh +created_at: 2/22/23 1:49 PM +""" + + +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + + +def main(): + pass + + +if __name__ == "__main__": + main() diff --git a/test/test_bt/test_embargo_management/test_conditions.py b/test/test_bt/test_embargo_management/test_conditions.py new file mode 100644 index 00000000..29b61e7e --- /dev/null +++ b/test/test_bt/test_embargo_management/test_conditions.py @@ -0,0 +1,172 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + +import unittest + +import vultron.bt.embargo_management.conditions as emc +from vultron.bt.base.node_status import NodeStatus +from vultron.bt.embargo_management.states import EM + + +class MockState: + q_em = None + pass + + +class TestEmbargoManagementConditions(unittest.TestCase): + def setUp(self): + self.emstates = tuple(EM) + self.checks = { + EM.NO_EMBARGO: emc.EMinStateNone, + EM.PROPOSED: emc.EMinStateProposed, + EM.ACTIVE: emc.EMinStateActive, + EM.REVISE: emc.EMinStateRevise, + EM.EXITED: emc.EMinStateExited, + } + + def tearDown(self): + pass + + def test_q_em_in_whatever(self): + for expect_success, check in self.checks.items(): + c = check() + c.bb = MockState() + c.bb.q_em = expect_success + self.assertEqual( + NodeStatus.SUCCESS, c.tick() + ), f"State {expect_success} should have succeeded" + + expect_fails = [x for x in self.emstates if x != expect_success] + for state in expect_fails: + c.bb.q_em = state + self.assertEqual( + NodeStatus.FAILURE, c.tick() + ), f"State {state} should have failed" + + def test_em_in_state_active(self): + """Test that the EMinStateActive node is instantiated with the correct state + + Returns: + + """ + cls = emc.EMinStateActive + should_succeed = (EM.ACTIVE,) + + self._test_in_state(cls, should_succeed) + + def test_em_in_state_none(self): + """Test that the EMinStateNone node is instantiated with the correct state + + Returns: + + """ + cls = emc.EMinStateNone + should_succeed = (EM.NO_EMBARGO,) + + self._test_in_state(cls, should_succeed) + + def _test_in_state(self, cls, expected_states): + for state in EM: + node = cls() + node.bb = MockState() + node.bb.q_em = state + + node.tick() + + if state in expected_states: + self.assertEqual(NodeStatus.SUCCESS, node.status) + else: + self.assertEqual(NodeStatus.FAILURE, node.status) + + def test_em_in_state_proposed(self): + """Test that the EMinStateProposed node is instantiated with the correct state + + Returns: + + """ + cls = emc.EMinStateProposed + should_succeed = (EM.PROPOSED,) + + self._test_in_state(cls, should_succeed) + + def test_em_in_state_revise(self): + """Test that the EMinStateRevise node is instantiated with the correct state + + Returns: + + """ + cls = emc.EMinStateRevise + should_succeed = (EM.REVISE,) + + self._test_in_state(cls, should_succeed) + + def test_em_in_state_exited(self): + """Test that the EMinStateExited node is instantiated with the correct state + + Returns: + + """ + cls = emc.EMinStateExited + should_succeed = (EM.EXITED,) + + self._test_in_state(cls, should_succeed) + + def test_em_in_state_active_or_revise(self): + """Test that the FallbackNode is instantiated with the correct children + + Returns: + + """ + node = emc.EMinStateActiveOrRevise + expected_states = (EM.ACTIVE, EM.REVISE) + + self._test_in_state(node, expected_states) + + def test_em_in_state_none_or_exited(self): + """Test that the FallbackNode is instantiated with the correct children + + Returns: + + """ + node = emc.EMinStateNoneOrExited + expected_states = (EM.NO_EMBARGO, EM.EXITED) + + self._test_in_state(node, expected_states) + + def test_em_in_state_propose_or_revise(self): + """Test that the FallbackNode is instantiated with the correct children + + Returns: + + """ + node = emc.EMinStateProposeOrRevise + expected_states = (EM.PROPOSED, EM.REVISE) + + self._test_in_state(node, expected_states) + + def test_em_in_state_none_or_proposed_or_revise(self): + """Test that the FallbackNode is instantiated with the correct children + + Returns: + + """ + node = emc.EMinStateNoneOrProposeOrRevise + expected_states = (EM.NO_EMBARGO, EM.PROPOSED, EM.REVISE) + + self._test_in_state(node, expected_states) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_embargo_management/test_embargo_states.py b/test/test_bt/test_embargo_management/test_embargo_states.py new file mode 100644 index 00000000..8e9a091c --- /dev/null +++ b/test/test_bt/test_embargo_management/test_embargo_states.py @@ -0,0 +1,44 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + +import unittest + +from vultron.bt.embargo_management.states import EM + + +class TestEmbargoStates(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_embargo_states(self): + self.assertIs(EM.NO_EMBARGO, EM.N) + self.assertIs(EM.PROPOSED, EM.P) + self.assertIs(EM.ACTIVE, EM.A) + self.assertIs(EM.REVISE, EM.R) + self.assertIs(EM.EXITED, EM.X) + + def test_embargo_state_names(self): + self.assertEqual("EMBARGO_MANAGEMENT_NONE", EM.N.name) + self.assertEqual("EMBARGO_MANAGEMENT_PROPOSED", EM.P.name) + self.assertEqual("EMBARGO_MANAGEMENT_ACTIVE", EM.A.name) + self.assertEqual("EMBARGO_MANAGEMENT_REVISE", EM.R.name) + self.assertEqual("EMBARGO_MANAGEMENT_EXITED", EM.X.name) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_embargo_management/test_transitions.py b/test/test_bt/test_embargo_management/test_transitions.py new file mode 100644 index 00000000..c7bd4fc4 --- /dev/null +++ b/test/test_bt/test_embargo_management/test_transitions.py @@ -0,0 +1,98 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +# +# See LICENSE for details + +import unittest + +import vultron.bt.embargo_management.transitions as emt +from vultron.bt.base.node_status import NodeStatus +from vultron.bt.embargo_management.states import EM + + +class MockState: + q_em = None + q_em_history = [] + + +class TestEMTransitions(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def _test_em_transition_factory(self, expect_success, end_state, factory): + # Make sure the end state is in the list of expected success states + if end_state not in expect_success: + expect_success.append(end_state) + + # Expect failure for all other states + expect_fail = [state for state in EM if state not in expect_success] + + for state in expect_success: + # set up the node + node = factory() + node.bb = MockState() + self.assertIsNotNone(node.bb) + self.assertIsNone(node.bb.q_em) + node.bb.q_em = state + node.setup() + self.assertIsNotNone(node.bb.q_em) + + self.assertEqual(NodeStatus.SUCCESS, node.tick()), state + + for state in expect_fail: + # set up the node + node = factory() + node.bb = MockState() + self.assertIsNotNone(node.bb) + self.assertIsNone(node.bb.q_em) + node.bb.q_em = state + node.setup() + self.assertIsNotNone(node.bb.q_em) + self.assertEqual(NodeStatus.FAILURE, node.tick()), state + + def test_q_em_to_P(self): + expect_success = emt._to_P.start_states + end_state = emt._to_P.end_state + factory = emt.q_em_to_P + self._test_em_transition_factory(expect_success, end_state, factory) + + def test_q_em_to_A(self): + expect_success = emt._to_A.start_states + end_state = emt._to_A.end_state + factory = emt.q_em_to_A + self._test_em_transition_factory(expect_success, end_state, factory) + + def test_q_em_to_R(self): + expect_success = emt._to_R.start_states + end_state = emt._to_R.end_state + factory = emt.q_em_to_R + self._test_em_transition_factory(expect_success, end_state, factory) + + def test_q_em_R_to_A(self): + expect_success = emt._R_to_A.start_states + end_state = emt._R_to_A.end_state + factory = emt.q_em_R_to_A + self._test_em_transition_factory(expect_success, end_state, factory) + + def test_q_em_to_X(self): + expect_success = emt._to_X.start_states + end_state = emt._to_X.end_state + factory = emt.q_em_to_X + self._test_em_transition_factory(expect_success, end_state, factory) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_report_management/__init__.py b/test/test_bt/test_report_management/__init__.py new file mode 100644 index 00000000..b8d147d8 --- /dev/null +++ b/test/test_bt/test_report_management/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University diff --git a/test/test_bt/test_report_management/test_conditions.py b/test/test_bt/test_report_management/test_conditions.py new file mode 100644 index 00000000..b0f91d2b --- /dev/null +++ b/test/test_bt/test_report_management/test_conditions.py @@ -0,0 +1,124 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +import unittest + +import vultron.bt.report_management.conditions as rmc +from vultron.bt.base.node_status import NodeStatus +from vultron.bt.report_management.states import RM +from vultron.bt.states import ActorState + + +class MyTestCase(unittest.TestCase): + def _test_generic_rm_in_state(self, cls, state): + node = cls() + node.bb = ActorState() + + self.assertEqual("q_rm", node.key) + self.assertEqual(state, node.state) + + for rm_state in RM: + node.bb.q_rm = rm_state + + node.tick() + + if rm_state == state: + # success when they agree + self.assertEqual(NodeStatus.SUCCESS, node.status) + else: + # failure when they disagree + self.assertEqual(NodeStatus.FAILURE, node.status) + + def _test_generic_rm_not_in_state(self, cls, state): + node = cls() + node.bb = ActorState() + + for rm_state in RM: + node.bb.q_rm = rm_state + + node.tick() + + if rm_state != state: + # success when they disagree + self.assertEqual(NodeStatus.SUCCESS, node.status) + else: + # failure when they agree + self.assertEqual(NodeStatus.FAILURE, node.status) + + def test_rm_in_state_start(self): + self._test_generic_rm_in_state(rmc.RMinStateStart, RM.START) + + def test_rm_in_state_closed(self): + self._test_generic_rm_in_state(rmc.RMinStateClosed, RM.CLOSED) + + def test_rm_in_state_received(self): + self._test_generic_rm_in_state(rmc.RMinStateReceived, RM.RECEIVED) + + def test_rm_in_state_invalid(self): + self._test_generic_rm_in_state(rmc.RMinStateInvalid, RM.INVALID) + + def test_rm_in_state_valid(self): + self._test_generic_rm_in_state(rmc.RMinStateValid, RM.VALID) + + def test_rm_in_state_deferred(self): + self._test_generic_rm_in_state(rmc.RMinStateDeferred, RM.DEFERRED) + + def test_rm_in_state_accepted(self): + self._test_generic_rm_in_state(rmc.RMinStateAccepted, RM.ACCEPTED) + + def test_rm_not_in_state_start(self): + self._test_generic_rm_not_in_state(rmc.RMnotInStateStart, RM.START) + + def test_rm_not_in_state_closed(self): + self._test_generic_rm_not_in_state(rmc.RMnotInStateClosed, RM.CLOSED) + + def _test_generic_multi_state_check(self, cls, states): + node = cls() + node.bb = ActorState() + + for rm_state in RM: + node.bb.q_rm = rm_state + + node.tick() + + if rm_state in states: + # success when they agree + self.assertEqual(NodeStatus.SUCCESS, node.status) + else: + # failure when they disagree + self.assertEqual(NodeStatus.FAILURE, node.status) + + def test_rm_in_state_deferred_or_accepted(self): + self._test_generic_multi_state_check( + rmc.RMinStateDeferredOrAccepted, [RM.DEFERRED, RM.ACCEPTED] + ) + + def test_rm_in_state_received_or_invalid(self): + self._test_generic_multi_state_check( + rmc.RMinStateReceivedOrInvalid, [RM.RECEIVED, RM.INVALID] + ) + + def test_rm_in_state_start_or_closed(self): + self._test_generic_multi_state_check( + rmc.RMinStateStartOrClosed, [RM.START, RM.CLOSED] + ) + + def test_rm_in_state_valid_or_deferred_or_accepted(self): + self._test_generic_multi_state_check( + rmc.RMinStateValidOrDeferredOrAccepted, + [RM.VALID, RM.DEFERRED, RM.ACCEPTED], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_report_management/test_states.py b/test/test_bt/test_report_management/test_states.py new file mode 100644 index 00000000..a0f0ebfe --- /dev/null +++ b/test/test_bt/test_report_management/test_states.py @@ -0,0 +1,81 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +import unittest +from typing import Sequence + +from vultron.bt.report_management.states import ( + RM, + RM_ACTIVE, + RM_CLOSABLE, + RM_UNCLOSED, +) + + +class MyTestCase(unittest.TestCase): + def test_rm_enum(self): + self.assertGreater(len(RM), 0) + + for ( + medname + ) in "START RECEIVED INVALID VALID DEFERRED ACCEPTED CLOSED".split(): + shortname = medname[0] + self.assertTrue( + hasattr(RM, shortname), f"RM does not have {shortname}" + ) + self.assertEqual( + medname, + getattr(RM, shortname).value, + f"RM.{shortname} does not have value {medname}", + ) + + self.assertTrue( + hasattr(RM, medname), f"RM does not have {medname}" + ) + self.assertEqual( + medname, + getattr(RM, medname).value, + f"RM.{medname} does not have value {medname}", + ) + + longname = f"REPORT_MANAGEMENT_{medname}" + self.assertTrue( + hasattr(RM, longname), f"RM does not have {longname}" + ) + self.assertEqual( + medname, + getattr(RM, longname).value, + f"RM.{longname} does not have value {medname}", + ) + + def _test_convenience_aliases(self, alias, values: Sequence[RM]): + for s in RM: + if s in values: + self.assertIn(s, alias) + else: + self.assertNotIn(s, alias) + + def test_closable(self): + self._test_convenience_aliases(RM_CLOSABLE, (RM.I, RM.D, RM.A)) + + def test_unclosed(self): + self._test_convenience_aliases( + RM_UNCLOSED, (RM.S, RM.R, RM.I, RM.V, RM.D, RM.A) + ) + + def test_active(self): + self._test_convenience_aliases(RM_ACTIVE, (RM.R, RM.V, RM.A)) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_report_management/test_transitions.py b/test/test_bt/test_report_management/test_transitions.py new file mode 100644 index 00000000..e36e7a69 --- /dev/null +++ b/test/test_bt/test_report_management/test_transitions.py @@ -0,0 +1,88 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +import unittest + +import vultron.bt.report_management.transitions as rmt +from vultron.bt.base.node_status import NodeStatus +from vultron.bt.report_management.states import RM + + +class MockActorState: + def __init__(self): + self.q_rm = None + self.q_rm_history = [] + + +class MyTestCase(unittest.TestCase): + def _test_generic_transition(self, cls, allowed, target): + for rm_state in RM: + node = cls() + + node.bb = MockActorState() + + node.bb.q_rm = rm_state + self.assertEqual(rm_state, node.bb.q_rm) + + node.tick() + + if rm_state in allowed: + # if the transition is allowed, the node should succeed + self.assertEqual(NodeStatus.SUCCESS, node.status) + # and the state should change to the target + self.assertEqual(node.bb.q_rm, target) + else: + # if the transition is not allowed, the node should fail + self.assertEqual(NodeStatus.FAILURE, node.status) + # and the state should not change + self.assertEqual(node.bb.q_rm, rm_state) + + def test_q_rm_to_R(self): + self._test_generic_transition( + rmt.q_rm_to_R, [RM.START, RM.RECEIVED], target=RM.RECEIVED + ) + + def test_q_rm_to_I(self): + self._test_generic_transition( + rmt.q_rm_to_I, [RM.RECEIVED, RM.INVALID], target=RM.INVALID + ) + + def test_q_rm_to_V(self): + self._test_generic_transition( + rmt.q_rm_to_V, [RM.RECEIVED, RM.INVALID, RM.VALID], target=RM.VALID + ) + + def test_q_rm_to_D(self): + self._test_generic_transition( + rmt.q_rm_to_D, + [RM.VALID, RM.ACCEPTED, RM.DEFERRED], + target=RM.DEFERRED, + ) + + def test_q_rm_to_A(self): + self._test_generic_transition( + rmt.q_rm_to_A, + [RM.VALID, RM.DEFERRED, RM.ACCEPTED], + target=RM.ACCEPTED, + ) + + def test_q_rm_to_C(self): + self._test_generic_transition( + rmt.q_rm_to_C, + [RM.INVALID, RM.DEFERRED, RM.ACCEPTED, RM.CLOSED], + target=RM.CLOSED, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bt/test_vultrabot.py b/test/test_bt/test_vultrabot.py new file mode 100644 index 00000000..207f3a28 --- /dev/null +++ b/test/test_bt/test_vultrabot.py @@ -0,0 +1,42 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +import unittest +from io import StringIO +from unittest.mock import patch + +from vultron.demo import vultrabot + + +class MyTestCase(unittest.TestCase): + # capture stdout + @patch("sys.stdout", new_callable=StringIO) + def test_main(self, stdout): + for i in range(10): + # capture the output + vultrabot._run_simulation() + vultrabot._print_sim_result() + # test the output + self.assertIsNotNone(stdout) + output = stdout.getvalue() + + # things that are consistently in the output + look_for = "q_rm q_em q_cs RS START NONE vfdpxa FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR CLOSED".split() + + for item in look_for: + with self.subTest(): + self.assertIn(item, output) + + +if __name__ == "__main__": + unittest.main() diff --git a/vultron/bt/__init__.py b/vultron/bt/__init__.py new file mode 100644 index 00000000..b8d147d8 --- /dev/null +++ b/vultron/bt/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University diff --git a/vultron/bt/base/__init__.py b/vultron/bt/base/__init__.py new file mode 100644 index 00000000..7aa1ad12 --- /dev/null +++ b/vultron/bt/base/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This package implements a basic Behavior Tree library. +""" diff --git a/vultron/bt/base/blackboard.py b/vultron/bt/base/blackboard.py new file mode 100644 index 00000000..22505572 --- /dev/null +++ b/vultron/bt/base/blackboard.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +""" +This module defines Blackboard class for sharing data between nodes in the tree. + +The default Blackboard class is a dict, which is good enough if you just want +to run a single tree in a single process. + +If you want multiple trees in a single process with shared state, just use +the same Blackboard object instance for each tree. + +If you want to run multiple trees in a single process without shared state, +you'll need to use a different Blackboard object instance for each tree. +You can still do that with the default Blackboard class, but you'll need to +create a new Blackboard object instance for each tree. + +If you want to run multiple trees in multiple processes with shared state, +you'll need to use a Blackboard object that can communicate with some external +data store, such as a key-value store on a server. The default Blackboard +class is not designed for that use case, but you can subclass it to use +any object that implements the `__getitem__` and `__setitem__` methods +to provide a python dict-like interface. +For example, mongodict and redis-dict provide such an interface for MongoDb and Redis, +respectively. +""" +from dataclasses import dataclass + + +@dataclass(kw_only=True) +class Blackboard: + """ + Provides a blackboard object for sharing data between nodes in the tree. + To use a custom blackboard object, subclass this class and set + the BehaviorTree's bbclass attribute to your subclass. + """ diff --git a/vultron/bt/base/bt.py b/vultron/bt/base/bt.py new file mode 100644 index 00000000..baebc914 --- /dev/null +++ b/vultron/bt/base/bt.py @@ -0,0 +1,191 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This module defines a Behavior Tree object. +""" + +import logging +from typing import Type + +from vultron.bt.base.blackboard import Blackboard +from vultron.bt.base.bt_node import BtNode +from vultron.bt.base.errors import ( + BehaviorTreeError, +) +from vultron.bt.base.node_status import NodeStatus + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + + +class BehaviorTree: + """BehaviorTree is the base class for all bt trees. + It is responsible for setting up the tree and running the root node. + + Attributes: + root: the root node of the tree + bb: the blackboard object + """ + + bbclass = Blackboard + + def __init__(self, root: BtNode = None, bbclass: Type[Blackboard] = None): + """ + Initializes the bt tree. + + Args: + root: the root node of the tree + + Returns: + None + """ + self.root: BtNode = root + if bbclass is not None: + self.bbclass = bbclass + + self.bb: Blackboard = self.bbclass() + self.status: NodeStatus = None + + # track whether we've done the pre-tick setup stuff + self._setup: bool = False + + # runtime context + def __enter__(self): + """ + + Returns: + + """ + self._ensure_setup() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """ + + Args: + exc_type: + exc_val: + exc_tb: + + Returns: + + """ + if exc_type is not None: + # where were we in the tree? + import inspect + + frm = inspect.trace()[-1] + obj = frm[0].f_locals["self"] + logger.debug(f"Exception in {obj.name}") + print(obj.name) + + while obj.parent is not None: + obj = obj.parent + print(obj.name) + logger.debug(f"parent: {obj.name}") + + # print(self.root.graph()) + return False + + def add_root(self, node: BtNode) -> None: + """Adds a root node to the tree. + + Args: + node: the root node to add + + Returns: + None + """ + self.root = node + self.root.bb = self.bb + + def _ensure_setup(self) -> None: + """Ensures that the tree has been set up. + + Args: + None + + Returns: + None + + Raises: + BehaviorTreeError: if setup() fails + """ + if self._setup: + return + + self.setup() + + if not self._setup: + raise BehaviorTreeError(f"{self.__class__.__name__} setup failed") + + def _pre_tick(self) -> None: + """_pre_tick() is called before the root node's tick() method. + + Args: + None + + Returns: + None + """ + self._ensure_setup() + + def tick(self) -> NodeStatus: + """tick() is the main entry point for running the bt tree. + It calls the root node's tick() method. + Two callbacks are provided for subclasses to override: + _pre_tick() and _post_tick(). + + Args: + None + + Returns: + NodeStatus: the status of the root node after the tick + + """ + self._pre_tick() + status = self.root.tick(depth=0) + self._post_tick() + self.status = status + return status + + def _post_tick(self) -> None: + """_post_tick() is called after the root node's tick() method. + + Returns: + None + """ + + # def graph(self) -> : + # return self.root.graph() + + def setup(self) -> None: + """Recursively calls the setup() method on all nodes in the tree starting at the root. + + Returns: + None + """ + self.root.bb = self.bb + self.root.setup() + self._setup = True + + +# def to_dot(node: BtNode, g=None): +# if g is None: +# g = graphviz.Digraph() +# +# g.node(node.name, label=node._node_label, shape=node._node_shape) +# for child in node.children: +# to_dot(child, g) +# g.edge(node.name, child.name) +# return g diff --git a/vultron/bt/base/bt_node.py b/vultron/bt/base/bt_node.py new file mode 100644 index 00000000..bb01be71 --- /dev/null +++ b/vultron/bt/base/bt_node.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This module provides the base class for all nodes in the Behavior Tree. + +It also provides a number of core node types that can be used to build a Behavior Tree. +""" + + +import logging +from copy import deepcopy +from typing import Union + +import networkx as nx + +from vultron.bt.base.errors import ( + ActionNodeError, + ConditionCheckError, + LeafNodeError, +) +from vultron.bt.base.node_status import NodeStatus + +logger = logging.getLogger(__name__) + + +def _indent(depth=0): + """Convenience method for indenting printed output.""" + return " | " * depth + + +class BtNode: + """BtNode is the base class for all nodes in the Behavior Tree.""" + + indent_level = 0 + name_pfx = None + _node_shape = "box" + _children = None + _objcount = 0 + + def __init__(self): + BtNode._objcount += 1 + + pfx = "" + if self.name_pfx is not None: + pfx = f"{self.name_pfx}_" + + # freeze object count into a string at instantiation time + self.name_sfx = f"_{BtNode._objcount}" + + self.name = f"{pfx}{self.__class__.__name__}{self.name_sfx}" + + self.parent = None + self.children = [] + + self.status = None + self.bb = None + + self._setup_complete = False + + self.add_children() + + def __enter__(self): + self.setup() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def setup(self) -> None: + """Sets up the node and its children. + + Returns: + None + """ + if self.parent is not None: + self.bb = self.parent.bb + for child in self.children: + child.setup() + + self._setup_complete = True + + def add_child(self, child: "BtNode") -> "BtNode": + """Adds a child to the node. + + Args: + child: the child node to add + + Returns: + the child node + """ + self.children.append(child) + child.indent_level = self.indent_level + 1 + child.parent = self + child.bb = self.bb + return child + + def add_children(self) -> None: + """ + Adds children to the node. Loops through the _children list and creates a new instance of each child class. + """ + if self._children is None: + return + + for child_class in self._children: + # instantiate the child class and add it to this node's children + child = child_class() + self.add_child(child) + + @property + def _pfx(self) -> str: + if self.name_pfx is not None: + return f"({self.name_pfx})" + return "" + + def _pre_tick(self, depth: int = 0) -> None: + """Called before the node is ticked. + Override this method in your subclass if you need to do something before the node is ticked. + Does nothing by default. + + Args: + depth: the node's depth in the tree + """ + + def tick(self, depth: int = 0) -> NodeStatus: + """Ticks the node. + Performs the following actions: + + - calls `_pre_tick()` + - calls `_tick()` + - calls `_post_tick()` + - sets the node's status based on the return value of `_tick()` + + Args: + depth: the node's depth in the tree + + Returns: + the node's status (as a NodeStatus enum) + """ + if self.name is not None: + logger.debug(_indent(depth) + f"{self._pfx} {self.name}") + + with self: + self._pre_tick(depth=depth) + status = self._tick(depth) + self.status = status + self._post_tick(depth=depth) + + if self.name is not None: + logger.debug(_indent(depth + 1) + f"= {self.status}") + + return status + + def _post_tick(self, depth: int = 0) -> None: + """Called after the node is ticked. + Override this method in your subclass if you need to do something after the node is ticked. + Does nothing by default. + + Args: + depth + """ + pass + + def _tick(self, depth: int = 0) -> NodeStatus: + """Called by tick(). + Implement this method in your subclass. + + Args: + depth + + Returns: + the node's status (as a NodeStatus enum) + """ + raise NotImplementedError + + @property + def _node_label(self) -> str: + if self.name_pfx is not None: + return f"{self.name_pfx} {self.name}" + + return self.name + + @property + def _is_leaf_node(self) -> bool: + """Returns True if the node is a leaf node, False otherwise.""" + return self._children is None or len(self._children) == 0 + + def _namestr(self, depth=0) -> str: + """Returns a string representation of the node's name.""" + return _indent(depth) + f"{self._pfx} {self.name}" + + def to_str(self, depth=0) -> str: + """Returns a string representation of the tree rooted at this node.""" + + namestring = self._namestr(depth) + "\n" + + if self._is_leaf_node: + # this is a leaf node + return namestring + + # recurse through children and return a string representation of the tree + parts = [ + namestring, + ] + for child in self.children: + parts.append(child.to_str(depth + 1)) + return "".join(parts) + + def to_graph(self) -> nx.DiGraph: + G = nx.DiGraph() + + # add a self node + # see note *** below + G.add_node(self.name, shape=self._node_shape) + + # walk the children + for child in self.children: + # add an edge from this node to the child node + G.add_edge(self.name, child.name) + # the child node will add itself to the graph with its shape because *** + # create a graph for the child node and add it to this graph + G = nx.compose(G, child.to_graph()) + + return G + + def to_mermaid(self, depth=0, topdown=True) -> str: + """Returns a string representation of the tree rooted at this node in mermaid format.""" + + import re + + if self._is_leaf_node: + # this is a leaf node, we aren't doing anything with it + return "" + + parts = [] + if depth == 0: + # add preamble + parts.append("```mermaid") + if topdown: + parts.append("graph TD") + else: + parts.append("graph LR") + + def fixname(nstr: str) -> str: + # TODO these should be subclass attributes + nstr = re.sub(r"^>_", "→ ", nstr) + nstr = re.sub(r"^\^_", "#8645; ", nstr) + nstr = re.sub(r"^z_", "#127922; ", nstr) + nstr = re.sub(r"^a_", "#9648; ", nstr) + nstr = re.sub(r"^c_", "#11052; ", nstr) + nstr = re.sub(r"^\?_", "? ", nstr) + nstr = re.sub(r"_\d+$", "", nstr) + + return nstr + + name = fixname(self.name) + sname = f"{self.__class__.__name__}{self.name_sfx}" + if depth == 0: + parts.append(f' {sname}["{name}"]') + + for child in self.children: + cname = f"{child.__class__.__name__}{child.name_sfx}" + parts.append(f' {cname}["{fixname(child.name)}"]') + + parts.append(f" {sname} --> {cname}") + # recurse through children and return a string representation of the tree below this node + parts.append(child.to_mermaid(depth + 1)) + + if depth == 0: + # add postamble + parts.append("```") + + parts = [p for p in parts if p != ""] + + return "\n".join(parts) + + +class LeafNode(BtNode): + """LeafNode is the base class for all leaf nodes in the Behavior Tree. + Leaf nodes are nodes that do not have children. + """ + + Exc = LeafNodeError + + def __init__(self): + """ + Raises a LeafNodeError if the node has children. + """ + if self.__class__._children is not None: + raise self.Exc("Behavior Tree Leaf Nodes cannot have children") + super().__init__() + + def func(self) -> Union[bool, None]: + """ + Override this method in your subclass. + Return True for success, False for failure, and None for running. + """ + + raise NotImplementedError + + def _tick(self, depth: int = 0) -> NodeStatus: + """ + Calls the node's func() method and returns the result. + + Args: + depth + + Returns: + the node's status (as a NodeStatus enum) + """ + + result = self.func() + + if result is None: + return NodeStatus.RUNNING + elif result: + return NodeStatus.SUCCESS + return NodeStatus.FAILURE + + +class ActionNode(LeafNode): + """ActionNode is the base class for all action nodes in the Behavior Tree. + Action nodes are leaf nodes that perform an action. + An action node's func() method returns True for success, False for failure, and None for running. + """ + + name_pfx = "a" + Exc = ActionNodeError + + +class ConditionCheck(LeafNode): + """ConditionCheck is the base class for all condition check nodes in the Behavior Tree. + Condition check nodes are leaf nodes that check a condition. + A condition check node's func() method returns True for success, False for failure. + Although it is possible to return None for running, this is not recommended. + """ + + name_pfx = "c" + _node_shape = "ellipse" + Exc = ConditionCheckError + + +class CountTicks(BtNode): + """CountTicks is a decorator node that counts the number of times it is ticked.""" + + start = 0 + + def __init__(self): + super().__init__() + self.counter = self.start + + def _tick(self, depth: int = 0) -> NodeStatus: + self.counter += 1 + return NodeStatus.SUCCESS + + +STATELOG = [] + + +class SnapshotState(BtNode): + """ + SnapshotState is a decorator node that takes a snapshot of the blackboard and appends it to the STATELOG list. + """ + + name = "Snapshot_state" + + def _tick(self, depth: int = 0) -> NodeStatus: + global STATELOG + snapshot = deepcopy(self.bb) + STATELOG.append(snapshot) + return NodeStatus.SUCCESS diff --git a/vultron/bt/base/composites.py b/vultron/bt/base/composites.py new file mode 100644 index 00000000..cef03d39 --- /dev/null +++ b/vultron/bt/base/composites.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This module implements Composite Nodes for a Behavior Tree. +""" + +import random + +from vultron.bt.base.bt_node import BtNode +from vultron.bt.base.node_status import NodeStatus + + +class SequenceNode(BtNode): + """SequenceNode is a composite node that ticks its children in order. + + - If a child returns SUCCESS, the SequenceNode ticks the next child. + - If a child returns RUNNING, the SequenceNode returns RUNNING. + - If a child returns FAILURE, the SequenceNode returns FAILURE. + - If all children return SUCCESS, the SequenceNode returns SUCCESS. + """ + + name_pfx = ">" + + # _node_shape = "rarrow" + + def _tick(self, depth=0): + for child in self.children: + child_status = child.tick(depth + 1) + if child_status == NodeStatus.RUNNING: + return NodeStatus.RUNNING + elif child_status == NodeStatus.FAILURE: + return NodeStatus.FAILURE + return NodeStatus.SUCCESS + + +class FallbackNode(BtNode): + """FallbackNode is a composite node that ticks its children in order. + + - If a child returns SUCCESS, the FallbackNode returns SUCCESS. + - If a child returns RUNNING, the FallbackNode returns RUNNING. + - If a child returns FAILURE, the FallbackNode ticks the next child. + - If all children return FAILURE, the FallbackNode returns FAILURE. + """ + + name_pfx = "?" + + # _node_shape = "diamond" + + def _tick(self, depth=0): + for child in self.children: + child_status = child.tick(depth + 1) + + if child_status == NodeStatus.RUNNING: + return NodeStatus.RUNNING + elif child_status == NodeStatus.SUCCESS: + return NodeStatus.SUCCESS + return NodeStatus.FAILURE + + +SelectorNode = FallbackNode + + +class ParallelNode(BtNode): + """ + ParallelNode is a composite node that ticks its children in parallel. + + When subclassing, you must set the `m` attribute indicating the minimum number of successes. + The maximum failures is then calculated as `N - m`, where N is the number of children. + + - When a child returns SUCCESS, the ParallelNode increments a success counter. + - When a child returns FAILURE, the ParallelNode increments a failure counter. + - If the success counter reaches the minimum number of successes, the ParallelNode returns SUCCESS. + - If the failure counter reaches the maximum number of failures, the ParallelNode returns FAILURE. + - If neither of the above conditions are met, the ParallelNode returns RUNNING. + + !!! warning "Not Fully Implemented" + + In the current implementation, the ParallelNode does not actually tick its children in parallel. + Instead, it ticks them in a random order and counts the results until either SUCCESS or FAILURE is + indicated as above. This is good enough for demonstration purposes, but it should be replaced with + a proper parallel implementation. + """ + + # todo this needs to have a policy for how to handle failures/successes in the children + # e.g., a Sequence-like policy where all children must succeed, or a Selector-like policy where only one child needs to succeed + # or just actually implement the parallelism as described in the docstring + + name_pfx = "=" + m = 1 + + # _node_shape = "parallelogram" + + @property + def N(self): + return len(self.children) + + def set_min_successes(self, m): + self.m = m + + def _tick(self, depth=0): + successes = 0 + failures = 0 + + # todo: this is a kludge because I'm too lazy to do real parallelism yet + # randomize order so we don't get stuck + children = list(self.children) + random.shuffle(children) + + for child in children: + child_status = child.tick(depth + 1) + if child_status == NodeStatus.SUCCESS: + successes += 1 + elif child_status == NodeStatus.FAILURE: + failures += 1 + + if successes >= self.m: + return NodeStatus.SUCCESS + elif failures > (self.N - self.m): + return NodeStatus.FAILURE + return NodeStatus.RUNNING diff --git a/vultron/bt/base/decorators.py b/vultron/bt/base/decorators.py new file mode 100644 index 00000000..88274f90 --- /dev/null +++ b/vultron/bt/base/decorators.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This module defines a number of Behavior Tree Decorator Nodes. +""" +import logging + +from vultron.bt.base.bt_node import BtNode +from vultron.bt.base.node_status import NodeStatus + +logger = logging.getLogger(__name__) + + +class BtDecorator(BtNode): + """ + BtDecorator is the base class for all decorators in the Behavior Tree. + """ + + name_pfx = "d" + + +class Invert(BtDecorator): + """Inverts the result of the child node. + + - If the child node returns SUCCESS, the Invert decorator will return FAILURE. + - If the child node returns FAILURE, the Invert decorator will return SUCCESS. + - If the child node returns RUNNING, the Invert decorator will return RUNNING. + """ + + name_pfx = "^" + + def _tick(self, depth=0): + only_child = self.children[0] + child_status = only_child.tick(depth + 1) + + if child_status == NodeStatus.FAILURE: + return NodeStatus.SUCCESS + elif child_status == NodeStatus.SUCCESS: + return NodeStatus.FAILURE + return NodeStatus.RUNNING + + +class RunningIsFailure(BtDecorator): + """RunningIsFailure decorator returns FAILURE if the child node returns RUNNING. + Otherwise, it returns the result of the child node. + """ + + def _tick(self, depth=0): + only_child = self.children[0] + child_status = only_child.tick(depth + 1) + + if child_status == NodeStatus.SUCCESS: + return NodeStatus.SUCCESS + return NodeStatus.FAILURE + + +class RunningIsSuccess(BtDecorator): + """RunningIsSuccess decorator returns SUCCESS if the child node returns RUNNING. + Otherwise, it returns the result of the child node. + """ + + def _tick(self, depth=0): + only_child = self.children[0] + child_status = only_child.tick(depth + 1) + + if child_status == NodeStatus.FAILURE: + return NodeStatus.FAILURE + return NodeStatus.SUCCESS + + +class ForceSuccess(BtDecorator): + """ForceSuccess decorator returns SUCCESS no matter what the child node returns.""" + + name_pfx = "S" + + def _tick(self, depth=0): + only_child = self.children[0] + child_status = only_child.tick(depth + 1) + logger.debug(f"Child {only_child.name} returns {child_status}") + return NodeStatus.SUCCESS + + +class ForceFailure(BtDecorator): + """ForceFailure decorator returns FAILURE no matter what the child node returns.""" + + name_pfx = "F" + + def _tick(self, depth=0): + only_child = self.children[0] + child_status = only_child.tick(depth + 1) + logger.debug(f"Child {only_child.name} returns {child_status}") + return NodeStatus.FAILURE + + +class ForceRunning(BtDecorator): + """ForceRunning decorator returns RUNNING no matter what the child node returns.""" + + name_pfx = "R" + + def _tick(self, depth=0): + only_child = self.children[0] + child_status = only_child.tick(depth + 1) + logger.debug(f"Child {only_child.name} returns {child_status}") + return NodeStatus.RUNNING + + +class LoopDecorator(BtDecorator): + """LoopDecorator is the base class for all decorators that loop.""" + + name_pfx = "l" + reset = False + + def __init__(self): + super().__init__() + self.count = 0 + + def _pre_tick(self, depth=0): + if self.reset: + self.count = 0 + + +class RetryN(LoopDecorator): + """ + Retry up to n times until the child returns success or running. + When subclassing RetryN, set the `n` class variable to the number of retries. + """ + + n = 1 + + def _tick(self, depth=0): + only_child = self.children[0] + for i in range(self.n): + child_status = only_child.tick(depth + 1) + self.count += 1 + if child_status == NodeStatus.FAILURE: + continue + if child_status == NodeStatus.SUCCESS: + return NodeStatus.SUCCESS + if child_status == NodeStatus.RUNNING: + return NodeStatus.RUNNING + return NodeStatus.FAILURE + + +class RepeatN(LoopDecorator): + """ + Repeat up to n times until the child returns failure or running. + When subclassing RepeatN, set the `n` class variable to the number of repeats. + """ + + n = 1 + + def _tick(self, depth=0): + only_child = self.children[0] + for i in range(self.n): + child_status = only_child.tick(depth + 1) + self.count += 1 + if child_status == NodeStatus.SUCCESS: + continue + if child_status == NodeStatus.FAILURE: + return NodeStatus.FAILURE + if child_status == NodeStatus.RUNNING: + return NodeStatus.RUNNING + + return NodeStatus.SUCCESS + + +class RepeatUntilFail(LoopDecorator): + """ + Repeat until the child returns FAILURE, then return SUCCESS. + """ + + def _tick(self, depth=0): + only_child = self.children[0] + while True: + child_status = only_child.tick(depth + 1) + self.count += 1 + if child_status == NodeStatus.FAILURE: + return NodeStatus.SUCCESS diff --git a/vultron/bt/base/demo/__init__.py b/vultron/bt/base/demo/__init__.py new file mode 100644 index 00000000..b8d147d8 --- /dev/null +++ b/vultron/bt/base/demo/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University diff --git a/vultron/bt/base/demo/pacman.py b/vultron/bt/base/demo/pacman.py new file mode 100644 index 00000000..76a17ba5 --- /dev/null +++ b/vultron/bt/base/demo/pacman.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This is a demo of the bt tree library. It is a stub implementation of a bot that plays Pacman. +""" + + +import logging +import random +import sys +from dataclasses import dataclass, field + +import vultron.bt.base.composites as bt +import vultron.bt.base.fuzzer as btz +from vultron.bt.base.blackboard import Blackboard +from vultron.bt.base.bt import BehaviorTree +from vultron.bt.base.bt_node import BtNode +from vultron.bt.base.factory import ( + action_node, + condition_check, + fallback_node, + fuzzer, + invert, + sequence_node, +) +from vultron.bt.common import show_graph + +logger = logging.getLogger(__name__) + +SCORE = 0 + +PER_DOT = 10 +GHOST_INC = 2 +GHOST_NAMES = ["Blinky", "Pinky", "Inky", "Clyde"] + + +@dataclass(kw_only=True) +class PacmanBlackboard(Blackboard): + dots: int = 240 + score: int = 0 + per_ghost: int = 200 + ghosts_scared: bool = False + ghosts_remaining: list = field(default_factory=lambda: GHOST_NAMES.copy()) + ticks: int = 0 + + +### Action Nodes + + +def eat_pill(obj: BtNode) -> bool: + dots = obj.bb.dots + if dots == 0: + return False + + obj.bb.dots -= 1 + obj.bb.score += PER_DOT + return True + + +EatPill = action_node("EatPill", eat_pill) + + +def inc_ghost_score(obj: BtNode) -> bool: + """increments the score for the next ghost.""" + obj.bb.per_ghost *= GHOST_INC + logger.info(f"Ghost score is now {obj.bb.per_ghost}") + return True + + +IncrGhostScore = action_node( + "IncrGhostScore", + inc_ghost_score, +) + + +def score_ghost(obj: BtNode) -> bool: + obj.bb.score += obj.bb.per_ghost + return True + + +ScoreGhost = action_node("ScoreGhost", score_ghost) + + +def decr_ghost_count(obj: BtNode) -> bool: + """decrements the ghost count""" + ghost = obj.bb.ghosts_remaining.pop() + logger.info(f"{ghost} was caught!") + return True + + +DecrGhostCount = action_node( + "DecrGhostCount", + decr_ghost_count, +) + + +### Condition Check Nodes + + +def ghosts_remain(obj: BtNode) -> bool: + """checks if there are any ghosts remaining.""" + return len(obj.bb.ghosts_remaining) > 0 + + +GhostsRemain = condition_check( + "GhostsRemain", + ghosts_remain, +) + + +def ghosts_scared(obj: BtNode) -> bool: + """checks if a ghost is scared.""" + return obj.bb.ghosts_scared + + +GhostsScared = condition_check( + "GhostsScared", + ghosts_scared, +) + + +### Fuzzer Nodes + + +GhostClose = fuzzer( + btz.OftenSucceed, "GhostClose", "determines if a ghost is close." +) +ChaseGhost = fuzzer(btz.OftenSucceed, "ChaseGhost", "chases a ghost.") +AvoidGhost = fuzzer(btz.OftenSucceed, "AvoidGhost", "avoids a ghost.") + + +### Composite Nodes + +NoMoreGhosts = invert( + "NoMoreGhosts", "inverts the result of GhostsRemain.", GhostsRemain +) +NoGhostClose = invert( + "NoGhostClose", "inverts the result of GhostClose.", GhostClose +) +CaughtGhost = sequence_node( + "CaughtGhost", + "handles actions after catching a ghost.", + DecrGhostCount, + ScoreGhost, + IncrGhostScore, +) +GhostsNotScared = invert( + "GhostsNotScared", "inverts the result of GhostsScared.", GhostsScared +) +ChaseIfScared = sequence_node( + "ChaseIfScared", + "implements chasing a ghost if it is scared.", + GhostsScared, + ChaseGhost, + CaughtGhost, +) +ChaseOrAvoidGhost = fallback_node( + "ChaseOrAvoidGhost", + "implements chasing a ghost if it is scared, otherwise it avoids the ghost.", + ChaseIfScared, + GhostsScared, + AvoidGhost, +) + +MaybeChaseOrAvoidGhost = fallback_node( + "MaybeChaseOrAvoidGhost", + """ + implements chasing a ghost if it is scared, otherwise it avoids the ghost. + + Returns: + SUCCESS if no ghosts remain, a ghost is caught or avoided, FAILURE otherwise + """, + NoMoreGhosts, + NoGhostClose, + ChaseOrAvoidGhost, +) + + +MaybeEatPills = sequence_node( + "MaybeEatPills", + """ + implements eating pills if no ghosts are close. + """, + MaybeChaseOrAvoidGhost, + EatPill, +) + + +def do_tick(bot, ticks): + bb = bot.bb + + logger.info(f"=== Tick {ticks} ===") + + # maybe make the ghosts scared + # note this also demonstrates the world changing outside the bot + if random.random() < 0.5: + bb.ghosts_scared = True + logger.info("Ghosts are scared!") + else: + bb.ghosts_scared = False + + bot.tick() + ticks += 1 + # die on the first failure + if bot.status == bt.NodeStatus.FAILURE: + logger.info( + f"Pacman died! He was eaten by {random.choice(bb.ghosts_remaining)}!" + ) + if bb.dots <= 0: + logger.info("Pacman cleared the board!") + if ticks > 1000: + logger.info("Pacman got bored and quit!") + return bot.status + + +def main(args): + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + handler = logging.StreamHandler() + logger.addHandler(handler) + + bot = BehaviorTree(root=MaybeEatPills(), bbclass=PacmanBlackboard) + bot.setup() + + if args.print_tree: + logger.info("Printing tree and exiting") + show_graph(MaybeEatPills) + sys.exit() + + ticks = 0 + while bot.bb.dots > 0: + ticks += 1 + result = do_tick(bot, ticks) + if result == bt.NodeStatus.FAILURE: + break + + logger.info(f"Final score: {bot.bb.score}") + logger.info(f"Ticks: {ticks}") + logger.info(f"Dots Remaining: {bot.bb.dots}") + + nghosts = len(bot.bb.ghosts_remaining) + if nghosts > 0: + ghosts = ", ".join(bot.bb.ghosts_remaining) + ghosts = f"({ghosts})" + else: + ghosts = "" + logger.info(f"Ghosts Remaining: {nghosts} {ghosts}") + + +if __name__ == "__main__": + args = _parse_args() + + main(args) diff --git a/vultron/bt/base/demo/robot.py b/vultron/bt/base/demo/robot.py new file mode 100644 index 00000000..d274ce5c --- /dev/null +++ b/vultron/bt/base/demo/robot.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python + +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +""" +This module demonstrates a simple robot behavior tree. +The robot has a number of tasks it must perform in order to complete its mission. +The robot must find a ball, approach the ball, grasp the ball, approach the bin, and place +the ball in the bin. If any of these tasks fail, the robot must ask for help. + +Oh, and the first time the robot picks up the ball, we knock it out of the robot's grasp, because +we're mean like that. + +The implementation also shows how to use the included bt tree fuzzer to exercise the bt tree. +""" + +import logging +import sys +from dataclasses import dataclass + +import vultron.bt.base.fuzzer as btz +from vultron.bt.base.blackboard import Blackboard +from vultron.bt.base.bt import BehaviorTree +from vultron.bt.base.bt_node import BtNode +from vultron.bt.base.factory import ( + action_node, + condition_check, + fallback_node, + sequence_node, +) +from vultron.bt.common import show_graph + +logger = logging.getLogger(__name__) + + +@dataclass(kw_only=True) +class RobotBlackboard(Blackboard): + ball_found: bool = False + ball_close: bool = False + ball_grasped: bool = False + bin_close: bool = False + ball_placed: bool = False + asked_for_help: bool = False + ticks: int = 0 + + +def ball_placed(obj: BtNode) -> bool: + """Checks whether the ball is placed in the bin""" + return obj.bb.ball_placed + + +BallPlaced = condition_check( + "BallPlaced", + ball_placed, +) + + +def set_ball_placed(obj: BtNode) -> bool: + """Records that the ball has been placed in the bin""" + obj.bb.ball_placed = True + return True + + +SetBallPlaced = action_node( + "SetBallPlaced", + set_ball_placed, +) + +PlaceBall = sequence_node( + "PlaceBall", + "This is a stub for a task that places the ball in the bin. In our stub implementation, we just stochastically " + "return Success or Failure to simulate placing the ball.", + btz.UsuallySucceed, + SetBallPlaced, +) + + +EnsureBallPlaced = fallback_node( + "EnsureBallPlaced", + "This is a fallback node that ensures the ball is placed in the bin. If the ball is placed, the task succeeds. If " + "the ball is not placed, and the robot cannot place the ball, the task fails.", + BallPlaced, + PlaceBall, +) + + +def bin_close(obj: BtNode) -> bool: + """Checks whether the bin is close""" + return obj.bb.bin_close + + +BinClose = condition_check( + "BinClose", + bin_close, +) + + +def set_bin_close(obj: BtNode) -> bool: + """Records that the bin is close""" + obj.bb.bin_close = True + return True + + +SetBinClose = action_node( + "SetBinClose", + set_bin_close, +) + +ApproachBin = sequence_node( + "ApproachBin", + "This is a stub for a task that approaches the bin. In our stub implementation, we just stochastically return " + "Success or Failure to simulate approaching the bin.", + btz.UsuallySucceed, + SetBinClose, +) + +EnsureBinClose = fallback_node( + "EnsureBinClose", + "This is a fallback node that ensures the bin is close. If the bin is close, the task succeeds. If the bin " + "is not close, and the robot cannot approach the bin, the task fails.", + BinClose, + ApproachBin, +) + + +def ball_grasped(obj: BtNode) -> bool: + """Checks whether the ball is grasped""" + return obj.bb.ball_grasped + + +BallGrasped = condition_check( + "BallGrasped", + ball_grasped, +) + + +def set_ball_grasped(obj: BtNode) -> bool: + """Records that the ball has been grasped""" + obj.bb.ball_grasped = True + return True + + +SetBallGrasped = action_node( + "SetBallGrasped", + set_ball_grasped, +) + +GraspBall = sequence_node( + "GraspBall", + "This is a stub for a task that grasps the ball. In our stub implementation, we just stochastically return " + "SUCCESS.", + btz.OftenFail, + SetBallGrasped, +) + +EnsureBallGrasped = fallback_node( + "EnsureBallGrasped", + "This is a fallback node that ensures the ball is grasped. If the ball is grasped, the task succeeds. If the ball " + "is not already grasped, and the robot cannot grasp the ball, the task fails.", + BallGrasped, + GraspBall, +) + + +def ball_close(obj: BtNode) -> bool: + """Checks whether the ball is close""" + return obj.bb.ball_close + + +BallClose = condition_check( + "BallClose", + ball_close, +) + + +def set_ball_close(obj: BtNode) -> bool: + """Records that the ball is close""" + obj.bb.ball_close = True + return True + + +SetBallClose = action_node( + "SetBallClose", + set_ball_close, +) + +ApproachBall = sequence_node( + "ApproachBall", + "This is a stub for a task that approaches the ball. In our stub implementation, we just stochastically return " + "Success or Failure to simulate approaching the ball.", + btz.UsuallySucceed, + SetBallClose, +) + + +EnsureBallClose = fallback_node( + "EnsureBallClose", + "This is a fallback node that ensures the ball is close. If the ball is close, the task succeeds. If the ball " + "is not close, and the robot cannot approach the ball, the task fails.", + BallClose, + ApproachBall, +) + + +def set_ball_found(obj: BtNode) -> bool: + """Records that the ball has been found""" + obj.bb.ball_found = True + return True + + +SetBallFound = action_node( + "SetBallFound", + set_ball_found, +) + +FindBall = sequence_node( + "FindBall", + "This is a stub for a task that finds the ball. In our stub implementation, we just stochastically return " + "Success or Failure to simulate finding the ball.", + btz.UsuallySucceed, + SetBallFound, +) + + +def ball_found(obj: BtNode) -> bool: + """Checks whether the ball is found""" + return obj.bb.ball_found + + +BallFound = condition_check( + "BallFound", + ball_found, +) + + +EnsureBallFound = fallback_node( + "EnsureBallFound", + "This is a fallback node that ensures the ball is found. If the ball is found, the task succeeds. If the ball " + "is not found, and the robot cannot find the ball, the task fails.", + BallFound, + FindBall, +) + + +def time_to_ask_for_help(obj: BtNode) -> bool: + """Checks if it is time to ask for help""" + return obj.bb.ticks > 10 + + +TimeToAskForHelp = condition_check( + "TimeToAskForHelp", + time_to_ask_for_help, +) + + +def ask_for_help(obj: BtNode) -> bool: + """Records that the robot has asked for help""" + logger.info("I need help!") + obj.bb.asked_for_help = True + return True + + +AskForHelp = action_node( + "AskForHelp", + ask_for_help, +) + +MaybeAskForHelp = sequence_node( + "MaybeAskForHelp", + "Decide whether we need to ask for help.", + TimeToAskForHelp, + AskForHelp, +) + +MainSequence = sequence_node( + "MainSequence", + "This is the main sequence of tasks the robot must perform in order to complete its mission.", + EnsureBallFound, + EnsureBallClose, + EnsureBallGrasped, + EnsureBinClose, + EnsureBallPlaced, +) + +Robot = fallback_node( + "Robot", + "This is the robot's main fallback node. It will try to complete its mission. If it fails, it will ask for help.", + BallPlaced, + MainSequence, + MaybeAskForHelp, +) + + +def main(args): + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) + + bot = BehaviorTree(root=Robot(), bbclass=RobotBlackboard) + + bot.setup() + + if args.print_tree: + logger.info("Printing tree and exiting") + show_graph(Robot) + sys.exit() + + knockout = True + while not bot.bb.ball_placed: + # maybe knock the ball out of the robot's grasp + if knockout and bot.bb.ball_grasped: + bot.bb.ball_grasped = False + logger.info("The ball was knocked out of the robot's grasp!") + knockout = False + + bot.tick() + bot.bb.ticks += 1 + + if bot.bb.asked_for_help: + logger.info("The robot asked for help!") + break + + if bot.bb.asked_for_help: + logger.info( + f"Robot failed to complete its mission after {bot.bb.ticks} ticks." + ) + elif bot.bb.ball_placed: + logger.info(f"Robot completed its mission in {bot.bb.ticks} ticks.") + else: + logger.info(f"Not sure what happened. {bot.bb}") + + +if __name__ == "__main__": + args = _parse_args() + main(args) diff --git a/vultron/bt/base/errors.py b/vultron/bt/base/errors.py new file mode 100644 index 00000000..fb0d6bd1 --- /dev/null +++ b/vultron/bt/base/errors.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This module defines error classes for the `vultron.bt.base` module +""" +from vultron.errors import VultronError + + +class BehaviorTreeError(VultronError): + """Raised when a BehaviorTree encounters an error""" + + +class BehaviorTreeFuzzerError(BehaviorTreeError): + """Raised when a BehaviorTreeFuzzer encounters an error""" + + +class LeafNodeError(BehaviorTreeError): + """Raised when a leaf node encounters an error""" + + +class ActionNodeError(LeafNodeError): + """Raised when an action node encounters an error""" + + +class ConditionCheckError(LeafNodeError): + """Raised when a condition check encounters an error""" diff --git a/vultron/bt/base/factory.py b/vultron/bt/base/factory.py new file mode 100644 index 00000000..c10442d0 --- /dev/null +++ b/vultron/bt/base/factory.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python +""" +Provides common tools for constructing behavior trees +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from typing import Callable, Type, TypeVar + +from vultron.bt.base.bt_node import ActionNode, BtNode, ConditionCheck +from vultron.bt.base.composites import FallbackNode, ParallelNode, SequenceNode +from vultron.bt.base.decorators import Invert, RepeatUntilFail +from vultron.bt.base.fuzzer import FuzzerNode + +NodeType = TypeVar("NodeType", bound=Type[BtNode]) + + +def node_factory( + node_type: Type[NodeType], + name: str, + docstr: str, + *child_classes: Type[BtNode], +) -> Type[NodeType]: + """ + Convenience function to create a node_cls with a docstring. + + Args: + node_type: a BtNode class + name: the name of the node_cls + docstr: the docstring for the node_cls + *child_classes: the child classes for the node_cls (if any) + + Returns: + A node_cls class with the given docstring and children + """ + + node_cls = type(name, (node_type,), {}) + node_cls.__doc__ = docstr + + if len(child_classes) > 0: + node_cls._children = child_classes + + return node_cls + + +def sequence_node( + name: str, description: str, *child_classes: Type[BtNode] +) -> Type[SequenceNode]: + """ + Convenience function to create a SequenceNode with a docstring. + + Args: + name: the name of the SequenceNode class + description: the docstring for the SequenceNode + *child_classes: the child classes for the SequenceNode + + Returns: + A SequenceNode class with the given docstring and children + """ + + if len(child_classes) < 2: + raise ValueError("SequenceNode should have at least two children") + + return node_factory(SequenceNode, name, description, *child_classes) + + +def fallback_node( + name: str, description: str, *child_classes: Type[BtNode] +) -> Type[FallbackNode]: + """ + Convenience function to create a FallbackNode with a docstring. + + Args: + name: the name of the FallbackNode class + description: the docstring for the FallbackNode + *child_classes: the child classes for the FallbackNode + + Returns: + object: + A FallbackNode class with the given docstring and children + """ + if len(child_classes) < 1: + raise ValueError( + "FallbackNode should have at least one child (preferably 2)" + ) + + return node_factory(FallbackNode, name, description, *child_classes) + + +def invert( + name: str, description: str, *child_classes: Type[BtNode] +) -> Type[Invert]: + """ + Convenience function to create an Invert decorator with a docstring. + + Args: + name: the name of the Invert decorator class + description: the docstring for the Invert decorator + *child_classes: the child class for the Invert decorator + + Returns: + An Invert decorator class with the given docstring and children + """ + if len(child_classes) != 1: + raise ValueError("Invert decorator can only take one child") + + return node_factory(Invert, name, description, *child_classes) + + +def fuzzer( + cls: Type[FuzzerNode], name: str, description: str +) -> Type[FuzzerNode]: + """ + Convenience function to create a WeightedSuccess fuzzer with a docstring. + + Args: + cls: the WeightedSuccess class to serve as the base class + name: the name of the WeightedSuccess class + description: the docstring for the WeightedSuccess class + + Returns: + A WeightedSuccess class with the given docstring + + """ + return node_factory(cls, name, description) + + +def condition_check( + name: str, + func: Callable[ + [ + BtNode, + ], + bool, + ], +) -> Type[ConditionCheck]: + """ + Convenience function to create a ConditionCheck node with a docstring. + The function's docstring will be used as the ConditionCheck's docstring. + + Args: + name: the name of the ConditionCheck class + func: the function to be used as the ConditionCheck's condition. Expects a BtNode as an argument and returns a bool + + Returns: + A ConditionCheck class with the given docstring and condition function + + """ + node_cls = node_factory(ConditionCheck, name, func.__doc__) + node_cls.func = func + return node_cls + + +def action_node( + name: str, + func: Callable[ + [ + BtNode, + ], + bool, + ], +) -> Type[ActionNode]: + """ + Convenience function to create an ActionNode with a docstring. + The function's docstring will be used as the ActionNode's docstring. + + Args: + name: the name of the ActionNode class + func: the function to be used as the ActionNode's action. Expects a BtNode as an argument and returns a bool + + Returns: + An ActionNode class with the given docstring and action function + """ + node_cls = node_factory(ActionNode, name, func.__doc__) + node_cls.func = func + return node_cls + + +def repeat_until_fail( + name: str, description: str, *child_classes: Type[BtNode] +) -> Type[RepeatUntilFail]: + """ + Convenience function to create a RepeatUntilFail node with a docstring. + + Args: + name: the name of the RepeatUntilFail class + description: the docstring for the RepeatUntilFail class + *child_classes: the child class to repeat + + Returns: + + """ + if len(child_classes) != 1: + raise ValueError("RepeatUntilFail can only take one child") + + return node_factory(RepeatUntilFail, name, description, *child_classes) + + +def parallel_node( + name: str, description: str, min_success: int, *child_classes: Type[BtNode] +) -> Type[ParallelNode]: + # make sure min_success is a positive integer + if ( + not isinstance(min_success, int) + or min_success < 1 + or min_success > len(child_classes) + ): + raise ValueError( + "min_success must be a positive integer less than or equal to the number of children" + ) + + # make sure child_classes is not empty + if len(child_classes) < 2: + raise ValueError("ParallelNode should have at least two children") + + node_cls = node_factory(ParallelNode, name, description, *child_classes) + node_cls.min_success = min_success + return node_cls diff --git a/vultron/bt/base/fuzzer.py b/vultron/bt/base/fuzzer.py new file mode 100644 index 00000000..eeae3b73 --- /dev/null +++ b/vultron/bt/base/fuzzer.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This module provides fuzzer node classes for a Behavior Tree. +It is intended to be used for testing, simulation, and debugging. + +Many of the classes provided are subclasses of the WeightedSuccess class. +Their names are based in part on the [Words of Estimative Probability](https://en.wikipedia.org/wiki/Words_of_estimative_probability) +from Wikipedia and the [Probability Survey](https://hbr.org/2018/07/if-you-say-something-is-likely-how-likely-do-people-think-it-is) +([github](https://github.com/amauboussin/probability-survey)) by Mauboussin and Mauboussin. +""" +import random + +from vultron.bt.base.bt_node import BtNode +from vultron.bt.base.node_status import NodeStatus + + +class FuzzerNode(BtNode): + pass + + +class AlwaysSucceed(FuzzerNode): + """Always returns NodeStatus.SUCCESS""" + + def _tick(self, depth=0): + return NodeStatus.SUCCESS + + +class AlwaysFail(FuzzerNode): + """Always returns NodeStatus.FAILURE""" + + def _tick(self, depth=0): + return NodeStatus.FAILURE + + +class AlwaysRunning(FuzzerNode): + """Always returns NodeStatus.RUNNING""" + + def _tick(self, depth=0): + return NodeStatus.RUNNING + + +class RandomActionNodeWithRunning(FuzzerNode): + """Returns a random NodeStatus, including NodeStatus.RUNNING, with equal probability.""" + + def _tick(self, depth=0): + return random.choice(list(NodeStatus)) + + +class SuccessOrRunning(FuzzerNode): + """Returns NodeStatus.SUCCESS or NodeStatus.RUNNING with equal probability.""" + + def _tick(self, depth=0): + return random.choice((NodeStatus.SUCCESS, NodeStatus.RUNNING)) + + +class WeightedSuccess(FuzzerNode): + """Returns NodeStatus.SUCCESS with a probability of success_rate. + Otherwise, returns NodeStatus.FAILURE. + + When subclassing, set the `success_rate` class attribute to the desired + probability of success. + """ + + success_rate = 0.5 + name_pfx = "z" + + # _node_shape = "invtrapezium" + + def _tick(self, depth=0): + if random.random() < self.success_rate: + return NodeStatus.SUCCESS + return NodeStatus.FAILURE + + def _namestr(self, depth=0): + base = super()._namestr(depth) + return f"{base} p=({self.success_rate})" + + +#################### +# From here on down, the classes are just different success rates +# for the WeightedSuccess class. +# The names of the success rates are based on the following: +# https://en.wikipedia.org/wiki/Words_of_estimative_probability +# http://www.probabilitysurvey.com/ +#################### + + +class OneNinetyNineInTwoHundred(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.995.""" + + success_rate = 199.0 / 200.0 + + +class NinetyNineInOneHundred(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.99.""" + + success_rate = 99.0 / 100.0 + + +class FortyNineInFifty(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.98.""" + + success_rate = 49.0 / 50.0 + + +class TwentyNineInThirty(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.96667.""" + + success_rate = 29.0 / 30.0 + + +class NineteenInTwenty(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.95.""" + + success_rate = 19.0 / 20.0 + + +class AlmostCertainlySucceed(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.93.""" + + success_rate = 93.0 / 100.0 + + +class AlmostAlwaysSucceed(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.90.""" + + success_rate = 9.0 / 10.0 + + +class UsuallySucceed(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.75.""" + + success_rate = 3.0 / 4.0 + + +class OftenSucceed(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.70.""" + + success_rate = 7.0 / 10.0 + + +class ProbablySucceed(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.6667.""" + + success_rate = 2.0 / 3.0 + + +class UniformSucceedFail(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.50.""" + + success_rate = 1.0 / 2.0 + + +class ProbablyFail(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.3333.""" + + success_rate = 1.0 / 3.0 + + +class OftenFail(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.30.""" + + success_rate = 3.0 / 10.0 + + +class UsuallyFail(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.25.""" + + success_rate = 1.0 / 4.0 + + +class AlmostAlwaysFail(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.10.""" + + success_rate = 1.0 / 10.0 + + +class AlmostCertainlyFail(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.07.""" + + success_rate = 7.0 / 100.0 + + +class OneInTwenty(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.05.""" + + success_rate = 1.0 / 20.0 + + +class OneInThirty(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.033333.""" + + success_rate = 1.0 / 30.0 + + +class OneInFifty(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.02.""" + + success_rate = 1.0 / 50.0 + + +class OneInOneHundred(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.01.""" + + success_rate = 1.0 / 100.0 + + +class OneInTwoHundred(WeightedSuccess): + """Returns NodeStatus.SUCCESS with a probability of 0.005.""" + + success_rate = 1.0 / 200.0 + + +# aliases +LikelyFail = OftenFail +RarelySucceed = AlmostAlwaysFail + +RandomSucceedFail = UniformSucceedFail +RandomConditionNode = UniformSucceedFail +RandomActionNode = UniformSucceedFail +LikelySucceed = OftenSucceed diff --git a/vultron/bt/base/node_status.py b/vultron/bt/base/node_status.py new file mode 100644 index 00000000..04049dfc --- /dev/null +++ b/vultron/bt/base/node_status.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This module defines a Behavior Tree Node Status object. +""" +from enum import Enum + + +class NodeStatus(Enum): + """NodeStatus is the return value of a node's tick() method. + Nodes can only return one of these values. + + Nodes return SUCCESS if they have completed their task successfully. + Nodes return FAILURE if they have completed their task unsuccessfully. + Nodes return RUNNING if they are still in the process of completing their task. + """ + + FAILURE = 0 + SUCCESS = 1 + RUNNING = 2 + + def __str__(self): + return self.name diff --git a/vultron/bt/behaviors.py b/vultron/bt/behaviors.py new file mode 100644 index 00000000..2946756d --- /dev/null +++ b/vultron/bt/behaviors.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +"""file: behaviors +author: adh +created_at: 4/26/22 1:49 PM +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +import logging +from copy import deepcopy + +from vultron.bt.base import bt +from vultron.bt.base.bt_node import ActionNode +from vultron.bt.base.composites import SequenceNode +from vultron.bt.base.node_status import NodeStatus +from vultron.bt.embargo_management.behaviors import EmbargoManagementBt +from vultron.bt.messaging.inbound.behaviors import ReceiveMessagesBt +from vultron.bt.report_management.behaviors import ReportManagementBt +from vultron.bt.report_management.states import RM +from vultron.bt.states import ActorState +from vultron.bt.vul_discovery.behaviors import DiscoverVulnerabilityBt + +logger = logging.getLogger(__name__) + +STATELOG = [] + + +def reset_statelog(): + global STATELOG + STATELOG = [] + + +class Snapshot(ActionNode): + name = "Snapshot" + + def _tick(self, depth=0): + global STATELOG + + attributes = [ + "msgs_received_this_tick", + "q_rm", + "q_em", + "q_cs", + "msgs_emitted_this_tick", + "CVD_role", + ] + + s = self.bb + row = {a: getattr(s, a) for a in attributes} + + STATELOG.append(row) + return NodeStatus.SUCCESS + + +class CvdProtocolRoot(SequenceNode): + _children = ( + Snapshot, + DiscoverVulnerabilityBt, + ReceiveMessagesBt, + ReportManagementBt, + EmbargoManagementBt, + ) + + +class CvdProtocolBt(bt.BehaviorTree): + bbclass = ActorState + + def __init__(self): + super().__init__() + + self.history = [] + + root = CvdProtocolRoot() + self.add_root(root) + self.setup() + + self.bb.q_rm_history.append(self.bb.q_rm) + self.bb.q_em_history.append(self.bb.q_em) + self.bb.q_cs_history.append(self.bb.q_cs) + + def _pre_tick(self): + # reset messages sent and received + self.bb.msgs_received_this_tick = [] + self.bb.msgs_emitted_this_tick = [] + + attributes = [ + "q_rm", + "q_em", + "q_cs", + "CVD_role", + "msgs_received_this_tick", + "msgs_emitted_this_tick", + ] + + self.preconditions = {} + for a in attributes: + logger.debug(f"State: {a}: {getattr(self.bb, a)}") + self.preconditions[a] = deepcopy(getattr(self.bb, a)) + logger.debug("--------") + + def _post_tick(self): + changes = [] + for k, then in self.preconditions.items(): + now = getattr(self.bb, k) + if now != then: + changes.append(f"State change: {k}: {then} -> {now} ") + + if len(changes): + logger.debug("--------") + for change in changes: + logger.debug(change) + + @property + def closed(self): + return self.bb.q_rm == RM.CLOSED + + +def main(): + pass + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/case_state/__init__.py b/vultron/bt/case_state/__init__.py new file mode 100644 index 00000000..db95c75c --- /dev/null +++ b/vultron/bt/case_state/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This package describes the CVD Case State Machine as a Behavior Tree. +""" diff --git a/vultron/bt/case_state/conditions.py b/vultron/bt/case_state/conditions.py new file mode 100644 index 00000000..64c7d315 --- /dev/null +++ b/vultron/bt/case_state/conditions.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This module defines CVD Case State conditions as Behavior Tree nodes. +""" + + +from vultron.bt.base.bt_node import BtNode +from vultron.bt.base.factory import condition_check, invert, sequence_node +from vultron.case_states.states import ( + AttackObservation, + ExploitPublication, + FixDeployment, + FixReadiness, + PublicAwareness, + VendorAwareness, +) + + +def cs_in_state_vendor_aware(obj: BtNode) -> bool: + """True when the vendor is aware of the vulnerability""" + return ( + obj.bb.q_cs.value.vfd_state.value.vendor_awareness == VendorAwareness.V + ) + + +CSinStateVendorAware = condition_check( + "CSinStateVendorAware", + cs_in_state_vendor_aware, +) + + +def cs_in_state_fix_ready(obj: BtNode) -> bool: + """True when the vendor has a fix ready""" + return obj.bb.q_cs.value.vfd_state.value.fix_readiness == FixReadiness.F + + +CSinStateFixReady = condition_check( + "CSinStateFixReady", + cs_in_state_fix_ready, +) + + +def cs_in_state_fix_deployed(obj: BtNode) -> bool: + """True when the fix has been deployed""" + return obj.bb.q_cs.value.vfd_state.value.fix_deployment == FixDeployment.D + + +CSinStateFixDeployed = condition_check( + "CSinStateFixDeployed", + cs_in_state_fix_deployed, +) + + +def cs_in_state_public_aware(obj: BtNode) -> bool: + """True when the public is aware of the vulnerability""" + return ( + obj.bb.q_cs.value.pxa_state.value.public_awareness == PublicAwareness.P + ) + + +CSinStatePublicAware = condition_check( + "CSinStatePublicAware", + cs_in_state_public_aware, +) + + +def cs_in_state_exploit_public(obj: BtNode) -> bool: + """True when an exploit is public for the vulnerability""" + return ( + obj.bb.q_cs.value.pxa_state.value.exploit_publication + == ExploitPublication.X + ) + + +CSinStateExploitPublic = condition_check( + "CSinStateExploitPublic", + cs_in_state_exploit_public, +) + + +def cs_in_state_attacks_observed(obj: BtNode) -> bool: + """True when attacks against the vulnerability have been observed""" + return ( + obj.bb.q_cs.value.pxa_state.value.attack_observation + == AttackObservation.A + ) + + +CSinStateAttacksObserved = condition_check( + "CSinStateAttacksObserved", + cs_in_state_attacks_observed, +) + + +CSinStatePublicAwareAndExploitPublic = sequence_node( + "CSinStatePublicAwareAndExploitPublic", + """Sequence node for whether the public is aware of the vulnerability and an exploit is public""", + CSinStatePublicAware, + CSinStateExploitPublic, +) + + +CSinStateVendorAwareAndFixReady = sequence_node( + "CSinStateVendorAwareAndFixReady", + """Sequence node for whether the vendor is aware of the vulnerability and has a fix ready""", + CSinStateVendorAware, + CSinStateFixReady, +) + +CSinStateVendorAwareAndFixReadyAndFixDeployed = sequence_node( + "CSinStateVendorAwareAndFixReadyAndFixDeployed", + """Sequence node for whether the vendor is aware of the vulnerability, + has a fix ready, and the fix has been deployed""", + CSinStateVendorAware, + CSinStateFixReady, + CSinStateFixDeployed, +) + +CSinStateVendorUnaware = invert( + "CSinStateVendorUnaware", + """Condition check for whether the vendor is unaware of the vulnerability""", + CSinStateVendorAware, +) + + +CSinStateFixNotReady = invert( + "CSinStateFixNotReady", + """Condition check for whether the vendor does not have a fix ready""", + CSinStateFixReady, +) + + +CSinStateFixNotDeployed = invert( + "CSinStateFixNotDeployed", + """Condition check for whether a fix has not been deployed""", + CSinStateFixDeployed, +) + + +CSinStatePublicUnaware = invert( + "CSinStatePublicUnaware", + """Condition check for whether the public is unaware of the vulnerability""", + CSinStatePublicAware, +) + +CSinStateNoExploitPublic = invert( + "CSinStateNoExploitPublic", + """Condition check for whether no exploit is public for the vulnerability""", + CSinStateExploitPublic, +) + +CSinStateNoAttacksObserved = invert( + "CSinStateNoAttacksObserved", + """Condition check for whether no attacks against the vulnerability have been observed""", + CSinStateAttacksObserved, +) + +CSinStateNotPublicNoExploitNoAttacks = sequence_node( + "CSinStateNotPublicNoExploitNoAttacks", + """Sequence node for whether the public is unaware of the vulnerability, no exploit is public, and no attacks + have been observed""", + CSinStatePublicUnaware, + CSinStateNoExploitPublic, + CSinStateNoAttacksObserved, +) + + +CSinStatePublicAwareOrExploitPublicOrAttacksObserved = invert( + "CSinStatePublicAwareOrExploitPublicOrAttacksObserved", + """Condition check for whether the public is aware of the vulnerability, an exploit is public, or attacks have + been observed""", + CSinStateNotPublicNoExploitNoAttacks, +) + + +CSinStateNotDeployedNotPublicNoExploitNoAttacks = sequence_node( + "CSinStateNotDeployedNotPublicNoExploitNoAttacks", + """Condition check for whether a fix has not been deployed, the public is unaware of the vulnerability, + no exploit is public, and no attacks have been observed""", + CSinStateFixNotDeployed, + CSinStateNotPublicNoExploitNoAttacks, +) + + +CSinStateNotDeployedButPublicAware = sequence_node( + "CSinStateNotDeployedButPublicAware", + """Condition check for whether a fix has not been deployed but the public is aware of the vulnerability""", + CSinStateFixNotDeployed, + CSinStatePublicAware, +) + + +CSinStateVendorAwareFixReadyFixNotDeployed = sequence_node( + "CSinStateVendorAwareFixReadyFixNotDeployed", + """Condition check for whether the vendor is aware of the vulnerability and has a fix ready, but the fix has not + been deployed""", + CSinStateVendorAware, + CSinStateFixReady, + CSinStateFixNotDeployed, +) + + +# aliases +CSisEmbargoCompatible = CSinStateNotPublicNoExploitNoAttacks +CSisEmbargoIncompatible = CSinStatePublicAwareOrExploitPublicOrAttacksObserved + +# shorthand +q_cs_in_V = CSinStateVendorAware +q_cs_in_F = CSinStateFixReady +q_cs_in_D = CSinStateFixDeployed +q_cs_in_P = CSinStatePublicAware +q_cs_in_X = CSinStateExploitPublic +q_cs_in_A = CSinStateAttacksObserved + +q_cs_in_PX = CSinStatePublicAwareAndExploitPublic +q_cs_in_VF = CSinStateVendorAwareAndFixReady +q_cs_in_VFD = CSinStateVendorAwareAndFixReadyAndFixDeployed + +q_cs_in_v = CSinStateVendorUnaware +q_cs_in_f = CSinStateFixNotReady +q_cs_in_d = CSinStateFixNotDeployed +q_cs_in_p = CSinStatePublicUnaware +q_cs_in_x = CSinStateNoExploitPublic +q_cs_in_a = CSinStateNoAttacksObserved + +q_cs_in_pxa = CSinStateNotPublicNoExploitNoAttacks +q_cs_not_in_pxa = CSinStatePublicAwareOrExploitPublicOrAttacksObserved +q_cs_in_dpxa = CSinStateNotDeployedNotPublicNoExploitNoAttacks +q_cs_in_dP = CSinStateNotDeployedButPublicAware +q_cs_in_VFd = CSinStateVendorAwareFixReadyFixNotDeployed diff --git a/vultron/bt/case_state/transitions.py b/vultron/bt/case_state/transitions.py new file mode 100644 index 00000000..47634027 --- /dev/null +++ b/vultron/bt/case_state/transitions.py @@ -0,0 +1,120 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This module defines the CVD Case State Machine as a Behavior Tree. +""" +from typing import Type + +from vultron.bt.base.bt_node import ActionNode, BtNode +from vultron.bt.base.factory import action_node, sequence_node +from vultron.bt.case_state.conditions import ( + CSinStateVendorAware, + CSinStateVendorAwareAndFixReady, +) +from vultron.case_states.states import CS + + +def cs_state_change( + name: str, + target_state: str = None, +) -> Type[ActionNode]: + """ + Factory function to create a class for transitioning to a new CS state. + + Args: + name: the name of the class + docstr: the docstring for the class + target_state: the target state shorthand for the transition (V,F,D,P,X,A) + + Returns: + A class for transitioning to the given state + + """ + + def _func(obj: BtNode) -> bool: + f"""Transition to the target state {target_state}""" + # get the current state name + current_state_name = obj.bb.q_cs.name + + # note: we are operating on the node name string because the values + # are more complex to work with (e.g. "Vendor Aware" vs "V") + + # force the corresponding letter in the state name to upper case + # if the lower case is not in the state name, this will do nothing + # which means this is a no-op for the upper-cased states + new_state_name = current_state_name.replace( + target_state.lower(), target_state + ) + + # set the state to the one with the new name + try: + new_state = CS[new_state_name] + except KeyError: + # just don't change the state if the new state name is invalid + return True + + obj.bb.q_cs = new_state + obj.bb.q_cs_history.append(new_state) + + # action node functions return True for success + return True + + node_cls = action_node(name, _func) + # add the target state as a class attribute (for testing) + node_cls.target_state = target_state + return node_cls + + +q_cs_to_V = cs_state_change("q_cs_to_V", "V") + +# We will need to wrap this in a sequence node to enforce that +# the vendor is aware of the vulnerability before allowing the transition to Fix Ready +_q_cs_to_F = cs_state_change( + "_q_cs_to_F", + "F", +) + +q_cs_to_F = sequence_node( + "q_cs_to_F", + """ + Sequence node for transitioning from V to F. + Enforces that the vendor is aware of the vulnerability before allowing the transition to Fix Ready + """, + CSinStateVendorAware, + _q_cs_to_F, +) + + +# We will need to wrap this in a sequence node to enforce that +# the fix is ready before allowing the transition to Fix Deployed +_q_cs_to_D = cs_state_change( + "_q_cs_to_D", + "D", +) + +q_cs_to_D = sequence_node( + "q_cs_to_D", + """ + Sequence node for transitioning from F to D. + Enforces that the vendor is aware of the vulnerability and has a fix ready before allowing the transition to Fix Deployed. + """, + CSinStateVendorAwareAndFixReady, + _q_cs_to_D, +) + + +# # The remaining transitions are simple and do not need to be wrapped +# # in a sequence node because they do not have any conditions that need to be enforced +q_cs_to_P = cs_state_change("q_cs_to_P", "P") +q_cs_to_X = cs_state_change("q_cs_to_X", "X") +q_cs_to_A = cs_state_change("q_cs_to_A", "A") diff --git a/vultron/bt/common.py b/vultron/bt/common.py new file mode 100644 index 00000000..f7ba4580 --- /dev/null +++ b/vultron/bt/common.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This module provides common Behavior Tree nodes for the Vultron package. +""" + +import logging +from dataclasses import dataclass +from enum import Enum +from typing import List, Type + +import networkx as nx + +from vultron.bt.base.bt_node import ActionNode, BtNode, ConditionCheck +from vultron.bt.base.composites import FallbackNode +from vultron.bt.base.factory import ( + action_node, + condition_check, + fallback_node, + sequence_node, +) + +logger = logging.getLogger(__name__) + + +@dataclass +class EnumStateTransition: + """ + Represents a transition between two states in an enum-based state machine + """ + + start_states: List[Enum] + end_state: Enum + + +def state_in( + key: str, + state: Enum, +) -> Type[ConditionCheck]: + """ + Factory method that returns a ConditionCheck class that checks if the blackboard[key] == state + + Args: + key: the blackboard key to check + state: the state to check for + + Returns: + A ConditionCheck class that checks if the blackboard[key] == state + """ + + def func(obj: BtNode) -> bool: + f"""True if the node's blackboard[{key}] == {state}""" + return getattr(obj.bb, key) == state + + node_cls = condition_check(f"{key}_in_{state}", func) + + # add some attributes to the node_cls so we can test it later + node_cls.key = key + node_cls.state = state + return node_cls + + +def to_end_state_factory(key: str, state: Enum) -> Type[ActionNode]: + """Factory method that returns an ActionNode class that updates key to state.""" + + def _func(obj: BtNode) -> bool: + # remember where we were so we can log the change + before = getattr(obj.bb, key) + + setattr(obj.bb, key, state) + + # record this bb in history + histkey = f"{key}_history" + history = list(getattr(obj.bb, histkey)) + if len(history) == 0 or (history[-1] != state): + history.append(state) + setattr(obj.bb, histkey, history) + + logger.debug(f"Transition {before} -> {state}") + return True + + node_cls = action_node( + f"{key}_to_{state}", + _func, + ) + + return node_cls + + +def state_change( + key: str, transition: EnumStateTransition +) -> Type[FallbackNode]: + """Factory method that returns a FallbackNode object that returns SUCCESS when the blackboard[key] + starts in one of start_states and changes to end_state, and FAILURE otherwise + """ + start_states = transition.start_states + end_state = transition.end_state + + # check that the end_state is in the start_states + start_state_checks = fallback_node( + f"allowed_start_states_for_{key}_{end_state}", + f"""SUCCESS when the current {key} is in one of {(s.name for s in start_states)}. FAILURE otherwise.""", + *[state_in(key, state) for state in start_states], + ) + + # transition to the end_state + sc_seq = sequence_node( + f"transition_to_{key}_{end_state}_if_allowed", + f"""Check for a valid start state in {(s.name for s in start_states)} and transition to {end_state}""", + start_state_checks, + to_end_state_factory(key, end_state), + ) + + # ensure we wind up in the end_state + _state_change = fallback_node( + f"transition_{key}_to_{end_state}", + f"""Transition from (one of) {(s.name for s in start_states)} to {end_state}""", + state_in(key, end_state), + sc_seq, + ) + + return _state_change + + +def show_graph(node_cls): + """Show the graph for the given node_cls""" + nx.write_network_text(node_cls().to_graph()) diff --git a/vultron/bt/embargo_management/__init__.py b/vultron/bt/embargo_management/__init__.py new file mode 100644 index 00000000..bfca8417 --- /dev/null +++ b/vultron/bt/embargo_management/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +#!/usr/bin/env python +""" +This package describes the CVD Embargo Management State Machine as a Behavior Tree. +""" diff --git a/vultron/bt/embargo_management/behaviors.py b/vultron/bt/embargo_management/behaviors.py new file mode 100644 index 00000000..c163e80c --- /dev/null +++ b/vultron/bt/embargo_management/behaviors.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python +""" +Provides behavior tree nodes for the Embargo Management process. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +from vultron.bt.base.factory import fallback_node, sequence_node +from vultron.bt.case_state.conditions import ( + CSinStateFixDeployed, + CSinStateFixReady, + CSinStateNotPublicNoExploitNoAttacks, + CSinStatePublicAwareOrExploitPublicOrAttacksObserved, +) +from vultron.bt.embargo_management.conditions import ( + EMinStateActive, + EMinStateActiveOrRevise, + EMinStateExited, + EMinStateNone, + EMinStateNoneOrExited, + EMinStateNoneOrPropose, + EMinStateProposeOrRevise, + EMinStateProposed, + EMinStateRevise, +) +from vultron.bt.embargo_management.fuzzer import ( + AvoidEmbargoCounterProposal, + CurrentEmbargoAcceptable, + EmbargoTimerExpired, + EvaluateEmbargoProposal, + ExitEmbargoForOtherReason, + ExitEmbargoWhenDeployed, + ExitEmbargoWhenFixReady, + OnEmbargoAccept, + OnEmbargoExit, + OnEmbargoReject, + ReasonToProposeEmbargoWhenDeployed, + SelectEmbargoOfferTerms, + StopProposingEmbargo, + WillingToCounterEmbargoProposal, +) +from vultron.bt.embargo_management.transitions import ( + q_em_R_to_A, + q_em_to_A, + q_em_to_N, + q_em_to_P, + q_em_to_R, + q_em_to_X, +) +from vultron.bt.messaging.outbound.behaviors import ( + EmitEA, + EmitEJ, + EmitEP, + EmitER, + EmitET, + EmitEV, +) +from vultron.bt.report_management.conditions import ( + RMinStateStartOrClosed, +) +from vultron.bt.roles.conditions import RoleIsNotDeployer + +_MaybeExitWhenDeployed = sequence_node( + "MaybeExitWhenDeployed", + "Sequence node for transitioning from R to X when the case state is in Fix Deployed.", + CSinStateFixDeployed, + ExitEmbargoWhenDeployed, +) + +_MaybeExitWhenFixReady = sequence_node( + "MaybeExitWhenFixReady", + "Sequence node for transitioning from R to X when the case state is in Fix Ready.", + CSinStateFixReady, + RoleIsNotDeployer, + ExitEmbargoWhenFixReady, +) + +_OtherReasonToExitEmbargo = fallback_node( + "OtherReasonToExitEmbargo", + "Fallback node for transitioning from R to X. Allows for various other reasons to exit an embargo.", + _MaybeExitWhenDeployed, + _MaybeExitWhenFixReady, + ExitEmbargoForOtherReason, +) + +_AvoidNewEmbargoesInCsDeployedUnlessReason = fallback_node( + "AvoidNewEmbargoesInCsDeployedUnlessReason", + "Fallback node for avoiding new embargoes when the case state is in Fix Deployed unless there is a reason to do so.", + CSinStateFixDeployed, + ReasonToProposeEmbargoWhenDeployed, +) + +_AvoidCounterProposal = sequence_node( + "AvoidCounterProposal", + "Sequence node for avoiding counter-proposing an embargo when the current embargo is acceptable.", + EMinStateProposeOrRevise, + AvoidEmbargoCounterProposal, +) + +_ProposeNewEmbargo = sequence_node( + "ProposeNewEmbargo", + "Sequence node for proposing a new embargo. Steps: 1. Check if the EM state is in None or Proposed. 2. Transition " + "to Proposed. 3. Emit an EP message indicating a new embargo is being proposed.", + EMinStateNoneOrPropose, + q_em_to_P, + EmitEP, +) + +_ProposeEmbargoRevision = sequence_node( + "ProposeEmbargoRevision", + "Sequence node for proposing a revision to the current embargo. Steps: 1. Check if the EM state is in Active or " + "Revise. 2. Transition to Revise. 3. Emit an EV message indicating the current embargo is being revised.", + EMinStateActiveOrRevise, + q_em_to_R, + EmitEV, +) + +_ConsiderProposingEmbargo = fallback_node( + "ConsiderProposingEmbargo", + "Fallback node for considering proposing an embargo. Steps: 1. Generally avoid counter-proposing an embargo in " + "favor of accepting the existing proposal and proposing a revision. 2. Propose a new embargo. 3. Propose a " + "revision to the current embargo.", + _AvoidCounterProposal, + _ProposeNewEmbargo, + _ProposeEmbargoRevision, +) + +_ProposeEmbargoBt = sequence_node( + "ProposeEmbargoBt", + """Propose an embargo. + Steps: + 1. Check if the EM state is in None or Proposed. + 2. Check if the case state is in Fix Not Deployed. + 3. Check if there is a reason to propose an embargo. + 4. Choose the terms of the potential embargo offer. + 5. Decide on whether to propose an embargo. + """, + EMinStateNoneOrPropose, + CSinStateNotPublicNoExploitNoAttacks, + _AvoidNewEmbargoesInCsDeployedUnlessReason, + SelectEmbargoOfferTerms, + _ConsiderProposingEmbargo, +) + +_SufficientCauseToAbandonProposedEmbargo = fallback_node( + "SufficientCauseToAbandonProposedEmbargo", + """Determine if there is sufficient cause to abandon the proposed embargo. + Reasons to abandon: + 1. The public is already aware of the vulnerability + 2. An exploit is already public + 3. Attacks have already been observed + 4. Another reason not covered by the above + """, + CSinStatePublicAwareOrExploitPublicOrAttacksObserved, + _OtherReasonToExitEmbargo, +) + + +_ConsiderAbandoningProposedEmbargo = sequence_node( + "ConsiderAbandoningProposedEmbargo", + """Consider abandoning the proposed embargo. + Steps: + 1. Check if the EM state is in Proposed. + 2. Check if there is sufficient cause to abandon the proposed embargo. + 3. Transition to None. + 4. Emit an ER message indicating the proposed embargo was rejected. + """, + EMinStateProposed, + _SufficientCauseToAbandonProposedEmbargo, + q_em_to_N, + EmitER, +) + +_SufficientCauseToTerminateActiveEmbargo = fallback_node( + "SufficientCauseToTerminateActiveEmbargo", + """Determine if there is sufficient cause to terminate the active embargo. + Reasons to terminate: + 1. The public is already aware of the vulnerability + 2. An exploit is already public + 3. Attacks have already been observed + 4. The embargo timer has expired + 5. Another reason not covered by the above + """, + CSinStatePublicAwareOrExploitPublicOrAttacksObserved, + EmbargoTimerExpired, + _OtherReasonToExitEmbargo, +) + +_ConsiderTerminatingActiveEmbargo = sequence_node( + "ConsiderTerminatingActiveEmbargo", + """Consider terminating the active embargo. + Steps: + 1. Check if the EM state is in Active or Revise. + 2. Check if there is sufficient cause to terminate the active embargo. + 3. Transition to Exited. + 4. Emit an ET message. + """, + EMinStateActiveOrRevise, + _SufficientCauseToTerminateActiveEmbargo, + OnEmbargoExit, + q_em_to_X, + EmitET, +) + +TerminateEmbargoBt = fallback_node( + "TerminateEmbargoBt", + """Terminate the embargo if the case state is in a state where the embargo should be terminated.""", + EMinStateNoneOrExited, + _ConsiderAbandoningProposedEmbargo, + _ConsiderTerminatingActiveEmbargo, +) + +_EnsureSufficientEffortToAchieveEmbargo = fallback_node( + "EnsureSufficientEffortToAchieveEmbargo", + """Continue to propose embargoes until sufficient effort has been made.""", + StopProposingEmbargo, + _ProposeEmbargoBt, +) + +_EmNone = sequence_node( + "EmNone", + """Check if the EM state is None. + If so, continue to propose embargoes until sufficient effort has been made. + """, + EMinStateNone, + _EnsureSufficientEffortToAchieveEmbargo, +) + + +# noinspection PyPep8Naming +_AvoidNewEmbargoWhenNotInCs_pxa = sequence_node( + "AvoidNewEmbargoWhenNotInCs_pxa", + """Avoid proposing new embargoes when + the public is aware of the vulnerability or + an exploit is already public or + attacks have been observed. + """, + CSinStatePublicAwareOrExploitPublicOrAttacksObserved, + EMinStateNone, +) + +_EvaluateAndAcceptProposedEmbargo = sequence_node( + "EvaluateAndAcceptProposedEmbargo", + """If the proposed embargo is acceptable, it will be accepted and + the EM state will transition to the Active state. + An EA message will be emitted. + """, + EvaluateEmbargoProposal, + OnEmbargoAccept, + q_em_to_A, + EmitEA, +) + +_CounterEmbargoProposal = sequence_node( + "CounterEmbargoProposal", + """If there is willingness to counter a proposed embargo, + an embargo counter-proposal will be made. + """, + WillingToCounterEmbargoProposal, + _ProposeEmbargoBt, +) + +_RejectProposedEmbargo = sequence_node( + "RejectProposedEmbargo", + """Rejects a proposed embargo. + The EM state returns to None. + An ER message is emitted. + """, + OnEmbargoReject, + q_em_to_N, + EmitER, +) + +_ChooseEmProposedResponse = fallback_node( + "ChooseEmProposedResponse", + """Chooses a response to a proposed embargo. + Options are: + - Terminate the embargo + - Accept the proposed embargo and transition to the Active state + - Counter the proposed embargo and stay in the Proposed state + - Reject the proposed embargo, returning to the None state + """, + TerminateEmbargoBt, + _EvaluateAndAcceptProposedEmbargo, + _CounterEmbargoProposal, + _RejectProposedEmbargo, +) + + +_EmProposed = sequence_node( + "EmProposed", + """Embargo management bt tree for the Proposed state. + Checks the EM state and chooses a response. + """, + EMinStateProposed, + _ChooseEmProposedResponse, +) + +_ChooseEmActiveResponse = fallback_node( + "ChooseEmActiveResponse", + """Chooses a response to an active embargo. + Options are: + - Terminate the embargo + - Keep the current embargo + - Propose a new embargo + """, + TerminateEmbargoBt, + CurrentEmbargoAcceptable, + _ProposeEmbargoBt, +) + + +_EmActive = sequence_node( + "EmActive", + """Embargo management bt tree for the Active state. + Checks the EM state and chooses a response. + """, + EMinStateActive, + _ChooseEmActiveResponse, +) + + +_RejectRevision = sequence_node( + "RejectRevision", + """Rejects a proposed embargo revision. + The EM state returns to Active and the current embargo is retained. + An EJ message is emitted. + """, + OnEmbargoReject, + q_em_R_to_A, + EmitEJ, +) + +_ChooseEmReviseResponse = fallback_node( + "ChooseEmReviseResponse", + """Chooses a response to a proposed embargo revision. + Options are: + - Terminate the embargo + - Evaluate and accept the proposed embargo + - Counter the proposed embargo + - Reject the proposed embargo and return to the Active state with the current embargo + """, + TerminateEmbargoBt, + _EvaluateAndAcceptProposedEmbargo, + _CounterEmbargoProposal, + _RejectRevision, +) + + +_EmRevise = sequence_node( + "EmRevise", + """Embargo management bt tree for the Revise state. + Checks the EM state and chooses the appropriate response. + """, + EMinStateRevise, + _ChooseEmReviseResponse, +) + +EmbargoManagementBt = fallback_node( + "EmbargoManagementBt", + """This is the top-level node for the embargo management bt tree. + It is responsible for choosing the appropriate embargo management bt tree for the current state. + """, + RMinStateStartOrClosed, + EMinStateExited, + _AvoidNewEmbargoWhenNotInCs_pxa, + _EmNone, + _EmProposed, + _EmActive, + _EmRevise, +) diff --git a/vultron/bt/embargo_management/conditions.py b/vultron/bt/embargo_management/conditions.py new file mode 100644 index 00000000..137fc0d4 --- /dev/null +++ b/vultron/bt/embargo_management/conditions.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +"""file: em_conditions +author: adh +created_at: 4/26/22 10:13 AM +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from typing import Type + +from vultron.bt.base.bt_node import ConditionCheck +from vultron.bt.base.factory import fallback_node +from vultron.bt.common import show_graph, state_in +from vultron.bt.embargo_management.states import EM + + +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +def em_state_in(state: EM) -> Type[ConditionCheck]: + if state not in EM: + raise ValueError(f"{state} is not a valid Embargo Management state") + + return state_in("q_em", state) + + +EMinStateNone = em_state_in(EM.NO_EMBARGO) +EMinStateProposed = em_state_in(EM.PROPOSED) +EMinStateActive = em_state_in(EM.ACTIVE) +EMinStateRevise = em_state_in(EM.REVISE) +EMinStateExited = em_state_in(EM.EXITED) + + +EMinStateActiveOrRevise = fallback_node( + "EMinStateActiveOrRevise", + """Check if the embargo management state is Active or Revise.""", + EMinStateActive, + EMinStateRevise, +) + + +EMinStateNoneOrExited = fallback_node( + "EMinStateNoneOrExited", + """Check if the embargo management state is None or Exited.""", + EMinStateNone, + EMinStateExited, +) + +EMinStateProposeOrRevise = fallback_node( + "EMinStateProposeOrRevise", + """Check if the embargo management state is Proposed or Revise.""", + EMinStateProposed, + EMinStateRevise, +) + + +EMinStateNoneOrPropose = fallback_node( + "EMinStateNoneOrPropose", + """Check if the embargo management state is None or Proposed.""", + EMinStateNone, + EMinStateProposed, +) + + +EMinStateNoneOrProposeOrRevise = fallback_node( + "EMinStateNoneOrProposeOrRevise", + """Check if the embargo management state is None or Proposed or Revise.""", + EMinStateNone, + EMinStateProposed, + EMinStateRevise, +) + + +def main(): + for cls in [ + EMinStateNone, + EMinStateProposed, + EMinStateActive, + EMinStateRevise, + EMinStateExited, + EMinStateActiveOrRevise, + EMinStateNoneOrExited, + EMinStateProposeOrRevise, + EMinStateNoneOrPropose, + EMinStateNoneOrProposeOrRevise, + ]: + print(cls) + show_graph(cls) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/embargo_management/errors.py b/vultron/bt/embargo_management/errors.py new file mode 100644 index 00000000..0070a6ba --- /dev/null +++ b/vultron/bt/embargo_management/errors.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +"""file: errors +author: adh +created_at: 5/23/22 12:01 PM +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +from vultron.bt.errors import CvdProtocolError + + +class EmbargoManagementError(CvdProtocolError): + """Base class for all EmbargoManagement errors""" + + +class EmbargoManagementConditionError(EmbargoManagementError): + """Raised when a condition error occurs""" diff --git a/vultron/bt/embargo_management/fuzzer.py b/vultron/bt/embargo_management/fuzzer.py new file mode 100644 index 00000000..41ef1386 --- /dev/null +++ b/vultron/bt/embargo_management/fuzzer.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python +"""file: fuzzer +author: adh +created_at: 4/26/22 1:20 PM + +Each of the following classes is a fuzzable bt tree node. +The fuzzing is done by the bt.fuzzer module. +These classes are the most likely places where one of the following is likely to apply: +1. a human might need to intervene to make a decision +2. a human might need to intervene to provide input to an automated decision process +3. an automated decision process might need to be implemented based on some site-specific logic + +But for now they are just stubs that return a random result so we can exercise the tree. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +import sys + +import vultron.bt.base.fuzzer as btz +from vultron.bt.base.factory import fuzzer, invert + +# implement as necessary, ask a human +ExitEmbargoWhenDeployed = fuzzer( + btz.ProbablyFail, + "ExitEmbargoWhenDeployed", + """Decide whether to exit the embargo when the fix has already been deployed. + This is a special case because the fix has already been deployed, so the embargo is usually no longer necessary. + But usually, deployment of the fix itself is not a sufficient reason to exit an active embargo. + For example, the fix may have been deployed in error, or the fix may have been deployed to a subset of the population. + """, +) + +# implement as necessary, ask a human +ExitEmbargoWhenFixReady = fuzzer( + btz.UsuallyFail, + "ExitEmbargoWhenFixReady", + """Decide whether to exit the embargo when the fix is ready. + When the fix is ready, the embargo is usually no longer necessary, + however there are some cases where it is. + For example, if the vendor is able to deploy the fix directly + to the affected population, then the embargo may still be useful. + """, +) + +# ask a human +ExitEmbargoForOtherReason = fuzzer( + btz.OneInTwoHundred, + "ExitEmbargoForOtherReason", + """Decide whether to exit the embargo for some reason *other* than: + - the fix is ready or deployed + - the embargo timer has expired + - the public is aware of the vulnerability + - an exploit has been published + - attacks have been observed + There aren't that many extraneous reasons to exit an embargo, so this is a rare case. + """, +) + +EmbargoTimerExpired = fuzzer( + btz.OneInOneHundred, + "EmbargoTimerExpired", + """Decide whether the embargo timer has expired. + We are simulating that the embargo is usually not expired. + But in reality, this would just be a check of the actual embargo timer. + """, +) + +# implement as necessary +OnEmbargoExit = fuzzer( + btz.AlwaysSucceed, + "OnEmbargoExit", + """Do whatever is necessary when the embargo is exited. + This is a stub for now. + It is a placeholder for site-specific logic for what to do when the embargo is exited. + In an actual implementation, this would probably be a call to a function that does whatever is necessary. + """, +) + +# Negotiating +# ask a human +StopProposingEmbargo = fuzzer( + btz.UsuallyFail, + "StopProposingEmbargo", + """Decide whether to stop proposing an embargo. + This would be a choice for the humans involved in the case to make. + We are modeling it here as if the humans are usually willing to keep trying to negotiate an embargo. + """, +) + + +# implement as necessary, ask a human +SelectEmbargoOfferTerms = fuzzer( + btz.AlwaysSucceed, + "SelectEmbargoOfferTerms", + """Select the terms of the embargo offer. + This is a stub for now. + In an actual implementation, this would probably be a call out + to either a human or a function that does whatever is necessary. + """, +) + + +# implement as necessary, ask a human +WantToProposeEmbargo = fuzzer( + btz.RandomSucceedFail, + "WantToProposeEmbargo", + """Decide whether to propose an embargo. + This is a stub for now. + In an actual implementation, this would probably be a call out + to either a human or a function that does whatever is necessary. + We'd suggest that the default bt is to propose an embargo. + But the fuzzer will exercise both cases. + """, +) + + +# implement as necessary, ask a human +WillingToCounterEmbargoProposal = fuzzer( + btz.UsuallyFail, + "WillingToCounterEmbargoProposal", + """Decide whether to counter an embargo proposal. + In an actual implementation, this would probably be a call out + to either a human or a function that does whatever is necessary. + However, we generally suggest that the default bt is not + to counter an embargo proposal. Instead, it is better to accept + the proposal on the table and then propose a revision to adjust + the terms. + """, +) + + +AvoidEmbargoCounterProposal = invert( + "AvoidEmbargoCounterProposal", + """This is a convenience class that inverts the result of WillingToCounterEmbargoProposal.""", + WillingToCounterEmbargoProposal, +) + + +# ask a human +ReasonToProposeEmbargoWhenDeployed = fuzzer( + btz.AlmostCertainlyFail, + "ReasonToProposeEmbargoWhenDeployed", + """Decide whether there is a reason to propose an embargo when the fix has already been deployed. + In most cases, there is no reason to propose an embargo when the fix has already been deployed. + Therefore we are modeling this as a rare case. + In an actual implementation, this would probably need to be a call out to a human. + """, +) + + +# Evaluating +# success = accept +# implement as necessary, ask a human +EvaluateEmbargoProposal = fuzzer( + btz.UsuallySucceed, + "EvaluateEmbargoProposal", + """Decide whether to accept an embargo proposal. + In an actual implementation, this would probably be a call out to a human. + Or it is conceivable that this could be a call out to a function that automatically evaluates the proposal. + We are modeling this as a case where the humans usually accept the proposal. + """, +) + + +# implement as necessary +OnEmbargoAccept = fuzzer( + btz.AlwaysSucceed, + "OnEmbargoAccept", + """This is a stub for now. + It serves as a placeholder for site-specific logic that would be triggered when an embargo is accepted. + In an actual implementation, this would probably be a call out to a function + that does whatever is necessary when an embargo is accepted. + E.g., it might trigger some automated process to notify internal stakeholders. + """, +) + + +# implement as necessary +OnEmbargoReject = fuzzer( + btz.AlwaysSucceed, + "OnEmbargoReject", + """This is a stub for now. + It serves as a placeholder for site-specific logic that would be triggered when an embargo is rejected. + In an actual implementation, this would probably be a call out to a function + that does whatever is necessary when an embargo is rejected. + E.g., it might trigger some automated process to notify internal stakeholders. + """, +) + + +# implement as necessary, ask a human +CurrentEmbargoAcceptable = fuzzer( + btz.AlmostAlwaysSucceed, + "CurrentEmbargoAcceptable", + """Decide whether the current embargo is acceptable. + In an actual implementation, this would probably be a call out to a human. + We are modeling this as a case where the humans usually choose to keep the current embargo. + """, +) + + +def main(): + import inspect + + # get all the classes in this module + classes = inspect.getmembers(sys.modules[__name__], inspect.isclass) + + print(sys.modules[__name__].__doc__) + print() + for name, cls in classes: + print(f"class {name}") + for k in cls.__bases__: + print(f" likelihood: {k.__name__}") + print(f" {cls.__doc__}") + print() + + pass + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/embargo_management/states.py b/vultron/bt/embargo_management/states.py new file mode 100644 index 00000000..1ec210f3 --- /dev/null +++ b/vultron/bt/embargo_management/states.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +"""file: em_states +author: adh +created_at: 4/7/22 11:22 AM +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +from enum import Enum + + +class EM(Enum): + """Embargo Management States + + NO_EMBARGO: No embargo is in effect + PROPOSED: Embargo is proposed but not yet active + ACTIVE: Embargo is active + REVISE: Embargo is active and a revision is proposed + EXITED: Embargo had been active but has been exited + """ + + EMBARGO_MANAGEMENT_NONE = "NONE" + EMBARGO_MANAGEMENT_PROPOSED = "PROPOSED" + EMBARGO_MANAGEMENT_ACTIVE = "ACTIVE" + EMBARGO_MANAGEMENT_REVISE = "REVISE" + EMBARGO_MANAGEMENT_EXITED = "EXITED" + + # convenience aliases + NO_EMBARGO = EMBARGO_MANAGEMENT_NONE + PROPOSED = EMBARGO_MANAGEMENT_PROPOSED + ACTIVE = EMBARGO_MANAGEMENT_ACTIVE + REVISE = EMBARGO_MANAGEMENT_REVISE + EXITED = EMBARGO_MANAGEMENT_EXITED + + N = EMBARGO_MANAGEMENT_NONE + P = EMBARGO_MANAGEMENT_PROPOSED + A = EMBARGO_MANAGEMENT_ACTIVE + R = EMBARGO_MANAGEMENT_REVISE + X = EMBARGO_MANAGEMENT_EXITED + + +def main(): + print("EM states:") + print("----------") + for x in EM: + print(x.name, x.value) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/embargo_management/transitions.py b/vultron/bt/embargo_management/transitions.py new file mode 100644 index 00000000..42417cde --- /dev/null +++ b/vultron/bt/embargo_management/transitions.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +"""file: em_transitions +author: adh +created_at: 4/7/22 11:28 AM +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +from dataclasses import dataclass +from typing import List + +from vultron.bt.common import EnumStateTransition, show_graph, state_change +from vultron.bt.embargo_management.states import EM + + +@dataclass +class EmTransition(EnumStateTransition): + """Represents a transition between two states in the q_em state machine""" + + start_states: List[EM] + end_state: EM + + +_to_P = EmTransition( + start_states=[EM.NO_EMBARGO, EM.PROPOSED], end_state=EM.PROPOSED +) +_to_N = EmTransition( + start_states=[EM.PROPOSED, EM.NO_EMBARGO], end_state=EM.NO_EMBARGO +) +_to_A = EmTransition( + start_states=[EM.PROPOSED, EM.REVISE], end_state=EM.ACTIVE +) +_to_R = EmTransition(start_states=[EM.ACTIVE, EM.REVISE], end_state=EM.REVISE) +_R_to_A = EmTransition(start_states=[EM.REVISE], end_state=EM.ACTIVE) +_to_X = EmTransition(start_states=[EM.ACTIVE, EM.REVISE], end_state=EM.EXITED) + +# Create the state change functions +q_em_to_P = state_change(key="q_em", transition=_to_P) +q_em_to_N = state_change(key="q_em", transition=_to_N) +q_em_to_A = state_change(key="q_em", transition=_to_A) +q_em_to_R = state_change(key="q_em", transition=_to_R) +q_em_to_X = state_change(key="q_em", transition=_to_X) + +q_em_R_to_A = state_change(key="q_em", transition=_R_to_A) + + +def main(): + for x in [ + q_em_to_P, + q_em_to_N, + q_em_to_A, + q_em_to_R, + q_em_to_X, + q_em_R_to_A, + ]: + print(x) + show_graph(x) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/errors.py b/vultron/bt/errors.py new file mode 100644 index 00000000..694013c4 --- /dev/null +++ b/vultron/bt/errors.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +"""file: errors +author: adh +created_at: 5/23/22 11:59 AM +""" + + +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +class CvdProtocolError(Exception): + pass + + +def main(): + pass + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/fuzzer.py b/vultron/bt/fuzzer.py new file mode 100644 index 00000000..ca73ec89 --- /dev/null +++ b/vultron/bt/fuzzer.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +"""file: cvd_proto_fuzzer +author: adh +created_at: 4/26/22 10:59 AM +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +import random + +from vultron.cvd_states.states import all_states + +from vultron.bt.embargo_management.states import EM +from vultron.bt.report_management.states import RM_UNCLOSED + + +def random_state(): + state = { + "q_rm": random.choice(RM_UNCLOSED), + "q_em": random.choice(list(EM)), + "q_cs": random.choice(all_states), + } + return state + + +def main(): + pass + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/messaging/__init__.py b/vultron/bt/messaging/__init__.py new file mode 100644 index 00000000..b8d147d8 --- /dev/null +++ b/vultron/bt/messaging/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University diff --git a/vultron/bt/messaging/behaviors.py b/vultron/bt/messaging/behaviors.py new file mode 100644 index 00000000..de20b859 --- /dev/null +++ b/vultron/bt/messaging/behaviors.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +""" +Provides Vultron messaging behaviors. +""" + +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +import logging + +logger = logging.getLogger(__name__) + + +def incoming_message(state, msg): + logger.debug("") + logger.debug(f"INCOMING MESSAGE: {msg}") + state.incoming_messages.append(msg) diff --git a/vultron/bt/messaging/conditions.py b/vultron/bt/messaging/conditions.py new file mode 100644 index 00000000..e2f975d7 --- /dev/null +++ b/vultron/bt/messaging/conditions.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python +""" +Provides messaging conditions for use in Vultron BTs. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +import logging +from typing import Type + +from vultron.bt.base.bt_node import BtNode, ConditionCheck +from vultron.bt.base.composites import FallbackNode +from vultron.bt.base.decorators import Invert +from vultron.bt.base.factory import condition_check, invert +from vultron.bt.common import show_graph +from vultron.bt.messaging.states import MessageTypes + +logger = logging.getLogger(__name__) + + +def msg_queue_not_empty(obj: BtNode) -> bool: + """True if the message queue is not empty""" + return bool(obj.bb.incoming_messages) + + +MsgQueueNotEmpty = condition_check("MsgQueueNotEmpty", msg_queue_not_empty) + +MsgQueueEmpty = invert( + "MsgQueueEmpty", "True if the message queue is empty", MsgQueueNotEmpty +) + + +def check_msg_type(msg_t: MessageTypes) -> Type[ConditionCheck]: + """Given a message type, return a condition check class for that message type""" + + if msg_t not in MessageTypes: + raise ValueError(f"Invalid message type: {msg_t}") + + def func(obj: BtNode) -> bool: + """True if the current message type is {msg_t}""" "" + return obj.bb.current_message.msg_type == msg_t + + node_cls = condition_check(f"IsMsgType_{msg_t}", func) + + return node_cls + + +IsMsgTypeRS = check_msg_type(MessageTypes.RS) +IsMsgTypeRI = check_msg_type(MessageTypes.RI) +IsMsgTypeRV = check_msg_type(MessageTypes.RV) +IsMsgTypeRD = check_msg_type(MessageTypes.RD) +IsMsgTypeRA = check_msg_type(MessageTypes.RA) +IsMsgTypeRC = check_msg_type(MessageTypes.RC) +IsMsgTypeRK = check_msg_type(MessageTypes.RK) +IsMsgTypeRE = check_msg_type(MessageTypes.RE) + +IsMsgTypeEP = check_msg_type(MessageTypes.EP) +IsMsgTypeER = check_msg_type(MessageTypes.ER) +IsMsgTypeEA = check_msg_type(MessageTypes.EA) +IsMsgTypeEV = check_msg_type(MessageTypes.EV) +IsMsgTypeEJ = check_msg_type(MessageTypes.EJ) +IsMsgTypeEC = check_msg_type(MessageTypes.EC) +IsMsgTypeET = check_msg_type(MessageTypes.ET) +IsMsgTypeEK = check_msg_type(MessageTypes.EK) +IsMsgTypeEE = check_msg_type(MessageTypes.EE) + +IsMsgTypeCV = check_msg_type(MessageTypes.CV) +IsMsgTypeCF = check_msg_type(MessageTypes.CF) +IsMsgTypeCD = check_msg_type(MessageTypes.CD) +IsMsgTypeCP = check_msg_type(MessageTypes.CP) +IsMsgTypeCX = check_msg_type(MessageTypes.CX) +IsMsgTypeCA = check_msg_type(MessageTypes.CA) +IsMsgTypeCK = check_msg_type(MessageTypes.CK) +IsMsgTypeCE = check_msg_type(MessageTypes.CE) + +IsMsgTypeGI = check_msg_type(MessageTypes.GI) +IsMsgTypeGK = check_msg_type(MessageTypes.GK) +IsMsgTypeGE = check_msg_type(MessageTypes.GE) + + +class IsRMMessage(FallbackNode): + _children = ( + IsMsgTypeRK, + IsMsgTypeRS, + IsMsgTypeRI, + IsMsgTypeRV, + IsMsgTypeRD, + IsMsgTypeRA, + IsMsgTypeRC, + IsMsgTypeRE, + ) + + +class IsEMMessage(FallbackNode): + _children = ( + IsMsgTypeEK, + IsMsgTypeEP, + IsMsgTypeER, + IsMsgTypeEA, + IsMsgTypeEV, + IsMsgTypeEJ, + IsMsgTypeEC, + IsMsgTypeET, + IsMsgTypeEE, + ) + + +class IsCSMessage(FallbackNode): + _children = ( + IsMsgTypeCK, + IsMsgTypeCV, + IsMsgTypeCF, + IsMsgTypeCD, + IsMsgTypeCP, + IsMsgTypeCX, + IsMsgTypeCA, + IsMsgTypeCE, + ) + + +class IsGMMessage(FallbackNode): + _children = (IsMsgTypeGK, IsMsgTypeGI, IsMsgTypeGE) + + +class NotRMMessage(Invert): + _children = (IsRMMessage,) + + +class NotEMMessage(Invert): + _children = (IsEMMessage,) + + +class NotCSMessage(Invert): + _children = (IsCSMessage,) + + +class NotGMMessage(Invert): + _children = (IsGMMessage,) + + +def main(): + for cls in [IsRMMessage, IsEMMessage, IsCSMessage, IsGMMessage]: + show_graph(cls) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/messaging/errors.py b/vultron/bt/messaging/errors.py new file mode 100644 index 00000000..615ca02b --- /dev/null +++ b/vultron/bt/messaging/errors.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +"""file: errors +author: adh +created_at: 4/26/22 2:07 PM +""" + + +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +class CvdProtocolError(Exception): + pass + + +class CvdProtocolMessagingError(CvdProtocolError): + pass + + +def main(): + pass + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/messaging/fuzzer.py b/vultron/bt/messaging/fuzzer.py new file mode 100644 index 00000000..8f753b0b --- /dev/null +++ b/vultron/bt/messaging/fuzzer.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +"""file: fuzzer +author: adh +created_at: 4/26/22 11:53 AM +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +import random + +from vultron.bt.messaging.states import MessageTypes + + +def random_message(): + """Return a random message type.""" + return random.choice(list(MessageTypes)) + + +def main(): + pass + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/messaging/inbound/__init__.py b/vultron/bt/messaging/inbound/__init__.py new file mode 100644 index 00000000..b8d147d8 --- /dev/null +++ b/vultron/bt/messaging/inbound/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University diff --git a/vultron/bt/messaging/inbound/_behaviors/__init__.py b/vultron/bt/messaging/inbound/_behaviors/__init__.py new file mode 100644 index 00000000..3a21dcd7 --- /dev/null +++ b/vultron/bt/messaging/inbound/_behaviors/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +#!/usr/bin/env python +""" +This package contains the behaviors that are used by the inbound message handler. +""" diff --git a/vultron/bt/messaging/inbound/_behaviors/common.py b/vultron/bt/messaging/inbound/_behaviors/common.py new file mode 100644 index 00000000..20842694 --- /dev/null +++ b/vultron/bt/messaging/inbound/_behaviors/common.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +""" +This module contains common behaviors that are used by the inbound message handler. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +import logging + +from vultron.bt.base.bt_node import BtNode +from vultron.bt.base.factory import action_node + +logger = logging.getLogger(__name__) + + +def pop_message(obj: BtNode) -> bool: + """Pop the next message off the blackboard's incoming message queue.""" + + # make sure there's not already a message to be handled + if obj.bb.current_message is not None: + return False + + if not obj.bb.incoming_messages: + return False + + # take one down + # pass it around + obj.bb.current_message = obj.bb.incoming_messages.popleft() + logger.debug(f"** <-- Recv {obj.bb.current_message.msg_type}") + return True + + +PopMessage = action_node("PopMessage", pop_message) + + +def push_message(obj: BtNode) -> bool: + """Push the current message back onto the blackboard's incoming message queue.""" + # if there's no message, we're done + if obj.bb.current_message is not None: + # there is a message, so see if we can + # put it back on the queue to be handled next + try: + obj.bb.incoming_messages.appendleft(obj.bb.current_message) + obj.bb.current_message = None + except IndexError as e: + logger.warning(f"Caught error: {e}") + return False + + return True + + +PushMessage = action_node( + "PushMessage", + push_message, +) + + +def log_message(obj: BtNode) -> bool: + """Log the current message.""" + + if obj.bb.current_message is not None: + msg_type = obj.bb.current_message.msg_type + obj.bb.msgs_received_this_tick.append(msg_type) + + return True + + +LogMsg = action_node( + "LogMsg", + log_message, +) + + +def unset_current_message(obj: BtNode) -> bool: + """Unset the current message in the blackboard.""" + + obj.bb.current_message = None + return True + + +UnsetCurrentMsg = action_node( + "UnsetCurrentMsg", + unset_current_message, +) diff --git a/vultron/bt/messaging/inbound/_behaviors/cs_messages.py b/vultron/bt/messaging/inbound/_behaviors/cs_messages.py new file mode 100644 index 00000000..87e3e869 --- /dev/null +++ b/vultron/bt/messaging/inbound/_behaviors/cs_messages.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python +""" +This module contains the behaviors that are used by the inbound message handler to process CS messages. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +from vultron.bt.base.factory import fallback_node, sequence_node +from vultron.bt.case_state.conditions import ( + CSinStateAttacksObserved, + CSinStateExploitPublic, + CSinStatePublicAware, + CSinStatePublicAwareAndExploitPublic, +) +from vultron.bt.case_state.transitions import q_cs_to_A, q_cs_to_P, q_cs_to_X +from vultron.bt.common import show_graph +from vultron.bt.embargo_management.behaviors import TerminateEmbargoBt +from vultron.bt.messaging.conditions import ( + IsCSMessage, + IsMsgTypeCA, + IsMsgTypeCD, + IsMsgTypeCE, + IsMsgTypeCF, + IsMsgTypeCK, + IsMsgTypeCP, + IsMsgTypeCV, + IsMsgTypeCX, +) +from vultron.bt.messaging.inbound._behaviors.fuzzer import ( + FollowUpOnErrorMessage, +) +from vultron.bt.messaging.outbound.behaviors import EmitCE, EmitCK, EmitCP + + +# CS messages + + +_HandleCe = sequence_node( + "_HandleCe", """Handle CE messages.""", IsMsgTypeCE, FollowUpOnErrorMessage +) + + +_EnsureCsInP = fallback_node( + "_EnsureCsInP", + """Ensure that the case state is in the public aware state.""", + CSinStatePublicAware, + q_cs_to_P, +) + + +_HandleCp = sequence_node( + "_HandleCp", + """Handle CP messages. + If the case state is not in the public aware state, transition to it. + """, + IsMsgTypeCP, + _EnsureCsInP, +) + + +_EnsureCsInX = fallback_node( + "_EnsureCsInX", + """Ensure that the case state is in the exploit public state.""", + CSinStateExploitPublic, + q_cs_to_X, +) + + +_CsToXThenP = sequence_node( + "_CsToXThenP", + """Transition to the exploit public state, then to the public aware state. + Emit a CP message to indicate that the case state has changed. + """, + _EnsureCsInX, + _EnsureCsInP, + EmitCP, +) + + +_EnsureCsInPX = fallback_node( + "_EnsureCsInPX", + """Ensure that the case state is in the PUBLIC_AWARE and EXPLOIT_PUBLIC states.""", + CSinStatePublicAwareAndExploitPublic, + _CsToXThenP, +) + + +_HandleCx = sequence_node( + "_HandleCx", + """Handle CX messages. + If the case state is not in the PUBLIC_AWARE and EXPLOIT_PUBLIC states, transition to them. + """, + IsMsgTypeCX, + _EnsureCsInPX, +) + + +_EnsureCsInA = fallback_node( + "_EnsureCsInA", + """Ensure that the case state is in the ATTACKS_OBSERVED state.""", + CSinStateAttacksObserved, + q_cs_to_A, +) + + +_HandleCa = sequence_node( + "_HandleCa", + """Handle CA messages. + If the case state is not in the ATTACKS_OBSERVED state, transition to it. + """, + IsMsgTypeCA, + _EnsureCsInA, +) + + +_CpCxCa = fallback_node( + "_CpCxCa", + """Handle CP, CX, and CA messages.""", + _HandleCp, + _HandleCx, + _HandleCa, +) + + +_HandleCpCxCa = sequence_node( + "_HandleCpCxCa", + """Handle CP, CX, and CA messages, and terminate any embargo that may be in effect.""", + _CpCxCa, + TerminateEmbargoBt, +) + + +# The status of some other vendor doesn't really affect us, so we don't do anything fancy here. +_HandleCv = IsMsgTypeCV +_HandleCf = IsMsgTypeCF +_HandleCd = IsMsgTypeCD + + +_HandleAckableCsMessages = fallback_node( + "_HandleAckableCsMessages", + """ + Handle CP, CX, CA, CV, CF, and CD messages. + """, + _HandleCpCxCa, + _HandleCv, + _HandleCf, + _HandleCd, + _HandleCe, +) + + +_HandleAndAckNormalCsMessages = sequence_node( + "_HandleAndAckNormalCsMessages", + """Handle CP, CX, CA, CV, CF, and CD messages, and emit a CK message to acknowledge receipt.""", + _HandleAckableCsMessages, + EmitCK, +) + + +_HandleCsMessageTypes = fallback_node( + "_HandleCsMessageTypes", + """Handle CP, CX, CA, CV, CF, CD, and CE messages. + Emit a CE message if there is an error. + """, + IsMsgTypeCK, + _HandleAndAckNormalCsMessages, + EmitCE, +) + + +ProcessCSMessagesBt = sequence_node( + "ProcessCSMessagesBt", + """Behavior tree for processing CS messages.""", + IsCSMessage, + _HandleCsMessageTypes, +) + + +def main(): + show_graph(ProcessCSMessagesBt) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/messaging/inbound/_behaviors/em_messages.py b/vultron/bt/messaging/inbound/_behaviors/em_messages.py new file mode 100644 index 00000000..a45fe0a6 --- /dev/null +++ b/vultron/bt/messaging/inbound/_behaviors/em_messages.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python +""" +Provides behaviors for handling embargo messages. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +from vultron.bt.base.factory import fallback_node, sequence_node +from vultron.bt.case_state.conditions import ( + CSinStateNotPublicNoExploitNoAttacks, + CSinStatePublicAwareOrExploitPublicOrAttacksObserved, +) +from vultron.bt.common import show_graph +from vultron.bt.embargo_management.behaviors import TerminateEmbargoBt +from vultron.bt.embargo_management.conditions import ( + EMinStateActive, + EMinStateActiveOrRevise, + EMinStateExited, + EMinStateNone, + EMinStateProposed, + EMinStateRevise, +) +from vultron.bt.embargo_management.transitions import ( + q_em_R_to_A, + q_em_to_A, + q_em_to_N, + q_em_to_P, + q_em_to_R, + q_em_to_X, +) +from vultron.bt.messaging.conditions import ( + IsEMMessage, + IsMsgTypeEA, + IsMsgTypeEC, + IsMsgTypeEE, + IsMsgTypeEJ, + IsMsgTypeEK, + IsMsgTypeEP, + IsMsgTypeER, + IsMsgTypeET, + IsMsgTypeEV, +) +from vultron.bt.messaging.inbound._behaviors.fuzzer import ( + FollowUpOnErrorMessage, +) +from vultron.bt.messaging.outbound.behaviors import EmitEE, EmitEK + + +_HandleEe = sequence_node( + "_HandleEe", + """Handle embargo error (EE) messages.""", + IsMsgTypeEE, + FollowUpOnErrorMessage, +) + + +_RecognizeEmbargoExit = sequence_node( + "_RecognizeEmbargoExit", + """Recognize an embargo exit.""", + EMinStateActiveOrRevise, + q_em_to_X, +) + + +_EnsureEmbargoExited = fallback_node( + "_EnsureEmbargoExited", + """Ensure that the embargo is exited.""", + EMinStateExited, + _RecognizeEmbargoExit, +) + + +_HandleEt = sequence_node( + "_HandleEt", + """Handle embargo termination (ET) messages.""", + IsMsgTypeET, + _EnsureEmbargoExited, +) + + +_RecognizeRejection = sequence_node( + "_RecognizeRejection", + """Recognize a rejected embargo proposal, retuning to the EM.None state.""", + EMinStateProposed, + q_em_to_N, +) + + +_EnsureRejectionRecognized = fallback_node( + "_EnsureRejectionRecognized", + """Ensure that the rejection is recognized and that the embargo is in the EM.None state.""", + EMinStateNone, + _RecognizeRejection, +) + + +_HandleEr = sequence_node( + "_HandleEr", + """Handle embargo rejection (ER) messages.""", + IsMsgTypeER, + _EnsureRejectionRecognized, +) + + +_RecognizeProposal = sequence_node( + "_RecognizeProposal", + """Recognize an embargo proposal, transitioning to the EM.Proposed state.""", + EMinStateNone, + q_em_to_P, +) + + +_EnsureProposalRecognized = fallback_node( + "_EnsureProposalRecognized", + """Ensure that the proposal is recognized and that the embargo is in the EM.Proposed state.""", + EMinStateProposed, + _RecognizeProposal, +) + + +_HandleEp = sequence_node( + "_HandleEp", + """Handle embargo proposal (EP) messages.""", + IsMsgTypeEP, + _EnsureProposalRecognized, +) + + +_RecognizeActivation = sequence_node( + "_RecognizeActivation", + """Recognize an embargo activation, transitioning to the EM.Active state.""", + EMinStateProposed, + q_em_to_A, +) + + +_EnsureEmbargoActivated = fallback_node( + "_EnsureEmbargoActivated", + """Ensure that the embargo is activated and that the embargo is in the EM.Active state.""", + EMinStateActive, + _RecognizeActivation, +) + + +_HandleEa = sequence_node( + "_HandleEa", + """Handle embargo activation (EA) messages.""", + IsMsgTypeEA, + _EnsureEmbargoActivated, +) + + +_RecognizeRevision = sequence_node( + "_RecognizeRevision", + """Recognize an embargo revision, transitioning to the EM.Revise state.""", + EMinStateActive, + q_em_to_R, +) + + +_EnsureEmbargoRevisionRecognized = fallback_node( + "_EnsureEmbargoRevisionRecognized", + """Ensure that the embargo is in the EM.Revise state.""", + EMinStateRevise, + _RecognizeRevision, +) + + +_HandleEv = sequence_node( + "_HandleEv", + """Handle embargo revision (EV) messages.""", + IsMsgTypeEV, + _EnsureEmbargoRevisionRecognized, +) + + +_HandleEj = sequence_node( + "_HandleEj", + """Handle embargo revision rejection (EJ) messages.""", + IsMsgTypeEJ, + q_em_R_to_A, +) + + +_HandleEc = sequence_node( + "_HandleEc", + """Handle embargo revision acceptance (EC) messages.""", + IsMsgTypeEC, + q_em_to_A, +) + + +_SelectEjOrEcResponse = fallback_node( + "_SelectEjOrEcResponse", + """Select the appropriate response to an EJ or EC message.""", + _HandleEj, + _HandleEc, +) + + +_HandleEjOrEcMsg = sequence_node( + "_HandleEjOrEcMsg", + """Handle an EJ or EC message.""", + EMinStateRevise, + _SelectEjOrEcResponse, +) + + +_EnsureEmActive = fallback_node( + "_EnsureEmActive", + """Ensure that the embargo is in the EM.Active state.""", + EMinStateActive, + _HandleEjOrEcMsg, +) + + +_CheckEjOrEcMsg = fallback_node( + "_CheckEjOrEcMsg", + """Check if the message is an EJ or EC message.""", + IsMsgTypeEJ, + IsMsgTypeEC, +) + + +_HandleRevisionResponse = sequence_node( + "_HandleRevisionResponse", + """Handle a revision response. Always returns to the EM.Active state.""", + _CheckEjOrEcMsg, + _EnsureEmActive, +) + + +_HandleMessagesInpxa = fallback_node( + "_HandleMessagesInpxa", + """Handle embargo messages when the case state is compatible with an embargo.""", + _HandleEp, + _HandleEa, + _HandleEv, + _HandleRevisionResponse, +) + + +_HandleCSpxa = sequence_node( + "_HandleCSpxa", + """Check if the case state is compatible with an embargo then handle embargo messages.""", + CSinStateNotPublicNoExploitNoAttacks, + _HandleMessagesInpxa, +) + + +_AvoidNonViableEmbargo = sequence_node( + "_AvoidNonViableEmbargo", + """Avoid non-viable embargo states. + If the case state is not compatible with an embargo, then the embargo is terminated. + """, + CSinStatePublicAwareOrExploitPublicOrAttacksObserved, + TerminateEmbargoBt, +) + + +_HandleAckable = fallback_node( + "_HandleAckable", + """Handle ackable messages.""", + _HandleEe, + _HandleEt, + _HandleEr, + _HandleCSpxa, + _AvoidNonViableEmbargo, +) + + +_HandleAndAckEmMsg = sequence_node( + "_HandleAndAckEmMsg", + """Handle an EM message and acknowledge it.""", + _HandleAckable, + EmitEK, +) + + +_HandleEmMessage = fallback_node( + "_HandleEmMessage", + """Handle an EM message. Emit an error (EE) message if there is an error.""", + IsMsgTypeEK, + _HandleAndAckEmMsg, + EmitEE, +) + + +ProcessEMMessagesBt = sequence_node( + "ProcessEMMessagesBt", + """The bt tree for processing incoming EM messages.""", + IsEMMessage, + _HandleEmMessage, +) + + +def main(): + show_graph(ProcessEMMessagesBt) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/messaging/inbound/_behaviors/fuzzer.py b/vultron/bt/messaging/inbound/_behaviors/fuzzer.py new file mode 100644 index 00000000..1be6c357 --- /dev/null +++ b/vultron/bt/messaging/inbound/_behaviors/fuzzer.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +"""Provides fuzzer behaviors for inbound messaging +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +from vultron.bt.base import fuzzer as btz +from vultron.bt.base.factory import fallback_node +from vultron.bt.messaging.outbound.behaviors import EmitGI + + +FollowUpOnErrorMessage = fallback_node( + "FollowUpOnErrorMessage", + """This is a stub for following up on an error message. In our stub implementation, we just stochastically (0.5 + probability) emit a GI message to simulate sending a follow-up inquiry message. + """, + btz.UniformSucceedFail, + EmitGI, +) diff --git a/vultron/bt/messaging/inbound/_behaviors/general_messages.py b/vultron/bt/messaging/inbound/_behaviors/general_messages.py new file mode 100644 index 00000000..f7720396 --- /dev/null +++ b/vultron/bt/messaging/inbound/_behaviors/general_messages.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +""" +Provides behaviors to handle general messages. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from vultron.bt.base.factory import fallback_node, sequence_node +from vultron.bt.common import show_graph +from vultron.bt.messaging.conditions import ( + IsGMMessage, + IsMsgTypeGE, + IsMsgTypeGI, + IsMsgTypeGK, +) +from vultron.bt.messaging.inbound._behaviors.fuzzer import ( + FollowUpOnErrorMessage, +) +from vultron.bt.messaging.outbound.behaviors import EmitGK + + +# GENERAL messages + + +_HandleGeMessage = sequence_node( + "_HandleGeMessage", + """Handle general error (GE) messages.""", + IsMsgTypeGE, + FollowUpOnErrorMessage, +) + + +_HandleGmMessageTypes = fallback_node( + "_HandleGmMessageTypes", + """Handle GI messages.""", + IsMsgTypeGI, + _HandleGeMessage, +) + + +_HandleAckableGmMessages = sequence_node( + "_HandleAckableGmMessages", + """Handle ackable GI messages.""", + _HandleGmMessageTypes, + EmitGK, +) + + +_HandleGmMessage = fallback_node( + "_HandleGmMessage", + """Handle GM messages.""", + IsMsgTypeGK, + _HandleAckableGmMessages, +) + + +ProcessMessagesOtherBt = sequence_node( + "ProcessMessagesOtherBt", + """Process GI messages""", + IsGMMessage, + _HandleGmMessage, +) + + +def main(): + show_graph(ProcessMessagesOtherBt) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/messaging/inbound/_behaviors/rm_messages.py b/vultron/bt/messaging/inbound/_behaviors/rm_messages.py new file mode 100644 index 00000000..5d9af7b8 --- /dev/null +++ b/vultron/bt/messaging/inbound/_behaviors/rm_messages.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python +""" +Provides behaviors to handle inbound RM messages. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from vultron.bt.base.factory import fallback_node, sequence_node +from vultron.bt.case_state.conditions import CSinStateVendorAware +from vultron.bt.case_state.transitions import q_cs_to_V +from vultron.bt.common import show_graph +from vultron.bt.messaging.conditions import ( + IsMsgTypeRE, + IsMsgTypeRK, + IsMsgTypeRS, + IsRMMessage, +) +from vultron.bt.messaging.inbound._behaviors.fuzzer import ( + FollowUpOnErrorMessage, +) +from vultron.bt.messaging.outbound.behaviors import EmitCV, EmitRE, EmitRK +from vultron.bt.report_management.conditions import RMnotInStateStart +from vultron.bt.report_management.transitions import q_rm_to_R +from vultron.bt.roles.conditions import RoleIsNotVendor + + +_HandleRe = sequence_node( + "_HandleRe", + """This is a stub for handling an RE message. + Steps: + 1. Check that the message is an RE message. + 2. Follow up on what the RE message means. + """, + IsMsgTypeRE, + FollowUpOnErrorMessage, +) + + +_LeaveRmStart = fallback_node( + "_LeaveRmStart", + """Leave the RM start state by transitioning to the RECEIVED state.""", + RMnotInStateStart, + q_rm_to_R, +) + + +_SetVendorAware = sequence_node( + "_SetVendorAware", + """Set the case state to vendor aware. + Steps: + 1. Check whether we're already in the VENDOR_AWARE state. + 2. If not, transition to the VENDOR_AWARE state. + 3. Emit a CV message to indicate the transition. + """, + CSinStateVendorAware, + q_cs_to_V, + EmitCV, +) + + +_RecognizeVendorNotifiedIfNecessary = fallback_node( + "_RecognizeVendorNotifiedIfNecessary", + """If we're a vendor, recognize that we've been notified. + Short-circuits if we're not a vendor. + """, + RoleIsNotVendor, + _SetVendorAware, +) + + +_HandleRs = sequence_node( + "_HandleRs", + """Handle an RS message. The RS message type indicates an incoming report. + Steps: + 1. Check the message type. + 2. If we're in the start state, leave the start state. + 3. If we're a vendor, recognize that we've been notified. + """, + IsMsgTypeRS, + _LeaveRmStart, + _RecognizeVendorNotifiedIfNecessary, +) + + +_ChooseRmMsgReaction = fallback_node( + "_ChooseRmMsgReaction", + """Choose the appropriate reaction to an RM message. + In most cases the only reaction is to acknowledge the message, which is handled by this node's parent. + Steps: + 1. If it's an RS message, handle it. + 2. If it's an RE message, handle it. + 3. If it's anything else, we just need to confirm that we're not in the start state. + """, + _HandleRs, + _HandleRe, + RMnotInStateStart, +) + + +_HandleAckableRmMessage = sequence_node( + "_HandleAckableRmMessage", + """Handle an RM message that expects an ACK. + Steps: + 1. Handle the message. + 2. Emit an RK message in acknowledgement. + If all succeed, the node will succeed. + If any fail, the node will fail. + """, + _ChooseRmMsgReaction, + EmitRK, +) + + +_HandleRmMessage = fallback_node( + "_HandleRmMessage", + """Handle an RM message. + Steps: + 1. If it's an RK message, no need to do anything. + 2. If it's a message that expects an ACK, handle it. + 3. If it wasn't one of the above, emit an RE message indicating an error. + If any these succeed, the node will succeed. + If all fail, the node will fail. + """, + IsMsgTypeRK, + _HandleAckableRmMessage, + EmitRE, +) + + +ProcessRMMessagesBt = sequence_node( + "ProcessRMMessagesBt", + """Behavior tree for processing RM messages.""", + IsRMMessage, + _HandleRmMessage, +) + + +def main(): + show_graph(ProcessRMMessagesBt) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/messaging/inbound/behaviors.py b/vultron/bt/messaging/inbound/behaviors.py new file mode 100644 index 00000000..44349b05 --- /dev/null +++ b/vultron/bt/messaging/inbound/behaviors.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +""" +Provides behavior for inbound messaging. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +import logging + +from vultron.bt.base.factory import ( + fallback_node, + repeat_until_fail, + sequence_node, +) +from vultron.bt.common import show_graph +from vultron.bt.messaging.conditions import MsgQueueNotEmpty +from vultron.bt.messaging.inbound._behaviors.common import ( + LogMsg, + PopMessage, + PushMessage, + UnsetCurrentMsg, +) +from vultron.bt.messaging.inbound._behaviors.cs_messages import ( + ProcessCSMessagesBt, +) +from vultron.bt.messaging.inbound._behaviors.em_messages import ( + ProcessEMMessagesBt, +) +from vultron.bt.messaging.inbound._behaviors.general_messages import ( + ProcessMessagesOtherBt, +) +from vultron.bt.messaging.inbound._behaviors.rm_messages import ( + ProcessRMMessagesBt, +) +from vultron.bt.report_management.conditions import RMnotInStateClosed + +logger = logging.getLogger(__name__) + + +_HandleMessage = fallback_node( + "_HandleMessage", + """ + Handle the current message. + Message handling is broken down into separate behaviors for each category of message type. + E.g., RM messages, EM messages, CS messages, etc. + """, + ProcessRMMessagesBt, + ProcessEMMessagesBt, + ProcessCSMessagesBt, + ProcessMessagesOtherBt, +) + + +_ProcessNextMessage = sequence_node( + "_ProcessNextMessage", + """Process the next message in the queue. + Steps: + 1. Check that the queue is not empty. + 2. Pop the next message off the queue. + 3. Log the message. + 4. Handle the message. + 5. Unset the current message. + """, + MsgQueueNotEmpty, + PopMessage, + LogMsg, + _HandleMessage, + UnsetCurrentMsg, +) + + +_ProcessMessage = fallback_node( + "_ProcessMessage", + """Process the current message. If that fails, then put the message back in the queue.""", + _ProcessNextMessage, + PushMessage, +) + + +_ReceiveNextMessage = sequence_node( + "_ReceiveNextMessage", + """Within the context of an active case, this bt tree will receive and process + the next message in the queue. + """, + RMnotInStateClosed, + MsgQueueNotEmpty, + _ProcessMessage, +) + + +ReceiveMessagesBt = repeat_until_fail( + "_ReceiveMessagesBt", + """This is the top-level bt tree for the incoming messaging subsystem.""", + _ReceiveNextMessage, +) + + +def main(): + show_graph(ReceiveMessagesBt) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/messaging/inbound/fuzzer.py b/vultron/bt/messaging/inbound/fuzzer.py new file mode 100644 index 00000000..d436b30a --- /dev/null +++ b/vultron/bt/messaging/inbound/fuzzer.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +""" +Provides fuzzer classes for inbound message handling +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +import random +from typing import Union + +from vultron.bt.base.blackboard import Blackboard +from vultron.bt.messaging.states import MessageTypes +from vultron.bt.report_management.states import RM +from vultron.sim.messages import Message + + +def _message_gen(msg_type: MessageTypes) -> Message: + msg = Message(sender="Vultrabot", body="body", msg_type=msg_type) + return msg + + +def random_external_event_message() -> Message: + p_public = 0.6 + p_attacks_not_public = 0.6 + # thus p_attacks = 0.4 * 0.6 = 0.24 + # and p_exploit = 1 - (0.6 + 0.24) = 0.16 + + if random.random() < p_public: + return _message_gen(MessageTypes.CP) + if random.random() < p_attacks_not_public: + return _message_gen(MessageTypes.CA) + return _message_gen(MessageTypes.CX) + + +def generate_inbound_message(state: Blackboard) -> Union[Message, None]: + # if no report yet, receive a report + if state.q_rm == RM.START: + if random.random() < 0.4: + return _message_gen(MessageTypes.RS) + + # otherwise, with 1 in 5 chance, maybe something happened out in the world + if random.random() < 0.10: + return random_external_event_message() + + # if you got here, just return None + return None diff --git a/vultron/bt/messaging/outbound/__init__.py b/vultron/bt/messaging/outbound/__init__.py new file mode 100644 index 00000000..b8d147d8 --- /dev/null +++ b/vultron/bt/messaging/outbound/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University diff --git a/vultron/bt/messaging/outbound/behaviors.py b/vultron/bt/messaging/outbound/behaviors.py new file mode 100644 index 00000000..97c6a9c8 --- /dev/null +++ b/vultron/bt/messaging/outbound/behaviors.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +""" +Provides outbound messaging behaviors for Vultron. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +import logging +from typing import Callable + +from vultron.bt.base.bt_node import ActionNode +from vultron.bt.base.factory import action_node +from vultron.bt.common import show_graph +from vultron.bt.messaging.behaviors import incoming_message +from vultron.bt.messaging.states import MessageTypes, MessageTypes as MT +from vultron.sim.messages import Message + +logger = logging.getLogger(__name__) + +# keep track of all emitters +_emitters = set() + + +def _emitter_func( + msg_type: MessageTypes, body: str = "msg_body" +) -> Callable[[ActionNode], bool]: + def func(obj: ActionNode) -> bool: + f"""Emit a message of type {msg_type}.""" + + msg = Message(sender=obj.bb.name, msg_type=msg_type, body=body) + emit = obj.bb.emit_func + if emit is not None: + emit(msg) + else: + logger.debug("Emitter not set") + + # append history + obj.bb.msg_history.append(msg) + obj.bb.msgs_emitted_this_tick.append(msg.msg_type) + incoming_message(obj.bb, msg) + + return True + + return func + + +def emitter(msg_type, body="msg_body"): + node_cls = action_node(f"Emit_{msg_type}", _emitter_func(msg_type, body)) + node_cls.name_pfx = "!" + node_cls.msg_type = msg_type + + _emitters.add(node_cls) + + return node_cls + + +EmitCV = emitter(MT.CV) +EmitCF = emitter(MT.CF) +EmitCD = emitter(MT.CD) +EmitCP = emitter(MT.CP) +EmitCX = emitter(MT.CX) +EmitCA = emitter(MT.CA) +EmitCE = emitter(MT.CE) +EmitCK = emitter(MT.CK) +EmitRS = emitter(MT.RS) +EmitRI = emitter(MT.RI) +EmitRV = emitter(MT.RV) +EmitRA = emitter(MT.RA) +EmitRD = emitter(MT.RD) +EmitRC = emitter(MT.RC) +EmitRE = emitter(MT.RE) +EmitRK = emitter(MT.RK) +EmitEP = emitter(MT.EP) +EmitER = emitter(MT.ER) +EmitEA = emitter(MT.EA) +EmitEV = emitter(MT.EV) +EmitEJ = emitter(MT.EJ) +EmitEC = emitter(MT.EC) +EmitET = emitter(MT.ET) +EmitEK = emitter(MT.EK) +EmitEE = emitter(MT.EE) +EmitGI = emitter(MT.GI) +EmitGE = emitter(MT.GE) +EmitGK = emitter(MT.GK) + + +def main(): + for emitter in _emitters: + show_graph(emitter) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/messaging/outbound/errors.py b/vultron/bt/messaging/outbound/errors.py new file mode 100644 index 00000000..0d6975ba --- /dev/null +++ b/vultron/bt/messaging/outbound/errors.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +"""file: errors +author: adh +created_at: 4/26/22 2:06 PM +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +from vultron.bt.messaging.errors import CvdProtocolMessagingError + + +class EmitMsgError(CvdProtocolMessagingError): + pass diff --git a/vultron/bt/messaging/states.py b/vultron/bt/messaging/states.py new file mode 100644 index 00000000..506dac8b --- /dev/null +++ b/vultron/bt/messaging/states.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python +"""file: message_types +author: adh +created_at: 4/7/22 12:39 PM +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +from enum import Enum + + +class MessageTypes(Enum): + """Represents the type of a message. + Message types are used to determine the type of a message and to determine the type of a message's response. + + Message types include: + + RS = Report Submission + RI = Report Invalid + RV = Report Valid + RD = Report Deferred + RA = Report Accepted + RC = Report Closed + RK = Report Management acknowledgement (general) + RE = Report Management error (general) + + EP = Embargo Proposal + ER = Embargo Rejected + EA = Embargo Accepted + EV = Embargo Revision Proposal + EJ = Embargo Revision Rejected + EC = Embargo Revision Accepted + ET = Embargo Terminated + EK = Embargo Management acknowledgement (general) + EE = Embargo Management error (general) + + CV = Vendor Aware + CF = Fix Ready + CD = Fix Deployed + CP = Public Aware + CX = Exploit Published + CA = Attacks Observed + CK = Case State acknowledgement (general) + CE = Case State error (general) + + GI = General Information Request + GK = General Information acknowledgement + GE = General Information error + """ + + VULTRON_MESSAGE_REPORT_SUBMISSION = "RS" + VULTRON_MESSAGE_REPORT_INVALID = "RI" + VULTRON_MESSAGE_REPORT_VALID = "RV" + VULTRON_MESSAGE_REPORT_DEFERRED = "RD" + VULTRON_MESSAGE_REPORT_ACCEPTED = "RA" + VULTRON_MESSAGE_REPORT_CLOSED = "RC" + VULTRON_MESSAGE_REPORT_MANAGEMENT_ACK = "RK" + VULTRON_MESSAGE_REPORT_MANAGEMENT_ERROR = "RE" + + VULTRON_MESSAGE_EMBARGO_PROPOSAL = "EP" + VULTRON_MESSAGE_EMBARGO_REJECTED = "ER" + VULTRON_MESSAGE_EMBARGO_ACCEPTED = "EA" + VULTRON_MESSAGE_EMBARGO_REVISION_PROPOSAL = "EV" + VULTRON_MESSAGE_EMBARGO_REVISION_REJECTED = "EJ" + VULTRON_MESSAGE_EMBARGO_REVISION_ACCEPTED = "EC" + VULTRON_MESSAGE_EMBARGO_TERMINATED = "ET" + VULTRON_MESSAGE_EMBARGO_MANAGEMENT_ACK = "EK" + VULTRON_MESSAGE_EMBARGO_MANAGEMENT_ERROR = "EE" + + VULTRON_MESSAGE_CASE_STATE_VENDOR_AWARE = "CV" + VULTRON_MESSAGE_CASE_STATE_FIX_READY = "CF" + VULTRON_MESSAGE_CASE_STATE_FIX_DEPLOYED = "CD" + VULTRON_MESSAGE_CASE_STATE_PUBLIC_AWARE = "CP" + VULTRON_MESSAGE_CASE_STATE_EXPLOIT_PUBLISHED = "CX" + VULTRON_MESSAGE_CASE_STATE_ATTACKS_OBSERVED = "CA" + VULTRON_MESSAGE_CASE_STATE_ACK = "CK" + VULTRON_MESSAGE_CASE_STATE_ERROR = "CE" + + VULTRON_MESSAGE_GENERAL_INFORMATION_REQUEST = "GI" + VULTRON_MESSAGE_GENERAL_INFORMATION_ACK = "GK" + VULTRON_MESSAGE_GENERAL_INFORMATION_ERROR = "GE" + + # convenience aliases + ReportSubmission = VULTRON_MESSAGE_REPORT_SUBMISSION + ReportInvalid = VULTRON_MESSAGE_REPORT_INVALID + ReportValid = VULTRON_MESSAGE_REPORT_VALID + ReportDeferred = VULTRON_MESSAGE_REPORT_DEFERRED + ReportAccepted = VULTRON_MESSAGE_REPORT_ACCEPTED + ReportClosed = VULTRON_MESSAGE_REPORT_CLOSED + ReportManagementAck = VULTRON_MESSAGE_REPORT_MANAGEMENT_ACK + ReportManagementError = VULTRON_MESSAGE_REPORT_MANAGEMENT_ERROR + + EmbargoProposal = VULTRON_MESSAGE_EMBARGO_PROPOSAL + EmbargoRejected = VULTRON_MESSAGE_EMBARGO_REJECTED + EmbargoAccepted = VULTRON_MESSAGE_EMBARGO_ACCEPTED + EmbargoRevisionProposal = VULTRON_MESSAGE_EMBARGO_REVISION_PROPOSAL + EmbargoRevisionRejected = VULTRON_MESSAGE_EMBARGO_REVISION_REJECTED + EmbargoRevisionAccepted = VULTRON_MESSAGE_EMBARGO_REVISION_ACCEPTED + EmbargoTerminated = VULTRON_MESSAGE_EMBARGO_TERMINATED + EmbargoManagementAck = VULTRON_MESSAGE_EMBARGO_MANAGEMENT_ACK + EmbargoManagementError = VULTRON_MESSAGE_EMBARGO_MANAGEMENT_ERROR + + CaseStateVendorAware = VULTRON_MESSAGE_CASE_STATE_VENDOR_AWARE + CaseStateFixReady = VULTRON_MESSAGE_CASE_STATE_FIX_READY + CaseStateFixDeployed = VULTRON_MESSAGE_CASE_STATE_FIX_DEPLOYED + CaseStatePublicAware = VULTRON_MESSAGE_CASE_STATE_PUBLIC_AWARE + CaseStateExploitPublished = VULTRON_MESSAGE_CASE_STATE_EXPLOIT_PUBLISHED + CaseStateAttacksObserved = VULTRON_MESSAGE_CASE_STATE_ATTACKS_OBSERVED + CaseStateAck = VULTRON_MESSAGE_CASE_STATE_ACK + CaseStateError = VULTRON_MESSAGE_CASE_STATE_ERROR + + GeneralInformationRequest = VULTRON_MESSAGE_GENERAL_INFORMATION_REQUEST + GeneralInformationAck = VULTRON_MESSAGE_GENERAL_INFORMATION_ACK + GeneralInformationError = VULTRON_MESSAGE_GENERAL_INFORMATION_ERROR + + # convenience aliases + RS = VULTRON_MESSAGE_REPORT_SUBMISSION + RI = VULTRON_MESSAGE_REPORT_INVALID + RV = VULTRON_MESSAGE_REPORT_VALID + RD = VULTRON_MESSAGE_REPORT_DEFERRED + RA = VULTRON_MESSAGE_REPORT_ACCEPTED + RC = VULTRON_MESSAGE_REPORT_CLOSED + RK = VULTRON_MESSAGE_REPORT_MANAGEMENT_ACK + RE = VULTRON_MESSAGE_REPORT_MANAGEMENT_ERROR + + EP = VULTRON_MESSAGE_EMBARGO_PROPOSAL + ER = VULTRON_MESSAGE_EMBARGO_REJECTED + EA = VULTRON_MESSAGE_EMBARGO_ACCEPTED + EV = VULTRON_MESSAGE_EMBARGO_REVISION_PROPOSAL + EJ = VULTRON_MESSAGE_EMBARGO_REVISION_REJECTED + EC = VULTRON_MESSAGE_EMBARGO_REVISION_ACCEPTED + ET = VULTRON_MESSAGE_EMBARGO_TERMINATED + EK = VULTRON_MESSAGE_EMBARGO_MANAGEMENT_ACK + EE = VULTRON_MESSAGE_EMBARGO_MANAGEMENT_ERROR + + CV = VULTRON_MESSAGE_CASE_STATE_VENDOR_AWARE + CF = VULTRON_MESSAGE_CASE_STATE_FIX_READY + CD = VULTRON_MESSAGE_CASE_STATE_FIX_DEPLOYED + CP = VULTRON_MESSAGE_CASE_STATE_PUBLIC_AWARE + CX = VULTRON_MESSAGE_CASE_STATE_EXPLOIT_PUBLISHED + CA = VULTRON_MESSAGE_CASE_STATE_ATTACKS_OBSERVED + CK = VULTRON_MESSAGE_CASE_STATE_ACK + CE = VULTRON_MESSAGE_CASE_STATE_ERROR + + GI = VULTRON_MESSAGE_GENERAL_INFORMATION_REQUEST + GK = VULTRON_MESSAGE_GENERAL_INFORMATION_ACK + GE = VULTRON_MESSAGE_GENERAL_INFORMATION_ERROR + + def __str__(self): + return self.name + + +# Report Management Message Types +RM_MESSAGE_TYPES = [ + MessageTypes.RS, + MessageTypes.RI, + MessageTypes.RV, + MessageTypes.RD, + MessageTypes.RA, + MessageTypes.RC, + MessageTypes.RK, + MessageTypes.RE, +] + +# Embargo Management Message Types +EM_MESSAGE_TYPES = [ + MessageTypes.EP, + MessageTypes.ER, + MessageTypes.EA, + MessageTypes.EV, + MessageTypes.EJ, + MessageTypes.EC, + MessageTypes.ET, + MessageTypes.EK, + MessageTypes.EE, +] + +# Case State Message Types +CS_MESSAGE_TYPES = [ + MessageTypes.CV, + MessageTypes.CF, + MessageTypes.CD, + MessageTypes.CP, + MessageTypes.CX, + MessageTypes.CA, + MessageTypes.CK, + MessageTypes.CE, +] + +# General Information Message Types +GM_MESSAGE_TYPES = [MessageTypes.GI, MessageTypes.GK, MessageTypes.GE] + + +def main(): + print("All Message Types:") + print("------------------") + for message_type in list(MessageTypes): + print(message_type, message_type.value) + + print() + print("Report Management Message Types:") + print("-------------------------------") + for message_type in RM_MESSAGE_TYPES: + print(message_type, message_type.value) + + print() + print("Embargo Management Message Types:") + print("---------------------------------") + for message_type in EM_MESSAGE_TYPES: + print(message_type, message_type.value) + + print() + print("Case State Message Types:") + print("-------------------------") + for message_type in CS_MESSAGE_TYPES: + print(message_type, message_type.value) + + print() + print("General Information Message Types:") + print("----------------------------------") + for message_type in GM_MESSAGE_TYPES: + print(message_type, message_type.value) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/report_management/__init__.py b/vultron/bt/report_management/__init__.py new file mode 100644 index 00000000..b203f7ed --- /dev/null +++ b/vultron/bt/report_management/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +#!/usr/bin/env python +""" +This package describes the CVD Report Management State Machine as a Behavior Tree. +""" diff --git a/vultron/bt/report_management/_behaviors/__init__.py b/vultron/bt/report_management/_behaviors/__init__.py new file mode 100644 index 00000000..0aca444d --- /dev/null +++ b/vultron/bt/report_management/_behaviors/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +#!/usr/bin/env python +""" +Provides various behavior implementations that support the Report Management process. +""" diff --git a/vultron/bt/report_management/_behaviors/acquire_exploit.py b/vultron/bt/report_management/_behaviors/acquire_exploit.py new file mode 100644 index 00000000..537a8440 --- /dev/null +++ b/vultron/bt/report_management/_behaviors/acquire_exploit.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +""" +Provides behaviors associated with exploit acquisition. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from vultron.bt.base.factory import fallback_node, sequence_node +from vultron.bt.report_management.fuzzer.acquire_exploit import ( + DevelopExploit, + EvaluateExploitPriority, + ExploitDeferred, + ExploitDesired, + ExploitPrioritySet, + FindExploit, + HaveExploit, + PurchaseExploit, +) + + +_EnsureExploitPriorityIsSet = fallback_node( + "_EnsureExploitPriorityIsSet", + """This node ensures that the exploit priority is set.""", + ExploitPrioritySet, + EvaluateExploitPriority, +) + + +_AttemptExploitAcquisition = fallback_node( + "_AttemptExploitAcquisition", + """This node attempts to acquire the exploit. + Steps: + 1. Attempt to find the exploit in the wild + 2. Attempt to develop the exploit in-house + 3. Attempt to purchase the exploit from someone else + If any of these steps succeed, the exploit is acquired. + If all of these steps fail, the exploit is not acquired. + """, + FindExploit, + DevelopExploit, + PurchaseExploit, +) + + +_AcquireExploitIfDesired = sequence_node( + "_AcquireExploitIfDesired", + """This node attempts to acquire the exploit if it is desired. + Steps: + 1. Ensure that the exploit priority is set + 2. Ensure that the exploit is desired + 3. Attempt to acquire the exploit + If all of these steps succeed, the exploit is acquired. + If any of these steps fail, the exploit is not acquired. + """, + _EnsureExploitPriorityIsSet, + ExploitDesired, + _AttemptExploitAcquisition, +) + + +AcquireExploit = fallback_node( + "AcquireExploit", + """This node attempts to acquire the exploit. + Steps: + 1. Check whether the exploit is already acquired + 2. Attempt to acquire the exploit if it is desired + 3. If the exploit is not acquired, decide whether to defer the acquisition attempt + If any of these steps succeed, the node succeeds. + If all of these steps fail, the node fails. + """, + HaveExploit, + _AcquireExploitIfDesired, + ExploitDeferred, +) diff --git a/vultron/bt/report_management/_behaviors/assign_vul_id.py b/vultron/bt/report_management/_behaviors/assign_vul_id.py new file mode 100644 index 00000000..609fdbe7 --- /dev/null +++ b/vultron/bt/report_management/_behaviors/assign_vul_id.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +""" +Provides Vulnerability ID assignment behaviors. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from vultron.bt.base.factory import fallback_node, sequence_node +from vultron.bt.report_management.fuzzer.assign_vul_id import ( + AssignId, + IdAssignable, + IdAssigned, + InScope, + IsIDAssignmentAuthority, + RequestId, +) + + +_AssignIdIfPossible = sequence_node( + "_AssignIdIfPossible", + """This node attempts to assign an ID to the vulnerability if possible. + Steps: + 1. Check whether we are an ID assignment authority + 2. Check whether the vulnerability is ID assignable per the ID assignment authority rules + 3. Assign an ID to the vulnerability + If all of these steps succeed, the vulnerability is assigned an ID. + If any of these steps fail, the vulnerability is not assigned an ID. + """, + IsIDAssignmentAuthority, + IdAssignable, + AssignId, +) + + +_AssignOrRequestId = fallback_node( + "_AssignOrRequestId", + """This node attempts to assign an ID to the vulnerability or request an ID from the ID assignment authority. + Steps: + 1. Attempt to assign an ID to the vulnerability + 2. Request an ID from the ID assignment authority + If either of these steps succeed, the vulnerability is assigned an ID. + If both of these steps fail, the vulnerability is not assigned an ID. + """, + _AssignIdIfPossible, + RequestId, +) + + +_AssignIdIfInScope = sequence_node( + "_AssignIdIfInScope", + """This node attempts to assign a vulnerability ID to the vulnerability if it is in scope. + Steps: + 1. Check whether the vulnerability is in scope for ID assignment + 2. Attempt to assign an ID to the vulnerability or request an ID from the ID assignment authority + If both of these steps succeed, the vulnerability is assigned an ID. + If any of these steps fail, the vulnerability is not assigned an ID. + """, + InScope, + _AssignOrRequestId, +) + + +AssignVulID = fallback_node( + "AssignVulID", + """This node attempts to assign a vulnerability ID to the vulnerability. + Steps: + 1. Check whether the vulnerability is already assigned an ID + 2. Check whether the vulnerability is in scope and assign an ID if possible + If any of these steps succeed, the vulnerability is assigned an ID. + If all of these steps fail, the vulnerability is not assigned an ID. + """, + IdAssigned, + _AssignIdIfInScope, +) diff --git a/vultron/bt/report_management/_behaviors/close_report.py b/vultron/bt/report_management/_behaviors/close_report.py new file mode 100644 index 00000000..b6b0c6cc --- /dev/null +++ b/vultron/bt/report_management/_behaviors/close_report.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +""" +Provides Vultron Report Closure Behaviors +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from vultron.bt.base.factory import fallback_node, sequence_node +from vultron.bt.case_state.conditions import CSinStateFixDeployed +from vultron.bt.messaging.outbound.behaviors import EmitRC +from vultron.bt.report_management.conditions import ( + RMinStateClosed, + RMinStateDeferred, + RMinStateInvalid, +) +from vultron.bt.report_management.fuzzer.close_report import ( + OtherCloseCriteriaMet, + PreCloseAction, +) +from vultron.bt.report_management.transitions import q_rm_to_C + + +_DeployedDeferredOrInvalid = fallback_node( + "_DeployedDeferredOrInvalid", + """This node returns success when the case is in a state that allows the report to be closed. + Possible success criteria: + 1. The case is in the FIX_DEPLOYED state. + 2. The report is in the DEFERRED state. + 3. The report is in the INVALID state. + If any of these criteria are met, the node returns success. + If none of these criteria are met, the node returns failure. + """, + CSinStateFixDeployed, + RMinStateDeferred, + RMinStateInvalid, +) + + +_CloseCriteriaMet = sequence_node( + "_CloseCriteriaMet", + """This node checks if the report is ready to be closed. + Steps: + 1. Check whether the case is in a state that allows the report to be closed. + 2. Check whether any other closure criteria are met. + """, + _DeployedDeferredOrInvalid, + OtherCloseCriteriaMet, +) + + +_CloseAndNotify = sequence_node( + "_CloseAndNotify", + """This node updates the report management state to CLOSED and emits a report closed message.""", + q_rm_to_C, + EmitRC, +) + + +_ReportClosureSequence = sequence_node( + "_ReportClosureSequence", + """This sequence handles the closing of a report. + Steps: + 1. Check if the report is ready to be closed. + 2. Perform any pre-close actions. + 3. Close the report and notify other stakeholders. + If all of these steps succeed, the report is closed. + If any of these steps fail, the report is not closed. + """, + _CloseCriteriaMet, + PreCloseAction, + _CloseAndNotify, +) + + +RMCloseBt = fallback_node( + "RMCloseBt", + """This bt tree handles the closing of a report. + Steps: + 1. Check if the report is in the CLOSED state. If so, return success. + 2. Otherwise start the report closure sequence. + If either of these steps succeed, the report is closed. + If both of these steps fail, the report is not closed. + """, + RMinStateClosed, + _ReportClosureSequence, +) diff --git a/vultron/bt/report_management/_behaviors/deploy_fix.py b/vultron/bt/report_management/_behaviors/deploy_fix.py new file mode 100644 index 00000000..b07d626a --- /dev/null +++ b/vultron/bt/report_management/_behaviors/deploy_fix.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python +""" +Provides fix deployment behaviors. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from vultron.bt.base.factory import fallback_node, sequence_node +from vultron.bt.case_state.conditions import ( + CSinStateFixDeployed, + CSinStateNotDeployedButPublicAware, + CSinStateVendorAwareFixReadyFixNotDeployed, +) +from vultron.bt.case_state.transitions import q_cs_to_D +from vultron.bt.messaging.outbound.behaviors import EmitCD, EmitRA, EmitRD +from vultron.bt.report_management.conditions import ( + RMinStateAccepted, + RMinStateDeferred, +) +from vultron.bt.report_management.fuzzer.deploy_fix import ( + DeployFix, + DeployMitigation, + MitigationAvailable, + MitigationDeployed, + MonitorDeployment, + MonitoringRequirement, + NoNewDeploymentInfo, + PrioritizeDeployment, +) +from vultron.bt.report_management.transitions import ( + q_rm_to_A, + q_rm_to_D, +) +from vultron.bt.roles.conditions import RoleIsDeployer, RoleIsVendor + + +_ShouldStayInRmDeferred = sequence_node( + "_ShouldStayInRmDeferred", + """This node represents the process of deciding whether to stay in the RMDeferred state. + It starts with a check that the report is in the DEFERRED state. + Then it checks if there is new deployment information. + If there is new info, it may be appropriate to reevaluate the deployment priority. + """, + RMinStateDeferred, + NoNewDeploymentInfo, +) + + +_DecideToAcceptDeploymentTasking = sequence_node( + "_DecideToAcceptDeploymentTasking", + """This node represents the process of deciding whether to accept a deployment tasking. + Steps: + 1. Determine the priority of the deployment tasking. + 2. Transition to the ACCEPTED state (from the deplyer's perspective). + 3. Emit a RA message indicating that the deployment tasking has been accepted. + If all of these steps succeed, then the deployment tasking is accepted. + If any of these steps fail, then the deployment tasking is not accepted. + """, + PrioritizeDeployment, + q_rm_to_A, + EmitRA, +) + + +_DeferDeploymentTasking = sequence_node( + "_DeferDeploymentTasking", + """This node represents the process of deferring a deployment tasking. + Steps: + 1. Transition to the DEFERRED state (from the deplyer's perspective). + 2. Emit a RD message indicating that the deployment tasking has been deferred. + If all of these steps succeed, then the deployment tasking is deferred. + If any of these steps fail, then the deployment tasking is not deferred. + """, + q_rm_to_D, + EmitRD, +) + + +_DecideWhetherToDeploy = fallback_node( + "_DecideWhetherToDeploy", + """This node represents the process of deciding whether to deploy a fix. + If the deployment task has already been prioritized, then the deployer's RM state + will be either ACCEPTED or DEFERRED state and there is no need to reevaluate the deployment priority. + If the deployment task has not been prioritized, then the deployer needs to decide the priority. + If the deployment tasking is not explicitly accepted, then it is implicitly deferred. + """, + RMinStateDeferred, + RMinStateAccepted, + _DecideToAcceptDeploymentTasking, + _DeferDeploymentTasking, +) + + +_DecideAbilityToDeploy = fallback_node( + "_DecideAbilityToDeploy", + """This node represents the process of deciding whether the deployer is able to deploy a fix. + There are two possible success conditions: + 1. The deployer is the vendor and can deploy a fix as soon as it is ready. + 2. The deployer is not the vendor and can deploy a fix only after it has been made public. + If neither of these conditions is met, then the deployer is not able to deploy a fix and the node fails. + """, + RoleIsVendor, + CSinStateNotDeployedButPublicAware, +) + + +_DeployFixWhenReady = sequence_node( + "_DeployFixWhenReady", + """This node represents the process of deploying a fix. + Steps: + 1. Determine whether the deployer is able to deploy a fix. + 2. Determine whether there is a fix availble to deploy. + 3. Deploy the fix. + 4. Transition to the FixDeployed state. + 5. Emit a CD message indicating that the fix has been deployed. + """, + _DecideAbilityToDeploy, + CSinStateVendorAwareFixReadyFixNotDeployed, + DeployFix, + q_cs_to_D, + EmitCD, +) + + +_DeployMitigationWhenReady = sequence_node( + "_DeployMitigationWhenReady", + """This node represents the process of deploying a mitigation. + Note that a mitigation is not a fix and may not be a complete solution. + However, it may be the best solution available at the time and may be deployed prior to a fix being available. + Steps: + 1. Determine whether there is a mitigation availble to deploy. + 2. Deploy the mitigation. + If all of these steps succeed, then the mitigation is deployed. + If any of these steps fail, then the mitigation is not deployed. + """, + MitigationAvailable, + DeployMitigation, +) + + +_Deploy = fallback_node( + "_Deploy", + """This node represents the process of deploying a fix or mitigation. + The process short-circuits if the deployer has deferred the deployment tasking. + It also short-circuits if the deployer has already deployed a fix. + Otherwise, the deployer will attempt to deploy a fix if one is available. + If no fix is available, the deployer will attempt to deploy a mitigation if one is available. + If neither a fix nor a mitigation is available, the node fails. + """, + RMinStateDeferred, + CSinStateFixDeployed, + _DeployFixWhenReady, + MitigationDeployed, + _DeployMitigationWhenReady, +) + + +_DeployIfDesired = sequence_node( + "_DeployIfDesired", + """This node represents the process of deciding whether to deploy a fix or mitigation. + It starts by confirming that the actor has the role of deployer. + Then it checks whether the deployment tasking has been accepted. + If the deployment tasking has been accepted, then the deployer will attempt to deploy a fix or mitigation. + """, + RoleIsDeployer, + _DecideWhetherToDeploy, + _Deploy, +) + + +_MonitorDeploymentIfDesired = sequence_node( + "_MonitorDeploymentIfDesired", + """This node represents the process of deciding whether to monitor a deployment and then monitoring it. + It starts with a check that the deployment is supposed to be monitored. + Then it performs the monitoring. + """, + MonitoringRequirement, + MonitorDeployment, +) + + +Deployment = fallback_node( + "Deployment", + """This bt represents the process of deploying a fix or mitigation and monitoring the deployment.""", + CSinStateFixDeployed, + _ShouldStayInRmDeferred, + _DeployIfDesired, + _MonitorDeploymentIfDesired, +) diff --git a/vultron/bt/report_management/_behaviors/develop_fix.py b/vultron/bt/report_management/_behaviors/develop_fix.py new file mode 100644 index 00000000..e2fb836c --- /dev/null +++ b/vultron/bt/report_management/_behaviors/develop_fix.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +""" +Provides fix development behaviors +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from vultron.bt.base.factory import fallback_node, sequence_node +from vultron.bt.case_state.conditions import CSinStateVendorAwareAndFixReady +from vultron.bt.case_state.transitions import q_cs_to_F +from vultron.bt.messaging.outbound.behaviors import EmitCF +from vultron.bt.report_management.conditions import RMinStateAccepted +from vultron.bt.report_management.fuzzer.develop_fix import CreateFix +from vultron.bt.roles.conditions import RoleIsNotVendor + + +_CreateFixForAcceptedReports = sequence_node( + "_CreateFixForAcceptedReports", + """This node represents the process of creating a fix for a report that is in the ACCEPTED state. + Steps: + 1. Check that the report is in the ACCEPTED state, implying that the vendor has prioritized the report for a fix. + 2. Actually create the fix. + 3. Transition the case state to the FIX_READY state. + 4. Emit a CF message indicating that the fix has been created. + """, + RMinStateAccepted, + CreateFix, + q_cs_to_F, + EmitCF, +) + + +DevelopFix = fallback_node( + "DevelopFix", + """This node represents the process of developing a fix for a vulnerability. + It short-circuits in the following situations: + 1. The actor is not a vendor and therefore cannot develop a fix. + 2. The case is already in the FIX_READY state. + + Otherwise, it attempts to create a fix for the reported vulnerability. + """, + RoleIsNotVendor, + CSinStateVendorAwareAndFixReady, + _CreateFixForAcceptedReports, +) diff --git a/vultron/bt/report_management/_behaviors/do_work.py b/vultron/bt/report_management/_behaviors/do_work.py new file mode 100644 index 00000000..aedc12fa --- /dev/null +++ b/vultron/bt/report_management/_behaviors/do_work.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +""" +Provides Vultron work behaviors. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from vultron.bt.base.factory import fallback_node +from vultron.bt.report_management._behaviors.acquire_exploit import ( + AcquireExploit, +) +from vultron.bt.report_management._behaviors.assign_vul_id import AssignVulID +from vultron.bt.report_management._behaviors.deploy_fix import Deployment +from vultron.bt.report_management._behaviors.develop_fix import DevelopFix +from vultron.bt.report_management._behaviors.monitor_threats import ( + MonitorThreats, +) +from vultron.bt.report_management._behaviors.publication import Publication +from vultron.bt.report_management._behaviors.report_to_others import ( + MaybeReportToOthers, +) +from vultron.bt.report_management.fuzzer.other_work import OtherWork + +potential_work = ( + AcquireExploit, + AssignVulID, + Deployment, + DevelopFix, + MonitorThreats, + Publication, + MaybeReportToOthers, + OtherWork, +) + + +RMDoWorkBt = fallback_node( + "RMDoWorkBt", + """ + This node represents the process of doing work on a report. + There are many different types of work that may be done on a report, and this node represents the process of + doing any of them. + The process of doing work on a report is a fallback node, meaning that it will try each of its children in turn, + and will succeed if any of its children succeed. + """, + Deployment, + DevelopFix, + MaybeReportToOthers, + MonitorThreats, + Publication, + AssignVulID, + AcquireExploit, + OtherWork, +) diff --git a/vultron/bt/report_management/_behaviors/monitor_threats.py b/vultron/bt/report_management/_behaviors/monitor_threats.py new file mode 100644 index 00000000..4b378e8e --- /dev/null +++ b/vultron/bt/report_management/_behaviors/monitor_threats.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +""" +Provides threat monitoring behaviors for the Vultron BT. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +from vultron.bt.base.factory import fallback_node, parallel_node, sequence_node +from vultron.bt.case_state.conditions import CSinStatePublicAware +from vultron.bt.case_state.transitions import q_cs_to_A, q_cs_to_P, q_cs_to_X +from vultron.bt.embargo_management.behaviors import TerminateEmbargoBt +from vultron.bt.messaging.outbound.behaviors import EmitCA, EmitCP, EmitCX +from vultron.bt.report_management.fuzzer.monitor_threats import ( + MonitorAttacks, + MonitorExploits, + MonitorPublicReports, + NoThreatsFound, +) + + +_NoticeAttack = sequence_node( + "_NoticeAttack", + """This node represents the process of noticing an attack on a vulnerability covered by a report. + If an attack is noticed, the case state is updated to reflect the attack, and a message is sent to the + case participants indicating the state change. + """, + MonitorAttacks, + q_cs_to_A, + EmitCA, +) + + +_MoveToCsPublic = sequence_node( + "_MoveToCsPublic", + """This node represents the process of moving the case state to the PUBLIC_AWARE state. + Steps: + 1. Transition the case state to the PUBLIC_AWARE state. + 2. Emit a CP message indicating that the case state has been updated. + """, + q_cs_to_P, + EmitCP, +) + + +_EnsureCsInPublic = fallback_node( + "_EnsureCsInPublic", + """This node represents the process of ensuring that the case state is in the PUBLIC_AWARE state. If the case state + is already in the PUBLIC_AWARE state, then this node succeeds. If the case state is not in the PUBLIC_AWARE + state, then this node attempts to move the case state to the PUBLIC_AWARE state. + """, + CSinStatePublicAware, + _MoveToCsPublic, +) + + +_NoticeExploit = sequence_node( + "_NoticeExploit", + """This node represents the process of noticing the public availability of an exploit for a vulnerability covered by + a report. If an exploit is noticed, the case state is updated to reflect the exploit, and a message is sent to + the case participants indicating the state change. + """, + MonitorExploits, + _EnsureCsInPublic, + q_cs_to_X, + EmitCX, +) + + +_NoticePublicReport = sequence_node( + "_NoticePublicReport", + """This node represents the process of noticing the public availability of a report for a vulnerability covered by a + report being coordinated by the case. If a public report is noticed, the case state is updated to reflect the + public report, and a message is sent to the case participants indicating the state change. + """, + MonitorPublicReports, + _MoveToCsPublic, +) + + +_MonitorExternalEvents = parallel_node( + "_MonitorExternalEvents", + """This node represents the process of monitoring external events for a report being coordinated by the case. + It monitors for attacks, exploits, and public reports while the case is being coordinated. + """, + 1, + _NoticeAttack, + _NoticeExploit, + _NoticePublicReport, +) + + +_EndEmbargoIfEventsWarrant = sequence_node( + "_EndEmbargoIfEventsWarrant", + """This node represents the process of ending the embargo if the events warrant it. + Events that warrant ending the embargo include: + 1. The public becoming aware of the vulnerability. + 2. The public becoming aware of an exploit for the vulnerability. + 3. The observation of an attack on the vulnerability. + + If any of these events are observed, then the embargo termination process is initiated. + """, + _MonitorExternalEvents, + TerminateEmbargoBt, +) + + +MonitorThreats = fallback_node( + "MonitorThreats", + """This node represents the process of monitoring threats to a report being coordinated by the case. + It monitors for attacks, exploits, and public reports while the case is being coordinated. + If any of these events are observed, then the embargo termination process will be initiated. + If no threats are observed, then the node will succeed anyway so that the case can continue to be coordinated. + """, + _EndEmbargoIfEventsWarrant, + NoThreatsFound, +) diff --git a/vultron/bt/report_management/_behaviors/prioritize_report.py b/vultron/bt/report_management/_behaviors/prioritize_report.py new file mode 100644 index 00000000..70fc04f7 --- /dev/null +++ b/vultron/bt/report_management/_behaviors/prioritize_report.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +""" +Provides report prioritization behaviors. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +import random + +from vultron.bt.base.bt_node import BtNode +from vultron.bt.base.factory import ( + action_node, + condition_check, + fallback_node, + sequence_node, +) +from vultron.bt.common import show_graph +from vultron.bt.messaging.outbound.behaviors import EmitRA, EmitRD +from vultron.bt.report_management.conditions import ( + RMinStateAccepted, + RMinStateDeferred, + RMinStateDeferredOrAccepted, + RMinStateValidOrDeferredOrAccepted, +) +from vultron.bt.report_management.fuzzer.prioritize_report import ( + EnoughPrioritizationInfo, + GatherPrioritizationInfo, + NoNewPrioritizationInfo, + OnAccept, + OnDefer, +) +from vultron.bt.report_management.report_priority_states import ( + ReportPriority, +) +from vultron.bt.report_management.transitions import ( + q_rm_to_A, + q_rm_to_D, +) + + +def priority_not_defer(obj: BtNode) -> bool: + return obj.bb.priority != ReportPriority.DEFER + + +_PriorityNotDefer = condition_check("PriorityNotDefer", priority_not_defer) + + +# todo: wire up SSVC lookup here +def evaluate_priority(obj: BtNode) -> bool: + """Dummy function that simulates a prioritization decision. + This implementation randomly chooses a new priority. + A real implementation could use something like SSVC to determine the priority. + """ + current_priority = obj.bb.priority + prioritization_count = obj.bb.prioritization_count + + willing_to_reprioritize = (current_priority is None) or ( + random.random() < (0.5**prioritization_count) + ) + + if willing_to_reprioritize: + new_priority = random.choice(list(ReportPriority)) + if new_priority != current_priority: + obj.bb.priority = new_priority + obj.bb.prioritization_count += 1 + + return True + + +_EvaluatePriority = action_node("EvaluatePriority", evaluate_priority) + + +_GetMorePrioritizationInfo = sequence_node( + "_GetMorePrioritizationInfo", + """This node represents the process of gathering more prioritization information.""", + GatherPrioritizationInfo, + NoNewPrioritizationInfo, +) + + +_EnsureAdequatePrioritizationInfo = fallback_node( + "_EnsureAdequatePrioritizationInfo", + """This node represents the process of ensuring that there is adequate prioritization information. + If there is adequate prioritization information, then this node succeeds. + If there is not adequate prioritization information, then this node attempts to gather more prioritization + information. + """, + EnoughPrioritizationInfo, + _GetMorePrioritizationInfo, +) + + +_ConsiderGatheringMorePrioritizationInfo = sequence_node( + "_ConsiderGatheringMorePrioritizationInfo", + """This node represents the process of considering whether to gather more prioritization information. + If the RM state is in DEFERRED or ACCEPTED, then we might want to gather more prioritization information. + """, + RMinStateDeferredOrAccepted, + _EnsureAdequatePrioritizationInfo, +) + + +_TransitionToRmAccepted = sequence_node( + "_TransitionToRmAccepted", + """This node represents the process of transitioning the RM state to the ACCEPTED state. + Steps: + 1. Run the OnAccept behavior. + 2. Transition the RM state to the ACCEPTED state. + 3. Emit a RA message indicating that the RM state has been updated. + """, + OnAccept, + q_rm_to_A, + EmitRA, +) + +_EnsureRmAccepted = fallback_node( + "_EnsureRmAccepted", + """This node represents the process of ensuring that the RM state is in the ACCEPTED state. + If the RM state is in the ACCEPTED state, then this node succeeds. + If the RM state is not in the ACCEPTED state, then this node attempts to transition the RM state to the + ACCEPTED state. + """, + RMinStateAccepted, + _TransitionToRmAccepted, +) + + +_DecideIfFurtherActionNeeded = sequence_node( + "_DecideIfFurtherActionNeeded", + """This node represents the process of deciding whether prioritization action is needed. + If the RM state is in VALID or DEFERRED or ACCEPTED, then further action is needed. + """, + RMinStateValidOrDeferredOrAccepted, + _EvaluatePriority, + _PriorityNotDefer, + _EnsureRmAccepted, +) + + +_TransitionToRmDeferred = sequence_node( + "_TransitionToRmDeferred", + """This node represents the process of transitioning the RM state to the DEFERRED state. + Steps: + 1. Run the OnDefer behavior. + 2. Transition the RM state to the DEFERRED state. + 3. Emit a RD message indicating that the RM state has been updated. + """, + OnDefer, + q_rm_to_D, + EmitRD, +) + + +_EnsureRmDeferred = fallback_node( + "_EnsureRmDeferred", + """This node ensures that the RM state is in the DEFERRED state. + If the RM state is already in the DEFERRED state, then this node succeeds. + If the RM state is not in the DEFERRED state, then this node attempts to transition the RM state to the + DEFERRED state. + """, + RMinStateDeferred, + _TransitionToRmDeferred, +) + + +RMPrioritizeBt = fallback_node( + "RMPrioritizeBt", + """This node represents the process of prioritizing a report. + Steps: + 1. Consider whether to gather more prioritization information. + 2. Decide whether prioritization action is needed. If so, possibly transition to the ACCEPTED state. + 3. If the previous steps fail, ensure that the RM state is in the DEFERRED state. + """, + _ConsiderGatheringMorePrioritizationInfo, + _DecideIfFurtherActionNeeded, + _EnsureRmDeferred, +) + + +def main(): + show_graph(RMPrioritizeBt) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/report_management/_behaviors/publication.py b/vultron/bt/report_management/_behaviors/publication.py new file mode 100644 index 00000000..58c9d454 --- /dev/null +++ b/vultron/bt/report_management/_behaviors/publication.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python +""" +Provides publication behaviors. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from vultron.bt.base.factory import fallback_node, sequence_node + +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +from vultron.bt.case_state.conditions import CSinStateVendorAwareAndFixReady +from vultron.bt.case_state.transitions import q_cs_to_P +from vultron.bt.common import show_graph +from vultron.bt.embargo_management.behaviors import EmbargoManagementBt +from vultron.bt.embargo_management.conditions import EMinStateNoneOrExited +from vultron.bt.messaging.outbound.behaviors import EmitCP +from vultron.bt.report_management._behaviors.acquire_exploit import ( + AcquireExploit, +) +from vultron.bt.report_management._behaviors.develop_fix import DevelopFix +from vultron.bt.report_management.fuzzer.publication import ( + AllPublished, + ExploitReady, + NoPublishExploit, + NoPublishFix, + NoPublishReport, + PrepareExploit, + PrepareFix, + PrepareReport, + PrioritizePublicationIntents, + PublicationIntentsSet, + Publish, + ReprioritizeExploit, + ReprioritizeFix, + ReprioritizeReport, +) + + +_ReadyExploitForPublication = fallback_node( + "_ReadyExploitForPublication", + """ + This node represents the process of preparing an exploit for publication. + Either the exploit has to be acquired or prepared for publication. + """, + AcquireExploit, + PrepareExploit, +) + + +_MaybePrepareExploitForPublication = fallback_node( + "_MaybePrepareExploitForPublication", + """This node represents the process of ensuring that the exploit is published if it is desired.""", + NoPublishExploit, + ExploitReady, + _ReadyExploitForPublication, + ReprioritizeExploit, +) + + +_ReadyFixForPublication = sequence_node( + "_ReadyFixForPublication", + "Develop a fix then prepare it for publication.", + DevelopFix, + PrepareFix, +) + + +_MaybePrepareFixForPublication = fallback_node( + "_MaybePrepareFixForPublication", + """Develop a fix if is desired and ready """, + NoPublishFix, + CSinStateVendorAwareAndFixReady, + _ReadyFixForPublication, + ReprioritizeFix, +) + + +_MaybePrepareReportForPublication = fallback_node( + "_MaybePrepareReportForPublication", + "Prepare a report for publication if it is desired.", + NoPublishReport, + PrepareReport, + ReprioritizeReport, +) + + +_PreparePublication = sequence_node( + "_PreparePublication", + "Prepare Report, Fix, and Exploit for publication.", + _MaybePrepareExploitForPublication, + _MaybePrepareFixForPublication, + _MaybePrepareReportForPublication, +) + + +_EnsurePublicationPriorityIsSet = fallback_node( + "_EnsurePublicationPriorityIsSet", + """Ensure that the publication priorities are set.""", + PublicationIntentsSet, + PrioritizePublicationIntents, +) + + +_EnsureAllDesiredItemsArePublished = sequence_node( + "_EnsureAllDesiredItemsArePublished", + """Ensure that all desired items are published.""", + _EnsurePublicationPriorityIsSet, + AllPublished, +) + + +_PublishWhenReady = sequence_node( + "_PublishWhenReady", + """Publish the report, fix, and exploit when they are ready. Check that the embargo is not active first.""", + _PreparePublication, + EmbargoManagementBt, + EMinStateNoneOrExited, + Publish, + q_cs_to_P, + EmitCP, +) + + +Publication = fallback_node( + "Publication", + """This node represents the process of publishing a report, fix, and exploit.""", + _EnsureAllDesiredItemsArePublished, + _PublishWhenReady, +) + + +def main(): + show_graph(Publication) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/report_management/_behaviors/report_to_others.py b/vultron/bt/report_management/_behaviors/report_to_others.py new file mode 100644 index 00000000..dcd96de1 --- /dev/null +++ b/vultron/bt/report_management/_behaviors/report_to_others.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python +""" +Provides report_to_others behavior tree nodes. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +import logging + +from vultron.bt.base.bt_node import BtNode +from vultron.bt.base.factory import ( + action_node, + condition_check, + fallback_node, + invert, + sequence_node, +) +from vultron.bt.common import show_graph +from vultron.bt.embargo_management.conditions import ( + EMinStateActiveOrRevise, + EMinStateNoneOrProposeOrRevise, +) +from vultron.bt.messaging.states import MessageTypes +from vultron.bt.report_management.fuzzer.report_to_others import ( + AllPartiesKnown, + ChooseRecipient, + FindContact, + HaveReportToOthersCapability, + InjectCoordinator, + InjectOther, + InjectVendor, + MoreCoordinators, + MoreOthers, + MoreVendors, + NotificationsComplete, + PolicyCompatible, + RcptNotInQrmS, + RecipientEffortExceeded, +) +from vultron.case_states.states import CS +from vultron.sim.messages import Message + +# from vultron.sim.communications import Message + +logger = logging.getLogger(__name__) + + +def reporting_effort_available(obj: BtNode) -> bool: + """True if reporting effort budget remains""" + return obj.bb.reporting_effort_budget > 0 + + +_ReportingEffortAvailable = condition_check( + "ReportingEffortAvailable", reporting_effort_available +) + +_TotalEffortLimitMet = invert( + "_TotalEffortLimitMet", + "Checks whether the effort limit has been met", + _ReportingEffortAvailable, +) + +_IdentifyVendors = sequence_node( + "_IdentifyVendors", """XXX""", MoreVendors, InjectVendor +) + + +_IdentifyCoordinators = sequence_node( + "_IdentifyCoordinators", """XXX""", MoreCoordinators, InjectCoordinator +) + + +_IdentifyOthers = sequence_node( + "_IdentifyOthers", """XXX""", MoreOthers, InjectOther +) + + +_IdentifyPotentialCaseParticipants = sequence_node( + "_IdentifyPotentialCaseParticipants", + "Identifies potential case participants: vendors, coordinators, and others", + _IdentifyVendors, + _IdentifyCoordinators, + _IdentifyOthers, +) + + +# TODO AllPartiesKnown should be a simulated annealing where p rises with tick count +# thereby forcing notification to happen toward the outset of a case +# but for now we'll just leave it as a dumb fuzzer +_IdentifyParticipants = fallback_node( + "_IdentifyParticipants", + "Checks whether all parties are known, otherwise identifies potential case", + AllPartiesKnown, + _IdentifyPotentialCaseParticipants, +) + + +_DecideWhetherToPruneRecipient = fallback_node( + "_DecideWhetherToPruneRecipient", + "Checks whether a recipient is already aware (RM != START) or effort budget exceeded", + RcptNotInQrmS, + RecipientEffortExceeded, +) + + +def remove_recipient(obj: BtNode) -> bool: + """Removes the current recipient from the list of potential participants""" + current = obj.bb.currently_notifying + if current is None: + return True + + logger.debug(f"Removing {current} from potential participants list") + try: + obj.bb.case.potential_participants.remove(current) + except ValueError: + logger.warning( + f"Unable to remove {current}, not in potential participants list" + ) + obj.bb.currently_notifying = None + return True + + +_RemoveRecipient = action_node("RemoveRecipient", remove_recipient) + + +_PruneRecipients = sequence_node( + "_PruneRecipients", + """XXX""", + _DecideWhetherToPruneRecipient, + _RemoveRecipient, +) + + +_EnsureRcptPolicyCompatibleWithExistingEmbargo = sequence_node( + "_EnsureRcptPolicyCompatibleWithExistingEmbargo", + "If there is an active embargo, then the recipient's policy must be compatible with the embargo policy", + EMinStateActiveOrRevise, + PolicyCompatible, +) + + +_EnsureOkToNotify = fallback_node( + "_EnsureOkToNotify", + "Verify that it is ok to notify a recipient", + EMinStateNoneOrProposeOrRevise, + _EnsureRcptPolicyCompatibleWithExistingEmbargo, +) + + +def report_to_new_participant(obj: BtNode) -> bool: + """Direct messages an initial report to a new participant in their inbox""" + # inject an RS message from us into their inbox + report = Message( + sender="", body="Initial report", msg_type=MessageTypes.RS + ) + + try: + dm = obj.bb.dm_func + dm(message=report, recipient=obj.bb.currently_notifying) + except AttributeError: + logger.warning(f"Node blackboard dm_func is not set. No message sent.") + return False + + return True + + +_ReportToNewParticipant = action_node( + "ReportToNewParticipant", report_to_new_participant +) + + +def connect_new_participant_to_case(obj: BtNode) -> bool: + """Wires up a new participant's inbox to the case""" + # wire up their inbox to the case + add_func = obj.bb.add_participant_func + if add_func is None: + logger.warning(f"Node blackboard add_participant_func is not set.") + return False + + add_func(obj.bb.currently_notifying) + return True + + +_ConnectNewParticipantToCase = action_node( + "ConnectNewParticipantToCase", connect_new_participant_to_case +) + + +def bring_new_participant_up_to_speed(obj: BtNode) -> bool: + """Sets a new participant's global state to match the current participant's global state""" + # EM state is global to the case + obj.bb.currently_notifying.bt.bb.q_em = obj.bb.q_em + + # FIXME this doesn't work with the new CS enumerations + # CS.PXA is 000111, so only the P, X, and A states carry over + obj.bb.currently_notifying.bt.bb.q_cs = CS.PXA & obj.bb.q_cs + + return True + + +_BringNewParticipantUpToSpeed = action_node( + "BringNewParticipantUpToSpeed", bring_new_participant_up_to_speed +) + + +_EngageParticipant = sequence_node( + "_EngageParticipant", + "Reports to a new participant, connects them to the case, and brings them up to speed", + _ReportToNewParticipant, + _ConnectNewParticipantToCase, + _BringNewParticipantUpToSpeed, +) + + +# FIXED Replace EmitRS with inject RS into new participant then add participant to case +# will also need to sync case pxa and embargo status with new participant +_NotifyRecipient = sequence_node( + "_NotifyRecipient", + "Checks if it is ok to notify a recipient, finds their contact info, then notifies them", + _EnsureOkToNotify, + FindContact, + _EngageParticipant, +) + + +_PruneOrNotifyRecipient = fallback_node( + "_PruneOrNotifyRecipient", + "Prunes a recipient if necessary, otherwise notifies them", + _PruneRecipients, + _NotifyRecipient, +) + + +_SelectAndProcessRecipient = sequence_node( + "_SelectAndProcessRecipient", + "Chooses a recipient, then prunes or notifies them", + ChooseRecipient, + _PruneOrNotifyRecipient, +) + + +_NotifyOthers = fallback_node( + "_NotifyOthers", + "Checks if there are more recipients to notify, then selects and processes a recipient", + NotificationsComplete, + _SelectAndProcessRecipient, +) + + +_IdentifyAndNotifyParticipants = sequence_node( + "_IdentifyAndNotifyParticipants", + "Identify participants and notify them", + _IdentifyParticipants, + _NotifyOthers, +) + + +_ReportToOthers = fallback_node( + "_ReportToOthers", + "Checks an effort budget remains, then identifies and notifies participants", + _TotalEffortLimitMet, + _IdentifyAndNotifyParticipants, +) + + +MaybeReportToOthers = sequence_node( + "MaybeReportToOthers", + "Checks for reporting capability, then reports to others if possible", + HaveReportToOthersCapability, + _ReportToOthers, +) + + +def main(): + show_graph(MaybeReportToOthers) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/report_management/_behaviors/validate_report.py b/vultron/bt/report_management/_behaviors/validate_report.py new file mode 100644 index 00000000..85d502e2 --- /dev/null +++ b/vultron/bt/report_management/_behaviors/validate_report.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +""" +Provides report validation behaviors for Vultron. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from vultron.bt.base.factory import fallback_node, sequence_node +from vultron.bt.common import show_graph +from vultron.bt.messaging.outbound.behaviors import EmitRI, EmitRV +from vultron.bt.report_management.conditions import ( + RMinStateInvalid, + RMinStateReceivedOrInvalid, + RMinStateValid, +) +from vultron.bt.report_management.fuzzer.validate_report import ( + EnoughValidationInfo, + EvaluateReportCredibility, + EvaluateReportValidity, + GatherValidationInfo, + NoNewValidationInfo, +) +from vultron.bt.report_management.transitions import q_rm_to_I, q_rm_to_V + + +_GetMoreValidationInfo = sequence_node( + "_GetMoreValidationInfo", + "Collect more validation info", + GatherValidationInfo, + NoNewValidationInfo, +) + + +_EnsureAdequateValidationInfo = fallback_node( + "_EnsureAdequateValidationInfo", + "Check if there is enough validation info. If not, get more.", + EnoughValidationInfo, + _GetMoreValidationInfo, +) + + +_HandleRmI = sequence_node( + "_HandleRmI", + "If we are in RM.INVALID, check to see if we need to collect more info", + RMinStateInvalid, + _EnsureAdequateValidationInfo, +) + + +_ValidateReport = sequence_node( + "_ValidateReport", + "Move to RM.VALID state and emit RV message", + q_rm_to_V, + EmitRV, +) + + +_ValidationSequence = sequence_node( + "_ValidationSequence", + """This node represents the process of validating a report. + Steps: + 1. Check if the report is in the RECEIVED or INVALID states. + 2. Evaluate the credibility of the report. + 3. Evaluate the validity of the report. + 4. Change the report management state to VALID if all previous steps succeeded + """, + RMinStateReceivedOrInvalid, + EvaluateReportCredibility, + EvaluateReportValidity, + _ValidateReport, +) + + +_InvalidateReport = sequence_node( + "_InvalidateReport", + "Move to RM.INVALID state and emit an RI message", + q_rm_to_I, + EmitRI, +) + + +RMValidateBt = fallback_node( + "RMValidateBt", + """This node represents the process of validating a report. + Steps: + 1. If the report is in the VALID state, then this node succeeds. + 2. If the report is in the RECEIVED or INVALID states, then this node attempts to validate the report. + 3. If validation fails, then move the report to INVALID. + """, + RMinStateValid, + _HandleRmI, + _ValidationSequence, + _InvalidateReport, +) + + +def main(): + show_graph(RMValidateBt) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/report_management/behaviors.py b/vultron/bt/report_management/behaviors.py new file mode 100644 index 00000000..52e53dc3 --- /dev/null +++ b/vultron/bt/report_management/behaviors.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides behaviors for the report management process +""" + +from vultron.bt.base.factory import fallback_node, node_factory, sequence_node +from vultron.bt.common import show_graph + +# noinspection PyProtectedMember +from vultron.bt.report_management._behaviors.close_report import RMCloseBt + +# noinspection PyProtectedMember +from vultron.bt.report_management._behaviors.do_work import RMDoWorkBt + +# noinspection PyProtectedMember +from vultron.bt.report_management._behaviors.prioritize_report import ( + RMPrioritizeBt, +) + +# noinspection PyProtectedMember +from vultron.bt.report_management._behaviors.validate_report import ( + RMValidateBt, +) +from vultron.bt.report_management.conditions import ( + RMinStateAccepted, + RMinStateClosed, + RMinStateDeferred, + RMinStateInvalid, + RMinStateReceived, + RMinStateStart, + RMinStateValid, +) + +_CloseOrValidate = fallback_node( + "CloseOrValidate" + "Try to close the report, and if that fails, validate the report. Report closure will fail if there is still work " + "to be done on the report.", + RMCloseBt, + RMValidateBt, +) + +_CloseOrPrioritize = fallback_node( + "CloseOrPrioritize", + "Try to close the report, and if that fails, prioritize the report. Report closure will fail if there is still " + "work to be done on the report.", + RMCloseBt, + RMPrioritizeBt, +) + +_PrioritizeDoWork = sequence_node( + "PrioritizeDoWork", + "Prioritize the report, and if prioritization is successful, do work on the report. Prioritization succeeds if " + "the prioritization result is not DEFERRED.", + RMPrioritizeBt, + RMDoWorkBt, +) + + +_CloseOrPrioritizeOrWork = fallback_node( + "CloseOrPrioritizeOrWork", + "Close the report, prioritize the report, or do work on the report.", + RMCloseBt, + _PrioritizeDoWork, +) + +_RmReceived = sequence_node( + "RmReceived", + "Handle the RECEIVED state. After checking that the report management state is in the RECEIVED state, " + "this node will attempt to validate the report.", + RMinStateReceived, + RMValidateBt, +) + +_RmInvalid = sequence_node( + "RmInvalid", + "Handle the INVALID state. After checking that the report management state is in the INVALID state, this node " + "will decide what to do next. Options are: Close the report, Validate the report, Stay in the INVALID state (do " + "nothing)", + RMinStateInvalid, + _CloseOrValidate, +) + +_RmStart = node_factory( + RMinStateStart, + "RmStart", + "Handle the START state. The start state is the initial state of the report management state machine, and is used " + "as a placeholder to represent the status of other participants in the case. Once a report is received, " + "the report management state machine will transition to the RECEIVED state, which is where the actual work begins.", +) + +_RmClosed = node_factory( + RMinStateClosed, + "RmClosed", + "Handle the CLOSED state. There is nothing left to be done for a report that is in the CLOSED state.", +) + +_RmValid = sequence_node( + "RmValid", + "Handle the VALID state. After checking that the report management state is in the VALID state, this node will " + "attempt to prioritize the report.", + RMinStateValid, + RMPrioritizeBt, +) + + +_RmDeferred = sequence_node( + "RmDeferred", + "Handle the DEFERRED state. After checking that the report management state is in the DEFERRED state, " + "this node will attempt to decide what to do next. Options are: Close the report, Prioritize the report", + RMinStateDeferred, + _CloseOrPrioritize, +) + +_RmAccepted = sequence_node( + "RmAccepted", + "Handle the ACCEPTED state. After checking that the report management state is in the ACCEPTED state, " + "this node will attempt to decide what to do next. Options are: Close the report, Prioritize the report, " + "Do work on the report", + RMinStateAccepted, + _CloseOrPrioritizeOrWork, +) + +ReportManagementBt = fallback_node( + "ReportManagementBt", + "The report management bt tree. This tree is responsible for managing the report management state machine.", + _RmStart, + _RmClosed, + _RmReceived, + _RmInvalid, + _RmValid, + _RmDeferred, + _RmAccepted, +) + + +def main(): + show_graph(ReportManagementBt) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/report_management/conditions.py b/vultron/bt/report_management/conditions.py new file mode 100644 index 00000000..2ac30dbc --- /dev/null +++ b/vultron/bt/report_management/conditions.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides condition nodes for report management states. +""" +from typing import Type + +from vultron.bt.base.bt_node import ConditionCheck +from vultron.bt.base.factory import fallback_node, invert +from vultron.bt.common import state_in +from vultron.bt.report_management.states import RM + + +def rm_state_in(state: RM) -> Type[ConditionCheck]: + """ + Convenience function to create a ConditionCheck for a Report Management state. + + Args: + state: a Report Management state + + Returns: + A ConditionCheck class for the given state + """ + if state not in RM: + raise ValueError(f"Invalid Report Management state: {state}") + + return state_in("q_rm", state) + + +RMinStateStart = rm_state_in(RM.START) +RMinStateReceived = rm_state_in(RM.RECEIVED) +RMinStateInvalid = rm_state_in(RM.INVALID) +RMinStateValid = rm_state_in(RM.VALID) +RMinStateDeferred = rm_state_in(RM.DEFERRED) +RMinStateAccepted = rm_state_in(RM.ACCEPTED) +RMinStateClosed = rm_state_in(RM.CLOSED) + + +RMnotInStateStart = invert( + "RMnotInStateStart", "True when RM not in START", RMinStateStart +) +RMnotInStateClosed = invert( + "RMnotInStateClosed", "True when RM not in CLOSED", RMinStateClosed +) + +RMinStateDeferredOrAccepted = fallback_node( + "RMinStateDeferredOrAccepted", + "SUCCESS when the report management state is in the DEFERRED or ACCEPTED state. FAILURE otherwise.", + RMinStateDeferred, + RMinStateAccepted, +) + +RMinStateReceivedOrInvalid = fallback_node( + "RMinStateReceivedOrInvalid", + "SUCCESS when the report management state is in the RECEIVED or INVALID state. FAILURE otherwise.", + RMinStateReceived, + RMinStateInvalid, +) + +RMinStateStartOrClosed = fallback_node( + "RMinStateStartOrClosed", + "SUCCESS when the report management state is in the START or CLOSED state. FAILURE otherwise.", + RMinStateStart, + RMinStateClosed, +) + + +RMinStateValidOrDeferredOrAccepted = fallback_node( + "RMinStateValidOrDeferredOrAccepted", + "SUCCESS when the report management state is in the VALID, DEFERRED, or ACCEPTED state. FAILURE otherwise.", + RMinStateValid, + RMinStateDeferred, + RMinStateAccepted, +) diff --git a/vultron/bt/report_management/errors.py b/vultron/bt/report_management/errors.py new file mode 100644 index 00000000..dd016df6 --- /dev/null +++ b/vultron/bt/report_management/errors.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +"""Provides Vultron Report Management Errors +""" + + +from vultron.bt.errors import CvdProtocolError + + +class ReportManagementError(CvdProtocolError): + """Base class for all errors raised by the report management module""" + + pass + + +class ReportManagementConditionError(ReportManagementError): + """Base class for all errors raised by report management condition checks""" diff --git a/vultron/bt/report_management/fuzzer/__init__.py b/vultron/bt/report_management/fuzzer/__init__.py new file mode 100644 index 00000000..3097546a --- /dev/null +++ b/vultron/bt/report_management/fuzzer/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +#!/usr/bin/env python +"""file: __init__.py +author: adh +created_at: 2/21/23 1:55 PM +""" diff --git a/vultron/bt/report_management/fuzzer/acquire_exploit.py b/vultron/bt/report_management/fuzzer/acquire_exploit.py new file mode 100644 index 00000000..7ffebebc --- /dev/null +++ b/vultron/bt/report_management/fuzzer/acquire_exploit.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +""" +Provides fuzzer leaf nodes in support of the process of acquiring an exploit for the vulnerability +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +from vultron.bt.base.factory import fuzzer +from vultron.bt.base.fuzzer import ( + AlmostAlwaysFail, + AlmostAlwaysSucceed, + AlwaysSucceed, + OftenFail, + OftenSucceed, + RarelySucceed, + UsuallyFail, +) + + +# check a database of exploits or threat intelligence feeds or ask a human +HaveExploit = fuzzer( + UsuallyFail, + "HaveExploit", + """This condition is used to check if the organization already has an exploit for the vulnerability. In a real + implementation, this could be a simple check to see if the organization already has an exploit for the + vulnerability. For example, by checking a database of exploits or by checking threat intelligence feeds. In our + stub implementation, this condition fails in 3 out of 4 attempts so that we can exercise the rest of the workflow. + """, +) + + +# check against the result of an exploit acquisition priority evaluation or ask a human +ExploitDeferred = fuzzer( + OftenSucceed, + "ExploitDeferred", + """This condition is used to check if the organization has decided to defer the acquisition of an exploit. This + could be because the organization has decided to focus on other vulnerabilities or because the organization has + decided to wait for a public exploit. In a real implementation, this could be a simple check against the result + of an exploit acquisition priroity evaluation. In our stub implementation, this condition succeeds in 7 out of 10 + attempts. + """, +) + + +# check process metadata or ask a human +ExploitPrioritySet = fuzzer( + AlmostAlwaysSucceed, + "ExploitPrioritySet", + """This condition checks to see if the organization has decided on a priority for acquiring an exploit. + In a real implementation, this could be a simple process metadata check. + In our stub implementation, this condition succeeds in 9 out of 10 attempts. + """, +) + + +# ask a human +EvaluateExploitPriority = fuzzer( + AlwaysSucceed, + "EvaluateExploitPriority", + """This node represents the process of evaluating the priority of acquiring an exploit. In a real implementation, + this would probably be a human decision based on a variety of factors. Or it might be automated if the + organization has a clear data-driven process for evaluating exploit acquisition priority. In our stub + implementation, this node always succeeds. + """, +) + + +# check against the result of an exploit acquisition priority evaluation or ask a human +ExploitDesired = fuzzer( + OftenFail, + "ExploitDesired", + """This condition represents the organization's decision to acquire an exploit, based on the priority evaluation. In + a real implementation, this could be a simple check against the result of an exploit acquisition priority + evaluation. In our stub implementation, this condition fails in 7 out of 10 attempts. + """, +) + + +# search for an exploit in a database of exploits or threat intelligence feeds or ask a human +FindExploit = fuzzer( + AlmostAlwaysFail, + "FindExploit", + """This node represents the process of finding an exploit for the vulnerability outside of the organization. In a + real implementation, this might be automated as a search for an exploit in a database of exploits or a search for + an exploit in threat intelligence feeds. Alternatively, this might be a prompt for a human to go search for an + exploit. In our stub implementation, this node fails in 9 out of 10 attempts, reflecting the assumption that + exploits are usually not readily available, while still succeeding often enough to exercise the rest of the + workflow. + """, +) + + +# ask a human +DevelopExploit = fuzzer( + OftenSucceed, + "DevelopExploit", + """This node represents the process of developing an exploit for the vulnerability. In a real implementation, + this would probably be a prompt for a human to develop an exploit. In our stub implementation, this node succeeds + in 7 out of 10 attempts, allowing us to exercise the rest of the workflow. + """, +) + + +# ask a human +PurchaseExploit = fuzzer( + RarelySucceed, + "PurchaseExploit", + """This node represents the process of purchasing an exploit for the vulnerability. In a real implementation, + this would probably be a prompt for a human to pursue purchasing an exploit. In our stub implementation, + this node succeeds in 1 out of 10 attempts, allowing us to exercise the rest of the workflow. + """, +) diff --git a/vultron/bt/report_management/fuzzer/assign_vul_id.py b/vultron/bt/report_management/fuzzer/assign_vul_id.py new file mode 100644 index 00000000..7eb1799d --- /dev/null +++ b/vultron/bt/report_management/fuzzer/assign_vul_id.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides fuzzer leaf nodes for the report management workflow. +""" +from vultron.bt.base.factory import fuzzer +from vultron.bt.base.fuzzer import ( + AlwaysSucceed, + OftenSucceed, + ProbablySucceed, + UsuallyFail, + UsuallySucceed, +) + +IdAssigned = fuzzer( + UsuallyFail, + "IdAssigned", + """This condition is used to check if the vulnerability has already been assigned an identity, such as a CVE ID. In + a real implementation, this would be a simple check to see if the vulnerability has already been assigned an + identity. In our stub implementation, this condition fails in 3 out of 4 attempts because we are assuming that + the ID assignment process is part of the workflow. + """, +) + +# check if the vulnerability has already been assigned an identity + + +InScope = fuzzer( + UsuallySucceed, + "InScope", + """This condition is used to check if the vulnerability is in scope for an ID assignment This is not the same as + checking whether the vulnerability is in scope for the organization's CVD process. In the case of CVE ID + assignment, the scope is defined in the CNA rules. Other ID assignment processes may have different scope + definitions. In a real implementation, this would probably be a prompt for a human to check the reported + vulnerability against the scope definition, if applicable. Hint: An ID space which allows for rapid assignment of + IDs may have a very broad scope and may not require this check. In our stub implementation, this condition + succeeds in 3 out of 4 attempts. + """, +) + +# check if the vulnerability is in scope for an ID assignment + + +RequestId = fuzzer( + UsuallySucceed, + "RequestId", + """This node represents the process of requesting a Vulnerability ID assignment. For example, in the case of CVE ID + assignment, this would be the process of submitting a CVE ID request to the CVE Numbering Authority (CNA). In a + real implementation, this could be automatable as an API call to the CNA, or it could be a prompt for a human to + submit the request. In our stub implementation, this node succeeds in 3 out of 4 attempts so that we can exercise + the rest of the workflow. + """, +) + +# submit an ID request to the ID assignment authority or prompt a human to do so + + +IsIDAssignmentAuthority = fuzzer( + OftenSucceed, + "IsIDAssignmentAuthority", + """This condition is used to check if the organization is an ID assignment authority itself with the ability to + assign IDs directly. We are using the CVE Numbering Authority (CNA) as an example of an ID assignment authority, + but this is intended to be a generic condition and not specific to CVE ID assignment except as an example. In a + real implementation, this would probably be a simple check against the organization's metadata. In our stub + implementation, this condition succeeds in 7 out of 10 attempts. + """, +) + +# check if the organization is an ID assignment authority + + +IdAssignable = fuzzer( + ProbablySucceed, + "IdAssignable", + """This condition checks to see if the vulnerability qualifies for an ID assignment by the entity evaluating this + conditio, based on the ID assignment rules governing the ID space. For example, in the case of CVE ID assignment, + this would be the process of checking the vulnerability against the CVE ID assignment rules. In a real + implementation, this would probably be a prompt for a human to check the vulnerability against the ID assignment + rules, if applicable. It may be possible to automate this check in some cases. Using the CVE ID assignment rules + as an example, if the vulnerability is in scope for CVE ID assignment, but the individual or organization + evaluating this condition is not the authoritatve CNA for the product, this condition would be expected to fail. + In our stub implementation, this condition succeeds in 2 out of 3 attempts. + """, +) + +# check if the vulnerability qualifies for an ID assignment by the entity evaluating this condition + + +AssignId = fuzzer( + AlwaysSucceed, + "AssignId", + """This node represents the process of assigning an ID to the vulnerability. + For example, in the case of CVE ID assignment, this would be the process of assigning a CVE ID to the vulnerability. + In a real implementation, this could be automatable as an API call to the ID assignment authority, + an automated process to assign an ID from a pool of available IDs, + or it could be a prompt for a human to assign the ID. + In our stub implementation, this node always succeeds. + """, +) + +# assign an ID to the vulnerability or prompt a human to do so diff --git a/vultron/bt/report_management/fuzzer/close_report.py b/vultron/bt/report_management/fuzzer/close_report.py new file mode 100644 index 00000000..213a0475 --- /dev/null +++ b/vultron/bt/report_management/fuzzer/close_report.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides fuzzer leaf nodes for the report management workflow. +""" +from vultron.bt.base.factory import fuzzer +from vultron.bt.base.fuzzer import AlwaysSucceed, UsuallyFail + +# ask a human +OtherCloseCriteriaMet = fuzzer( + UsuallyFail, + "OtherCloseCriteriaMet", + """This condition is used to check if other criteria have been met that would allow the report to be closed. + In an actual implementation, this condition would likely be implemented as a human decision, or perhaps + as a check of the case status versus a set of site-specific criteria or policies. + In our stub implementation, this condition fails with a probability of 3/4. + """, +) + + +# perform a pre-close action or actions +PreCloseAction = fuzzer( + AlwaysSucceed, + "PreCloseAction", + """This node represents the process of performing a pre-close action. An actual implementation of this node could + automate some activity that must be performed before a report can be closed. For example, a quality assurance + process might be triggered at this point. + In our stub implementation, this node always succeeds. + """, +) diff --git a/vultron/bt/report_management/fuzzer/deploy_fix.py b/vultron/bt/report_management/fuzzer/deploy_fix.py new file mode 100644 index 00000000..9c3a9dbe --- /dev/null +++ b/vultron/bt/report_management/fuzzer/deploy_fix.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides fuzzer leaf nodes for the report management workflow. +""" +from vultron.bt.base.factory import fuzzer +from vultron.bt.base.fuzzer import ( + AlmostAlwaysFail, + AlmostAlwaysSucceed, + AlwaysSucceed, + OftenSucceed, + UsuallyFail, + UsuallySucceed, +) + +# ask a human or check if the report has been updated +NoNewDeploymentInfo = fuzzer( + UsuallySucceed, + "NoNewDeploymentInfo", + """This condition is used to check if there is new deployment information. + In most cases, there will be no new deployment information, so this condition will succeed. + In an actual implementation, it might be possible to automate this node as a condition check + based on whether or not new deployment information has been added to the report. + For example, if the report has been updated, then this condition might fail. + """, +) + + +# ask a human to prioritize the deployment +PrioritizeDeployment = fuzzer( + AlmostAlwaysSucceed, + "PrioritizeDeployment", + """This node represents the process of prioritizing the deployment of a fix. In an actual implementation, + this node would likely be implemented as prompt for a human to prioritize the deployment. In our stub + implementation, this node almost always succeeds with a probability of 0.9 so that we can test the rest of the + process. + """, +) + + +# check the status of the mitigation deployment +MitigationDeployed = fuzzer( + UsuallyFail, + "MitigationDeployed", + """This condition is used to check if a mitigation has been deployed. In an actual implementation, + this condition could be a simple status check on the case. + In our stub implementation, this condition fails with a probability of 3/4. + """, +) + + +# check whether mitigations are available to deploy +MitigationAvailable = fuzzer( + OftenSucceed, + "MitigationAvailable", + """This condition is used to check if a mitigation is available. In an actual implementation, this condition could + be a simple status check on the case after mitigation options have been evaluated. + """, +) + + +# prompt a human to deploy a mitigation +DeployMitigation = fuzzer( + UsuallySucceed, + "DeployMitigation", + """This node represents the process of deploying a mitigation. In an actual implementation, + this node would likely be implemented as a prompt for a human to deploy a mitigation. In our stub + implementation, this node usually succeeds with a probability of 3/4. + """, +) + + +# check whether monitoring is required +MonitoringRequirement = fuzzer( + OftenSucceed, + "MonitoringRequirement", + """This condition is used to check whether it is necessary to monitor the deployment of a fix. + This is typically a policy decision, so in an actual implementation, this condition would likely be implemented + a policy check. In our stub implementation, this condition succeeds in 3 of 10 cases. + """, +) + + +MonitorDeployment = fuzzer( + AlwaysSucceed, + "MonitorDeployment", + """This node represents the process of monitoring the deployment of a fix. In an actual implementation, + this node would likely be implemented as a prompt for a human to monitor the deployment. + In our stub implementation, this node always succeeds. + """, +) + + +DeployFix = fuzzer( + AlmostAlwaysFail, + "DeployFix", + """This node represents the process of deploying a fix. In an actual implementation, this node would likely be + implemented as a prompt for a human to deploy a fix. In our stub implementation, this node almost always fails + with a probability of 0.9 so that we can test the rest of the process. + """, +) diff --git a/vultron/bt/report_management/fuzzer/develop_fix.py b/vultron/bt/report_management/fuzzer/develop_fix.py new file mode 100644 index 00000000..ce0c0dc0 --- /dev/null +++ b/vultron/bt/report_management/fuzzer/develop_fix.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides fuzzer leaf nodes for the report management workflow. +""" +from vultron.bt.base.factory import fuzzer +from vultron.bt.base.fuzzer import AlmostAlwaysSucceed + +CreateFix = fuzzer( + AlmostAlwaysSucceed, + "CreateFix", + """This node represents the process of creating a fix for the vulnerability. + In a real implementation, this would probably be a prompt for a human to create a fix, for example by + initiating an internal development process (e.g., in a bug tracking system). + In our stub implementation, this node succeeds in 9 out of 10 attempts, allowing us to exercise the rest of the workflow. + """, +) + +# prompt a human to create a fix diff --git a/vultron/bt/report_management/fuzzer/monitor_threats.py b/vultron/bt/report_management/fuzzer/monitor_threats.py new file mode 100644 index 00000000..22cf0fd7 --- /dev/null +++ b/vultron/bt/report_management/fuzzer/monitor_threats.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides fuzzer leaf nodes for the report management workflow. +""" + +from vultron.bt.base.factory import fuzzer +from vultron.bt.base.fuzzer import AlmostAlwaysFail, AlwaysSucceed, UsuallyFail + +MonitorAttacks = fuzzer( + AlmostAlwaysFail, + "MonitorAttacks", + """This node represents the process of monitoring for attacks against the vulnerability that is the subject of the + report. In an actual implementation, this node would likely be implemented as a process that checks for attacks + in threat intelligence feeds or similar sources. In our stub implementation, this node almost always fails with a + probability of 0.9, reflecting the unlikely possibility that an attack against the vulnerability will be detected + while the report is being processed. + """, +) + +# check threat intelligence feeds + + +MonitorExploits = fuzzer( + AlmostAlwaysFail, + "MonitorExploits", + """This node represents the process of monitoring for exploits against the vulnerability that is the subject of the + report. In an actual implementation, this node would likely be implemented as a process that checks for exploits + in threat intelligence feeds or similar sources. In our stub implementation, this node almost always fails with a + probability of 0.9, reflecting the unlikely possibility that an exploit against the vulnerability will be + detected while the report is being processed. + """, +) + +# check threat intelligence feeds + + +MonitorPublicReports = fuzzer( + UsuallyFail, + "MonitorPublicReports", + """This node represents the process of monitoring for public reports of attacks or exploits against the vulnerability + that is the subject of the report. In an actual implementation, this node would likely be implemented as a process + that checks for public reports in threat intelligence feeds, media reports, or similar sources. In our stub + implementation, this node usually fails with a probability of 3/4, reflecting the unlikely possibility that a public + disclosure of the vulnerability will be detected during the coordination process. + """, +) + +# check threat intelligence feeds, news reports, etc. + + +NoThreatsFound = fuzzer( + AlwaysSucceed, + "NoThreatsFound", + """This condition is set to always succeed so that the fallback node above it will be guaranteed to succeed even + when no monitoring nodes (attacks, exploits, or public reports) have anything to report. + """, +) + +# always return success to keep the process moving diff --git a/vultron/bt/report_management/fuzzer/other_work.py b/vultron/bt/report_management/fuzzer/other_work.py new file mode 100644 index 00000000..edcd4527 --- /dev/null +++ b/vultron/bt/report_management/fuzzer/other_work.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides fuzzer leaf nodes for the report management workflow. +""" +from vultron.bt.base.factory import fuzzer +from vultron.bt.base.fuzzer import AlwaysSucceed + + +# do other work under this node +OtherWork = fuzzer( + AlwaysSucceed, + "OtherWork", + """Placeholder for other work that may be done by in the do_work bt. + This node could be replaced with any other tree of nodes that represent other work that may be done. + In our stub implementation, this node always succeeds. + """, +) diff --git a/vultron/bt/report_management/fuzzer/prioritize_report.py b/vultron/bt/report_management/fuzzer/prioritize_report.py new file mode 100644 index 00000000..4fe1baaf --- /dev/null +++ b/vultron/bt/report_management/fuzzer/prioritize_report.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +""" +This module provides a fuzzer for the report/case prioritization process. +It contains stub implementations of prioritization nodes and conditions. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from vultron.bt.base.factory import fuzzer +from vultron.bt.base.fuzzer import ( + AlmostAlwaysSucceed, + AlwaysSucceed, + ProbablySucceed, + UsuallySucceed, +) + +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +# ask a human +EnoughPrioritizationInfo = fuzzer( + UsuallySucceed, + "EnoughPrioritizationInfo", + """This condition is used to check if there is enough information to prioritize the report. + In an actual implementation, this condition is likely to be implemented as a human decision. + In our stub implementation, this condition succeeds with a probability of 3/4. + """, +) + + +# ask a human to gather prioritization information +GatherPrioritizationInfo = fuzzer( + AlmostAlwaysSucceed, + "GatherPrioritizationInfo", + """This node represents the process of gathering prioritization information. In an actual implementation, + this node would likely be implemented as prompt for a human to gather prioritization information. In our stub + implementation, this node almost always succeeds with a probability of 0.9. + """, +) + + +# ask a human or check if the report has been updated +NoNewPrioritizationInfo = fuzzer( + ProbablySucceed, + "NoNewPrioritizationInfo", + """This condition is used to check if there is new prioritization information. + In most cases, there will be no new prioritization information, so this condition will succeed. + In an actual implementation, it might be possible to automate this node as a condition check + based on whether or not new information has been added to the report. + For example, if the report has been updated, then this condition might fail. + + In this stub implementation, the condition succeeds with a probability of 2/3. + """, +) + + +# implement site-specific logic +OnDefer = fuzzer( + AlwaysSucceed, + "OnDefer", + """This is a stub for now. + It serves as a placeholder for site-specific logic that should be executed when a report is deferred. + + In an actual implementation, this would probably be a call out to a function that + does whatever is necessary when a report is deferred. + E.g., it might trigger some automated process to notify internal stakeholders that the report has been deferred, + or schedule a follow-up task to check on the report at a later date. + """, +) + + +# implement site-specific logic +OnAccept = fuzzer( + AlwaysSucceed, + "OnAccept", + """This is a stub for now. + It serves as a placeholder for site-specific logic that should be executed when a report is accepted. + + In an actual implementation, this would probably be a call out to a function that + does whatever is necessary when a report is accepted. + E.g., it might trigger some automated process to notify internal stakeholders that the report has been accepted, + or set up a series of standard tasks to be completed once the report is accepted. + """, +) diff --git a/vultron/bt/report_management/fuzzer/publication.py b/vultron/bt/report_management/fuzzer/publication.py new file mode 100644 index 00000000..082535be --- /dev/null +++ b/vultron/bt/report_management/fuzzer/publication.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This module provides fuzzer leaf nodes in support of the process of publishing the vulnerability and its artifacts +""" +from vultron.bt.base.factory import fuzzer +from vultron.bt.base.fuzzer import ( + AlmostAlwaysFail, + AlmostAlwaysSucceed, + AlwaysSucceed, + OftenSucceed, + UsuallyFail, + UsuallySucceed, +) + +# check if all of the artifacts for the vulnerability have been published +AllPublished = fuzzer( + AlmostAlwaysFail, + "AllPublished", + """This condition is used to check if all of the artifacts for the vulnerability have been published. In a real + implementation, this would be a simple check to see if all of the intended artifacts for the vulnerability have + been published. In our stub implementation, this condition fails in 9 out of 10 attempts because we are assuming + that the publishing process is part of the workflow we are modeling. + """, +) + + +# check if the publication intents for the vulnerability have been set +PublicationIntentsSet = fuzzer( + UsuallyFail, + "PublicationIntentsSet", + """This condition is used to check if the publication intents for the vulnerability have been set. Publication + intents are used to indicate what artifacts about the vulnerability should be published. In a real + implementation, this would be a simple check to see if the publication intents have been set. In our stub + implementation, this condition fails in 3 out of 4 attempts because we are assuming that the publication intent + setting process is part of the workflow we are modeling. + """, +) + + +# publish the artifacts for the vulnerability or prompt a human to do so +Publish = fuzzer( + AlmostAlwaysSucceed, + "Publish", + """This node represents the process of publishing the artifacts for the vulnerability. In a real implementation, + this could be automatable as an API call to a publishing system, such as a content management system, or it could + be a prompt for a human to publish the artifacts. In our stub implementation, this node succeeds in 9 out of 10 + attempts, allowing us to exercise the rest of the workflow. + """, +) + + +# prioritize the artifacts to be published for the vulnerability or prompt a human to do so +PrioritizePublicationIntents = fuzzer( + AlwaysSucceed, + "PrioritizePublicationIntents", + """This node represents the process of prioritizing the artifacts to be published for the vulnerability. + In a real implementation, this would be a prompt for a human to prioritize the artifacts to be published, or + it could be automated based on the organization's publication priorities and policies. + In our stub implementation, this node always succeeds. + """, +) + + +# check if the exploit for the vulnerability should be published +NoPublishExploit = fuzzer( + UsuallySucceed, + "NoPublishExploit", + """This condition is used to check if the exploit for the vulnerability should be published. + In a real implementation, this would be a simple check to see if the exploit should be published. + In our stub implementation, this condition succeeds in 3 out of 4 attempts so we can + model the concept that exploit publication is not always required. + """, +) + + +# check if the exploit for the vulnerability is ready to be published +ExploitReady = fuzzer( + OftenSucceed, + "ExploitReady", + """This condition is used to check if the exploit for the vulnerability is ready to be published. + In a real implementation, this would be a simple check to see if the exploit is available to be published. + In our stub implementation, this condition succeeds in 7 out of 10 attempts so we can + exercise the rest of the workflow. + """, +) + + +# prompt a human to prepare the exploit for publication +PrepareExploit = fuzzer( + AlmostAlwaysSucceed, + "PrepareExploit", + """This node represents the process of preparing the exploit for the vulnerability to be published. In a real + implementation, this would probably be a prompt for a human to prepare the exploit for publication, for example + by creating the exploit artifact, preparing a description of the exploit, and staging the exploit and associated + files in a publishing system. + """, +) + + +# reprioritize the production of the exploit or prompt a human to do so +ReprioritizeExploit = fuzzer( + AlwaysSucceed, + "ReprioritizeExploit", + """This node represents the process of reprioritizing the exploit for the vulnerability. + The priority of the exploit may change as the vulnerability is analyzed and the exploit is prepared for publication. + In a real implementation, this would be a prompt for a human to reprioritize the production of the exploit. + In our stub implementation, this node always succeeds. + """, +) + + +# check if the fix for the vulnerability should be published +NoPublishFix = fuzzer( + AlmostAlwaysFail, + "NoPublishFix", + """This condition is used to check if the fix for the vulnerability should be published. In a real implementation, + this would be a simple check to see if the fix should be published based on the publication intents set elsewhere + in the workflow. In most cases the fix should be published, so this condition fails in 9 out of 10 attempts in + our stub implementation. + """, +) + + +# prompt a human to prepare the fix for publication +PrepareFix = fuzzer( + AlmostAlwaysSucceed, + "PrepareFix", + """This node represents the process of preparing the fix for the vulnerability to be published. In a real + implementation, this would probably be a prompt for a human to prepare the fix for publication, for example by + creating the fix artifact, preparing a description of the fix, and staging the fix and associated files in a + publishing system. In our stub implementation, this node succeeds in 9 out of 10 attempts, allowing us to + exercise the rest of the workflow. + """, +) + + +# reprioritize the production of the fix or prompt a human to do so +ReprioritizeFix = fuzzer( + AlwaysSucceed, + "ReprioritizeFix", + """This node represents the process of reprioritizing the fix for the vulnerability. + The priority of the fix may change as the vulnerability is analyzed and the fix is prepared for publication. + In a real implementation, this would be a prompt for a human to reprioritize the production of the fix. + In our stub implementation, this node always succeeds. + """, +) + + +# check if the report for the vulnerability should be published +NoPublishReport = fuzzer( + AlmostAlwaysFail, + "NoPublishReport", + """This condition is used to check if the report for the vulnerability should be published. In a real + implementation, this would be a simple check to see if the report should be published based on the publication + intents. In most cases the report should be published, so this condition fails in 9 out of 10 attempts in our + stub implementation. + """, +) + + +# prompt a human to prepare the report for publication +PrepareReport = fuzzer( + AlmostAlwaysSucceed, + "PrepareReport", + """This node represents the process of preparing the report for the vulnerability to be published. In a real + implementation, this would probably be a prompt for a human to prepare the report for publication, for example by + creating the report artifact, preparing a description of the report, and staging the report and associated files + in a publishing system. In our stub implementation, this node succeeds in 9 out of 10 attempts, allowing us to + exercise the rest of the workflow. + """, +) + + +# reprioritize the production of the report or prompt a human to do so +ReprioritizeReport = fuzzer( + AlwaysSucceed, + "ReprioritizeReport", + """This node represents the process of reprioritizing the report for the vulnerability. + The priority of the report may change as the vulnerability is analyzed and the report is prepared for publication. + In a real implementation, this would be a prompt for a human to reprioritize the production of the report. + In our stub implementation, this node always succeeds. + """, +) diff --git a/vultron/bt/report_management/fuzzer/report_to_others.py b/vultron/bt/report_management/fuzzer/report_to_others.py new file mode 100644 index 00000000..0f3d5392 --- /dev/null +++ b/vultron/bt/report_management/fuzzer/report_to_others.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This module provides fuzzer leaf nodes in support of the process of notifying other parties of the report/case +""" +from vultron.bt.base.factory import fuzzer + + +from vultron.bt.base.fuzzer import ( + AlmostAlwaysFail, + AlmostAlwaysSucceed, + AlmostCertainlyFail, + AlwaysSucceed, + ProbablySucceed, + SuccessOrRunning, + UniformSucceedFail, + UsuallyFail, + UsuallySucceed, +) + + +AllPartiesKnown = fuzzer( + UniformSucceedFail, + "AllPartiesKnown", + """This condition is used to check if all parties that should be involved in the report have been identified. + In an actual implementation, this condition would likely be implemented as a human decision. + In our stub implementation, this condition succeeds with a probability of 0.5. + """, +) + + +# prompt for a human to identify vendors +IdentifyVendors = fuzzer( + SuccessOrRunning, + "IdentifyVendors", + """This node represents the process of identifying vendors that should be involved in the report. + In an actual implementation, this node would likely be implemented as prompt for a human to identify vendors. + In our stub implementation, this node succeeds with a probability of 0.5. + """, +) + + +# prompt for a human to identify coordinators +IdentifyCoordinators = fuzzer( + SuccessOrRunning, + "IdentifyCoordinators", + """This node represents the process of identifying coordinators that should be involved in the report. + In an actual implementation, this node would likely be implemented as prompt for a human to identify coordinators. + In our stub implementation, this node succeeds with a probability of 0.5. + """, +) + + +# prompt for a human to identify other parties +IdentifyOthers = fuzzer( + AlwaysSucceed, + "IdentifyOthers", + """This node represents the process of identifying other parties that should be involved in the report. + In an actual implementation, this node would likely be implemented as prompt for a human to identify other parties. + In our stub implementation, this node always succeeds. + """, +) + + +# check if all identified parties have been notified +NotificationsComplete = fuzzer( + UniformSucceedFail, + "NotificationsComplete", + """This condition is used to check if all notifications have been sent. In an actual implementation, this condition + could be a simple check to see if all identified parties have been notified. In our stub implementation, + this condition succeeds with a probability of 0.5. + """, +) + + +# choose a recipient from a list of identified parties +ChooseRecipient = fuzzer( + AlwaysSucceed, + "ChooseRecipient", + """This node represents the process of choosing a recipient for the notification. In a real implementation, + this could be fully automated by choosing a recipient to notify from a list of identified parties. In our stub + implementation, this node always succeeds so that we can exercise the rest of the workflow. + """, +) + + +# remove a recipient from the list of identified parties +RemoveRecipient = fuzzer( + AlwaysSucceed, + "RemoveRecipient", + """This node represents the process of removing a recipient from the list of identified parties. In a real + implementation, this could be fully automated by removing a recipient from the list of identified parties based + on some criteria. In our stub implementation, this node always succeeds so that we can exercise the rest of the + workflow. + """, +) + + +# check effort against some threshold or ask a human +RecipientEffortExceeded = fuzzer( + AlmostCertainlyFail, + "RecipientEffortExceeded", + """This condition is used to check if the effort expended to notify a recipient has exceeded some threshold. In a + real implementation, this could be a simple check to see if the effort expended to notify a recipient has + exceeded some threshold. For example, an organization might have a policy that no more than 3 attempts should be + made to notify a recipient. Alternatively, an organization might have a policy that no more than 1 hour of effort + should be expended to notify a recipient. It may also be left up to the discretion of the analyst to determine if + the effort expended to notify a recipient has exceeded some threshold. In our stub implementation, this condition + only succeeds in 7 out of 100 attempts. + """, +) + + +# compare the intended recipient's embargo policy with the policy associated with the case or ask a human +PolicyCompatible = fuzzer( + ProbablySucceed, + "PolicyCompatible", + """This node represents the process to see if the potential recipient's embargo policy is compatible with the + expectations set out in the case. In a real implementation, this might be automated as a comparison between the + intended recipient's embargo policy and the policy associated with the case. Alternatively, this might be a + prompt for a human to determine if the potential recipient's embargo policy is compatible with the case. In our + stub implementation, this node succeeds with a probability of 2/3. + """, +) + + +# lookup contact info in a database or ask a human +FindContact = fuzzer( + UsuallySucceed, + "FindContact", + """This node represents the process of finding contact info for the potential recipient. In a real implementation, + this might be automated as a lookup in a database of contacts, or a prompt for a human to find contact info. In + our stub implementation, this node succeeds with a probability of 3/4. + """, +) + + +# check if the potential recipient is in the RM.START state +RcptNotInQrmS = fuzzer( + AlmostAlwaysSucceed, + "RcptNotInQrmS", + """This condition is used to check if the potential recipient is in the RM.START state, indicating that they have + not been notified yet. In a real implementation, this might be a simple check to see if the potential recipient + is in the RM.START state. In our stub implementation, this condition only fails in 1 out of 10 attempts. + """, +) + + +# set the potential recipient's state to RM.RECEIVED +SetRcptQrmR = fuzzer( + AlwaysSucceed, + "SetRcptQrmR", + """This node represents the process of setting the potential recipient's state to RM.RECEIVED. + In a real implementation, this might be automated as a simple state transition from RM.START to RM.RECEIVED. + In our stub implementation, this node always succeeds. + """, +) + + +# check effort against some threshold or ask a human +TotalEffortLimitMet = fuzzer( + AlmostAlwaysFail, + "TotalEffortLimitMet", + """This condition is used to check if the total effort expended to notify all recipients has exceeded some + threshold. In a real implementation, this could be a simple check to see if the total effort expended to notify + all recipients has exceeded some threshold. For example, an organization might have a policy that no more than 20 + hours of effort should be expended to notify all recipients. It may also be left up to the discretion of the + analyst to determine if the total effort expended to notify all recipients has exceeded some threshold. In our + stub implementation, this condition fails in 9 out of 10 attempts. + """, +) + + +HaveReportToOthersCapability = fuzzer( + UsuallySucceed, + "HaveReportToOthersCapability", + """ + This node represents the ability to report to others. In a real implementation, it would be replaced + with a capability check. + """, +) + + +MoreVendors = fuzzer( + UsuallyFail, + "MoreVendors", + """ + This condition is used to check if there are more vendors to notify. In a real implementation, this could be a + query to a database of vendors. In our stub implementation, this condition fails stochastically. + """, +) + + +MoreCoordinators = fuzzer( + AlmostAlwaysFail, + "MoreCoordinators", + """ + This condition is used to check if there are more coordinators to notify. In a real implementation, this could be a + query to a database of coordinators. In our stub implementation, this condition fails stochastically. + """, +) + + +MoreOthers = fuzzer( + AlmostAlwaysFail, + "MoreOthers", + """ + This condition is used to check if there are more other parties to notify. In a real implementation, this could be a + query to a database of other parties. In our stub implementation, this condition fails stochastically. + """, +) + +InjectParticipant = fuzzer( + AlwaysSucceed, + "InjectParticipant", + """ + Inject a participant into a case. + """, +) + +InjectVendor = fuzzer( + InjectParticipant, + "InjectVendor", + InjectParticipant.__doc__, +) + +InjectCoordinator = fuzzer( + InjectParticipant, + "InjectCoordinator", + InjectParticipant.__doc__, +) + +InjectOther = fuzzer( + InjectParticipant, + "InjectOther", + InjectParticipant.__doc__, +) diff --git a/vultron/bt/report_management/fuzzer/validate_report.py b/vultron/bt/report_management/fuzzer/validate_report.py new file mode 100644 index 00000000..518caf3b --- /dev/null +++ b/vultron/bt/report_management/fuzzer/validate_report.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This is a fuzzer for the validation process. +It provides stub implementations of the validation conditions and actions. +""" +from vultron.bt.base.factory import fuzzer +from vultron.bt.base.fuzzer import ( + AlmostAlwaysSucceed, + ProbablySucceed, + UsuallySucceed, +) + +# ask a human or check if the report has been updated +NoNewValidationInfo = fuzzer( + ProbablySucceed, + "NoNewValidationInfo", + """This condition is used to check if there is new validation information. + In most cases, there will be no new validation information, so this condition will succeed. + + In an actual implementation, it might be possible to automate this node as a condition check + based on whether or not new information has been added to the report. + For example, if the report has been updated, then this condition could succeed. Otherwise, if the report has not been + updated, then this condition could fail. Or it could be left to a human to decide. + + In this stub implementation, the condition succeeds with a probability of 2/3. + """, +) + + +# ask a human +EvaluateReportCredibility = fuzzer( + AlmostAlwaysSucceed, + "EvaluateReportCredibility", + """This condition is used to evaluate the credibility of the report. + Suggestions for evaluating report credibility are provided in the SSVC documentation. + + In actual implementation, it is likely that this condition would be implemented as a human decision. + + In this stub implementation, the condition almost always succeeds with a probability of 0.9. + """, +) + +# ask a human +EvaluateReportValidity = fuzzer( + AlmostAlwaysSucceed, + "EvaluateReportValidity", + """This condition is used to evaluate the validity of the report. + Report validity is contextual to the receiving organization. + In order for a report to be valid, it must be credible, and it + must meet the organization's criteria for validity. + For example, a report may be credible, but it may not be valid + if it is not in scope for the organization. + Note that invalid reports are not necessarily invalid for all + organizations, nor does an invalid report necessarily mean + that the report is false or not credible. + """, +) + + +# ask a human +EnoughValidationInfo = fuzzer( + UsuallySucceed, + "EnoughValidationInfo", + """This condition is used to check if there is enough information to validate the report. + In an actual implementation, this condition is likely to be implemented as a human decision. + In our stub implementation, this condition succeeds with a probability of 3/4. + """, +) + + +# ask a human to gather validation information +GatherValidationInfo = fuzzer( + AlmostAlwaysSucceed, + "GatherValidationInfo", + """This node represents the process of gathering validation information. + In an actual implementation, this node would likely be implemented as prompt for a human to gather validation information. + In our stub implementation, this node almost always succeeds with a probability of 0.9. + """, +) diff --git a/vultron/bt/report_management/report_priority_states.py b/vultron/bt/report_management/report_priority_states.py new file mode 100644 index 00000000..8f3e447d --- /dev/null +++ b/vultron/bt/report_management/report_priority_states.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides a report priority enumeration class to reflect basic defer/act priority +""" + + +import enum + + +class ReportPriority(enum.Enum): + """Report priority states + + DEFER: Report is deferred, no further action is required at this time + SCHEDULED: Report should be scheduled for processing at the next available opportunity + OUT_OF_CYCLE: Report should be processed out of cycle, but not necessarily immediately + IMMEDIATE: Report should be processed immediately (stay late, work on the weekend, etc.) + + Note that these states reflect those used in the Stakeholder Specific Vulnerability Categorization (SSVC) model. + For convenience, we have mapped the CVSS v3.1 severity levels to the corresponding SSVC priority states. + """ + + # SSVC 2.0 Priority States + DEFER = 0 + SCHEDULED = 1 + OUT_OF_CYCLE = 2 + IMMEDIATE = 3 + + # CVSS v3.1 Severity to Report Priority mapping + LOW = DEFER + MEDIUM = SCHEDULED + HIGH = OUT_OF_CYCLE + CRITICAL = IMMEDIATE diff --git a/vultron/bt/report_management/states.py b/vultron/bt/report_management/states.py new file mode 100644 index 00000000..91c729dc --- /dev/null +++ b/vultron/bt/report_management/states.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides an enumeration of Report Management states. +""" + +import logging +from enum import Enum + +logger = logging.getLogger(__name__) + + +class RM(Enum): + """ + Report Management States + + START: Report has not yet been received + RECEIVED: Report has been received but not yet validated + INVALID: Report has been received but is invalid + VALID: Report has been received and is valid + DEFERRED: Report has been received and is valid but has been deferred + ACCEPTED: Report has been received, is valid, and has been accepted + CLOSED: Report has been closed + """ + + REPORT_MANAGEMENT_START = "START" + REPORT_MANAGEMENT_RECEIVED = "RECEIVED" + REPORT_MANAGEMENT_INVALID = "INVALID" + REPORT_MANAGEMENT_VALID = "VALID" + REPORT_MANAGEMENT_DEFERRED = "DEFERRED" + REPORT_MANAGEMENT_ACCEPTED = "ACCEPTED" + REPORT_MANAGEMENT_CLOSED = "CLOSED" + + # convenience aliases + START = REPORT_MANAGEMENT_START + RECEIVED = REPORT_MANAGEMENT_RECEIVED + INVALID = REPORT_MANAGEMENT_INVALID + VALID = REPORT_MANAGEMENT_VALID + DEFERRED = REPORT_MANAGEMENT_DEFERRED + ACCEPTED = REPORT_MANAGEMENT_ACCEPTED + CLOSED = REPORT_MANAGEMENT_CLOSED + + S = REPORT_MANAGEMENT_START + R = REPORT_MANAGEMENT_RECEIVED + I = REPORT_MANAGEMENT_INVALID + V = REPORT_MANAGEMENT_VALID + D = REPORT_MANAGEMENT_DEFERRED + A = REPORT_MANAGEMENT_ACCEPTED + C = REPORT_MANAGEMENT_CLOSED + + +# Report Management States that can be closed +RM_CLOSABLE = ( + RM.INVALID, + RM.DEFERRED, + RM.ACCEPTED, +) + +# Report Management States that are not closed +RM_UNCLOSED = ( + RM.START, + RM.RECEIVED, + RM.INVALID, + RM.VALID, + RM.DEFERRED, + RM.ACCEPTED, +) + +# Report Management States that are active +RM_ACTIVE = ( + RM.RECEIVED, + RM.VALID, + RM.ACCEPTED, +) + + +def main(): + rootlogger = logging.getLogger() + rootlogger.setLevel(logging.DEBUG) + hdlr = logging.StreamHandler() + rootlogger.addHandler(hdlr) + + logger.debug("Available Report Management States:") + for k, v in RM.__members__.items(): + logger.debug(f"state: {k} -> value: {v.value}") + + logger.debug(f"RMclosable = {RM_CLOSABLE}") + logger.debug(f"RMunclosed = {RM_UNCLOSED}") + logger.debug(f"RMactive = {RM_ACTIVE}") + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/report_management/transitions.py b/vultron/bt/report_management/transitions.py new file mode 100644 index 00000000..f2114db7 --- /dev/null +++ b/vultron/bt/report_management/transitions.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides the transitions between states in the q_rm state machine +""" + +from dataclasses import dataclass +from typing import List + +from vultron.bt.common import EnumStateTransition, state_change +from vultron.bt.report_management.states import RM + + +@dataclass +class RmTransition(EnumStateTransition): + """Represents a transition between two states in the q_rm state machine""" + + start_states = List[RM] + end_state = RM + + +# Create the allowed transitions +_to_R = RmTransition(start_states=[RM.START], end_state=RM.RECEIVED) +_to_I = RmTransition(start_states=[RM.RECEIVED], end_state=RM.INVALID) +_to_V = RmTransition( + start_states=[RM.RECEIVED, RM.INVALID], end_state=RM.VALID +) +_to_D = RmTransition( + start_states=[RM.VALID, RM.ACCEPTED], end_state=RM.DEFERRED +) +_to_A = RmTransition( + start_states=[RM.VALID, RM.DEFERRED], end_state=RM.ACCEPTED +) +_to_C = RmTransition( + start_states=[RM.INVALID, RM.DEFERRED, RM.ACCEPTED], end_state=RM.CLOSED +) + +# Create the state change functions +q_rm_to_R = state_change(key="q_rm", transition=_to_R) +q_rm_to_I = state_change(key="q_rm", transition=_to_I) +q_rm_to_V = state_change(key="q_rm", transition=_to_V) +q_rm_to_D = state_change(key="q_rm", transition=_to_D) +q_rm_to_A = state_change(key="q_rm", transition=_to_A) +q_rm_to_C = state_change(key="q_rm", transition=_to_C) diff --git a/vultron/bt/roles/__init__.py b/vultron/bt/roles/__init__.py new file mode 100644 index 00000000..b8d147d8 --- /dev/null +++ b/vultron/bt/roles/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University diff --git a/vultron/bt/roles/conditions.py b/vultron/bt/roles/conditions.py new file mode 100644 index 00000000..9921c9c8 --- /dev/null +++ b/vultron/bt/roles/conditions.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +"""file: cvd_role_conditions +author: adh +created_at: 4/26/22 10:23 AM +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +from vultron.bt.base.bt_node import BtNode +from vultron.bt.base.factory import condition_check, invert +from vultron.bt.roles.states import CVDRoles + + +def role_is_vendor(obj: BtNode) -> bool: + """True if the CVD role is vendor""" + return obj.bb.CVD_role & CVDRoles.VENDOR + + +RoleIsVendor = condition_check("RoleIsVendor", role_is_vendor) + + +def role_is_deployer(obj: BtNode) -> bool: + """True if the CVD role is deployer""" + return obj.bb.CVD_role & CVDRoles.DEPLOYER + + +RoleIsDeployer = condition_check("RoleIsDeployer", role_is_deployer) + + +def role_is_coordinator(obj: BtNode) -> bool: + """True if the CVD role is coordinator""" + return obj.bb.CVD_role & CVDRoles.COORDINATOR + + +RoleIsCoordinator = condition_check("RoleIsCoordinator", role_is_coordinator) + + +RoleIsNotVendor = invert( + "RoleIsNotVendor", "SUCCEED if CVD role is not Vendor", RoleIsVendor +) + +RoleIsNotDeployer = invert( + "RoleIsNotDeployer", "SUCCEED if CVD role is not Deployer", RoleIsDeployer +) + +RoleIsNotCoordinator = invert( + "RoleIsNotCoordinator", + "SUCCEED if CVD role is not Coordinator", + RoleIsCoordinator, +) + + +def main(): + pass + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/roles/states.py b/vultron/bt/roles/states.py new file mode 100644 index 00000000..c53dcc77 --- /dev/null +++ b/vultron/bt/roles/states.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +"""Provides CVD Role states +""" + + +from enum import Flag, auto + + +class CVDRoles(Flag): + """Coordinated Vulnerability Disclosure Roles as a Flag (bitwise) + + NO_ROLE: No role is assigned + FINDER: CVD Finder (entity that finds the vulnerability) + REPORTER: CVD Reporter (often the same as the finder) + VENDOR: CVD Vendor (supplier of the affected product or service, usually provider of the fix) + DEPLOYER: CVD Deployer (entity that deploys the fix, sometimes the same as the vendor) + COORDINATOR: CVD Coordinator (entity that coordinates the CVD process) + OTHER: Other role + """ + + NO_ROLE = 0 + FINDER = auto() + REPORTER = auto() + VENDOR = auto() + DEPLOYER = auto() + COORDINATOR = auto() + OTHER = auto() + + # shorthand + F = FINDER + R = REPORTER + V = VENDOR + D = DEPLOYER + C = COORDINATOR + O = OTHER + + # frequent combinations + FINDER_REPORTER = FINDER | REPORTER + FINDER_VENDOR = FINDER | VENDOR + FINDER_REPORTER_VENDOR = FINDER | REPORTER | VENDOR + FINDER_REPORTER_VENDOR_DEPLOYER = FINDER | REPORTER | VENDOR | DEPLOYER + FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR = ( + FINDER | REPORTER | VENDOR | DEPLOYER | COORDINATOR + ) + + VENDOR_DEPLOYER = VENDOR | DEPLOYER + VENDOR_COORDINATOR = VENDOR | COORDINATOR + + FINDER_COORDINATOR = FINDER | COORDINATOR + FINDER_DEPLOYER = FINDER | DEPLOYER + + FR = FINDER_REPORTER + FV = FINDER_VENDOR + FRV = FINDER_REPORTER_VENDOR + FRVD = FINDER_REPORTER_VENDOR_DEPLOYER + FRVDC = FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR + + +def add_role(old_role, new_role): + return old_role | new_role + + +def main(): + for x in CVDRoles.__members__: + print("\t".join([x, str(CVDRoles[x]), str(CVDRoles[x].value)])) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/states.py b/vultron/bt/states.py new file mode 100644 index 00000000..61db5c5c --- /dev/null +++ b/vultron/bt/states.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides state management for the Vultron Behavior Tree +""" + +from collections import deque +from dataclasses import dataclass, field +from enum import Flag, auto +from typing import Any, Callable, Deque, Dict, List + +from vultron.bt.base.bt import Blackboard as Blackboard +from vultron.bt.embargo_management.states import EM +from vultron.bt.messaging.states import MessageTypes as MT +from vultron.bt.report_management.report_priority_states import ( + ReportPriority, +) +from vultron.bt.report_management.states import RM +from vultron.bt.roles.states import CVDRoles +from vultron.case_states.states import CS + + +class CapabilityFlag(Flag): + NoCapability = 0 + DiscoverVulnerability = auto() + ReportToOthers = auto() + DevelopFix = auto() + DeployFix = auto() + + +@dataclass +class ActorState(Blackboard): + CVD_role: CVDRoles = CVDRoles.NO_ROLE + others: Dict = field(default_factory=dict) + + q_rm: RM = RM.START + q_em: EM = EM.NO_EMBARGO + q_cs: CS = CS.vfdpxa + + q_rm_history: List[RM] = field(default_factory=list) + q_em_history: List[EM] = field(default_factory=list) + q_cs_history: List[CS] = field(default_factory=list) + + incoming_messages: Deque = field(default_factory=deque) + + emit_func: Callable = None + + msgs_emitted_this_tick: List[MT] = field(default_factory=list) + msgs_received_this_tick: List[MT] = field(default_factory=list) + msg_history: List[MT] = field(default_factory=list) + current_message: MT = None + + priority: ReportPriority = ReportPriority.DEFER + prioritization_count = 0 + + capabilities: CapabilityFlag = 0 + + reporting_effort_budget: int = 200 + + add_participant_func: Callable = None + currently_notifying: Any = None + + case: Any = None + + name: str = "ActorName" + + +def main(): + from pprint import pprint + + a = ActorState() + pprint(a.__dict__) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/vul_discovery/__init__.py b/vultron/bt/vul_discovery/__init__.py new file mode 100644 index 00000000..aa360b38 --- /dev/null +++ b/vultron/bt/vul_discovery/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Models the vulnerability discovery process as a behavior tree +""" diff --git a/vultron/bt/vul_discovery/behaviors.py b/vultron/bt/vul_discovery/behaviors.py new file mode 100644 index 00000000..a5938b6e --- /dev/null +++ b/vultron/bt/vul_discovery/behaviors.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides vulnerability discovery behaviors +""" +from vultron.bt.base.bt_node import BtNode +from vultron.bt.base.factory import ( + condition_check, + fallback_node, + sequence_node, +) +from vultron.bt.case_state.conditions import CSinStateVendorUnaware +from vultron.bt.case_state.transitions import q_cs_to_V +from vultron.bt.common import show_graph +from vultron.bt.messaging.outbound.behaviors import EmitCV, EmitRS +from vultron.bt.report_management.conditions import RMnotInStateStart +from vultron.bt.report_management.transitions import q_rm_to_R +from vultron.bt.roles.conditions import RoleIsNotVendor +from vultron.bt.roles.states import CVDRoles +from vultron.bt.vul_discovery.fuzzer import ( + DiscoverVulnerability, + HaveDiscoveryPriority, + NoVulFound, +) + + +def have_discovery_capability(obj: BtNode) -> bool: + """True if the participant has the ability to discover vulnerabilities.""" + return obj.bb.CVD_role & CVDRoles.FINDER + + +HaveDiscoveryCapability = condition_check( + "HaveDiscoveryCapability", + have_discovery_capability, +) + +VendorBecomesAware = sequence_node( + "VendorBecomesAware", + """Vendor becomes aware of vulnerability. + Steps: + 1. Check whether the vendor is already aware of the vulnerability. + 2. If not, transition the case state to Vendor Aware. + 3. Emit a CV message to announce that the vendor is aware of the vulnerability. + """, + CSinStateVendorUnaware, + q_cs_to_V, + EmitCV, +) + + +SeeIfVendorIsAware = fallback_node( + "SeeIfVendorIsAware", + """Check if the vendor is aware of the vulnerability. + If the finder is not the vendor, stop. + Otherwise, note that the vendor is aware of the vulnerability. + """, + RoleIsNotVendor, + VendorBecomesAware, +) + +FindVulnerabilities = sequence_node( + "FindVulnerabilities", + """This node represents the process of finding vulnerabilities. + Steps: + 1. Check if the participant has the ability to discover vulnerabilities. + 2. Check if the participant has a priority for discovering vulnerabilities. + 3. If the participant has the ability to discover vulnerabilities and has a priority for discovering vulnerabilities, + then discover vulnerabilities. + 5. If a vulnerability is discovered, emit an RS message and set the RM state to Received for this participant. + 6. Check to see if we are the vendor. If so, note that the vendor is aware of the vulnerability. + """, + HaveDiscoveryCapability, + HaveDiscoveryPriority, + DiscoverVulnerability, + q_rm_to_R, + EmitRS, + SeeIfVendorIsAware, +) + +DiscoverVulnerabilityBt = fallback_node( + "DiscoverVulnerabilityBt", + """This is the top-level node for the vulnerability discovery process. + Steps: + 1. Check if the RM state is not in the start state. If so, stop. + 2. Find vulnerabilities. If a vulnerability is found, stop. + 3. If no vulnerabilities are found, note that no vulnerabilities were found. + """, + RMnotInStateStart, + FindVulnerabilities, + NoVulFound, +) + + +def main(): + show_graph(DiscoverVulnerabilityBt) + + +if __name__ == "__main__": + main() diff --git a/vultron/bt/vul_discovery/fuzzer.py b/vultron/bt/vul_discovery/fuzzer.py new file mode 100644 index 00000000..9fa5cc95 --- /dev/null +++ b/vultron/bt/vul_discovery/fuzzer.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +"""file: fuzzer +author: adh +created_at: 6/23/22 12:11 PM +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from vultron.bt.base.factory import fuzzer +from vultron.bt.base.fuzzer import AlwaysSucceed, ProbablySucceed + +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +NoVulFound = fuzzer( + AlwaysSucceed, + "NoVulFound", + "Placeholder for the case where no vulnerability is found." + "Always succeeds to keep the workflow moving.", +) + + +HaveDiscoveryPriority = fuzzer( + AlwaysSucceed, + "HaveDiscoveryPriority", + "Condition check to see if we have prioritized vulnerability discovery as a goal.", +) + +DiscoverVulnerability = fuzzer( + ProbablySucceed, + "DiscoverVulnerability", + "Placeholder for the process of discovering a vulnerability." + "In our stub implementation, we need to succeed relatively often to keep the workflow moving.", +) diff --git a/vultron/case_states/states.py b/vultron/case_states/states.py index 3c496194..966fdd8e 100644 --- a/vultron/case_states/states.py +++ b/vultron/case_states/states.py @@ -18,7 +18,7 @@ It also provides functions for converting between state strings and enums. """ -from enum import Enum, Flag, IntEnum +from enum import Enum, IntEnum from typing import NamedTuple, Tuple from vultron.case_states.validations import ensure_valid_state @@ -297,12 +297,11 @@ class CS_pxa(Enum): class CompoundState(NamedTuple): - vfd_state: VfdState - pxa_state: PxaState + vfd_state: CS_vfd + pxa_state: CS_pxa -# TODO this is still broken -class CS_vfdpxa(Enum): +class CS(Enum): # vfd pxa vfdpxa = CompoundState(CS_vfd.vfd, CS_pxa.pxa) # vfd Pxa @@ -372,164 +371,6 @@ class CS_vfdpxa(Enum): VFDPXA = CompoundState(CS_vfd.VFD, CS_pxa.PXA) -class CS(Flag): - """Represents the state of a case. The state is a combination of the vendor fix path state and the public state. - - Vendor fix path states: - v = vendor unaware - V = vendor aware - - f = fix not ready - F = fix ready - - d = fix not deployed - D = fix deployed - - Public case states: - p = public unaware - P = public aware - - x = exploit not published - X = exploit published - - a = attacks not observed - A = attacks observed - """ - - vfDpxa = 32 - vFdpxa = 16 - Vfdpxa = 8 - vfdPxa = 4 - vfdpXa = 2 - vfdpxA = 1 - vfdpxa = 0 - - D = vfDpxa - F = vFdpxa - V = Vfdpxa - P = vfdPxa - X = vfdpXa - A = vfdpxA - - VFdpxa = V | F - VFDpxa = V | F | D - - vfdpXA = X | A - vfdPxA = P | A - vfdPXa = P | X - vfdPXA = P | X | A - - VfdpxA = V | A - VfdpXa = V | X - VfdpXA = V | X | A - VfdPxa = V | P - VfdPxA = V | P | A - VfdPXa = V | P | X - VfdPXA = V | P | X | A - - VFdpxA = V | F | A - VFdpXa = V | F | X - VFdpXA = V | F | X | A - VFdPxa = V | F | P - VFdPxA = V | F | P | A - VFdPXa = V | F | P | X - VFdPXA = V | F | P | X | A - - VFDpxA = V | F | D | A - VFDpXa = V | F | D | X - VFDpXA = V | F | D | X | A - VFDPxa = V | F | D | P - VFDPxA = V | F | D | P | A - VFDPXa = V | F | D | P | X - VFDPXA = V | F | D | P | X | A - - vfd = vfdpxa - Vfd = Vfdpxa - VFd = VFdpxa - VFD = VFDpxa - - pxa = vfdpxa - Pxa = vfdPxa - pXa = vfdpXa - pxA = vfdpxA - PXa = vfdPXa - pXA = vfdpXA - PxA = vfdPxA - PXA = vfdPXA - - # pairs - VF = VFdpxa - VP = VfdPxa - VX = VfdpXa - VA = VfdpxA - - # triples - VFP = VFdPxa - VFX = VFdpXa - VFA = VFdpxA - VPA = VfdPxA - VPX = VfdPXa - VXA = VfdpXA - - # quads - VFDP = VFDPxa - VFDX = VFDpXa - VFDA = VFDpxA - - VFPX = VFdPXa - VFPA = VFdPxA - VFXA = VFdpXA - - VPXA = VfdPXA - - # quintuples - VFDPA = VFDPxA - VFDPX = VFDPXa - VFDXA = VFDpXA - VFPXA = VFdPXA - - -# convenience aliases for the states -all_states = ( - # vendor unaware - CS.vfdpxa, - CS.vfdpxA, - CS.vfdpXa, - CS.vfdpXA, - CS.vfdPxa, - CS.vfdPxA, - CS.vfdPXa, - CS.vfdPXA, - # vendor aware, fix not ready - CS.Vfdpxa, - CS.VfdpxA, - CS.VfdpXa, - CS.VfdpXA, - CS.VfdPxa, - CS.VfdPxA, - CS.VfdPXa, - CS.VfdPXA, - # vendor aware, fix ready, fix not deployed - CS.VFdpxa, - CS.VFdpxA, - CS.VFdpXa, - CS.VFdpXA, - CS.VFdPxa, - CS.VFdPxA, - CS.VFdPXa, - CS.VFdPXA, - # vendor aware, fix ready, fix deployed - CS.VFDpxa, - CS.VFDpxA, - CS.VFDpXa, - CS.VFDpXA, - CS.VFDPxa, - CS.VFDPxA, - CS.VFDPXa, - CS.VFDPXA, -) - - def _last3(s): return s[-3:] diff --git a/vultron/demo/__init__.py b/vultron/demo/__init__.py new file mode 100644 index 00000000..0da36b8e --- /dev/null +++ b/vultron/demo/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides demo functionality for Vultron behaviors. +""" diff --git a/vultron/demo/vultrabot.py b/vultron/demo/vultrabot.py new file mode 100644 index 00000000..99e4281b --- /dev/null +++ b/vultron/demo/vultrabot.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides a simulated Vultron behavior tree simulator bot. +""" +import argparse +import dataclasses +import logging +import sys + +import pandas as pd + +from vultron.bt.behaviors import CvdProtocolBt, CvdProtocolRoot, STATELOG +from vultron.bt.common import show_graph +from vultron.bt.messaging.behaviors import incoming_message +from vultron.bt.messaging.inbound.fuzzer import generate_inbound_message +from vultron.bt.roles.states import CVDRoles + +logger = logging.getLogger(__name__) + +pd.set_option("display.max_rows", 500) +pd.set_option("display.max_columns", 500) +pd.set_option("display.width", 1000) + + +def main(args) -> None: + """ + Instantiates a `CvdProtocolBt` object and runs it until either it closes or 1000 ticks have passed. + This demo is basically simulating a CVD agent coordinating a CVD case with itself. + The agent's role is set to `FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR`, which means it will + perform all the roles in the CVD case. + Messages emitted in one tick might be received later in the same tick, or in a future tick. + + !!! tip + + This demo leverages the ability to use leaf nodes as stochastic process fuzzers. + Using this feature, we can simulate the behavior of a CVD agent without having to + implement any actual communication mechanisms or simulate any complex real-world processes. + + One interesting effect of this design is that the places where the demo uses a fuzzer node are often + indicative of places where an actual bot would either need to call out to either a data source or + a human to decide what to do next. This is a good example of how the Vultron behavior tree + can be used to model complex reactive processes in a way that is still easy to understand and reason about. + + !!! note + + There is no underlying communication mechanism in this demo, so messages are not actually + sent anywhere. Instead, they are just added to the blackboard's incoming message queue. + They also have no content, and are only represented as message types. + + !!! warning + + This demo is not intended to be a fully realistic simulation of a CVD case. It is only intended + to demonstrate the behavior of the Vultron behavior tree. + """ + _setup_logger(args) + + if args.print_tree: + logger.info("Printing tree and exiting") + show_graph(CvdProtocolRoot) + sys.exit() + + _run_simulation() + _print_sim_result() + + +def _run_simulation(): + tick = 0 + with CvdProtocolBt() as tree: + tree.bb.CVD_role = CVDRoles.FINDER_REPORTER_VENDOR_DEPLOYER_COORDINATOR + + for tick in range(1000): + tick += 1 + logger.debug(f"# tick {tick} #") + tree.tick() + + # maybe add a random message to the incoming queue + msg = generate_inbound_message(tree.bb) + if msg is not None: + incoming_message(tree.bb, msg) + + if tree.closed: + # do one last snapshot + tree.root.children[0].tick() + break + logger.info(f"Closed in {tick} ticks") + for k, v in dataclasses.asdict(tree.bb).items(): + if "history" in k: + logger.info(f"### {k} ###") + for i, row in enumerate(v, start=1): + logger.info(f" {i} {row}") + + +def _shorten_names(y): + return tuple([x.value for x in y]) + + +def _print_sim_result(): + df = pd.DataFrame(STATELOG) + df.index += 1 + + df.q_rm = df.q_rm.apply(lambda x: x.value) + df.q_em = df.q_em.apply(lambda x: x.value) + df.msgs_received_this_tick = df.msgs_received_this_tick.apply( + _shorten_names + ) + df.msgs_emitted_this_tick = df.msgs_emitted_this_tick.apply(_shorten_names) + df.CVD_role = df.CVD_role.apply(lambda x: x.name) + df.q_cs = df.q_cs.apply(lambda x: x.name) + df = df.drop_duplicates() + print(df) + + +def _setup_logger(args): + global logger + logger = logging.getLogger() + logger.setLevel(args.log_level) + hdlr = logging.StreamHandler() + logger.addHandler(hdlr) + + +def _parse_args(): + parser = argparse.ArgumentParser( + description="Run a Vultron behavior tree demo" + ) + parser.add_argument( + "-v", + "--verbose", + dest="log_level", + action="store_const", + const=logging.INFO, + default=logging.WARNING, + help="verbose output", + ) + parser.add_argument( + "-d", + "--debug", + dest="log_level", + action="store_const", + const=logging.DEBUG, + default=logging.WARNING, + help="debug output", + ) + parser.add_argument( + "-q", + "--quiet", + dest="log_level", + action="store_const", + const=logging.WARNING, + default=logging.WARNING, + help="quiet output", + ) + parser.add_argument( + "-t", + "--print-tree", + dest="print_tree", + action="store_true", + default=False, + help="print the tree and exit", + ) + + return parser.parse_args() + + +if __name__ == "__main__": + main(_parse_args()) diff --git a/vultron/scripts/__init__.py b/vultron/scripts/__init__.py new file mode 100644 index 00000000..b8d147d8 --- /dev/null +++ b/vultron/scripts/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University diff --git a/vultron/scripts/vultrabot.py b/vultron/scripts/vultrabot.py new file mode 100644 index 00000000..a7ee6486 --- /dev/null +++ b/vultron/scripts/vultrabot.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +""" +Implements a command line interface behavior tree demo for the Vultron package. +""" +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +import argparse +import logging + +from vultron.bt.base.demo.pacman import main as run_pacman +from vultron.bt.base.demo.robot import main as run_robot +from vultron.demo.vultrabot import main as run_cvd + + +def main(): + """ + Implements a command line interface behavior tree demo for the Vultron package. + """ + parser = argparse.ArgumentParser( + description="Vultrabot Command Line Interface" + ) + # debug + parser.add_argument( + "-d", + "--debug", + dest="log_level", + action="store_const", + const=logging.DEBUG, + default=logging.WARNING, + help="Enable debug output", + ) + # verbose + parser.add_argument( + "-v", + "--verbose", + dest="log_level", + action="store_const", + const=logging.INFO, + default=logging.WARNING, + help="Enable verbose output", + ) + # quiet + parser.add_argument( + "-q", + "--quiet", + dest="log_level", + action="store_const", + const=logging.ERROR, + default=logging.WARNING, + help="Enable quiet output", + ) + # pacman + parser.add_argument( + "--pacman", + dest="select_demo", + action="store_const", + const="pacman", + default="cvd", + help="Run the Pacman demo", + ) + # robot + parser.add_argument( + "--robot", + dest="select_demo", + action="store_const", + const="robot", + default="cvd", + help="Run the Robot demo", + ) + # cvd (default if neither pacman nor robot) + parser.add_argument( + "--cvd", + dest="select_demo", + action="store_const", + const="cvd", + default="cvd", + help="Run the CVD demo", + ) + + # if print-tree just print the tree and exit + parser.add_argument( + "-t", + "--print-tree", + dest="print_tree", + action="store_true", + default=False, + help="print the tree and exit", + ) + + args = parser.parse_args() + + # select the demo + selector = { + "pacman": run_pacman, + "robot": run_robot, + "cvd": run_cvd, + } + + selector[args.select_demo](args) + + +if __name__ == "__main__": + main() diff --git a/vultron/sim/__init__.py b/vultron/sim/__init__.py new file mode 100644 index 00000000..7be78203 --- /dev/null +++ b/vultron/sim/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides classes to facilitate Vultron behavior simulation. +""" diff --git a/vultron/sim/messages.py b/vultron/sim/messages.py new file mode 100644 index 00000000..5644b027 --- /dev/null +++ b/vultron/sim/messages.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides a basic message class for use in a Vultron simulation. +""" + +from dataclasses import dataclass + +from vultron.bt.messaging.states import MessageTypes + + +@dataclass(kw_only=True) +class Message: + """ + Represents a message with a sender and body and optional message type. + """ + + sender: str + body: str + msg_type: MessageTypes = MessageTypes.GI