repokid


Namerepokid JSON
Version 0.18.6 PyPI version JSON
download
home_pagehttps://github.com/Netflix/repokid
SummaryAWS Least Privilege for Distributed, High-Velocity Deployment
upload_time2021-05-04 19:11:34
maintainer
docs_urlNone
author
requires_python>=3.7
licenseApache 2.0
keywords aws iam access_advisor
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            Repokid
=======
[![NetflixOSS Lifecycle](https://img.shields.io/osslifecycle/Netflix/osstracker.svg)]()
[![Build Status](https://travis-ci.com/Netflix/repokid.svg?branch=master)](https://travis-ci.com/Netflix/repokid)
[![Coverage Status](https://coveralls.io/repos/github/Netflix/repokid/badge.svg?branch=master)](https://coveralls.io/github/Netflix/repokid?branch=master)
[![Discord chat](https://img.shields.io/discord/754080763070382130?logo=discord)](https://discord.gg/9kwMWa6)

<img align="center" alt="Repokid Logo" src="docs/images/Repokid.png" width="25%" display="block">

Repokid uses Access Advisor provided by [Aardvark](https://github.com/Netflix-Skunkworks/aardvark)
to remove permissions granting access to unused services from the inline policies of IAM roles in
an AWS account.

## Getting Started

### Install

```bash
mkvirtualenv repokid
git clone git@github.com:Netflix/repokid.git
cd repokid
pip install -e .
repokid config config.json
```

#### DynamoDB

You will need a [DynamoDB](https://aws.amazon.com/dynamodb/) table called `repokid_roles` (specify account and endpoint in `dynamo_db` in config file).

The table should have the following properties:
 - `RoleId` (string) as a primary partition key, no primary sort key
 - A global secondary index named `Account` with a primary partition key of `Account` and `RoleId` and `Account` as projected attributes
 - A global secondary index named `RoleName` with a primary partition key of `RoleName` and `RoleId` and `RoleName` as projected attributes

For development, you can run dynamo [locally](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html).

To run locally:

```bash
docker-compose up
```

The endpoint for DynamoDB will be `http://localhost:8000`. A DynamoDB admin panel can be found at `http://localhost:8001`.

If you run the development version the table and index will be created for you automatically.

#### IAM Permissions

Repokid needs an IAM Role in each account that will be queried.  Additionally, Repokid needs to be launched with a role or user which can `sts:AssumeRole` into the different account roles.

RepokidInstanceProfile:
- Only create one.
- Needs the ability to call `sts:AssumeRole` into all of the RepokidRoles.
- DynamoDB permissions for the `repokid_roles` table and all indexes (specified in `assume_role` subsection of `dynamo_db` in config) and the ability to run `dynamodb:ListTables`

RepokidRole:
- Must exist in every account to be managed by repokid.
- Must have a trust policy allowing `RepokidInstanceProfile`.
- Name must be specified in `connection_iam` in config file.
- Has these permissions:
 ```json
 {
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "iam:DeleteInstanceProfile",
        "iam:DeleteRole",
        "iam:DeleteRolePolicy",
        "iam:GetAccountAuthorizationDetails",
        "iam:GetInstanceProfile",
        "iam:GetRole",
        "iam:GetRolePolicy",
        "iam:ListInstanceProfiles",
        "iam:ListInstanceProfilesForRole",
        "iam:ListRolePolicies",
        "iam:PutRolePolicy",
        "iam:UpdateRoleDescription"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}
```

So if you are monitoring `n` accounts, you will always need `n+1` roles. (`n` RepokidRoles and `1` RepokidInstanceProfile).

#### Editing config.json

Running `repokid config config.json` creates a file that you will need to edit.  Find and update these fields:
- `dynamodb`: If using dynamo locally, set the endpoint to `http://localhost:8010`.  If using AWS hosted dynamo, set the `region`, `assume_role`, and `account_number`.
- `aardvark_api_location`: The location to your Aardvark REST API.  Something like `https://aardvark.yourcompany.net/api/1/advisors`
- `connection_iam`: Set `assume_role` to `RepokidRole`, or whatever you have called it.

## Optional Config
Repokid uses filters to decide which roles are candidates to be repoed.  Filters may be configured to suit your
environment as described below.

### Blocklist Filter
Roles may be excluded by adding them to the Blocklist filter.  One common reason to exclude a role is if
the corresponding workload performs occasional actions that may not have been observed but are known to be
required.  There are two ways to exclude a role:

 - Exclude role name for all accounts: add it to a list in the config `filter_config.BlocklistFilter.all`
 - Exclude role name for specific account: add it to a list in the config `filter_config.BlocklistFilter.<ACCOUNT_NUMBER>`

 Blocklists can also be maintained in an S3 blocklist file.  They should be in the following form:
 ```json
 {
   "arns": ["arn1", "arn2"],
   "names": {"role_name_1": ["all", "account_number_1"], "role_name_2": ["account_number_2", "account_number_3"]}
 }
 ```

### Exclusive Filter
If you prefer to repo only certain roles you can use the Exclusive Filter. Maybe you want to consider only roles used in production or by certain teams.
To select roles for repo-ing you may list their names in the configuration files. Shell style glob patterns are also supported.
Role selection can be specified per individual account or globally.
To activate this filter put `"repokid.filters.exclusive:ExclusiveFilter"`in the section `active_filters` of the config file.
To configure it you can start with the autogenerated config file, which has an example config in the `"filter_config"` section:

```
"ExclusiveFilter": {
                   "all": [
                     "<GLOB_PATTERN>"
                     ],
                   "<ACCOUNT_NUMBER>": [
                     "<GLOB_PATTERN>"
                    ]
                   }
```

### Age Filter
By default the age filter excludes roles that are younger than 90 days.  To change this edit the config setting:
`filter_config.AgeFilter.minimum_age`.

### Active Filters

New filters can be created to support internal logic.  At Netflix we have several that are specific to our
use cases.  To make them active make sure they are in the Python path and add them in the config to the list in
the section `active_filters`.

## Extending Repokid

### Hooks

Repokid is extensible via hooks that are called before, during, and after various operations as listed below.

| Hook name | Context |
|-----------|---------|
| `AFTER_REPO` | role, errors |
| `AFTER_REPO_ROLES` | roles, errors |
| `BEFORE_REPO_ROLES` | account_number, roles |
| `AFTER_SCHEDULE_REPO` | roles |
| `DURING_REPOABLE_CALCULATION` | role_id, arn, account_number, role_name, potentially_repoable_permissions, minimum_age |
| `DURING_REPOABLE_CALCULATION_BATCH` | role_batch, potentially_repoable_permissions, minimum_age |

Hooks must adhere to the following interface:

```python
from repokid.hooks import implements_hook
from repokid.types import RepokidHookInput, RepokidHookOutput

@implements_hook("TARGET_HOOK_NAME", 1)
def custom_hook(input_dict: RepokidHookInput) -> RepokidHookOutput:
    """Hook functions are called with a dict containing the keys listed above based on the target hook.
    Any mutations made to the input and returned in the output will be passed on to subsequent hook funtions.
    """
    ...
```

Examples of hook implementations can be found in [`repokid.hooks.loggers`](repokid/hooks/loggers/__init__.py).

### Filters

Custom filters can be written to exclude roles from being repoed. Filters must adhere to the following interface:

```python
from repokid.filters import Filter
from repokid.types import RepokidFilterConfig
from repokid.role import RoleList


class CustomFilterName(Filter):
    def __init__(self, config: RepokidFilterConfig = None) -> None:
        """Filters are initialized with a dict containing the contents of `filter_config.FilterName`
        from the config file. This example would be initialized with `filter_config.CustomFilterName`.
        The configuration can be accessed via `self.config`

        If you don't need any custom initialization logic, you can leave this function out of your
        filter class.
        """
        super().__init__(config=config)
        # custom initialization logic goes here
        ...

    def apply(self, input_list: RoleList) -> RoleList:
        """Determine roles to be excluded and return them as a RoleList"""
        ...
```

A simple filter implementation can be found in [`repokid.filters.age`](repokid/filters/age/__init__.py). A more complex example is in [`repokid.blocklist.age`](repokid/filters/blocklist/__init__.py).

## How to Use

Once Repokid is configured, use it as follows:

### Standard flow
 - Update role cache: `repokid update_role_cache <ACCOUNT_NUMBER>`
 - Display role cache: `repokid display_role_cache <ACCOUNT_NUMBER>`
 - Display information about a specific role: `repokid display_role <ACCOUNT_NUMBER> <ROLE_NAME>`
 - Repo a specific role: `repokid repo_role <ACCOUNT_NUMBER> <ROLE_NAME>`
 - Repo all roles in an account: `repokid repo_all_roles <ACCOUNT_NUMBER> -c`

### Scheduling
Rather than running a repo right now you can schedule one (`schedule_repo` command). The duration between scheduling and eligibility is configurable, but by default roles can be repoed 7 days after scheduling.  You can then run a command `repo_scheduled_roles` to only repo roles which have already been scheduled.

### Targeting a specific permission

Say that you find a given permission especially dangerous in your environment.  Here I'll use `s3:PutObjectACL` as an example. You can use Repokid to find all roles that have this permission (even those hidden in a wildcard), and then remove just that single permission.

Find & Remove:
 - Ensure the role cache is updated before beginning.
 - Find roles with a given permission: `repokid find_roles_with_permissions <permission>... [--output=ROLE_FILE]`
 - Remove permission from roles: `repokid remove_permissions_from_roles --role-file=ROLE_FILE <permission>... [-c]`

Example:
```
$ repokid find_roles_with_permissions "s3:putobjectacl" "sts:assumerole" --output=myroles.json
...
$ repokid remove_permissions_from_roles --role-file=myroles.json "s3:putobjectacl" "sts:assumerole" -c
```

### Rolling back
Repokid stores a copy of each version of inline policies it knows about.  These are added when
a different version of a policy is found during `update_role_cache` and any time a repo action
occurs.  To restore a previous version run:

See all versions of roles: `repokid rollback_role <ACCOUNT_NUMBER> <ROLE_NAME>`
Restore a specific version: `repokid rollback_role <ACCOUNT_NUMBER> <ROLE_NAME> --selection=<NUMBER> -c`

### Stats
Repokid keeps counts of the total permissions for each role.  Stats are added any time an `update_role_cache` or
`repo_role` action occur.  To output all stats to a CSV file run: `repokid repo_stats <OUTPUT_FILENAME>`.  An optional account number can be specified to output stats for a specific account only.

### Library

> New in `v0.14.2`

Repokid can be called as a library using the `repokid.lib` module:

```python
from repokid.lib import display_role, repo_role, update_role_cache

account_number = "123456789012"

display_role(account_number, "superCoolRoleName")
update_role_cache(account_number)
repo_role(account_number, "superCoolRoleName", commit=True)
```

## Dispatcher ##
Repokid Dispatcher is designed to listen for messages on a queue and perform actions.  So far the actions are:
 - List repoable services from a role
 - Set or remove an opt-out
 - List and perform rollbacks for a role

Repokid will respond on a configurable SNS topic with information about any success or failures. The Dispatcher
component exists to help with operationalization of the repo lifecycle across your organization. You may choose
to expose the queue directly to developers, but more likely this should be guarded because rolling back can be
a destructive action if not done carefully.

## Development

### Releasing

Versioning is handled by [setupmeta](https://github.com/zsimic/setupmeta). To create a new release:

```bash
python setup.py version --bump patch --push

# Inspect output and make sure it's what you expect
# If all is well, commit and push the new tag:
python setup.py version --bump patch --push --commit
```


            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/Netflix/repokid",
    "name": "repokid",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.7",
    "maintainer_email": "",
    "keywords": "aws,iam,access_advisor",
    "author": "",
    "author_email": "",
    "download_url": "https://files.pythonhosted.org/packages/44/90/87b20f34dd7354f8fb24a034b13fe8c5e0b20e6017a901ea11adf1c67107/repokid-0.18.6.tar.gz",
    "platform": "",
    "description": "Repokid\n=======\n[![NetflixOSS Lifecycle](https://img.shields.io/osslifecycle/Netflix/osstracker.svg)]()\n[![Build Status](https://travis-ci.com/Netflix/repokid.svg?branch=master)](https://travis-ci.com/Netflix/repokid)\n[![Coverage Status](https://coveralls.io/repos/github/Netflix/repokid/badge.svg?branch=master)](https://coveralls.io/github/Netflix/repokid?branch=master)\n[![Discord chat](https://img.shields.io/discord/754080763070382130?logo=discord)](https://discord.gg/9kwMWa6)\n\n<img align=\"center\" alt=\"Repokid Logo\" src=\"docs/images/Repokid.png\" width=\"25%\" display=\"block\">\n\nRepokid uses Access Advisor provided by [Aardvark](https://github.com/Netflix-Skunkworks/aardvark)\nto remove permissions granting access to unused services from the inline policies of IAM roles in\nan AWS account.\n\n## Getting Started\n\n### Install\n\n```bash\nmkvirtualenv repokid\ngit clone git@github.com:Netflix/repokid.git\ncd repokid\npip install -e .\nrepokid config config.json\n```\n\n#### DynamoDB\n\nYou will need a [DynamoDB](https://aws.amazon.com/dynamodb/) table called `repokid_roles` (specify account and endpoint in `dynamo_db` in config file).\n\nThe table should have the following properties:\n - `RoleId` (string) as a primary partition key, no primary sort key\n - A global secondary index named `Account` with a primary partition key of `Account` and `RoleId` and `Account` as projected attributes\n - A global secondary index named `RoleName` with a primary partition key of `RoleName` and `RoleId` and `RoleName` as projected attributes\n\nFor development, you can run dynamo [locally](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html).\n\nTo run locally:\n\n```bash\ndocker-compose up\n```\n\nThe endpoint for DynamoDB will be `http://localhost:8000`. A DynamoDB admin panel can be found at `http://localhost:8001`.\n\nIf you run the development version the table and index will be created for you automatically.\n\n#### IAM Permissions\n\nRepokid needs an IAM Role in each account that will be queried.  Additionally, Repokid needs to be launched with a role or user which can `sts:AssumeRole` into the different account roles.\n\nRepokidInstanceProfile:\n- Only create one.\n- Needs the ability to call `sts:AssumeRole` into all of the RepokidRoles.\n- DynamoDB permissions for the `repokid_roles` table and all indexes (specified in `assume_role` subsection of `dynamo_db` in config) and the ability to run `dynamodb:ListTables`\n\nRepokidRole:\n- Must exist in every account to be managed by repokid.\n- Must have a trust policy allowing `RepokidInstanceProfile`.\n- Name must be specified in `connection_iam` in config file.\n- Has these permissions:\n ```json\n {\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Action\": [\n        \"iam:DeleteInstanceProfile\",\n        \"iam:DeleteRole\",\n        \"iam:DeleteRolePolicy\",\n        \"iam:GetAccountAuthorizationDetails\",\n        \"iam:GetInstanceProfile\",\n        \"iam:GetRole\",\n        \"iam:GetRolePolicy\",\n        \"iam:ListInstanceProfiles\",\n        \"iam:ListInstanceProfilesForRole\",\n        \"iam:ListRolePolicies\",\n        \"iam:PutRolePolicy\",\n        \"iam:UpdateRoleDescription\"\n      ],\n      \"Effect\": \"Allow\",\n      \"Resource\": \"*\"\n    }\n  ]\n}\n```\n\nSo if you are monitoring `n` accounts, you will always need `n+1` roles. (`n` RepokidRoles and `1` RepokidInstanceProfile).\n\n#### Editing config.json\n\nRunning `repokid config config.json` creates a file that you will need to edit.  Find and update these fields:\n- `dynamodb`: If using dynamo locally, set the endpoint to `http://localhost:8010`.  If using AWS hosted dynamo, set the `region`, `assume_role`, and `account_number`.\n- `aardvark_api_location`: The location to your Aardvark REST API.  Something like `https://aardvark.yourcompany.net/api/1/advisors`\n- `connection_iam`: Set `assume_role` to `RepokidRole`, or whatever you have called it.\n\n## Optional Config\nRepokid uses filters to decide which roles are candidates to be repoed.  Filters may be configured to suit your\nenvironment as described below.\n\n### Blocklist Filter\nRoles may be excluded by adding them to the Blocklist filter.  One common reason to exclude a role is if\nthe corresponding workload performs occasional actions that may not have been observed but are known to be\nrequired.  There are two ways to exclude a role:\n\n - Exclude role name for all accounts: add it to a list in the config `filter_config.BlocklistFilter.all`\n - Exclude role name for specific account: add it to a list in the config `filter_config.BlocklistFilter.<ACCOUNT_NUMBER>`\n\n Blocklists can also be maintained in an S3 blocklist file.  They should be in the following form:\n ```json\n {\n   \"arns\": [\"arn1\", \"arn2\"],\n   \"names\": {\"role_name_1\": [\"all\", \"account_number_1\"], \"role_name_2\": [\"account_number_2\", \"account_number_3\"]}\n }\n ```\n\n### Exclusive Filter\nIf you prefer to repo only certain roles you can use the Exclusive Filter. Maybe you want to consider only roles used in production or by certain teams.\nTo select roles for repo-ing you may list their names in the configuration files. Shell style glob patterns are also supported.\nRole selection can be specified per individual account or globally.\nTo activate this filter put `\"repokid.filters.exclusive:ExclusiveFilter\"`in the section `active_filters` of the config file.\nTo configure it you can start with the autogenerated config file, which has an example config in the `\"filter_config\"` section:\n\n```\n\"ExclusiveFilter\": {\n                   \"all\": [\n                     \"<GLOB_PATTERN>\"\n                     ],\n                   \"<ACCOUNT_NUMBER>\": [\n                     \"<GLOB_PATTERN>\"\n                    ]\n                   }\n```\n\n### Age Filter\nBy default the age filter excludes roles that are younger than 90 days.  To change this edit the config setting:\n`filter_config.AgeFilter.minimum_age`.\n\n### Active Filters\n\nNew filters can be created to support internal logic.  At Netflix we have several that are specific to our\nuse cases.  To make them active make sure they are in the Python path and add them in the config to the list in\nthe section `active_filters`.\n\n## Extending Repokid\n\n### Hooks\n\nRepokid is extensible via hooks that are called before, during, and after various operations as listed below.\n\n| Hook name | Context |\n|-----------|---------|\n| `AFTER_REPO` | role, errors |\n| `AFTER_REPO_ROLES` | roles, errors |\n| `BEFORE_REPO_ROLES` | account_number, roles |\n| `AFTER_SCHEDULE_REPO` | roles |\n| `DURING_REPOABLE_CALCULATION` | role_id, arn, account_number, role_name, potentially_repoable_permissions, minimum_age |\n| `DURING_REPOABLE_CALCULATION_BATCH` | role_batch, potentially_repoable_permissions, minimum_age |\n\nHooks must adhere to the following interface:\n\n```python\nfrom repokid.hooks import implements_hook\nfrom repokid.types import RepokidHookInput, RepokidHookOutput\n\n@implements_hook(\"TARGET_HOOK_NAME\", 1)\ndef custom_hook(input_dict: RepokidHookInput) -> RepokidHookOutput:\n    \"\"\"Hook functions are called with a dict containing the keys listed above based on the target hook.\n    Any mutations made to the input and returned in the output will be passed on to subsequent hook funtions.\n    \"\"\"\n    ...\n```\n\nExamples of hook implementations can be found in [`repokid.hooks.loggers`](repokid/hooks/loggers/__init__.py).\n\n### Filters\n\nCustom filters can be written to exclude roles from being repoed. Filters must adhere to the following interface:\n\n```python\nfrom repokid.filters import Filter\nfrom repokid.types import RepokidFilterConfig\nfrom repokid.role import RoleList\n\n\nclass CustomFilterName(Filter):\n    def __init__(self, config: RepokidFilterConfig = None) -> None:\n        \"\"\"Filters are initialized with a dict containing the contents of `filter_config.FilterName`\n        from the config file. This example would be initialized with `filter_config.CustomFilterName`.\n        The configuration can be accessed via `self.config`\n\n        If you don't need any custom initialization logic, you can leave this function out of your\n        filter class.\n        \"\"\"\n        super().__init__(config=config)\n        # custom initialization logic goes here\n        ...\n\n    def apply(self, input_list: RoleList) -> RoleList:\n        \"\"\"Determine roles to be excluded and return them as a RoleList\"\"\"\n        ...\n```\n\nA simple filter implementation can be found in [`repokid.filters.age`](repokid/filters/age/__init__.py). A more complex example is in [`repokid.blocklist.age`](repokid/filters/blocklist/__init__.py).\n\n## How to Use\n\nOnce Repokid is configured, use it as follows:\n\n### Standard flow\n - Update role cache: `repokid update_role_cache <ACCOUNT_NUMBER>`\n - Display role cache: `repokid display_role_cache <ACCOUNT_NUMBER>`\n - Display information about a specific role: `repokid display_role <ACCOUNT_NUMBER> <ROLE_NAME>`\n - Repo a specific role: `repokid repo_role <ACCOUNT_NUMBER> <ROLE_NAME>`\n - Repo all roles in an account: `repokid repo_all_roles <ACCOUNT_NUMBER> -c`\n\n### Scheduling\nRather than running a repo right now you can schedule one (`schedule_repo` command). The duration between scheduling and eligibility is configurable, but by default roles can be repoed 7 days after scheduling.  You can then run a command `repo_scheduled_roles` to only repo roles which have already been scheduled.\n\n### Targeting a specific permission\n\nSay that you find a given permission especially dangerous in your environment.  Here I'll use `s3:PutObjectACL` as an example. You can use Repokid to find all roles that have this permission (even those hidden in a wildcard), and then remove just that single permission.\n\nFind & Remove:\n - Ensure the role cache is updated before beginning.\n - Find roles with a given permission: `repokid find_roles_with_permissions <permission>... [--output=ROLE_FILE]`\n - Remove permission from roles: `repokid remove_permissions_from_roles --role-file=ROLE_FILE <permission>... [-c]`\n\nExample:\n```\n$ repokid find_roles_with_permissions \"s3:putobjectacl\" \"sts:assumerole\" --output=myroles.json\n...\n$ repokid remove_permissions_from_roles --role-file=myroles.json \"s3:putobjectacl\" \"sts:assumerole\" -c\n```\n\n### Rolling back\nRepokid stores a copy of each version of inline policies it knows about.  These are added when\na different version of a policy is found during `update_role_cache` and any time a repo action\noccurs.  To restore a previous version run:\n\nSee all versions of roles: `repokid rollback_role <ACCOUNT_NUMBER> <ROLE_NAME>`\nRestore a specific version: `repokid rollback_role <ACCOUNT_NUMBER> <ROLE_NAME> --selection=<NUMBER> -c`\n\n### Stats\nRepokid keeps counts of the total permissions for each role.  Stats are added any time an `update_role_cache` or\n`repo_role` action occur.  To output all stats to a CSV file run: `repokid repo_stats <OUTPUT_FILENAME>`.  An optional account number can be specified to output stats for a specific account only.\n\n### Library\n\n> New in `v0.14.2`\n\nRepokid can be called as a library using the `repokid.lib` module:\n\n```python\nfrom repokid.lib import display_role, repo_role, update_role_cache\n\naccount_number = \"123456789012\"\n\ndisplay_role(account_number, \"superCoolRoleName\")\nupdate_role_cache(account_number)\nrepo_role(account_number, \"superCoolRoleName\", commit=True)\n```\n\n## Dispatcher ##\nRepokid Dispatcher is designed to listen for messages on a queue and perform actions.  So far the actions are:\n - List repoable services from a role\n - Set or remove an opt-out\n - List and perform rollbacks for a role\n\nRepokid will respond on a configurable SNS topic with information about any success or failures. The Dispatcher\ncomponent exists to help with operationalization of the repo lifecycle across your organization. You may choose\nto expose the queue directly to developers, but more likely this should be guarded because rolling back can be\na destructive action if not done carefully.\n\n## Development\n\n### Releasing\n\nVersioning is handled by [setupmeta](https://github.com/zsimic/setupmeta). To create a new release:\n\n```bash\npython setup.py version --bump patch --push\n\n# Inspect output and make sure it's what you expect\n# If all is well, commit and push the new tag:\npython setup.py version --bump patch --push --commit\n```\n\n",
    "bugtrack_url": null,
    "license": "Apache 2.0",
    "summary": "AWS Least Privilege for Distributed, High-Velocity Deployment",
    "version": "0.18.6",
    "split_keywords": [
        "aws",
        "iam",
        "access_advisor"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "md5": "d8988cbe68b2ff7a276b2edccd58f5a1",
                "sha256": "aa22134600caa34969564704cb5efb9bf928bd9454a625a104728cb6e66bd499"
            },
            "downloads": -1,
            "filename": "repokid-0.18.6-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "d8988cbe68b2ff7a276b2edccd58f5a1",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.7",
            "size": 83230,
            "upload_time": "2021-05-04T19:11:32",
            "upload_time_iso_8601": "2021-05-04T19:11:32.014992Z",
            "url": "https://files.pythonhosted.org/packages/7b/55/cb0e6000884cd3fb689c1bcb6b86b2eac365dc9a1165458704533d0f47e7/repokid-0.18.6-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "b7d9e195cee6e1800994dc60557de51e",
                "sha256": "a2e882bd8d946c37f046808a083a84ac802bffc568aecc021acfcd187ecbff61"
            },
            "downloads": -1,
            "filename": "repokid-0.18.6.tar.gz",
            "has_sig": false,
            "md5_digest": "b7d9e195cee6e1800994dc60557de51e",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7",
            "size": 65651,
            "upload_time": "2021-05-04T19:11:34",
            "upload_time_iso_8601": "2021-05-04T19:11:34.347258Z",
            "url": "https://files.pythonhosted.org/packages/44/90/87b20f34dd7354f8fb24a034b13fe8c5e0b20e6017a901ea11adf1c67107/repokid-0.18.6.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2021-05-04 19:11:34",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "github_user": null,
    "github_project": "Netflix",
    "error": "Could not fetch GitHub repository",
    "lcname": "repokid"
}
        
Elapsed time: 0.25188s