Skip to content

Commit

Permalink
Implemented "Copy Flow" button.
Browse files Browse the repository at this point in the history
  • Loading branch information
mbushkov committed Mar 7, 2017
1 parent a103753 commit b31a004
Show file tree
Hide file tree
Showing 14 changed files with 799 additions and 124 deletions.
195 changes: 195 additions & 0 deletions grr/gui/plugins/flow_copy_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#!/usr/bin/env python
"""Test flow copy UI."""


from grr.gui import gui_test_lib
from grr.gui import runtests_test

from grr.lib import aff4
from grr.lib import flags
from grr.lib import flow
from grr.lib import output_plugin
from grr.lib import test_lib
from grr.lib.flows.general import processes as flows_processes
from grr.lib.output_plugins import email_plugin
from grr.lib.rdfvalues import client as rdf_client


class DummyOutputPlugin(output_plugin.OutputPlugin):
"""Output plugin that does nothing."""

name = "dummy"
description = "Dummy do do."
args_type = flows_processes.ListProcessesArgs

def ProcessResponses(self, responses):
pass


class TestFlowCopy(gui_test_lib.GRRSeleniumTest):

def setUp(self):
super(TestFlowCopy, self).setUp()

# Prepare our fixture.
with self.ACLChecksDisabled():
self.client_id = rdf_client.ClientURN("C.0000000000000001")
test_lib.ClientFixture(self.client_id, self.token)
self.RequestAndGrantClientApproval("C.0000000000000001")

self.email_descriptor = output_plugin.OutputPluginDescriptor(
plugin_name=email_plugin.EmailOutputPlugin.__name__,
plugin_args=email_plugin.EmailOutputPluginArgs(
email_address="test@localhost", emails_limit=42))

def testOriginalFlowArgsAreShownInCopyForm(self):
args = flows_processes.ListProcessesArgs(
filename_regex="test[a-z]*", fetch_binaries=True)
flow.GRRFlow.StartFlow(
flow_name=flows_processes.ListProcesses.__name__,
args=args,
client_id=self.client_id,
output_plugins=[self.email_descriptor],
token=self.token)

# Navigate to client and select newly created flow.
self.Open("/#c=C.0000000000000001")
self.Click("css=a[grrtarget='client.flows']")
self.Click("css=td:contains('ListProcesses')")

# Open wizard and check if flow arguments are copied.
self.Click("css=button[name=copy_flow]")

self.WaitUntil(self.IsTextPresent, "Copy ListProcesses flow")

self.WaitUntilEqual("test[a-z]*", self.GetValue,
"css=label:contains('Filename Regex') ~ * input")

self.WaitUntil(self.IsChecked, "css=label:contains('Fetch Binaries') "
"~ * input[type=checkbox]")

# Check that output plugin info is also copied.
self.WaitUntilEqual("string:EmailOutputPlugin", self.GetValue,
"css=label:contains('Plugin') ~ * select")
self.WaitUntilEqual("test@localhost", self.GetValue,
"css=label:contains('Email address') ~ * input")
self.WaitUntilEqual("42", self.GetValue,
"css=label:contains('Emails limit') ~ * input")

def testCopyingFlowUpdatesFlowListAndSelectsNewFlow(self):
args = flows_processes.ListProcessesArgs(
filename_regex="test[a-z]*", fetch_binaries=True)
flow.GRRFlow.StartFlow(
flow_name=flows_processes.ListProcesses.__name__,
args=args,
client_id=self.client_id,
token=self.token)

# Navigate to client and select newly created flow.
self.Open("/#c=C.0000000000000001")
self.Click("css=a[grrtarget='client.flows']")
self.Click("css=td:contains('ListProcesses')")

# Check that there's only one ListProcesses flow.
self.WaitUntilNot(
self.IsElementPresent,
"css=grr-client-flows-list tr:contains('ListProcesses'):nth(1)")

# Open wizard and check if flow arguments are copied.
self.Click("css=button[name=copy_flow]")
self.Click("css=button:contains('Launch'):not([disabled])")

# Check that flows list got updated and that the new flow is selected.
self.WaitUntil(
self.IsElementPresent,
"css=grr-client-flows-list tr:contains('ListProcesses'):nth(1)")
self.WaitUntil(self.IsElementPresent, "css=grr-client-flows-list "
"tr:contains('ListProcesses'):nth(0).row-selected")

def testAddingOutputPluginToCopiedFlowWorks(self):
args = flows_processes.ListProcessesArgs(
filename_regex="test[a-z]*", fetch_binaries=True)
flow.GRRFlow.StartFlow(
flow_name=flows_processes.ListProcesses.__name__,
args=args,
client_id=self.client_id,
token=self.token)

