示例#1
0
    def __init__(self, app_window):
        super(ScriptsDialog, self).__init__(app_window)

        self.script = None
        self._app_window = app_window
        self._script_manager = ScriptsManager()
        self._git = Git()

        self.setMinimumWidth(800)

        box = QVBoxLayout()
        self.table = ScriptsTable(self)
        self.table.onScriptSelected.connect(self._item_selected)
        self.table.setMinimumWidth(800)

        # create a centered dot icon
        _section_width = self.table.header().sectionSize(3)
        self._new_pixmap = QPixmap(max(_section_width, 40), 20)
        self._new_pixmap.fill(Qt.transparent)
        painter = QPainter(self._new_pixmap)
        rect = QRect((_section_width * 0.5) - 5, 0, 20, 20)
        painter.setBrush(QColor('#666'))
        painter.setPen(QColor('#666'))
        painter.drawEllipse(rect)
        self._dot_icon = QIcon(self._new_pixmap)

        box.addWidget(self.table)
        lbl = QLabel('OS Support - A: Android I: IOS W: Windows')
        box.addWidget(lbl)
        self.setLayout(box)
        self._init_list()
示例#2
0
    def get_paths(self, path):  # type: (str) -> t.List[str]
        """Return the list of available content paths under the given path."""
        git = Git(path)

        paths = git.get_file_names(['--cached', '--others', '--exclude-standard'])

        return paths
示例#3
0
def get_merged_commit(args, commit):  # pylint: disable=unused-argument
    """
    :type args: CommonConfig
    :type commit: str
    :rtype: str | None
    """
    if not commit:
        return None

    git = Git()

    try:
        show_commit = git.run_git(
            ['show', '--no-patch', '--no-abbrev', commit])
    except SubprocessError as ex:
        # This should only fail for pull requests where the commit does not exist.
        # Merge runs would fail much earlier when attempting to checkout the commit.
        raise ApplicationError(
            'Commit %s was not found:\n\n%s\n\n'
            'GitHub may not have fully replicated the commit across their infrastructure.\n'
            'It is also possible the commit was removed by a force push between job creation and execution.\n'
            'Find the latest run for the pull request and restart failed jobs as needed.'
            % (commit, ex.stderr.strip()))

    head_commit = git.run_git(['show', '--no-patch', '--no-abbrev', 'HEAD'])

    if show_commit == head_commit:
        # Commit is HEAD, so this is not a pull request or the base branch for the pull request is up-to-date.
        return None

    match_merge = re.search(r'^Merge: (?P<parents>[0-9a-f]{40} [0-9a-f]{40})$',
                            head_commit,
                            flags=re.MULTILINE)

    if not match_merge:
        # The most likely scenarios resulting in a failure here are:
        # A new run should or does supersede this job, but it wasn't cancelled in time.
        # A job was superseded and then later restarted.
        raise ApplicationError(
            'HEAD is not commit %s or a merge commit:\n\n%s\n\n'
            'This job has likely been superseded by another run due to additional commits being pushed.\n'
            'Find the latest run for the pull request and restart failed jobs as needed.'
            % (commit, head_commit.strip()))

    parents = set(match_merge.group('parents').split(' '))

    if len(parents) != 2:
        raise ApplicationError('HEAD is a %d-way octopus merge.' %
                               len(parents))

    if commit not in parents:
        raise ApplicationError('Commit %s is not a parent of HEAD.' % commit)

    parents.remove(commit)

    last_commit = parents.pop()

    return last_commit
示例#4
0
    def __init__(self, parent=None, device_type='usb'):
        super().__init__(parent=parent)

        # dont show for local
        if device_type != 'usb':
            return

        self.parent = parent
        self.wait_for_devtype = device_type
        self.is_waiting = True
        self._adb = Adb()

        if not self._adb.min_required:
            return

        self._git = Git()
        self.setAutoFillBackground(True)
        self.setStyleSheet(
            'background-color: crimson; color: white; font-weight: bold; margin: 0; padding: 10px;'
        )
        self.setup()
        self._timer = QTimer()
        self._timer.setInterval(500)
        self._timer.timeout.connect(self._on_timer)
        self._timer.start()
        self._timer_step = 0
        frida.get_device_manager().on('added', self._on_device)
        frida.get_device_manager().on('removed', self._on_device)
        self.devices_thread = DevicesUpdateThread(self)
        self.devices_thread.onAddDevice.connect(self.on_add_deviceitem)
        self.devices_thread.onDevicesUpdated.connect(self._on_devices_finished)
        self._update_thread = FridaUpdateThread(self)
        self._update_thread._adb = self._adb
        self._update_thread.onStatusUpdate.connect(self._update_statuslbl)
        self._update_thread.onFinished.connect(self._frida_updated)
        self._update_thread.onError.connect(self._on_download_error)
        self.updated_frida_version = ''
        self.updated_frida_assets_url = {}
        self._device_id = None
        self._devices = []
        remote_frida = self._git.get_frida_version()
        if remote_frida is None:
            self.updated_frida_version = ''
            self.updated_frida_assets_url.clear()
        else:
            remote_frida = remote_frida[0]
            self.updated_frida_version = remote_frida['tag_name']
            for asset in remote_frida['assets']:
                try:
                    name = asset['name']
                    tag_start = name.index('android-')
                    if name.index('server') >= 0:
                        tag = name[tag_start + 8:-3]
                        self.updated_frida_assets_url[tag] = asset[
                            'browser_download_url']
                except ValueError:
                    pass
示例#5
0
class ScriptsManager(QObject):
    """ ScriptManager

        signals:
            scriptsUpdated()
    """

    scriptsUpdated = pyqtSignal(name='scriptsUpdated')

    def __init__(self):
        super(ScriptsManager, self).__init__()
        self._git = Git()
        self.scripts = {}

        self.update_scripts()

    def update_scripts(self):
        scripts = self._git.get_dwarf_scripts()

        if scripts is None:
            return

        scripts = scripts.replace(' ', '').replace('\t', '').split('\n')
        submodule_path = '[submodule"'
        url_path = 'url='
        module_name = ''

        for line in scripts:
            if line.startswith(submodule_path):
                module_name = line.replace(submodule_path, "")
                module_name = module_name[:-2]
            elif line.startswith(url_path):
                url = line.replace(url_path, "")
                if url.endswith('.git'):
                    url = url[:-4]
                url = url.replace('https://github.com',
                                  'https://raw.githubusercontent.com')

                info_url = url + '/master/dwarf.json'
                script_url = url + '/master/script.js'
                info = self._git.get_script_info(info_url)
                if info is None:
                    continue
                self.scripts[module_name] = {
                    'info': info,
                    'script': script_url
                }

        self.scriptsUpdated.emit()

    def get_script(self, script_name):
        return self.scripts[script_name]

    def get_scripts(self):
        return self.scripts
