-
Notifications
You must be signed in to change notification settings - Fork 13
Initial depth check setup, testing has also been completed #141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
1b3fc55
Initial depth check setup, testing has also been completed
54c8b2d
Added set depth during creation of the depth check object
c3b4379
Add Check-Depth function to all tests that use the schema for testing
04da0aa
Removed check for not query, as this will not check a mutation that c…
fda995b
Implemented better depth check analysis
5f6eb4f
Typo in class definition
0d5f7b6
Fixed call to class
93a8b5e
Added in cost analysis check, with very basic cost map
ad57940
Created new cost calculating method, updated name, and applied to all…
38d2039
Added tests for costs, removed unused dicts for invalid tests
f3761d3
Removed unused imports
b04e6d0
Cleaned up imports
967c6b8
Fixed imports
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| from typing import ( | ||
| Dict, | ||
| List | ||
| ) | ||
| from graphql.language.ast import ( | ||
| FragmentDefinition, | ||
| OperationDefinition | ||
| ) | ||
|
|
||
|
|
||
| def get_fragments(definitions) -> Dict[str, FragmentDefinition]: | ||
| return { | ||
| definition.name.value: definition | ||
| for definition in definitions | ||
| if isinstance(definition, FragmentDefinition) | ||
| } | ||
|
|
||
|
|
||
| def get_queries_and_mutations(definitions) -> List[OperationDefinition]: | ||
| return [ | ||
| definition | ||
| for definition in definitions | ||
| if isinstance(definition, OperationDefinition) | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| from typing import ( | ||
| Dict | ||
| ) | ||
| from graphql.language.ast import ( | ||
| Document, | ||
| FragmentDefinition, | ||
| OperationDefinition, | ||
| Node, | ||
| FragmentSpread, | ||
| Field, | ||
| InlineFragment | ||
| ) | ||
| from backend import ( | ||
| get_fragments, | ||
| get_queries_and_mutations | ||
| ) | ||
|
|
||
| from backend.cost_map import cost_map | ||
|
|
||
|
|
||
| class CostLimitReached(Exception): | ||
| pass | ||
|
|
||
|
|
||
| def measure_cost(node: Node, fragments: Dict[str, FragmentDefinition]) -> int: | ||
| """ | ||
| A function which recursively measures the cost of a Graphene Query | ||
| :type node: Node | ||
| :param node: Graphql-core object used for query traversal/indexing | ||
| :type fragments: dict | ||
| :param fragments: The fragments of the query | ||
| :rtype: int | ||
| :return: The cost of the node | ||
| """ | ||
| if isinstance(node, FragmentSpread): | ||
| fragment = fragments.get(node.name.value) | ||
| return measure_cost(node=fragment, fragments=fragments) | ||
|
|
||
| elif isinstance(node, Field): | ||
| if node.name.value.lower() in ["__schema", "__introspection"]: | ||
| return 0 | ||
| if not node.selection_set: | ||
| return cost_map.get(node.name.value, 1) | ||
| costs = [] | ||
| for selection in node.selection_set.selections: | ||
| cost = measure_cost(node=selection, fragments=fragments) | ||
| costs.append(cost) | ||
| return sum(costs) + cost_map.get(node.name.value, 1) | ||
| elif ( | ||
| isinstance(node, FragmentDefinition) | ||
| or isinstance(node, OperationDefinition) | ||
| or isinstance(node, InlineFragment) | ||
| ): | ||
| costs = [] | ||
| for selection in node.selection_set.selections: | ||
| cost = measure_cost(node=selection, fragments=fragments) | ||
| costs.append(cost) | ||
| return sum(costs) | ||
| else: | ||
| raise Exception("Unknown node") | ||
|
|
||
|
|
||
| def check_cost_analysis(max_cost: int, document: Document): | ||
| fragments = get_fragments(document.definitions) | ||
| queries = get_queries_and_mutations(document.definitions) | ||
|
|
||
| for query in queries: | ||
| total_cost = measure_cost(query, fragments) | ||
| if total_cost > max_cost: | ||
| raise CostLimitReached( | ||
| 'Query cost is too high' | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| cost_map = { | ||
| 'getSectorById': 1, | ||
| 'getOrgById': 1, | ||
| 'getGroupById': 1 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| from typing import Dict | ||
| from graphql.language.ast import ( | ||
| Document, | ||
| FragmentDefinition, | ||
| OperationDefinition, | ||
| Node, | ||
| FragmentSpread, | ||
| Field, | ||
| InlineFragment | ||
| ) | ||
|
|
||
| from backend import ( | ||
| get_fragments, | ||
| get_queries_and_mutations | ||
| ) | ||
|
|
||
|
|
||
| class DepthLimitReached(Exception): | ||
| pass | ||
|
|
||
|
|
||
| def measure_depth(node: Node, fragments: Dict[str, FragmentDefinition]) -> int: | ||
| """ | ||
| A function which recursively measures the depth of a Graphene Query | ||
| :type node: Node | ||
| :param node: Graphql-core object used for query traversal/indexing | ||
| :type fragments: dict | ||
| :param fragments: The fragments of the query | ||
| :rtype: int | ||
| :return: The max depth of the node | ||
| """ | ||
| if isinstance(node, FragmentSpread): | ||
| fragment = fragments.get(node.name.value) | ||
| return measure_depth(node=fragment, fragments=fragments) | ||
|
|
||
| elif isinstance(node, Field): | ||
| if node.name.value.lower() in ["__schema", "__introspection"]: | ||
| return 0 | ||
| if not node.selection_set: | ||
| return 1 | ||
| depths = [] | ||
| for selection in node.selection_set.selections: | ||
| depth = measure_depth(node=selection, fragments=fragments) | ||
| depths.append(depth) | ||
| return 1 + max(depths) | ||
| elif ( | ||
| isinstance(node, FragmentDefinition) | ||
| or isinstance(node, OperationDefinition) | ||
| or isinstance(node, InlineFragment) | ||
| ): | ||
| depths = [] | ||
| for selection in node.selection_set.selections: | ||
| depth = measure_depth(node=selection, fragments=fragments) | ||
| depths.append(depth) | ||
| return max(depths) | ||
| else: | ||
| raise Exception("Unknown node") | ||
|
|
||
|
|
||
| def check_max_depth(max_depth: int, document: Document): | ||
| fragments = get_fragments(document.definitions) | ||
| queries = get_queries_and_mutations(document.definitions) | ||
|
|
||
| for query in queries: | ||
| depth = measure_depth(query, fragments) | ||
| if depth > max_depth: | ||
| raise DepthLimitReached( | ||
| 'Query is too complex' | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| from typing import ( | ||
| Union, | ||
| Optional, | ||
| Any | ||
| ) | ||
| from graphql import GraphQLDocument, GraphQLSchema | ||
| from graphql.backend.core import GraphQLCoreBackend | ||
| from graphql.language.ast import Document | ||
|
|
||
| from backend.depth_check import check_max_depth | ||
| from backend.cost_check import check_cost_analysis | ||
|
|
||
|
|
||
| class SecurityAnalysisBackend(GraphQLCoreBackend): | ||
| def __init__(self, max_depth=10, max_cost=1000, executor: Optional[Any] = None): | ||
| super().__init__(executor=executor) | ||
| self.max_depth = max_depth | ||
| self.max_cost = max_cost | ||
|
|
||
| def document_from_string(self, schema: GraphQLSchema, document_string: Union[Document, str]) -> GraphQLDocument: | ||
| document = super().document_from_string(schema, document_string) | ||
| check_max_depth(max_depth=self.max_depth, document=document.document_ast) | ||
| check_cost_analysis(max_cost=self.max_cost, document=document.document_ast) | ||
| return document |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,38 +1,41 @@ | ||
| from graphql import GraphQLError | ||
| from schemas.sectors import Sectors, SectorsModel | ||
| from model_enums.sectors import SectorEnums | ||
| from manage import app | ||
|
|
||
|
|
||
| # Resolvers | ||
| def resolve_get_sector_by_id(self, info, **kwargs): | ||
| """Return a sector by its row ID""" | ||
| sector_id = kwargs.get('id', 1) | ||
| query = Sectors.get_query(info).filter( | ||
| SectorsModel.id == sector_id | ||
| ) | ||
| if not len(query.all()): | ||
| raise GraphQLError("Error, Invalid ID") | ||
| return query.all() | ||
| """Return a sector by its row ID""" | ||
| sector_id = kwargs.get('id', 1) | ||
| with app.app_context(): | ||
| query = Sectors.get_query(info).filter( | ||
| SectorsModel.id == sector_id | ||
| ) | ||
| if not len(query.all()): | ||
| raise GraphQLError("Error, Invalid ID") | ||
| return query.all() | ||
|
|
||
|
|
||
| def resolve_get_sectors_by_sector(self, info, **kwargs): | ||
| """Return a list of sectors by its sector""" | ||
| sector = kwargs.get('sector', 'EMPTY') | ||
| query = Sectors.get_query(info).filter( | ||
| SectorsModel.sector == sector | ||
| ) | ||
|
|
||
| if not len(query.all()): | ||
| raise GraphQLError("Error, Sector does not exist") | ||
| return query.all() | ||
| """Return a list of sectors by its sector""" | ||
| sector = kwargs.get('sector', 'EMPTY') | ||
| with app.app_context(): | ||
| query = Sectors.get_query(info).filter( | ||
| SectorsModel.sector == sector | ||
| ) | ||
| if not len(query.all()): | ||
| raise GraphQLError("Error, Sector does not exist") | ||
| return query.all() | ||
|
|
||
|
|
||
| def resolve_get_sector_by_zone(self, info, **kwargs): | ||
| """Return a list of sectors by their zone""" | ||
| zone = kwargs.get('zone') | ||
| query = Sectors.get_query(info).filter( | ||
| SectorsModel.zone == zone | ||
| ) | ||
| if not len(query.all()): | ||
| raise GraphQLError("Error, Zone does not exist") | ||
| return query.all() | ||
| """Return a list of sectors by their zone""" | ||
| zone = kwargs.get('zone') | ||
| with app.app_context(): | ||
| query = Sectors.get_query(info).filter( | ||
| SectorsModel.zone == zone | ||
| ) | ||
| if not len(query.all()): | ||
| raise GraphQLError("Error, Zone does not exist") | ||
| return query.all() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😻