# Navigate to client and select newly created flow.
self.Open("/#c=C.0000000000000001")
self.Click("css=a[grrtarget='client.flows']")
self.Click("css=td:contains('ListProcesses')")

# Open wizard and check if flow arguments are copied.
self.Click("css=button[name=copy_flow]")

self.Click("css=label:contains('Output Plugins') ~ * button")
self.WaitUntil(self.IsElementPresent,
"css=label:contains('Plugin') ~ * select")

def testUserChangesToCopiedFlowAreRespected(self):
args = flows_processes.ListProcessesArgs(
filename_regex="test[a-z]*", fetch_binaries=True)
flow.GRRFlow.StartFlow(
flow_name=flows_processes.ListProcesses.__name__,
args=args,
client_id=self.client_id,
output_plugins=[self.email_descriptor],
token=self.token)

# Navigate to client and select newly created flow.
self.Open("/#c=C.0000000000000001")
self.Click("css=a[grrtarget='client.flows']")
self.Click("css=td:contains('ListProcesses')")

# Open wizard and change the arguments.
self.Click("css=button[name=copy_flow]")

self.Type("css=label:contains('Filename Regex') ~ * input",
"somethingElse*")

self.Click("css=label:contains('Fetch Binaries') ~ * input[type=checkbox]")

# Change output plugin and add another one.
self.Click("css=label:contains('Output Plugins') ~ * button")
self.Select("css=grr-output-plugin-descriptor-form "
"label:contains('Plugin') ~ * select:eq(0)",
"DummyOutputPlugin")
self.Type("css=grr-output-plugin-descriptor-form "
"label:contains('Filename Regex'):eq(0) ~ * input:text",
"foobar!")

self.Click("css=button:contains('Launch')")

# Check that flows list got updated and that the new flow is selected.
self.WaitUntil(
self.IsElementPresent,
"css=grr-client-flows-list tr:contains('ListProcesses'):nth(1)")
self.WaitUntil(self.IsElementPresent, "css=grr-client-flows-list "
"tr:contains('ListProcesses'):nth(0).row-selected")

# Now open the last flow and check that it has the changes we made.
with self.ACLChecksDisabled():
fd = aff4.FACTORY.Open(self.client_id.Add("flows"), token=self.token)
flows = sorted(fd.ListChildren(), key=lambda x: x.age)
fobj = aff4.FACTORY.Open(flows[-1], token=self.token)

self.assertEqual(fobj.args,
flows_processes.ListProcessesArgs(
filename_regex="somethingElse*",))
self.assertListEqual(
list(fobj.runner_args.output_plugins), [
output_plugin.OutputPluginDescriptor(
plugin_name=DummyOutputPlugin.__name__,
plugin_args=flows_processes.ListProcessesArgs(
filename_regex="foobar!")), self.email_descriptor
])


def main(argv):
# Run the full test suite
runtests_test.SeleniumTestProgram(argv=argv)


if __name__ == "__main__":
flags.StartMain(main)
2 changes: 2 additions & 0 deletions grr/gui/plugins/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from grr.gui.plugins import cron_view_test
from grr.gui.plugins import fileview_test
from grr.gui.plugins import flow_archive_test
from grr.gui.plugins import flow_copy_test
from grr.gui.plugins import flow_create_hunt_test
from grr.gui.plugins import flow_export_test
from grr.gui.plugins import flow_management_test
from grr.gui.plugins import flow_notifications_test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ ClientFlowsListController.prototype.cancelButtonClicked = function() {
};

/**
* Shows 'New Hunt' dialog prefilled with the data of the currently selected
* Shows a 'New Hunt' dialog prefilled with the data of the currently selected
* flow.
*
* @export
Expand Down Expand Up @@ -129,6 +129,44 @@ ClientFlowsListController.prototype.createHuntFromFlow = function() {
};


/**
* Shows a 'New Hunt' dialog prefilled with the data of the currently selected
* hunt.
*
* @export
*/
ClientFlowsListController.prototype.copyFlow = function() {
var newFlowId;

var modalScope = this.scope_.$new();
modalScope['clientId'] = this.scope_['clientId'];
modalScope['flowId'] = this.scope_['selectedFlowId'];
modalScope['resolve'] = function(newFlowObj) {
newFlowId = newFlowObj['value']['flow_id']['value'];
modalInstance.close();
}.bind(this);
modalScope['reject'] = function() {
modalInstance.dismiss();
}.bind(this);

this.scope_.$on('$destroy', function() {
modalScope.$destroy();
});

var modalInstance = this.uibModal_.open({
template: '<grr-copy-flow-form on-resolve="resolve(flow)" ' +
'on-reject="reject()" flow-id="flowId" client-id="clientId" />',
scope: modalScope,
windowClass: 'wide-modal high-modal',
size: 'lg'
});
modalInstance.result.then(function resolve() {
this.grrRoutingService_.go('client.flows', {flowId: newFlowId});
this.triggerUpdate();
}.bind(this));
};