示例#6
0
    def __init__(self, app_window):
        self.app_window = app_window
        self.app = app_window.get_app_instance()

        self.java_available = False
        self.loading_library = False

        # frida device
        self.device = None

        # process
        self.pid = 0
        self.process = None
        self.script = None

        # hooks
        self.hooks = {}
        self.on_loads = {}
        self.java_hooks = {}
        self.temporary_input = ''
        self.native_pending_args = None
        self.java_pending_args = None

        # core utils
        self.prefs = Prefs()
        self.git = Git()
        self.script_manager = ScriptsManager(self)

        self.keystone_installed = False
        try:
            import keystone.keystone_const
            self.keystone_installed = True
        except:
            pass
示例#7
0
def detect_changes_local(args):
    """
    :type args: TestConfig
    :rtype: list[str]
    """
    git = Git(args)
    result = LocalChanges(args, git)

    display.info(
        'Detected branch %s forked from %s at commit %s' %
        (result.current_branch, result.fork_branch, result.fork_point))

    if result.untracked and not args.untracked:
        display.warning(
            'Ignored %s untracked file(s). Use --untracked to include them.' %
            len(result.untracked))

    if result.committed and not args.committed:
        display.warning(
            'Ignored %s committed change(s). Omit --ignore-committed to include them.'
            % len(result.committed))

    if result.staged and not args.staged:
        display.warning(
            'Ignored %s staged change(s). Omit --ignore-staged to include them.'
            % len(result.staged))

    if result.unstaged and not args.unstaged:
        display.warning(
            'Ignored %s unstaged change(s). Omit --ignore-unstaged to include them.'
            % len(result.unstaged))

    names = set()

    if args.tracked:
        names |= set(result.tracked)
    if args.untracked:
        names |= set(result.untracked)
    if args.committed:
        names |= set(result.committed)
    if args.staged:
        names |= set(result.staged)
    if args.unstaged:
        names |= set(result.unstaged)

    if not args.metadata.changes:
        args.metadata.populate_changes(result.diff)

        for path in result.untracked:
            if is_binary_file(path):
                args.metadata.changes[path] = ((0, 0), )
                continue

            with open(path, 'r') as source_fd:
                line_count = len(source_fd.read().splitlines())

            args.metadata.changes[path] = ((1, line_count), )

    return sorted(names)
示例#8
0
    def run(self):
        self.on_status_text.emit('fetching commit list...')

        try:
            utils.do_shell_command('git --version')
        except IOError as io_error:
            if io_error.errno == 2:
                # git command not available
                self.on_status_text.emit(
                    'error: git not available on your system')
                return
        _git = Git()
        data = _git.get_dwarf_commits()
        if data is None:
            self.on_status_text.emit('Failed to fetch commit list. Try later.')
            return

        most_recent_remote_commit = ''
        most_recent_local_commit = utils.do_shell_command(
            'git log -1 master --pretty=format:%H')
        most_recent_date = ''
        for commit in data:
            if most_recent_remote_commit == '':
                most_recent_remote_commit = commit['sha']
                if most_recent_remote_commit != most_recent_local_commit:
                    self.on_update_available.emit()

            commit = commit['commit']
            date = commit['committer']['date'].split('T')
            if most_recent_date != date[0]:
                if most_recent_date != '':
                    self.on_add_commit.emit('', True)
                self.on_add_commit.emit(date[0], True)
                most_recent_date = date[0]

            s = ('{0} - {1} ({2})'.format(date[1][:-1], commit['message'],
                                          commit['author']['name']))
            self.on_add_commit.emit(s, False)

        if most_recent_remote_commit != most_recent_local_commit:
            self.on_finished.emit(
                'There is an newer Version available... You can use the UpdateButton in Menu'
            )
        else:
            # keep: it clears status text
            self.on_finished.emit('')
示例#9
0
    def backup_repos(self, repos):
        info('[%s] backing up repositories' % self.type)

        for repo in repos:
            try:
                repo_name = get_repo_name(repo, self.type)
                path = os.path.join(self.directory, repo_name)

                git = Git(repo, path, repo_name, self.verbose)

                if os.path.isdir(path):
                    remote_refs = len(git.list_remote_refs())
                    if remote_refs == 0:
                        continue

                    git.fetch()
                    git.reset_origin_hard()
                else:
                    git.clone()
            except Exception:
                self.errors.append(repo_name)
示例#10
0
文件: core.py 项目: zbx91/Dwarf
    def __init__(self, app_window):
        self.app_window = app_window
        self.app = app_window.get_app_instance()

        self.java_available = False
        self.loading_library = False

        # frida device
        self.device = None

        # process
        self.pid = 0
        self.process = None
        self.script = None

        # kernel
        self.kernel = Kernel(self)

        # hooks
        self.hooks = {}
        self.on_loads = {}
        self.java_hooks = {}
        self.temporary_input = ''
        self.native_pending_args = None
        self.java_pending_args = None

        # context
        self.arch = ''
        self.pointer_size = 0
        self.contexts = {}
        self.context_tid = 0

        # tracers
        self.native_traced_tid = 0

        # core utils
        self.bus = EventBus()
        self.emulator = Emulator(self)
        self.git = Git()
        self.prefs = Prefs()
        self.script_manager = ScriptsManager(self)

        self.keystone_installed = False
        try:
            import keystone.keystone_const
            self.keystone_installed = True
        except:
            pass
