Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
@bscotch/bravo
Advanced tools
Butterscotch Shenanigans Presents:
⚠Warning⚠ Bravo is in active development and may change substantially with any release. Check the changelog before updating!
See the Roadmap for which Favro API features are implemented, planned, or backburnered.
Favro is an amazing project management tool and a great way to centralize all kinds of workflows. However, Favro's integrations with external tools and services, and its own internal automations, are limited.
Fortunately, Favro provides an HTTP API and Webhooks so developers can fill in those gaps themselves. Unfortunately, using raw HTTP APIs is unpleasant and time-consuming. That's where Bravo comes in!
Butterscotch Shenanigans® and Bravo (this project) are not affiliated with Favro.
Install from npm:
npm install -g @bscotch/bravo
Then import Bravo into your Node project, instance a Bravo client, and you're all set!
// ESM-style (Typescript)
import {BravoClient} from '@bscotch/bravo';
// -or-
// CommonJS style (regular Node)
const {BravoClient} = require('@bscotch/bravo');
const bravoClient = new BravoClient({
token:'your-token',
userEmail: 'your-favro-account-email',
organizationId:'your-organization-id',
});
async function doFavroStuff() {
await bravoClient.getCurrentOrganization();
// Find a Widget (a.k.a. Board)
const widget = await bravoClient.findWidgetByName('My To Do List');
// Add a card to that widget
const newCard = await widget.createCard({
name: 'Talk to so-and-so',
detailedDescription: 'We need to maximize synergy.'
});
// Find the userId for an assignee
const assignee = await bravoClient.findMemberByName('Scam Likely');
// Use the update-builder to create and send an update
// that covers multiple fields.
newCard.updateBuilder
.assign([assignee.userId])
.setStartDate(new Date())
.addTagsByName(['todo']);
// Submit and clear the update-builder's changes
await newCard.update();
// Add an attachment
await newCard.attach('some-data.txt', "Ooooh, a text file!");
// If no data is provided, treats the path as an actual
// file and uploads its contents
await newCard.attach('path/to/a/file.txt');
// Delete the card
await newCard.delete();
}
To have Bravo access Favro on your behalf, you'll need to provide it with the credentials listed below. You can do so via environment variables as shown here, or directly when instancing the Bravo client as shown in the example above:
FAVRO_TOKEN
: Your Favro API token. To create one, go to your Profile, then "API Tokens" → "New API Token".FAVRO_USER_EMAIL
: Your Favro account email.FAVRO_ORGANIZATION_ID
: The Organization ID that you are targeting. You can get your Organization ID from the URL when using Favro in a browser. Favro URLs look like this: favro.com/organization/{organizationId}
.The Favro API gives us access to some of the underlying data models used by Favro. While not exactly unintuitive, it is tricky to figure out how everything works together and what Favro's terms and names mean.
Below is a summary of how it all comes together.
The term "Collection" is used the same way in the webapp and API. A Collection is essentially a dashboard that contains Widgets, and Widgets can live in multiple Collections.
A Favro Board (e.g. a KanBan board or Backlog) is called a "Widget" in the Favro API.
⚠ The API does not have any access to the "Views" concept that we can use via the UI (e.g. to create Sheet and Kanban views on the same Board/Widget).
A Widget has "Columns", which have a narrower meaning than you might expect: each Column corresponds to one value from the "Status" field that is created when you make a new Widget via the GUI. In other words, "Column" and "Status" are interchangeable.
💡 Favro's Webhooks are attached to Columns: Webhooks only trigger for registered events occurring on their associated Columns.
⚠ While the default Status field is synonymous with "Columns", that is not true for custom Status fields! Custom Status fields are their own completely separate thing (discussed below).
A Card can exist in multiple Widgets at once. Favro models this by saying that a Card is "instanced" separately on each Widget. Cards have multiple identifiers acting differently on the global vs. "instance" scopes:
Identifier Name | Scope | Purpose |
---|---|---|
cardCommonId | Global | Identifying a Card |
cardId | Per Widget | Identifying a Card instance |
sequentialId | Global | Human-friendly id (for URLs) |
When you list Cards in a Widget-level scope (e.g. by limiting the search to a specific Widget), you'll get exactly one instance of each returned Card.
When you list Cards in a global-level scope (e.g. by filtering on sequentialId
or cardCommonId
, or otherwise not filtering by Widget or forcing uniqueness), you can end up getting multiple near-identical copies of the same Card! One per Widget that the Card is "instanced" on.
Many of the Card's data is completely identical in each instance. Only the Widget-specific data changes, which is largely about position information within the Widget. Example per-Widget Card data (non-exhaustive):
Card Instance Field | Meaning |
---|---|
widgetCommonId | The parent Widget's ID (for this Card instance) |
columnId | The identifier for the Status/Column this instance is in on the parent Widget |
parentCardId | If the card has a parent on the Widget (a la Sheet-view), the parent Card's Widget-specific cardId |
listPosition | The index position of the Card on the Widget |
timeOnBoard | How long the Card has been on the Widget |
archived | Whether or not the Card is archived on its parent Widget |
💡 To get all Board Statuses for a Card, you would need to do a search using a global identifier for that card and then loop through all returned instances.
All cards come with a handful of fields by default. These fields are global (not Widget-specific), and include:
name
: The title/name of the card (the part you fill in first when making a new one)detailedDescription
: The body of the card, as either Markdown or some sort of sad text file with Markdown removedcardCommonId
: The globally unique identifier for this cardtags
: Identifiers for tags used on this card (tags are global)sequentialId
: An incrementing numeric identifier, used for URLsstartDate
: The work start-date for the carddueDate
: The work due-date for the cardassignments
: Who is assigned, and whether they've marked themselves "complete"attachments
: List of files attached and their URLscustomFields
: While this field is on every card, its values depend on what data has been attached to the card via Custom Fields (see below)favroAttachments
: A list of Favro data objects attached to the card (like other cards and Widgets, shown as embeds in the body)⚠ Custom Field data from the Favro API cannot be filtered by scope and does not contain scope information, unlike what you see in the Favro UI. This makes working with Custom Fields via the API (and Bravo) difficult. Upvote the associated Favro Feature request!
The idea of "Custom Fields" is a bit confusing, since the Favro GUI doesn't make it clear which fields are built-in, or that the default "Status" field is a totally different kind of thing (a collection of "Columns"). Further, "Custom Fields" overlap many of the built-ins in type: you can have a Custom Status Field, or a Custom Members field, and so on.
All fields that are neither the build-ins nor the Widget-specific "Status" (Column) fields are "Custom Fields".
In the GUI you can change a Custom Field's visibility and scope, and when you open a Card GUI you'll see all in-scope (global-, collection-, and widget-specific) Custom Fields shown, even for fields that are not yet set on that specific card. This makes it easy to figure out which field is which when using the app.
When using webhooks or the public API, however, all custom fields are global and they contain no information to help determine their scope. In other words, if any two of your (likely hundreds of) custom fields have the same name, you will not be able to tell them apart in the data returned by the Favro API.
This problem is exacerbated by the facts that:
Collectively, these prevent you from using existing Cards (fetched while narrowing the search scope to a specific Widget) to infer what fields are associated with a given Widget.
Currently, the best way to find Custom Field identifiers for use by the Favro API (or Bravo) is to use a browser's dev tools in the web-app. For example, by using the browser's "Inspect element" on a Custom Field shown inside a card, you can find its ID in the HTML element's id
field.
Some lessons learned while building Bravo.
Favro's API rate limits are quite strict and differ by plan tier. At the time of writing, based on the pricing page and my experience using the API, the limits appear to be:
Tier | Requests/Hour |
---|---|
Free (trial) | 100 |
Lite | 100 |
Standard | 1000 |
Enterprise | 10000 |
Rate limits appear to be per user-organization pair. It is easy to hit the lower limits even with fairly light automation.
⚠ The Favro API has extremely limited search functionality. Upvote the feature request!
The Favro API does provide some filtering options, e.g. to restrict the "get Cards" endpoint to a specific Widget, but there is no way to further restrict by any content of the cards themselves (e.g. no text search on the name/description fields, nor filtering by assigned user, nor tags, etc).
To find something via the API then requires an exhaustive search followed by local filtering. Bravo adds some convenience methods for things like finding a card by name, but it does so by doing an exhaustive search behind the scenes.
Bravo does some caching and lazy-loading to reduce the impact of this on the number of API requests it makes, but the end result is always going to be that search functionality in Bravo has to consume a lot of API requests, especially if you have a lot of stuff in Favro.
⚠ Webhooks do not send Markdown. To get full Markdown content in response to a Webhook, you'll have to fetch the same card again via the API. Upvote the feature request!
Favro implements a limited subset of Markdown. Which subset seems to differ based on context (e.g. a Card description vs. a Comment).
Despite having some Markdown support, the API and Webhook data defaults to a "plaintext" format, which apparently means "strip out all Markdown". This is a particular problem for Webhooks, since there is no way to cause them to send Markdown instead.
Bravo defaults to requesting Markdown where that's possible.
Items in Favro (cards, boards, comments, custom fields, etc.) are all identified by unique identifiers. Different types of items are fetched independently, with relationships indicated by identifiers pointing to other items.
For example, if you fetch a Card from the API (or a webhook) you'll also get a list of Widget identifiers in that card, but not the data about those widgets. Similarly, a Card contains a list of its Custom Fields and corresponding values, but most of the information is in the form of Custom Field identifiers. In both cases, if you wanted to see the names or other information for those associated items, you'll need to make API requests for those specific items using their IDs and appropriate API endpoints.
Bravo tries to handle much of this for you behind the scenes.
Cards have a field called sequentialId
that corresponds directly to the visible identifier shown in the Card UI, from which users can copy a card URL.
Note that the card-search Favro API endpoint allows use of sequentialId
as a query parameter. They've done us a huge solid here by allowing us to use any of the following as its value while still serving up the expected card:
BSC-123
).Note that all of these also appear to work for the user-friendly Card URLs (e.g. where the default URL ends with ?card=BSC-123
you could instead use ?card=123
).
cardId
sFavro Cards are always return as an instance of that Card, each with its own cardId
.
Despite this ID being Widget-specific, and the existence of global idtentifiers for Cards, most of the Favro API card endpoints use this Widget-scoped cardId
. Presumably this is to prevent them having to have distinct endpoints for managing global vs. Widget-scoped properties.
💡 Create boards manually via the app to get all the features you expect from Favro.
When creating a board via the Favro API, there appears to be no way to have the resulting board work the same way as one created via the app. In particular, when creating a Kanban board the columns are not linked to a "Status" type Custom Field and there does not seem to be a way to create such a connection after the fact.There appears to be a planned fix.
There is also no way to create views using the Favro API.
0.2.0 (2021-08-21)
FAQs
Bravo: A Favro SDK with full Typescript support and tons of convenience features. By Butterscotch Shenanigans
The npm package @bscotch/bravo receives a total of 2 weekly downloads. As such, @bscotch/bravo popularity was classified as not popular.
We found that @bscotch/bravo demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.