/**
* FlowsListDirective definition.
Expand Down
8 changes: 8 additions & 0 deletions grr/gui/static/angular-components/flow/client-flows-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
ng-click="controller.cancelButtonClicked()">
<img src="/static/images/editdelete.png" class="toolbar_icon">
</button>

<button class="btn btn-default" name="copy_flow"
title="Copy Flow"
ng-disabled="!selectedFlowId"
ng-click="controller.copyFlow()">
<img src="/static/images/copy.png" class="toolbar-icon" />
</button>

<button title="Create Hunt From Flow" class="btn btn-default" name="create_hunt"
ng-disabled="!selectedFlowId"
ng-click="controller.createHuntFromFlow()">
Expand Down
116 changes: 116 additions & 0 deletions grr/gui/static/angular-components/flow/copy-flow-form-directive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
'use strict';

goog.provide('grrUi.flow.copyFlowFormDirective.CopyFlowFormController');
goog.provide('grrUi.flow.copyFlowFormDirective.CopyFlowFormDirective');
goog.require('grrUi.core.apiService.stripTypeInfo');

goog.scope(function() {

var stripTypeInfo = grrUi.core.apiService.stripTypeInfo;

/**
* Controller for CopyFlowFormDirective.
*
* @param {!angular.Scope} $scope
* @param {!grrUi.core.apiService.ApiService} grrApiService
* @constructor
* @ngInject
*/
grrUi.flow.copyFlowFormDirective.CopyFlowFormController =
function($scope, grrApiService) {
/** @private {!angular.Scope} */
this.scope_ = $scope;

/** @private {!grrUi.core.apiService.ApiService} */
this.grrApiService_ = grrApiService;

/** @private {Object} */
this.flow;

this.scope_.$watchGroup(['flowId', 'clientId'],
this.onFlowIdClientIdChange_.bind(this));
};

var CopyFlowFormController =
grrUi.flow.copyFlowFormDirective.CopyFlowFormController;


/**
* Handles flowId/clientId attribute changes.
*
* @param {Array<string>} newValues
* @private
*/
CopyFlowFormController.prototype.onFlowIdClientIdChange_ = function(
newValues) {
if (newValues.every(angular.isDefined)) {
var flowUrl = ['clients',
this.scope_['clientId'],
'flows',
this.scope_['flowId']].join('/');
this.grrApiService_.get(flowUrl).then(function(response) {
this.flow = response['data'];
}.bind(this));
}
};


/**
* Handles clicks on dialog's 'proceed' button.
*
* @return {!angular.$q.Promise} Promise that resolves to a message that's
* displayed in a dialog.
* @export
*/
CopyFlowFormController.prototype.proceed = function() {
var strippedFlow = stripTypeInfo(this.flow);

return this.grrApiService_.post('clients/' + this.scope_['clientId'] + '/flows', {
flow: {
runner_args: strippedFlow['runner_args'],
args: strippedFlow['args']
}

}).then(function success(response) {
this.scope_['onResolve']({'flow': response['data']});
return 'Flow was successfully launched!';

}.bind(this), function failure(response) {
var e = response['data']['message'] || 'Unknown error';
this.scope_['onReject']({'error': e});
return e;

}.bind(this));
};

/**
* Displays a "start flow" form with fills prefilled from the existing flow.
*
* @return {angular.Directive} Directive definition object.
*/
grrUi.flow.copyFlowFormDirective.CopyFlowFormDirective = function() {
return {
scope: {
flowId: '=',
clientId: '=',
onResolve: '&',
onReject: '&'
},
restrict: 'E',
templateUrl: '/static/angular-components/flow/copy-flow-form.html',
controller: CopyFlowFormController,
controllerAs: 'controller'
};
};


/**
* Directive's name in Angular.
*
* @const
* @export
*/
grrUi.flow.copyFlowFormDirective.CopyFlowFormDirective.directive_name =
'grrCopyFlowForm';

}); // goog.scope
Loading

0 comments on commit b31a004

Please sign in to comment.