示例#11
0
def detect_changes_local(args):
    """
    :type args: TestConfig
    :rtype: list[str]
    """
    git = Git(args)
    result = LocalChanges(args, git)

    display.info(
        'Detected branch %s forked from %s at commit %s' %
        (result.current_branch, result.fork_branch, result.fork_point))

    if result.untracked and not args.untracked:
        display.warning(
            'Ignored %s untracked file(s). Use --untracked to include them.' %
            len(result.untracked))

    if result.committed and not args.committed:
        display.warning(
            'Ignored %s committed change(s). Omit --ignore-committed to include them.'
            % len(result.committed))

    if result.staged and not args.staged:
        display.warning(
            'Ignored %s staged change(s). Omit --ignore-staged to include them.'
            % len(result.staged))

    if result.unstaged and not args.unstaged:
        display.warning(
            'Ignored %s unstaged change(s). Omit --ignore-unstaged to include them.'
            % len(result.unstaged))

    names = set()

    if args.tracked:
        names |= set(result.tracked)
    if args.untracked:
        names |= set(result.untracked)
    if args.committed:
        names |= set(result.committed)
    if args.staged:
        names |= set(result.staged)
    if args.unstaged:
        names |= set(result.unstaged)

    return sorted(names)
示例#12
0
def detect_changes_shippable(args):
    """Initialize change detection on Shippable.
    :type args: CommonConfig
    :rtype: list[str]
    """
    git = Git(args)
    result = ShippableChanges(args, git)

    if result.is_pr:
        job_type = 'pull request'
    elif result.is_tag:
        job_type = 'tag'
    else:
        job_type = 'merge commit'

    display.info('Processing %s for branch %s commit %s' % (job_type, result.branch, result.commit))

    return result.paths
    def setup(self):
        self.repo_path = "../repo/path/"
        self.commit_range = "tag1..tag2"
        self.file = "file.txt"
        self.message = "1.2.3.4 release notes"

        self.tag_cmd = list(TAG_CMD)
        self.tag_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.tag_cmd[2] = '--work-tree=' + self.repo_path

        self.log_cmd = list(LOG_CMD)
        self.log_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.log_cmd[2] = '--work-tree=' + self.repo_path
        self.log_cmd.append(self.commit_range)

        self.add_cmd = list(ADD_CMD)
        self.add_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.add_cmd[2] = '--work-tree=' + self.repo_path
        self.add_cmd.append(self.file)

        self.cmt_cmd = list(CMT_CMD)
        self.cmt_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.cmt_cmd[2] = '--work-tree=' + self.repo_path
        self.cmt_cmd.append('"' + self.message + '"')

        self.push_cmd = list(PUSH_CMD)
        self.push_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.push_cmd[2] = '--work-tree=' + self.repo_path

        self.pull_cmd = list(PULL_CMD)
        self.pull_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.pull_cmd[2] = '--work-tree=' + self.repo_path

        self.fetch_cmd = list(FETCH_CMD)
        self.fetch_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.fetch_cmd[2] = '--work-tree=' + self.repo_path

        self.checkout_cmd = list(CHECKOUT_CMD)
        self.checkout_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.checkout_cmd[2] = '--work-tree=' + self.repo_path

        self.sut = Git()
示例#14
0
def detect_changes_shippable(args):
    """Initialize change detection on Shippable.
    :type args: TestConfig
    :rtype: list[str] | None
    """
    git = Git(args)
    result = ShippableChanges(args, git)

    if result.is_pr:
        job_type = 'pull request'
    elif result.is_tag:
        job_type = 'tag'
    else:
        job_type = 'merge commit'

    display.info('Processing %s for branch %s commit %s' % (job_type, result.branch, result.commit))

    if not args.metadata.changes:
        args.metadata.populate_changes(result.diff)

    return result.paths
示例#15
0
def main():
    git = Git(config.GIT_REPO, config.GIT_WORKING_COPY)
    git.clone()
示例#16
0
parent_parsers = [spreadsheet_parser, github_parser]
parser = argparse.ArgumentParser(
    description='Add YouTube metadata and publish new videos',
    parents=parent_parsers)

args = parser.parse_args()

spreadsheet = Spreadsheet(args.spreadsheet_id)
youtube = YouTube()

print('Getting all talks from spreadsheet with YouTube IDs')
talks = spreadsheet.get_unpublished_youtube_talks()
print('{} unpublished talks'.format(len(talks)))
ids = [talk.youtube_id for talk in talks]
print('Getting videos from YouTube')
videos = youtube.get_videos(ids)

for talk in talks:
    print('Unpublished talk: {}'.format(talk.title))
    video = filter(lambda x: talk.youtube_id == x.youtube_id, videos)[0]
    print('\tYouTube ID: {}'.format(video.youtube_id))
    print('\tSetting metadata and setting status to UNLISTED')
    title = '{} ({})'.format(talk.title, talk.speakers)
    video.publish(title, talk.description)

    print('\tAdding to website')
    git = Git(args.repo_user, args.repo)
    git.add_youtube_to_talk(talk.slug, video.youtube_id)

    print('\tUpdating spreadsheet to set published status to true')
    talk.published_status = True
示例#17
0
class DeviceBar(QWidget):
    """ DeviceBar

        Signals:
            onDeviceUpdated()
            onDeviceSelected(str) # str = id
            onDeviceChanged(str) # str = id

    """

    onDeviceUpdated = pyqtSignal(str, name="onDeviceUpdated")
    onDeviceSelected = pyqtSignal(str, name="onDeviceSelected")
    onDeviceChanged = pyqtSignal(str, name="onDeviceChanged")

    def __init__(self, parent=None, device_type='usb'):
        super().__init__(parent=parent)

        # dont show for local
        if device_type != 'usb':
            return

        self.parent = parent
        self.wait_for_devtype = device_type
        self.is_waiting = True
        self._adb = Adb()

        if not self._adb.min_required:
            return

        self._git = Git()
        self.setAutoFillBackground(True)
        self.setStyleSheet(
            'background-color: crimson; color: white; font-weight: bold; margin: 0; padding: 10px;'
        )
        self.setup()
        self._timer = QTimer()
        self._timer.setInterval(500)
        self._timer.timeout.connect(self._on_timer)
        self._timer.start()
        self._timer_step = 0
        frida.get_device_manager().on('added', self._on_device)
        frida.get_device_manager().on('removed', self._on_device)
        self.devices_thread = DevicesUpdateThread(self)
        self.devices_thread.onAddDevice.connect(self.on_add_deviceitem)
        self.devices_thread.onDevicesUpdated.connect(self._on_devices_finished)
        self._update_thread = FridaUpdateThread(self)
        self._update_thread._adb = self._adb
        self._update_thread.onStatusUpdate.connect(self._update_statuslbl)
        self._update_thread.onFinished.connect(self._frida_updated)
        self._update_thread.onError.connect(self._on_download_error)
        self.updated_frida_version = ''
        self.updated_frida_assets_url = {}
        self._device_id = None
        self._devices = []
        remote_frida = self._git.get_frida_version()
        if remote_frida is None:
            self.updated_frida_version = ''
            self.updated_frida_assets_url.clear()
        else:
            remote_frida = remote_frida[0]
            self.updated_frida_version = remote_frida['tag_name']
            for asset in remote_frida['assets']:
                try:
                    name = asset['name']
                    tag_start = name.index('android-')
                    if name.index('server') >= 0:
                        tag = name[tag_start + 8:-3]
                        self.updated_frida_assets_url[tag] = asset[
                            'browser_download_url']
                except ValueError:
                    pass

    def setup(self):
        """ Setup ui
        """
        h_box = QHBoxLayout()
        h_box.setContentsMargins(0, 0, 0, 0)
        self.update_label = QLabel('Waiting for Device')
        self.update_label.setFixedWidth(self.parent.width())
        self.update_label.setOpenExternalLinks(True)
        self.update_label.setTextFormat(Qt.RichText)
        self.update_label.setFixedHeight(35)
        self.update_label.setTextInteractionFlags(Qt.TextBrowserInteraction)
        self._install_btn = QPushButton('Install Frida', self.update_label)
        self._install_btn.setStyleSheet('padding: 0; border-color: white;')
        self._install_btn.setGeometry(self.update_label.width() - 110, 5, 100,
                                      25)
        self._install_btn.clicked.connect(self._on_install_btn)
        self._install_btn.setVisible(False)
        self._start_btn = QPushButton('Start Frida', self.update_label)
        self._start_btn.setStyleSheet('padding: 0; border-color: white;')
        self._start_btn.setGeometry(self.update_label.width() - 110, 5, 100,
                                    25)
        self._start_btn.clicked.connect(self._on_start_btn)
        self._start_btn.setVisible(False)
        self._update_btn = QPushButton('Update Frida', self.update_label)
        self._update_btn.setStyleSheet('padding: 0; border-color: white;')
        self._update_btn.setGeometry(self.update_label.width() - 110, 5, 100,
                                     25)
        self._update_btn.clicked.connect(self._on_install_btn)
        self._update_btn.setVisible(False)
        self._restart_btn = QPushButton('Restart Frida', self.update_label)
        self._restart_btn.setStyleSheet('padding: 0; border-color: white;')
        self._restart_btn.setGeometry(self.update_label.width() - 110, 5, 100,
                                      25)
        self._restart_btn.clicked.connect(self._on_restart_btn)
        self._restart_btn.setVisible(False)
        self._devices_combobox = QComboBox(self.update_label)
        self._devices_combobox.setStyleSheet(
            'padding: 2px 5px; border-color: white;')
        self._devices_combobox.setGeometry(self.update_label.width() - 320, 5,
                                           200, 25)
        self._devices_combobox.currentIndexChanged.connect(
            self._on_device_changed)
        self._devices_combobox.setVisible(False)
        h_box.addWidget(self.update_label)
        self.setLayout(h_box)

    def on_add_deviceitem(self, device_ident):
        """ Adds an Item to the DeviceComboBox
        """
        if device_ident['type'] == self.wait_for_devtype:
            if device_ident['name'] not in self._devices:
                self._devices.append(device_ident)
            self._timer_step = -1
            self.is_waiting = False

    def _on_device_changed(self, index):
        device = None
        device_id = self._devices_combobox.itemData(index)
        if device_id:
            try:
                device = frida.get_device(device_id)
            except:
                return

            if device:
                self._device_id = device.id
                self._check_device(device)
                self.onDeviceChanged.emit(self._device_id)

    def _check_device(self, frida_device):
        self.update_label.setStyleSheet('background-color: crimson;')
        self._install_btn.setVisible(False)
        self._update_btn.setVisible(False)
        self._start_btn.setVisible(False)
        self._restart_btn.setVisible(False)
        self._adb.device = frida_device.id
        self._device_id = frida_device.id
        if self._adb.available():
            self.update_label.setText('Device: ' + frida_device.name)
            # try getting frida version
            device_frida = self._adb.get_frida_version()
            # frida not found show install button
            if device_frida is None:
                self._install_btn.setVisible(True)
            else:
                # frida is old show update button
                if self.updated_frida_version != device_frida:
                    self._start_btn.setVisible(True)
                    self._update_btn.setVisible(False)
                    # old frida is running allow use of this version
                    if self._adb.is_frida_running():
                        self._start_btn.setVisible(False)
                        if self.updated_frida_assets_url:
                            self._update_btn.setVisible(True)
                        self.update_label.setStyleSheet(
                            'background-color: yellowgreen;')
                        self.onDeviceUpdated.emit(frida_device.id)
                # frida not running show start button
                elif device_frida and not self._adb.is_frida_running():
                    self._start_btn.setVisible(True)
                # frida is running with last version show restart button
                elif device_frida and self._adb.is_frida_running():
                    self.update_label.setStyleSheet(
                        'background-color: yellowgreen;')
                    self._restart_btn.setVisible(True)
                    self.onDeviceUpdated.emit(frida_device.id)

    def _on_devices_finished(self):
        if len(self._devices) > 1:
            self._devices_combobox.clear()
            self._devices_combobox.setVisible(True)
            self.update_label.setText('Please select the Device: ')
            for device in self._devices:
                self._devices_combobox.addItem(device['name'], device['id'])
        else:
            self._devices_combobox.setVisible(False)
            try:
                device = frida.get_device(self._devices[0]['id'])
                self._check_device(device)
            except:
                pass

    def _on_timer(self):
        if self._timer_step == -1:
            self._timer.stop()
            return

        if self._timer_step == 0:
            self.update_label.setText(self.update_label.text() + ' .')
            self._timer_step = 1
        elif self._timer_step == 1:
            self.update_label.setText(self.update_label.text() + '.')
            self._timer_step = 2
        elif self._timer_step == 2:
            self.update_label.setText(self.update_label.text() + '.')
            self._timer_step = 3
        else:
            self.update_label.setText(
                self.update_label.text()[:-self._timer_step])
            self._timer_step = 0
            if self.is_waiting and self.devices_thread is not None:
                if not self.devices_thread.isRunning():
                    self.devices_thread.start()

    def _on_download_error(self, text):
        self._timer_step = -1
        self.update_label.setStyleSheet('background-color: crimson;')
        self.update_label.setText(text)
        self._install_btn.setVisible(True)
        self._update_btn.setVisible(False)

    def _on_device(self):
        self._timer_step = 4
        self.is_waiting = True
        self._on_timer()

    def _on_install_btn(self):
        # urls are empty
        if not self.updated_frida_assets_url:
            return

        arch = self._adb.get_device_arch()
        request_url = ''

        if arch is not None and len(arch) > 1:
            arch = arch.join(arch.split())

            if arch == 'arm64' or arch == 'arm64-v8a':
                request_url = self.updated_frida_assets_url['arm64']
            elif arch == 'armeabi-v7a':
                request_url = self.updated_frida_assets_url['arm']
            else:
                if arch in self.updated_frida_assets_url:
                    request_url = self.updated_frida_assets_url[arch]

            try:
                if self._adb.available() and request_url.index(
                        'https://') == 0:
                    self._install_btn.setVisible(False)
                    self._update_btn.setVisible(False)

                    if self._update_thread is not None:
                        if not self._update_thread.isRunning():
                            self._update_thread.frida_update_url = request_url
                            self._update_thread.adb = self._adb
                            self._update_thread.start()

            except ValueError:
                # something wrong in .git_cache folder
                print("request_url not set")

    def _update_statuslbl(self, text):
        self._timer.stop()
        self._timer_step = 0
        self._timer.start()
        self.update_label.setText(text)

    def _frida_updated(self):
        #self._timer_step = 3
        #self.is_waiting = True
        self._on_devices_finished()

    def _on_start_btn(self):
        if self._adb.available():
            self._start_btn.setVisible(False)
            if self._adb.start_frida():
                #self.onDeviceUpdated.emit(self._device_id)
                self._on_devices_finished()
            else:
                self._start_btn.setVisible(True)

    def _on_restart_btn(self):
        if self._adb.available():
            self._restart_btn.setVisible(False)
            if self._adb.start_frida(restart=True):
                self._restart_btn.setVisible(True)
                #self.onDeviceUpdated.emit(self._device_id)
                self._on_devices_finished()
示例#18
0
class DeviceBar(QWidget):

    onDeviceUpdated = pyqtSignal()

    def __init__(self, parent=None, device_type='usb'):
        super().__init__(parent=parent)
        if device_type == 'local':
            return
        self.parent = parent
        self.wait_for_devtype = device_type
        self.is_waiting = True
        self._adb = Adb()
        self._git = Git()
        self.setAutoFillBackground(True)
        self.setStyleSheet(
            'background-color: crimson; color: white; font-weight: bold; margin: 0; padding: 10px;'
        )
        self.setup()
        self._timer = QTimer()
        self._timer.setInterval(500)
        self._timer.timeout.connect(self._on_timer)
        self._timer.start()
        self._timer_step = 0
        frida.get_device_manager().on('added', self._on_device)
        frida.get_device_manager().on('removed', self._on_device)
        self.devices_thread = DevicesUpdateThread(self)
        self.devices_thread.onAddDevice.connect(self.on_add_deviceitem)
        self._update_thread = FridaUpdateThread(self)
        self._update_thread.on_status_text.connect(self._update_statuslbl)
        self._update_thread.on_finished.connect(self._frida_updated)
        self._update_thread.onError.connect(self._on_download_error)
        self.updated_frida_version = ''
        self.updated_frida_assets_url = {}
        remote_frida = self._git.get_frida_version()
        if remote_frida is None:
            self.updated_frida_version = ''
            self.updated_frida_assets_url.clear()
        else:
            remote_frida = remote_frida[0]
            self.updated_frida_version = remote_frida['tag_name']
            for asset in remote_frida['assets']:
                try:
                    name = asset['name']
                    tag_start = name.index('android-')
                    if name.index('server') >= 0:
                        tag = name[tag_start + 8:-3]
                        self.updated_frida_assets_url[tag] = asset[
                            'browser_download_url']
                except ValueError:
                    pass

    def setup(self):
        """ Setup ui
        """
        h_box = QHBoxLayout()
        h_box.setContentsMargins(0, 0, 0, 0)
        self.update_label = QLabel('Waiting for Device')
        self.update_label.setFixedWidth(self.parent.width())
        self.update_label.setOpenExternalLinks(True)
        self.update_label.setTextFormat(Qt.RichText)
        self.update_label.setFixedHeight(35)
        self.update_label.setTextInteractionFlags(Qt.TextBrowserInteraction)
        self._install_btn = QPushButton('Install Frida', self.update_label)
        self._install_btn.setStyleSheet('padding: 0; border-color: white;')
        self._install_btn.setGeometry(self.update_label.width() - 110, 5, 100,
                                      25)
        self._install_btn.clicked.connect(self._on_install_btn)
        self._install_btn.setVisible(False)
        self._start_btn = QPushButton('Start Frida', self.update_label)
        self._start_btn.setStyleSheet('padding: 0; border-color: white;')
        self._start_btn.setGeometry(self.update_label.width() - 110, 5, 100,
                                    25)
        self._start_btn.clicked.connect(self._on_start_btn)
        self._start_btn.setVisible(False)
        self._update_btn = QPushButton('Update Frida', self.update_label)
        self._update_btn.setStyleSheet('padding: 0; border-color: white;')
        self._update_btn.setGeometry(self.update_label.width() - 110, 5, 100,
                                     25)
        self._update_btn.clicked.connect(self._on_install_btn)
        self._update_btn.setVisible(False)
        self._restart_btn = QPushButton('Restart Frida', self.update_label)
        self._restart_btn.setStyleSheet('padding: 0; border-color: white;')
        self._restart_btn.setGeometry(self.update_label.width() - 110, 5, 100,
                                      25)
        self._restart_btn.clicked.connect(self._on_restart_btn)
        self._restart_btn.setVisible(False)
        h_box.addWidget(self.update_label)
        self.setLayout(h_box)

    def on_add_deviceitem(self, device_name, device_type):
        """ Adds an Item to the DeviceComboBox
        """
        if device_type == self.wait_for_devtype:
            self._timer_step = -1
            self.is_waiting = False
            self.update_label.setStyleSheet('background-color: yellowgreen;')
            self.update_label.setText('Device found: ' + device_name)
            self._adb._check_requirements()
            if self._adb.available():
                device_frida = self._adb.get_frida_version()
                if device_frida is None:
                    self._install_btn.setVisible(True)
                else:
                    if self.updated_frida_version != device_frida:
                        self._update_btn.setVisible(True)
                        if self._adb.is_frida_running():
                            self.onDeviceUpdated.emit()
                    elif device_frida and not self._adb.is_frida_running():
                        self._start_btn.setVisible(True)
                    elif device_frida and self._adb.is_frida_running():
                        self._restart_btn.setVisible(True)
                        self.onDeviceUpdated.emit()

    def _on_timer(self):
        if self._timer_step == -1:
            self._timer.stop()
            return

        if self._timer_step == 0:
            self.update_label.setText(self.update_label.text() + ' .')
            self._timer_step = 1
        elif self._timer_step == 1:
            self.update_label.setText(self.update_label.text() + '.')
            self._timer_step = 2
        elif self._timer_step == 2:
            self.update_label.setText(self.update_label.text() + '.')
            self._timer_step = 3
        else:
            self.update_label.setText(
                self.update_label.text()[:-self._timer_step])
            self._timer_step = 0
            if self.is_waiting and self.devices_thread is not None:
                if not self.devices_thread.isRunning():
                    self.devices_thread.start()

    def _on_download_error(self, text):
        self._timer_step = -1
        self.update_label.setStyleSheet('background-color: crimson;')
        self.update_label.setText(text)
        self._install_btn.setVisible(True)
        self._update_btn.setVisible(False)

    def _on_device(self):
        self._timer_step = 4
        self.is_waiting = True
        self._on_timer()

    def _on_install_btn(self):
        # urls are empty
        if not self.updated_frida_assets_url:
            return

        arch = self._adb.get_device_arch()
        request_url = ''

        if arch is not None and len(arch) > 1:
            arch = arch.join(arch.split())

            if arch == 'arm64' or arch == 'arm64-v8a':
                request_url = self.updated_frida_assets_url['arm64']
            elif arch == 'armeabi-v7a':
                request_url = self.updated_frida_assets_url['arm']
            else:
                if arch in self.updated_frida_assets_url:
                    request_url = self.updated_frida_assets_url[arch]

            try:
                if self._adb.available() and request_url.index(
                        'https://') == 0:
                    self._install_btn.setVisible(False)
                    self._update_btn.setVisible(False)

                    if self._update_thread is not None:
                        if not self._update_thread.isRunning():
                            self._update_thread.frida_url = request_url
                            self._update_thread.adb = self._adb
                            self._update_thread.start()

            except ValueError:
                # something wrong in .git_cache folder
                print("request_url not set")

    def _update_statuslbl(self, text):
        self._timer.stop()
        self._timer_step = 0
        self._timer.start()
        self.update_label.setText(text)

    def _frida_updated(self):
        self._timer_step = 3
        self.is_waiting = True
        self._on_timer()

    def _on_start_btn(self):
        if self._adb.available():
            self._start_btn.setVisible(False)
            if self._adb.start_frida():
                self.onDeviceUpdated.emit()
            else:
                self._start_btn.setVisible(True)

    def _on_restart_btn(self):
        if self._adb.available():
            self._restart_btn.setVisible(False)
            if self._adb.start_frida(restart=True):
                self._restart_btn.setVisible(True)
                self.onDeviceUpdated.emit()
示例#19
0
from lib.EmailHandler import EmailHandler

import itsdangerous
import hashlib

import geopy
import tinys3
from lib.git import Git

from flask import Flask, request
app = Flask(__name__)
logger = app.logger
logger.setLevel(logging.DEBUG)

geocoder = geopy.geocoders.OpenCage(config.OPENCAGE_API_KEY, timeout=5)
git = Git(config.GIT_REPO, config.GIT_WORKING_COPY)
s3 = tinys3.Connection(
    config.AWS_ACCESS_KEY_ID,
    config.AWS_SECRET_ACCESS_KEY,
    default_bucket=config.S3_IMAGE_BUCKET,
    tls=True,
)

mail_handler = EmailHandler(s3, config.S3_IMAGE_PATH_PREFIX, geocoder, git,
                            config.COMMIT_CHANGES)
signer = itsdangerous.Signer(config.ADDR_VALIDATION_HMAC_KEY,
                             sep="^",
                             digest_method=hashlib.sha256)


@app.route("/email/<sender>/<addr_hash>", methods=["POST"])
示例#20
0
    def __init__(self):
        super(ScriptsManager, self).__init__()
        self._git = Git()
        self.scripts = {}

        self.update_scripts()
示例#21
0
class ScriptsDialog(QDialog):
    """ Scripts
    """
    def __init__(self, app_window):
        super(ScriptsDialog, self).__init__(app_window)

        self.script = None
        self._app_window = app_window
        self._script_manager = ScriptsManager()
        self._git = Git()

        self.setMinimumWidth(800)

        box = QVBoxLayout()
        self.table = ScriptsTable(self)
        self.table.onScriptSelected.connect(self._item_selected)
        self.table.setMinimumWidth(800)

        # create a centered dot icon
        _section_width = self.table.header().sectionSize(3)
        self._new_pixmap = QPixmap(max(_section_width, 40), 20)
        self._new_pixmap.fill(Qt.transparent)
        painter = QPainter(self._new_pixmap)
        rect = QRect((_section_width * 0.5) - 5, 0, 20, 20)
        painter.setBrush(QColor('#666'))
        painter.setPen(QColor('#666'))
        painter.drawEllipse(rect)
        self._dot_icon = QIcon(self._new_pixmap)

        box.addWidget(self.table)
        lbl = QLabel('OS Support - A: Android I: IOS W: Windows')
        box.addWidget(lbl)
        self.setLayout(box)
        self._init_list()

    def _init_list(self):
        for script_name in sorted(self._script_manager.get_scripts().keys()):
            script = self._script_manager.get_script(script_name)
            info = script['info']

            _name = QStandardItem()
            _name.setText(script_name)
            _name.setToolTip(info['name'])

            _author = QStandardItem()
            if 'author' in info:
                _author.setTextAlignment(Qt.AlignCenter)
                _author.setText(info['author'])

            _android = QStandardItem()
            if 'android' in info:
                _android.setIcon(self._dot_icon)

            _ios = QStandardItem()
            if 'ios' in info:
                _ios.setIcon(self._dot_icon)

            _windows = QStandardItem()
            if 'windows' in info:
                _windows.setIcon(self._dot_icon)

            _desc = QStandardItem()
            if 'description' in info:
                _desc.setText(info['description'])

            self.table.add_item(
                [_name, _author, _android, _ios, _windows, _desc])

    def _item_selected(self, script_name):
        script_url = self._script_manager.get_script(script_name)['script']
        script = self._git.get_script(script_url)
        self.script = script
        self.accept()

    @staticmethod
    def pick(app):
        """ helper
        """
        dialog = ScriptsDialog(app)
        result = dialog.exec_()
        return result == QDialog.Accepted, dialog.script
示例#22
0
def run_dwarf():
    """ fire it up
    """
    #os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
    os.environ["QT_SCALE_FACTOR"] = "1"
    os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "0"
    os.environ["QT_SCREEN_SCALE_FACTORS"] = "1"

    args = process_args()
    #_check_dependencies() # not enabled atm

    from lib import utils
    from lib.git import Git
    from lib.prefs import Prefs
    from ui.app import AppWindow

    _prefs = Prefs()
    local_update_disabled = _prefs.get('disable_local_frida_update', False)

    if not local_update_disabled:
        _git = Git()
        import frida
        remote_frida = _git.get_frida_version()
        local_frida = frida.__version__

        if remote_frida and local_frida != remote_frida[0]['tag_name']:
            print('Updating local frida version to ' + remote_frida[0]['tag_name'])
            try:
                res = utils.do_shell_command('pip3 install frida --upgrade --user')
                if 'Successfully installed frida-' + remote_frida[0]['tag_name'] in res:
                    _on_restart()
                elif 'Requirement already up-to-date' in res:
                    if os.path.exists('.git_cache'):
                        shutil.rmtree('.git_cache', ignore_errors=True)
                else:
                    print('failed to update local frida')
                    print(res)
            except Exception as e: # pylint: disable=broad-except, invalid-name
                print('failed to update local frida')
                print(str(e))

    if os.name == 'nt':
        # windows stuff
        import ctypes
        try:
            # write ini to show folder with dwarficon
            folder_stuff = "[.ShellClassInfo]\n"
            folder_stuff += "IconResource=assets\\dwarf.ico,0\n"
            folder_stuff += "[ViewState]\n"
            folder_stuff += "Mode=\n"
            folder_stuff += "Vid=\n"
            folder_stuff += "FolderType=Generic\n"
            try:
                with open('desktop.ini', 'w') as ini:
                    ini.writelines(folder_stuff)

                # set fileattributes to hidden + systemfile
                ctypes.windll.kernel32.SetFileAttributesW(
                    r'desktop.ini', 0x02 | 0x04
                )  # FILE_ATTRIBUTE_HIDDEN = 0x02 | FILE_ATTRIBUTE_SYSTEM = 0x04
            except PermissionError:
                # its hidden+system already
                pass

            # fix for showing dwarf icon in windows taskbar instead of pythonicon
            _appid = u'iGio90.dwarf.debugger'
            ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
                _appid)

            ctypes.windll.user32.SetProcessDPIAware()

        except Exception:  # pylint: disable=broad-except
            pass

    from PyQt5.QtCore import Qt
    from PyQt5.QtGui import QIcon
    from PyQt5.QtWidgets import QApplication

    qapp = QApplication([])

    qapp.setDesktopSettingsAware(True)
    qapp.setAttribute(Qt.AA_EnableHighDpiScaling)
    qapp.setAttribute(Qt.AA_UseHighDpiPixmaps)
    qapp.setLayoutDirection(Qt.LeftToRight)

    qapp.setOrganizationName("https://github.com/iGio90/Dwarf")
    qapp.setApplicationName("dwarf")

    # set icon
    if os.name == "nt" and os.path.exists(
            utils.resource_path('assets/dwarf.ico')):
        _icon = QIcon(utils.resource_path('assets/dwarf.ico'))
        qapp.setWindowIcon(_icon)
    else:
        if os.path.exists(utils.resource_path('assets/dwarf.png')):
            _icon = QIcon(utils.resource_path('assets/dwarf.png'))
            qapp.setWindowIcon(_icon)

    app_window = AppWindow(args)
    app_window.setWindowIcon(_icon)
    app_window.onRestart.connect(_on_restart)

    try:
        sys.exit(qapp.exec_())
    except SystemExit as sys_err:
        if sys_err.code == 0:
            # thanks for using dwarf
            print('Thank\'s for using Dwarf\nHave a nice day...')
        else:
            # something was wrong
            print('sysexit with: %d' % sys_err.code)
class TestGit():

    def setup(self):
        self.repo_path = "../repo/path/"
        self.commit_range = "tag1..tag2"
        self.file = "file.txt"
        self.message = "1.2.3.4 release notes"

        self.tag_cmd = list(TAG_CMD)
        self.tag_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.tag_cmd[2] = '--work-tree=' + self.repo_path

        self.log_cmd = list(LOG_CMD)
        self.log_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.log_cmd[2] = '--work-tree=' + self.repo_path
        self.log_cmd.append(self.commit_range)

        self.add_cmd = list(ADD_CMD)
        self.add_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.add_cmd[2] = '--work-tree=' + self.repo_path
        self.add_cmd.append(self.file)

        self.cmt_cmd = list(CMT_CMD)
        self.cmt_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.cmt_cmd[2] = '--work-tree=' + self.repo_path
        self.cmt_cmd.append('"' + self.message + '"')

        self.push_cmd = list(PUSH_CMD)
        self.push_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.push_cmd[2] = '--work-tree=' + self.repo_path

        self.pull_cmd = list(PULL_CMD)
        self.pull_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.pull_cmd[2] = '--work-tree=' + self.repo_path

        self.fetch_cmd = list(FETCH_CMD)
        self.fetch_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.fetch_cmd[2] = '--work-tree=' + self.repo_path

        self.checkout_cmd = list(CHECKOUT_CMD)
        self.checkout_cmd[1] = '--git-dir=' + self.repo_path + '.git'
        self.checkout_cmd[2] = '--work-tree=' + self.repo_path

        self.sut = Git()

    def test___map_cmd_does_not_mutate_cmd_template(self):
        expected = ('git', '--git-dir=____', '--work-tree=____', 'tag')
        self.sut._Git__map_cmd(TAG_CMD, self.repo_path)
        eq_(expected, TAG_CMD)

    def test___map_cmd_replaces_git_dir_as_expected(self):
        expected = '--git-dir=' + self.repo_path + '.git'
        actual = self.sut._Git__map_cmd(TAG_CMD, self.repo_path)[1]
        eq_(expected, actual)

    def test___map_cmd_replaces_work_tree_as_expected(self):
        expected = '--work-tree=' + self.repo_path
        actual = self.sut._Git__map_cmd(TAG_CMD, self.repo_path)[2]
        eq_(expected, actual)

    @mock.patch('lib.git.subprocess')
    def test_tag_calls_subprocess_check_output_as_expected(self, mock_subprocess):
        self.sut.tag(self.repo_path)
        ok_(mock_subprocess.check_output.called)
        mock_subprocess.check_output.assert_called_once_with(self.tag_cmd)

    @mock.patch('lib.git.subprocess')
    def test_tag_returns_expected_value(self, mock_subprocess):
        mock_return = "a\nb\nc"
        expected = ["a", "b", "c"]
        mock_subprocess.check_output.return_value = mock_return
        eq_(expected, self.sut.tag(self.repo_path))

    @mock.patch('lib.git.subprocess')
    def test_log_calls_subprocess_check_output_as_expected(self, mock_subprocess):
        self.sut.log(self.commit_range, self.repo_path)
        ok_(mock_subprocess.check_output.called)
        mock_subprocess.check_output.assert_called_once_with(self.log_cmd)

    @mock.patch('lib.git.subprocess')
    def test_log_returns_expected_value(self, mock_subprocess):
        mock_return = "abc\n" + LOG_SEPARATOR + \
            "\ndef\n" + LOG_SEPARATOR + "\n"
        expected = ["abc", "def"]
        mock_subprocess.check_output.return_value = mock_return
        eq_(expected, self.sut.log(self.commit_range, self.repo_path))

    @mock.patch('lib.git.subprocess')
    def test_log_operates_as_expected(self, mock_subprocess):
        # mock_subprocess.check_output.side_effect = log_for_tag_side_effect
        self.sut.log(self.commit_range, self.repo_path)
        mock_subprocess.check_output.assert_called_once_with(self.log_cmd)

    def test_get_tag_range_operates_as_expected(self):
        tags = ["my", "dog", "fido"]

        actual = self.sut.get_tag_range("my", tags)
        eq_("my", actual)

        actual = self.sut.get_tag_range("dog", tags)
        eq_("my..dog", actual)

    @raises(ValueError)
    def test_get_tag_range_raises_ValueError_when_tag_not_found(self):
        tags = ["my", "dog", "fido"]
        self.sut.get_tag_range("tag", tags)

    @mock.patch('lib.git.subprocess')
    def test_add_calls_check_output_as_expected(self, mock_subprocess):
        self.sut.add(self.file, self.repo_path)
        ok_(mock_subprocess.check_output.called)
        mock_subprocess.check_output.assert_called_once_with(self.add_cmd)

    @mock.patch('lib.git.subprocess')
    def test_commit_calls_check_output_as_expected(self, mock_subprocess):
        self.sut.commit(self.message, self.repo_path)
        ok_(mock_subprocess.check_output.called)
        mock_subprocess.check_output.assert_called_once_with(self.cmt_cmd)

    @mock.patch('lib.git.subprocess')
    def test_push_calls_check_output_as_expected(self, mock_subprocess):
        remote = "myremote"
        branch = "mybranch"
        push_cmd = self.push_cmd
        push_cmd.append(remote)
        push_cmd.append(branch)

        self.sut.push(remote, branch, self.repo_path)
        ok_(mock_subprocess.check_output.called)
        mock_subprocess.check_output.assert_called_once_with(push_cmd)

    @mock.patch('lib.git.subprocess')
    def test_pull_calls_check_output_as_expected(self, mock_subprocess):
        remote = "myremote"
        branch = "mybranch"
        pull_cmd = self.pull_cmd
        pull_cmd.append(remote)
        pull_cmd.append(branch)

        self.sut.pull(remote, branch, self.repo_path)
        ok_(mock_subprocess.check_output.called)
        mock_subprocess.check_output.assert_called_once_with(pull_cmd)

    @mock.patch('lib.git.subprocess')
    def test_fetch_calls_check_output_as_expected(self, mock_subprocess):
        remote = "myremote"
        fetch_cmd = self.fetch_cmd
        fetch_cmd[4] = remote

        self.sut.fetch(remote, self.repo_path)
        ok_(mock_subprocess.check_output.called)
        mock_subprocess.check_output.assert_called_once_with(fetch_cmd)

    @mock.patch('lib.git.subprocess')
    def test_checkout_calls_check_output_as_expected(self, mock_subprocess):
        branch = "mybranch"
        checkout_cmd = self.checkout_cmd
        checkout_cmd.append(branch)

        self.sut.checkout(branch, self.repo_path)
        ok_(mock_subprocess.check_output.called)
        mock_subprocess.check_output.assert_called_once_with(checkout_cmd)

    @mock.patch('lib.git.util')
    def test_get_tags_by_pattern_operates_as_expected(self, mock_util):
        pattern = "tag11"
        tags = ["tag1", "tag11", "tag21"]
        self.sut.tag = mock.MagicMock()
        self.sut.tag.return_value = tags

        mock_util.string_has_pattern.side_effect = [False, True, False]

        actual = self.sut.get_tags_by_pattern(pattern, self.repo_path)
        self.sut.tag.assert_called_once_with(self.repo_path)
        eq_(3, mock_util.string_has_pattern.call_count)
        eq_([tags[1]], actual)